likes
comments
collection
share

自己造轮子系列——实现一个简易的React框架?(一)

作者站长头像
站长
· 阅读数 19

写在前面

今天就来造轮子,实现一个自己的React简易框架深入学习一下React框架的实现原理加深一下对React框架的理解和应用。

就程序而言,最好学习知识点的方法或许就是在项目中实战,再次一等也是要写个demo运行起来,最好看懂示例代码后自己实现一遍,往往会发现很多细节值得推敲,很多实现思路值得借鉴。

背景知识

要开发自己的React框架,就不能不提一些原生JSDOM操作,我们现在大量使用第三方框架,一些基础的DOM操作可能都生疏了,这里汇总一些这次造轮子将要用的API:

  1. document.getElementById根据节点id获取真实dom;
  2. document.createElement 根据类型创建DOM,比如div,h1,span之类;
  3. document.createTextNode 创建文本节点;
  4. dom.appendChild 添加子节点;

先列举这几个马上要用到的,后面有用到新的API,再添加;

概述

React框架具体的实现细节十分庞杂,这里就挑几个最具代表性的特性——JSXFunction ComponentHooks,当然每一个特性的实现细节也十分繁琐,我们的目标是实现核心功能理解原理就好。另外对于React最新的Fibers树结构并发模型,也简单探讨下。

总结下来,具体要实现的轮子:

  1. React.createElement 函数——核心功能,解析JSX;
  2. ReactDOM.render 函数——核心功能;
  3. Concurrent Mode并发模型——探讨;
  4. Fibers Tree——探讨;
  5. Function Component;
  6. Hooks;

开始造轮子

React背后的渲染逻辑

我们先来看看React框架最基本的使用方式,核心只要三行就可以了:

    const element = <h1 title="foo">Hello</h1>
    const container = document.getElementById("root")
    ReactDOM.render(element, container)

先声明一个最简单JSX组件,然后获取到html上根节点的DOM对象,最后通过ReactDOM.render方法把element组件渲染container里;container的作用是appendChild这里就不再讨论;通过和原生JS对比,我们把主要差异聚焦在:element这个jsx组件最后被怎么添加到container里的

首先JSX并不是一个合法JS表达式,需要通过babel编译成合法JS,具体实现就是通过调用React.createElement方法,传入JSX里的tag,props和children。

JSX编译后结构如下:

const element = React.createElement(
    "h1",
    { title: "foo" },
    "Hello"
)

React.createElement方法的返回的对象,结构如下:

const element = {
    type: "h1",
    props: {
        title: "foo",
        children: "Hello",
    },
}

现在我们先跳过React.createElement这个把JSX把转化为JS的实现,先来看看原生JS该怎么把element对象渲染,来感受下React.createElement的内部的实现逻辑

const container = document.getElementById("root")

const node = document.createElement(element.type)

node["title"] = element.props.title

const text = document.createTextNode("")

text["nodeValue"] = element.props.children

node.appendChild(text)

container.appendChild(node)

可以看到,对于一个简单的element对象,我们直接调用了document.createElement方法,拿到node节点;然后给node节点添加了element对象里props属性;而对于”Hello“这个文本children则通过document.createTextNode来创建;通过appendChild添加到node节点中,最后再通把node节点添加到container中,渲染完成。

对于一个简单的H1标签我们这样实现没啥问题,对于一个页面或者一个项目也这样一点点通过原生JS就显然不太合理了。那前端框架本身就是对节点渲染逻辑进行一层抽象,React框架创造性地适配了大家熟悉XML形式的Dom树结构——也就是现在的JSX

当然我们需要理解JSX解析成JS对象,这就是我们接下来要讲的React.createElement方法了。

React.createElement

我们先来回忆下JSX在React中的使用方式:

const element = (
    <div id="foo">
    <a>bar</a>
    <b />
    </div>
)
const container = document.getElementById("root");
ReactDOM.render(element, container)

element这个JSX在传给ReactDOM.render方法,编译后的代码是这样的:

const element = React.createElement(
    "div",
    { id: "foo" },
    React.createElement("a", null, "bar"),
    React.createElement("b")
)

那我们接下来实现一个自己的createElement:

function createElement(type, props, ...children) {
    return {
        type,
        props: {
            ...props,
            children: children.map(child =>
                typeof child === "object"
                ? child
                : createTextElement(child)
                ),
        },
    }
}

function createTextElement(text) {
    return {
        type: "TEXT_ELEMENT",
        props: {
            nodeValue: text,
            children: [],
        },
    }
}

目前只处理了两层节点嵌套,文本也算作一层节点。

在我们自己的createElement方法里,接收一个标签typeprops属性,和children,然后创建了一个节点对象

另外需要特别注意的是,JSX转化为JSbabel编译时完成的,所以就需要告诉babel,使用我们自己定义createElement,而不是使用React.createElement。

如何React区分开来?我们给自己的React轮子起名叫Didact

const Didact = {
    createElement,
}
// babel编译后
const element = Didact.createElement(
    "div",
    { id: "foo" },
    Didact.createElement("a", null, "bar"),
    Didact.createElement("b")
)

如上面这段代码,我们希望babel编译时用自己定义的Didact.createElement来解析JSX, 只需要在JSX前面加一行babel注释/** @jsx Didact.createElement */

/** @jsx Didact.createElement */
const element = (
    <div id="foo">
        <a>bar</a>
        <b />
    </div>
)
const container = document.getElementById("root")
ReactDOM.render(element, container)

现在babel在编译JSX的时候,就会调用我们Didact.createElement方法了,最后我们还是先调用ReactDOM.render方法进行渲染,运行代码H1正常渲染就OK了。

render

接下来我们来类比ReactDOM.render来实现我们自己的Didact.render渲染方法:

function render(element, container) {
// TODO create dom nodes
}

const Didact = {
    createElement,
    render,
}

/** @jsx Didact.createElement */
const element = (
    <div id="foo">
        <a>bar</a>
        <b />
    </div>
)

const container = document.getElementById("root")
Didact.render(element, container)

这里我们彻底抛弃了React框架,全部替换了自己的轮子Didact,接下来我们来实现render方法:

这里我们来回忆下babel编译之后的节点对象

const element = {
    type: "h1",
    props: {
        title: "foo",
        children: "Hello",
    },
}

然后我们把这样的节点对象添加container里:

function render(element, container) {
    const dom =
        element.type == "TEXT_ELEMENT"
        ? document.createTextNode("")
        : document.createElement(element.type)

    const isProperty = key => key !== "children"
    Object.keys(element.props)
        .filter(isProperty)
        .forEach(name => {
            dom[name] = element.props[name]
        })

    element.props.children.forEach(child =>render(child, dom))
    container.appendChild(dom)
}

可以看到render方法接受两个参数,第一个参数就是前面babel通过Didact.createElement方法编译返回节点对象,而不是直接传入的JSX;第二个参数就是真实的顶层dom容器,用来appendChild

render函数中,对文本节点调用了document.createTextNode,而对非文本节点调用了document.createElement;然后把props除children的属性添加给新创建的dom;对于children属性则通过递归调用render函数来进行添加props属性添加到父节点里;最后把所有的节点通过appendChild层层添加根节点上container。

在线调试

后记

关于React框架中,JSX渲染涉及到的React.createElementReact.render方法做了基础的实现,并使用自己的Didact渲染了简单的JSX组件。

在实际开发中,JSX的嵌套层级不止层,同时一个组件不止有JSX,还有很多业务逻辑,也就是我们后面要讲到的Function Component,另外后面我们还会讲到FiberHooks的实现原理。

敬请期待吧!