Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
li-jia-nan opened this issue Oct 15, 2022 · 0 comments
Open

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

li-jia-nan opened this issue Oct 15, 2022 · 0 comments

Comments

@li-jia-nan
Copy link
Owner

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

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

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

  • 第一行通过jsx定义了一个dom
  • 第二行从html中获取一个根节点
  • 第三行将自己定义的dom注入到根节点中

注意知道的是:在这里我们只实现函数本身,不会关心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

  • type是一个字符串,表示标签的标签名,它的作用是指定我们想要创建的DOM节点的类型,用来传递给document的tagName
  • props是另一个对象,它包含来自jsx的所有键和值,它还有一个特殊的属性:children,用来表示标签的子元素,这个例子中的子元素是个字符串,但通常children是一个包含很多元素的数组,这也就是为什么dom节点是一个树

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

  • 首先,我们使用type创建一个节点,在本例中是h1
  • 然后将所有的props属性赋值给该节点,在本例中只有title
  • 然后创建子节点,在本例中只有一个字符串作为子节点,因此我们创建一个文本节点
  • 最后,我们将子节点append到父节点,并将父节点append到根节点
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将始终是一个数组,例如:

  • createElement("div")返回:
{
  "type": "div",
  "props": { "children": [] },
}
  • createElement("div", null, a)返回:
{
  "type": "div",
  "props": { "children": [a] },
}
  • createElement("div", null, a, b)返回:
{
  "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函数:

  • 首先使用type创建DOM节点
  • 还需要处理文本节点,如果type是TEXT_ELEMENT,我们将创建一个文本节点
  • 然后递归地对每个children执行相同的操作
  • 最后将新节点append到父节点中
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函数。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant