我们将从头一步一步来开始重写React,遵循真正的React代码的架构,但没有优化和非必要的功能。本次博客基于16.8版本使用hooks:
- createElement函数
- render渲染函数
- 并发模式
- Fibers
- 渲染和提交阶段
- 调和Reconciliation
- 函数组件
- Hooks
首先,先回顾一下基本概念:我们先定义了一个react元素,接着获取节点,最后渲染到容器中:
const element = <h1 title="foo">Hello</h1>
const container = document.getElementById("root")
ReactDOM.render(element, container)
让我们用javascript来代替react这种写法:首先我们使用JSX语法去定义一个元素,JSX可以被Babel等构建工具转换为JS,只需要将标签名、属性和子元素作为参数传递即可。
const element = <h1 title="foo">Hello</h1>
使用React.createElement根据指定的第一个参数去创建一个react元素
const element = React.createElement(
"h1",
{ title: "foo" },
"Hello"
)
生成的元素长这个样子:是一个拥有两个属性的对象
- type 为字符串类型,指定我们要创建DOM节点的类型,当你想创建一个HTML元素时,它就是咱们document.createElement的tagName,它也可以是一个函数。【】
- props 为对象类型,它具有所有来自JSX属性的键和值,它还有一个特殊的属性:children,children在这里是一个字符串,但它通常是数组
const element = {
type: "h1",
props: {
title: "foo",
children: "Hello",
},
}
接着,我们需要替换的另一段React代码是对ReactDOM.render的调用。render是React改变DOM的地方,所以让我们自己来做更新:
ReactDOM.render(element, container)
首先我们创建一个node节点,添加一个标题title属性,接着为子节点创建一个text文本节点,也添加一个值nodeValue属性:
const element = {
type: "h1",
props: {
title: "foo",
children: "Hello",
},
}
const node = document.createElement(element.type)
node["title"] = element.props.title
const text = document.createTextNode("")
text["nodeValue"] = element.props.children
最后,我们将text节点添加到node节点,node节点添加到容器root里:
const container = document.getElementById("root")
node.appendChild(text)
container.appendChild(node)
现在我们的代码和之前的相同没有使用React。
const element = {
type: "h1",
props: {
title: "foo",
children: "Hello",
},
}
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)
1.createElement函数
我们将编写我们自己的createElement开始,让我们把JSX转换为JS,这样我们就可以看到createElement的调用。比如以这个为例:
const element = (
<div id="foo">
<a>bar</a>
<b />
</div>
)
const container = document.getElementById("root")
ReactDOM.render(element, container)
JSX可以被Babel等构建工具转换为JS:
const element = React.createElement(
"div",
{ id: "foo" },
React.createElement("a", null, "bar"),
React.createElement("b")
)
接下来我们去创建createElement函数,形参为type、props、children,对props使用扩展运算符将数组或者对象转为用逗号分隔的参数序列,对children使用rest剩余参数将一个不定数量的参数表示为一个数组。
function createElement(type, props, ...children) {
return {
type,
props: {
...props,
children,
},
}
}
比如: 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数组也可以包含字符串或者数字,因此,我们把所有不是对象的包在自己的元素里,并为它们创建一个特殊的标签类型:TEXT_ELEMENT
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: [],
},
}
}
我们仍然使用React的createElement,为了取代它,我们重新自定义名为Didact,但我们仍然想在这里使用JSX。我们如何告诉babel使用Didact的createElement而不是React的?
const element = React.createElement(
"div",
{ id: "foo" },
React.createElement("a", null, "bar"),
React.createElement("b")
)
const Didact = {
createElement,
}
const element = Didact.createElement(
"div",
{ id: "foo" },
Didact.createElement("a", null, "bar"),
Didact.createElement("b")
)
如果我们有一个像这样的注释,当babel转置JSX时,它将使用我们定义的函数。
/** @jsx Didact.createElement */
const element = (
<div id="foo">
<a>bar</a>
<b />
</div>
)
2. render渲染函数
接下来,我们将编写我们的ReactDOM.render函数,我们只关心在DOM中添加东西,以后会处理更新和删除。
function render(element, container) {
// TODO create dom nodes
}
const Didact = {
createElement,
render,
}
我们首先使用元素类型创建DOM节点,然后将新节点追加到容器中。
function render(element, container) {
const dom = document.createElement(element.type)
container.appendChild(dom)
}
我们递归地对每个child进行同样的处理:
function render(element, container) {
const dom = document.createElement(element.type)
element.props.children.forEach(child =>
render(child, dom)
)
container.appendChild(dom)
}
我们还需要处理文本元素,如果元素类型是TEXT_ELEMENT,我们就创建一个文本节点,而不是普通节点。
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分配给节点。
const isProperty = key => key !== "children"
Object.keys(element.props)
.filter(isProperty)
.forEach(name => {
dom[name] = element.props[name]
})
我们现在有了一个可以将JSX渲染到DOM的库啦!
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: [],
},
}
}
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)
}
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)
可以通过codesandbox来练习练习: