这篇文章带大家实现一个简单的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函数。