React原理学习–Build your own React
这是一篇基于Rodrigo Pombo的博客Build your own React 简要了解React运作原理的学习笔记,React16.8版本。这篇笔记只是为了方便之后复习所作,整体基本是翻译原文,原文中代码会跟随设计思路渐近切换和高亮,有更好的交互体验,强烈建议看下原文。前端小小白,有问题请大家指正!!
Step 0 :了解React、JSX和DOM的运作关系
首先忽略React的一些相关细节代码,从最简单的以下3行代码切入:
//生成一个h1节点添加到root节点下
const element = <h1 title="foo">Hello</h1>
const container = document.getElementById("root")
ReactDOM.render(element, container)
JSX一种JavaScript语法扩展,在React中可以方便地用来显式定义UI。本质上,JSX为我们提供了创建React元素方法(React.createElement(component, props, ...children)
)的语法糖。
首先第一行用JSX语法生成了一个element(为了便于区分,React构建的元素称为element,DOM 元素称为node),经过Babel编译后变为JS。编译过程就是将JSX表达式中的tag name,props,children等作为参数调用React.creactElement函数生成React元素。
//JSX语法编译过程
const element = <h1 title="foo">Hello</h1>
|
|
V
const element = React.createElement(
"h1", //tag name
{
title: "foo" }, //props
"Hello" //children
)
//最终函数生成的对象
const element = {
type: "h1",
props: {
title: "foo",
children: "Hello",
},
}
最终生成的React element就是一个包含两个属性type,props的对象(还包含其他属性,但暂时只关心这两个属性)。其中type可以是一个string,对应DOM node名称以及我们使用传统js方法document.createElement时传入的tagName。type也可以是一个function,但到Step VIII再解释。
props是一个对象属性,包含了JSX语法传入的所有属性的键值对以及children。在这个例子中children是一个字符串“Hello”,但通常是一个array,里面包含其他的React Elements,由于这样的嵌套关系每个React element也是一棵树。
除了第一行的JSX语法,第3行也是React代码,render的过程是React改变DOM的过程,它的编译过程简化如下:
ReactDOM.render(element, container)
|
|
V
const node = document.createElement(element.type) // 根据第一行中生成的element对象中的type属性创建DOM node,'h1'
node["title"] = element.props.title // 'foo' 绑定各个属性
//创建node的children
//这里使用creatTextNode的方式而不是innerText的方式,保证我们接下来对其他类型的孩子元素也可以用相同的方式创建
const text = document.createTextNode("")
text["nodeValue"] = element.props.children
//将构建的DOM nodes依次组装起来添加到定义的container中
node.appendChild(text)
container.appendChild(node)
到这里我们将所有React相关的语句都编译成了纯JS的操作。
Step I: creatElement实现
在前1步中我们了解了JSX语句编译过程中调用creatElement函数生成了一个包含type和props属性的JS对象。实现creatElement就是构建这个对象的过程。
const element = (
<div id="foo">
<a>bar</a>
<b />
</div>
)
|
|
V
const element = React.createElement(
"div",
{
id: "foo" },
React.createElement("a", null, "bar"),
React.createElement("b")
)
function createElement(type, props, ...children) {
return {
type,
props: {
...props,
children,
},
}
}
用扩展操作符…对props进行扩展,用rest运算符收集children元素,这样element元素中props属性的children属性就能对应一个array。
element的children可以是element或者string或者number,在上面的代码中我们只包含了element的情况,还需要为字符串和数字类型的children创建一种生成方法。
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中并不会包装原始值或在没用children的情况下创建空数组,但在此篇教程中为了简化实现,便于理解原文做了上述功能。接下来我们将用我们自己实现的类React功能的库Didact进行讲解和进一步实现。
const Didact = {
createElement,
}
const element = Didact.createElement(
"div",
{
id: "foo" },
Didact.createElement("a", null, "bar"),
Didact.createElement("b")
)
Step II: render初步实现
这一步中将实现我们自己的ReactDOM.render函数,这里只关注DOM添加的操作,更新和删除的步骤下面的步骤中再介绍。
render函数中,用传进的element.type属性创建一个DOM node并添加这个新建节点到container中,对每个孩子元素将递归调用这个步骤。
function render(element, container) {
//根据emlement.type创建DOM元素
const dom =
element.type == "TEXT_ELEMENT"
? document.createTextNode("")
: document.createElement(element.type)
//props赋值给新建的DOM node
const isProperty = key => key !== "children"
Object.keys(element.props)
.filter(isProperty)
.forEach(name => {
dom[name] = element.props[name]
})
//对element的每个children递归调用render生成DOM元素并组装成树
element.props.children.forEach(child =>
render(child, dom)
)
container.appendChild(dom)
}
Step III:unit式render
在编写更多功能前,我们还需要对上一步中的render进行完善,因为render中children是递归生成的,我们无法中断这个过程,只能等到整棵树构建完成。
如果这棵树很大,那么它的构建将阻断主线程较长时间,而当主线程中有诸如处理用户输入,保证动画平滑等需要较高优先级的操作时这样的等待是不利的。
所以我们将这整个render过程分成许多小单元(unit),每个小单元执行完如果主线程有操作需要执行,则将会允许中断渲染。
let nextUnitOfWork = null
function workLoop(deadline) {
let shouldYield = false
//render整个过程被分为许多unit,每执行完一个unit判断是否到允许中断的时间
//当render完毕(没有unit)或到系统中断时间跳出循环
while (nextUnitOfWork && !shouldYield) {
//操作当前unit并返回下一个work unit
nextUnitOfWork = performUnitOfWork(
nextUnitOfWork
)
shouldYield = deadline.timeRemaining() < 1
}
//control交给浏览器主线程,实现浏览器主线程同render操作间的交替循环
requestIdleCallback(workLoop)
}
//可以看作是一个没有明确时延的setTimeout,当主线程空闲时它将会执行回调函数
requestIdleCallback