原文链接:Build your own React
- Step I: The createElement Function ✅
- Step II: The render Function ✅
- Step III: Concurrent Mode 并发模式
- Step IV: Fibers
- Step V: Render and Commit Phases
- Step VI: Reconciliation
- Step VII: Function Components
- Step VIII: Hooks
** To avoid confusion, I’ll use “element” to refer to React elements and “node” for DOM elements.*
前言
上文中我们做了一些基本的准备工作,这节开始正式来编写我们自己的React(建议从上文开始看起)。
The createElement Function
上文中我们得知一个react元素被转译后实际上就是一个包含了type以及props的js对象,那么我们在这里就是要通过createElement这个函数创建这么一个对象。
对props使用扩展运算符,对children使用rest参数语法,这样children将使用是一个数组格式。我们可以编写第一版createElement Function了:
// react/createElement.js function createElement(type, props, ...children) { return { type, props: { ...props, children, }, }; } export default createElement; // react/index.js // 统一管理createElement函数以及render函数 // 以便main.js中直接引入即可 import createElement from "./createElement"; export { createElement };return示例:
createElement('div') returns: { "type": "div", "props": { "children": [] } } createElement("div", null, a) returns: { "type": "div", "props": { "children": [a] } } createElement("div", null, a, b) returns: { "type": "div", "props": { "children": [a, b] } }
在上一节的示例中,我们知道子节点有可能是一个常量(字符串或者数字类型),所以我们在这里不考虑其他类型,如果子节点是文本节点的话,为其赋值一个特殊type值:TEXT_ELEMENT,接下来继续改造我们的createElement函数。
// react/createElement.js
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函数就完成了,编写一个例子看看返回的对象长什么样子:
// main.js
const element = createElement(
"h1",
{ id: "title" },
"hello world",
createElement("a", { href: "https://xxx.com" }, "yyy")
);
output =>
{
type: 'h1',
props: {
id: "title",
children: [
{type: 'TEXT_ELEMENT', props: {nodeValue: 'hello world', children: []}},
{type: 'a', props: {href: "https://xxx.com", children: [
{type: 'TEXT_ELEMENT', props: {nodeValue: 'yyy', children: []}}
]}}
]
}
}
大家可以多观察下返回的对象结构,最好做到熟记于心。
The render Function
第一节我们简单模拟了jsx到虚拟DOM的转换,如果想详细了解React源码中怎么处理的,可以参考jsx如何转成虚拟dom,那么继续往下进行,编写render函数把虚拟DOM渲染成真实DOM节点显示到页面上:
目前我们只关心新增节点,后面会介绍更新和删除DOM。
我们首先使用元素类型创建DOM节点,然后将节点插入到根节点中,使用递归处理子节点
// react/render.js function render(element, container) { const dom = document.createElement(element.type); element.props.children.forEach((child) => render(child, dom)); container.appendChild(dom); } export default render; // react/index.js import createElement from "./createElement"; import render from "./render"; export { createElement, render };可以注意到,我们有些节点类型是TEXT_ELEMENT,所以这种类型我们要特殊处理下:
// react/render.js - const dom = document.createElement(element.type); + const dom = + element.type === "TEXT_ELEMENT" + ? document.createTextNode("") + : document.createElement(element.type);示例:
// main.js import { createElement, render } from "./react"; const element = createElement( "h1", { id: "title" }, "hello world", createElement("a", { href: "https://xxx.com" }, "yyy") ); const container = document.getElementById("root"); render(element, container);
运行项目,可以看到页面虽然什么都没有,但是在chrome devtools Elements面板中,可以看到h1标签以及a标签都已经被渲染出来并且挂载到根节点中了:
接下来我们需要为各个节点添加其属性(props):
// react/render.js + Object.keys(element.props) + .filter((key) => key !== "children") + .forEach((name) => (dom[name] = element.props[name]));
观察页面:
至此,初级版render函数也成功完成了,render函数完整版:
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);
}
总结
综上我们已经基本完成了createElement函数以及render函数,可是仔细想我们的render函数是存在一些问题的,由于递归渲染子节点不会中断,那么如果我们的DOM树过于庞大,想一想是不是就会一直占用我们浏览器的主线程,会影响一些其他例如输入输出事件等等。
这些问题将会在后续文章讲解如何解决,希望大家不要操之过急,先把这篇文章中基本的createElement函数,及其返回的对象格式,以及简单版本render函数理解透彻,才能更快的往下进行。