li-jia-nan / my-blog

个人技术博客,同步掘金,文章写在 Issues 里
43 stars 1 forks source link

40行代码实现简易版React render #8

Open li-jia-nan opened 1 year ago

li-jia-nan commented 1 year ago

这篇文章带大家实现一个简单的render函数,在此之前,你需要对jsx语法和DOM元素的工作原理有基本了解

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

我们将实现这个React渲染函数,只有三行代码:

注意知道的是:在这里我们只实现函数本身,不会关心jsx是如何编译成render函数的,因为那是编译器(比如Babel)的工作,jsx在编译的时候,通过某些构建工具(比如Babel)转换为js,转换过程很简单:用createElement函数代替我们定义的内容,同时将标签名、props、子元素作为参数传递给createElement函数

让我们删除所有React的代码,用普通的JavaScript代替它:

在上面的代码中,第一行是用jsx定义的元素,实际上它并不是有效的JavaScript,所以为了使用有效的js,首先我们需要用createElement替换掉jsx:

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

然后再把上面代码中的createElement函数转换成普通的element对象,可以写成下面这样:

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

可以看到,createElement函数的作用是根据其参数创建一个对象,对象有两个属性:type和props

我们需要替换的另一部分React代码是对ReactDOM.render的调用:

const element = {
  type: "h1",
  props: {
    title: "foo",
    children: "Hello",
  },
};
const container = document.getElementById("root"); // 获取根节点
const node = document.createElement(element.type); // 使用type创建一个节点
node["title"] = element.props.title; // 将title属性赋值给该节点
const text = document.createTextNode(""); // 创建子节点
text["nodeValue"] = element.props.children; // 用nodeValue设置子节点的内容
node.appendChild(text); // 将子节点append到父节点
container.appendChild(node); // 将父节点append到根节点

现在,我们有了和开始几行代码一样的功能,但是没有使用React

接下来,让我们开始编写自己的createElement函数:

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

正如我们在前面的步骤中所看到的:element是一个具有type和props的对象。我们的函数唯一需要做的就是创建这个对象:

function createElement(type, props, ...children) {
    return {
        type,
        props: {
            ...props,
            children,
        },
    };
}

我们对props使用扩展操作符,对children参数使用rest语法,这样可以保证子元素props将始终是一个数组,例如:

{
  "type": "div",
  "props": { "children": [] },
}
{
  "type": "div",
  "props": { "children": [a] },
}
{
  "type": "div",
  "props": { "children": [a, b] },
}

需要注意的是:children还可以包含string或者number基本类型的值。因此,我们需要先写一个函数区分基本类型的值和对象类型的值,然后把所有不是对象类型的值封装一下,并为它们创建一个特殊的类型:TEXT_ELEMENT

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

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

需要知道的是,在react源码中,当没有子元素时,React不会封装原始值、也不会创建空数组,而我们这样做的原因,仅仅是因为我太懒,这样写起来简单,并且对于我们简易版的render函数,我更喜欢简单的代码、而不是性能代码。所以不用去追求细节(躺平就完了)

然后仍然回到React的createElement函数:

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

为了替换它,我们给自己的函数起个新的名字:myReactRender

const myReactRender = {
    createElement,
};

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

目前已经实现了createElement函数,接下来需要实现render函数:

function render(element, container) {
    // TODO create dom nodes
}
const myReactRender = {
    createElement,
    render,
};
const element = myReactRender.createElement(
    "div",
    { id: "foo" },
    myReactRender.createElement("a", null, "bar"),
    myReactRender.createElement("b"),
);
const container = document.getElementById("root");
myReactRender.render(element, container);

接下来需要处理render函数:

function render(element, container) {
    const dom = element.type == "TEXT_ELEMENT"
        ? document.createTextNode("")
        : document.createElement(element.type);
    element.props.children.forEach(child => render(child, dom));
    container.appendChild(dom);
}

最后一件事,就是将props的每一个值分配给创建的dom:

function render(element, container) {
    const dom = element.type == "TEXT_ELEMENT"
        ? document.createTextNode("")
        : document.createElement(element.type);
    Object.keys(element.props)
        .filter(key => key !== "children")
        .forEach(name => { dom[name] = element.props[name] });
    element.props.children.forEach(child => render(child, dom));
    container.appendChild(dom);
}

最后一步,如果我们仍然想在这里使用jsx语法,我们如何告诉Babel使用myReactRender的渲染函数而不是React的? 我们只需要添加这样的注释就好了,当Babel编译jsx时,它将使用我们定义的函数:

/** @jsx myReactRender.createElement */
const element = (
    <div id="foo">
        <a>11111</a>
    </div>
);

就是这样,现在我们有了一个可以将JSX渲染到DOM的库,完整代码如下:

function createTextElement(text) {
    return {
        type: "TEXT_ELEMENT",
        props: {
            nodeValue: text,
            children: [],
        },
    };
}
function createElement(type, props, ...children) {
    return {
        type,
        props: {
            ...props,
            children: children.map(child => (
                typeof child === "object" ? child : createTextElement(child)
            )),
        },
    }
}
function render(element, container) {
    const dom = element.type == "TEXT_ELEMENT"
        ? document.createTextNode("")
        : document.createElement(element.type);
    Object.keys(element.props)
        .filter(key => key !== "children")
        .forEach(name => { dom[name] = element.props[name] });
    element.props.children.forEach(child => render(child, dom));
    container.appendChild(dom);
}
const myReactRender = { createElement, render };
const container = document.getElementById("root");
/** @jsx myReactRender.createElement */
const element = (
    <div id="foo">
        <a>abc</a>
    </div>
);
myReactRender.render(element, container);

截止目前,我们用40行代码实现了react的render函数。