本篇文章参考以下博文
文章目录
前言
本篇文章带大家一起认识一下 React 的实现原理,然后实现一个小型 React 库。
如果是刚刚接触 React 的同学,不建议直接阅读,文中很多专业名词需要在会使用的基础上再加以理解,如果你对于 creatElement,render,hook,jsx 这些名词运用很熟练,但是不明白其中的实现原理,那么这篇文章正适合你。
准备:Review
我们平常在使用 React 的时候,需要最少三行代码,如下:
- 首先定义一个 React 元素。
- 其次获取一个 DOM 节点作为容器。
- 最后把 React 元素渲染到容器中。
const element = <h1 title="foo">Hello</h1>
const container = document.getElementById("root")
ReactDOM.render(element, container)
接下来我们要做的就是用 JS 来替换掉 React 相关的代码。
上面第一行代码定义的元素属于 JSX ,这是 React 为了编码可视化定义的一种规则,不属于 JavaScript 代码,所以第一步我们就需要把它替换了。
我们可以用类似 Babel 的构建工具转化 JSX ,规则很简单:我们可以调用 creatElement 来替换标记的代码,然后把 标签名 , props , chlidren 作为参数。效果如下:
const element = React.createElement(
"h1",
{ title: "foo" },
"Hello"
)
React.createElement 会用这些参数创建一个对象,除了一些验证,就没别人什么特殊功能了,所以简化之后的替换结果应该如下:
const element = {
type: "h1",
props: {
title: "foo",
children: "Hello",
},
}
上面对象有两个属性(其实还有更多,但是我们只需要关心这两个就够了)
type 对应一个字符串,指定要创建的 DOM 节点类型,或者 type 也可以是一个函数,这一点我们放到步骤七来解释。
props 是一个对象,里面包含了 JSX 的一些属性,其中有一个特殊的属性 children 。
children 在这里是一个字符串,但是在元素比较多的页面里,chlidren 一般都是一个包含多个元素的数组。 chlidren 里面还有 children 这样就形成了一个元素树。
接下来需要替换的就是第三行代码。
ReactDOM.render(element, container)
render 是 React 修改 DOM 的地方,接下来更新 DOM 的工作需要我们自己完成。
首先我们需要用刚才的 element 创建一个 node * 节点,然后把 props 里面的所有元素分配给这个节点。
const node = document.createElement(element.type)
node["title"] = element.props.title
(*为了避免混淆,我们用 element 代表 React 元素, 用 Node 代表 DOM 元素)
下面我们创建子节点,样例中子节点是字符串,所以我们创建一个文本节点 textNode 。
const text = document.createTextNode("")
text["nodeValue"] = element.props.children
这里用 textNode 而不是 innerText 是为了以后让所有元素都统一。
注意我们还需要设置一个 nodeValue 属性,类似上面 h1 的 title 属性。
最后我们把 textNode 添加到 h1 中,再把 h1 添加到 container 中。
const container = document.getElementById("root")
...
node.appendChild(text)
container.appendChild(node)
完整的代码如下:
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)
我们得到一个没有 React 的 app 。
第一步:creatElement Function
接下来我们就该一一实现上面的 React 函数了。首先是 creatElement
从上一步我们知道, creatElement 返回的是一个对象,里面有 type 和 props ,那我们自己的 creatElement 只需要把这个对象创建出来就行。举一个稍微复杂点的 app 。
const element = (
<div id="foo">
<a>bar</a>
<b />
</div>
)
const container = document.getElementById("root")
ReactDOM.render(element, container)
上面可以看到, foo 有两个孩子节点,对应成 element 的形式如下:
const element = React.createElement(
"div",
{ id: "foo" },
React.createElement("a", null, "bar"),
React.createElement("b")
)
请注意,下面的两个 … 是不同的含义,第二个 … 是扩展运算符,目的是把 props 里面的属性分别展开来。第一个是 rest 参数,是把除去 type , props 以外的后面所有参数都合并成一个数组,这样可以保证 children 一定是数组。
function createElement(type, props, ...children) {
return {
type,
props: {
...props,
children,
},
}
}
//For example
createElement("div") returns:
{
"type": "div",
"props": { "children": [] }
}
createElement("div", null, a) returns:
{
"type": "div",
"props": { "children": [a] }
}
and createElement("div", null, a, b) returns:
{
"type": "div",
"props": { "children": [a, b] }
}
chlidren 数组可能包含原始值,比如数组,字符串之类的。因此,我们将所有不是对象的元素封装在内部,并创建一个特殊类型: 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 并不会保留原始值也不会在没有子节点的时候创建空数组,我们这么做是为了简化代码,此代码库不注重性能,只求能方便理解。
const element = React.createElement(
"div",
{ id: "foo" },
React.createElement("a", null, "bar"),
React.createElement("b")
)
理解了上面代码的含义之后,我们需要给 React 的 creatElement 换个新名字,名称保持相似,暗示是从 react 演变过来的,起名 Didact 。
const Didact = {
createElement,
}
const element = Didact.createElement(
"div",
{ id: "foo" },
Didact.createElement("a", null, "bar"),
Didact.createElement("b")
)
我们还是希望使用 JSX 语法,这个可以方便我们开发代码,毕竟上面那种形式不是我们熟悉 HTML ,不方便阅读。
接下来的问题就是,应该怎么告诉 Babel 我们用的是 Didact ,而不是 React ?
/** @jsx Didact.createElement */
const element = (
<div id="foo">
<a>bar</a>
<b />
</div>
)
上面注释内容可以告诉 Babel 用 JSX 的语法,转化 Didact 。
第二步:The render Function
接下来该写我们迷你版本的 ReactDOM.render function 了。
function render(element, container) {
// TODO create dom nodes
}
const Didact = {
createElement,
render,
}
Didact.render(element, container)
目前我们先只处理添加元素到 DOM ,后面再处理更新和删除。
function render(element, container) {
const dom = document.createElement(element.type)
element.props.children.forEach(child =>
render(child, dom)
)
container.appendChild(dom)
}
上面代码我们首先根据 type 创建要添加的元素,然后递归遍历 children 节点,最后把初始创建的 dom 添加到容器中。
我们还需要处理文本元素,如果元素类型是 TEXT_ELEMENT 就创建文本节点而不是常规节点。修改后 dom 如下:
const dom =
element.type == "TEXT_ELEMENT"
? document.createTextNode("")
: document.createElement(element.type)
最后一件事我们需要添加的就是,将 props 分配给节点。
const isProperty = key => key !== "children"
Object.keys(element.props)
.filter(isProperty)
.forEach(name => {
dom[name] = element.props[name]
})
完整的代码如下:
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)
第三步:并发模式
上一步我们写的代码是 react15 的版本,该版本使用递归来遍历子节点,一旦开始了,就不能停止,必须完成所有元素树。
如果元素树很大,就可能会阻塞主线程很长一段时间。如果浏览器这个时候需要执行诸如处理用户输入或平滑动画等高优先级的工作,则它必须等待渲染完成。
直接问题就体现在页面会出现卡顿,所以在 react16 进行了一次重构,由之前的递归调用变为可中断的工作单元。
每个工作单元完成后,如果需要执行其他任何操作,我们将让浏览器中断渲染。实现如下:
let nextUnitOfWork = null
function workLoop(deadline) {
let shouldYield = false
while (nextUnitOfWork && !shouldYield) {
nextUnitOfWork = performUnitOfWork(
nextUnitOfWork
)
shouldYield = deadline.timeRemaining() < 1
}
requestIdleCallback(workLoop)
}
requestIdleCallback(workLoop)
function performUnitOfWork(nextUnitOfWork) {
// TODO
}
上面代码我们用 requestIdleCallback 来进行循环,这是 window 对象提供的一个 API ,可以把他想象成一个定时器,这个定时器有点特殊,不是我们去告诉他何时执行回调,而是浏览器,浏览器会在主线程空闲的时候运行回调。
React 现在已经不再使用 requestIdleCallback 了,现在使用的是 scheduler package 。
对于我们搭建 Didact 来说概念上是一样的。
requestIdleCallback 给我们提供了一个结束时间参数 deadline 。我们可以使用它来检查浏览器主线剩余的空闲时间还有多少。
截止到 React 最新的版本17.0.1, Concurrent Mode (并发模式)还是实验性质的,并不稳定。
循环的稳定版本更像下面这样。
while (nextUnitOfWork) {
nextUnitOfWork = performUnitOfWork(
nextUnitOfWork
)
}
要开始使用循环,我们需要设置第一个工作单元,然后编写一个 performUnitOfWork 不仅要执行第一个工作单元,还需要返回下一个工作单元函数。
第四步:Fibers
为了能组织工作单元,我们需要一种数据结构: Fiber Tree 。
每个元素都会有一个 fiber ,每个 fiber 都会作为一个工作单元。这个说有点抽象,举个例子:
我们想要渲染一个元素树。
Didact.render(
<div>
<h1>
<p />
<a />
</h1>
<h2 />
</div>,
container
)
在 render 中,我们需要先设立一个 root fiber ,然后把它赋值给 nextUnitOfWork ,剩下的事情交给 performUnitOfWork 函数,对于每一个 fiber 我们需要做三件事情:
- 将元素添加到 DOM 上。
- 为元素的子节点创建 fiber
- 指定下个工作单元
针对上面的例子,创建出来的 fiber tree 如下:
构建 fiber 的的目的之一就是方便查找下一个工作单元,这就是为什么每一个 fiber 都会链接父节点,子节点和兄弟节点。
当一个 fiber 节点的工作完成之后,如果有子节点,那么子节点的 fiber 就会成为下一工作单元。看上面的例子,当 div fiber 的任务完成后, h1 fiber 就是下一个工作单元。
如果当前 fiber 没有子节点,那么我把 sibling (兄弟)节点作为下一工作单元,对应上面当 p 执行完之后,把 a 最为下一个工作单元。
如果一个 fiber 没有 children 也没有 sibling 那就设置“叔叔”节点为下一工作单元。如果没有“叔叔”就一直往上找,直到找到根节点。
执行到根节点后,就表示我们已经完成了 render 的所有工作。
逻辑梳理清楚了,接下来我们转化成代码。
首先我们需要删除以下 render 中的代码,我们要重构它。
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)
}
let nextUnitOfWork = null
我们将创建 DOM 节点的部分保留在其自身功能中,后面使用。
function createDom(fiber) {
const dom =
fiber.type == "TEXT_ELEMENT"
? document.createTextNode("")
: document.createElement(fiber.type)
const isProperty = key => key !== "children"
Object.keys(fiber.props)
.filter(isProperty)
.forEach(name => {
dom[name] = fiber.props[name]
})
return dom
}
function render(element, container) {
// TODO set next unit of work
}
let nextUnitOfWork = null
在 render 函数中,我们设置 nextUnitOfWork 为 fiber tree 的根。
function render(element, container) {
nextUnitOfWork = {
dom: container,
props: {
children: [element],
},
}
}
然后,当浏览器准备就绪时,它会调用 workLoop ,我们就开始执行单元工作。
function workLoop(deadline) {
let shouldYield = false
while (nextUnitOfWork && !shouldYield) {
nextUnitOfWork = performUnitOfWork(
nextUnitOfWork
)
shouldYield = deadline.timeRemaining() < 1
}
requestIdleCallback(workLoop)
}
requestIdleCallback(workLoop)
function performUnitOfWork(fiber) {
// TODO add dom node
// TODO create new fibers
// TODO return next unit of work
}
首先创建一个新 node 节点在 DOM 上,然后通过 fiber.dom 来跟踪这个节点。
function performUnitOfWork(fiber) {
if (!fiber.dom) {
fiber.dom = createDom(fiber)
}
if (fiber.parent) {
fiber.parent.dom.appendChild(fiber.dom)
}
// TODO create new fibers
// TODO return next unit of work
}
然后为每一个子节点创建新的 fiber 。
function performUnitOfWork(fiber) {
// TODO add dom node
const elements = fiber.props.children
let index = 0
let prevSibling = null
while (index < elements.length) {
const element = elements[index]
const newFiber = {
type: element.type,
props: element.props,
parent: fiber,
dom: null,
}
}
// TODO return next unit of work
}
然后添加到 fiber tree 中,并设置成 子节点或兄弟节点,具体取决于它是否是第一个子节点。
function performUnitOfWork(fiber) {
// TODO add dom node
/**
*上一步内容暂时注释
*const elements = fiber.props.children
*let index = 0
*let prevSibling = null
*
*while (index < elements.length) {
* const element = elements[index]
*
* const newFiber = {
* type: element.type,
* props: element.props,
* parent: fiber,
* dom: null,
* }
*/
if (index === 0) {
fiber.child = newFiber
} else {
prevSibling.sibling = newFiber
}
prevSibling = newFiber
index++
//}
// TODO return next unit of work
}
最后,我们搜索下一个工作单元。我们首先尝试设置子节点,然后设置兄弟节点,最后设置叔叔节点,依次类推。
function performUnitOfWork(fiber) {
// TODO add dom node
// TODO create new fibers
if (fiber.child) {
return fiber.child
}
let nextFiber = fiber
while (nextFiber) {
if (nextFiber.sibling) {
return nextFiber.sibling
}
nextFiber = nextFiber.parent
}
}
这样我们就完成了整个的 performUnitOfWork 。
function performUnitOfWork(fiber) {
if (!fiber.dom) {
fiber.dom = createDom(fiber)
}
if (fiber.parent) {
fiber.parent.dom.appendChild(fiber.dom)
}
const elements = fiber.props.children
let index = 0
let prevSibling = null
while (index < elements.length) {
const element = elements[index]
const newFiber = {
type: element.type,
props: element.props,
parent: fiber,
dom: null,
}
if (index === 0) {
fiber.child = newFiber
} else {
prevSibling.sibling = newFiber
}
prevSibling = newFiber
index++
}
if (fiber.child) {
return fiber.child
}
let nextFiber = fiber
while (nextFiber) {
if (nextFiber.sibling) {
return nextFiber.sibling
}
nextFiber = nextFiber.parent
}
}
第五步:渲染和提交阶段
这里还有一个问题,每次在执行完工作单元后,都会往 DOM 里添加一个新的 node 节点,在渲染结束前,浏览器可能会中断我们的工作,这会导致用户看到的是不完整的 UI ,我们不想让这种情况发生。
所以对于上面的代码,我们需要删除添加 node 节点的操作。
function performUnitOfWork(fiber) {
if (!fiber.dom) {
fiber.dom = createDom(fiber)
}
//删除下方注释代码
//if (fiber.parent) {
// fiber.parent.dom.appendChild(fiber.dom)
//}
const elements = fiber.props.children
let index = 0
let prevSibling = null
}
相反,我们会跟踪 fiber tree 的根节点,这个称之为 wipRoot (the work in progress root)进行中的根。
render 函数修改如下:(未改动部分暂时注释)
function render(element, container) {
wipRoot = {
// dom: container,
// props: {
// children: [element],
// },
// }
nextUnitOfWork = wipRoot
}
//let nextUnitOfWork = null
let wipRoot = null
一旦完成了所有工作,当没有下一工作单元的时候,我们就知道,现在可以更新 DOM 了,然后就将整个 fiber tree 交给 DOM ,进行渲染。
function commitRoot() {
// TODO add nodes to dom
}
//function render(element, container) {
// wipRoot = {
// dom: container,
// props: {
// children: [element],
// },
// }
// nextUnitOfWork = wipRoot
//}
//let nextUnitOfWork = null
//let wipRoot = null
function workLoop(deadline) {
// let shouldYield = false
// while (nextUnitOfWork && !shouldYield) {
// nextUnitOfWork = performUnitOfWork(
// nextUnitOfWork
// )
// shouldYield = deadline.timeRemaining() < 1
// }
if (!nextUnitOfWork && wipRoot) {
commitRoot()
}
// requestIdleCallback(workLoop)
}
第六步:Reconciliation
到目前为止,我们只知道如何往 DOM 里添加,揭晓来还需要研究下怎么更新和删除。
我们需要对比一下从 render 提交过来的 fiber tree 和 DOM 中最后一次提交的 Fiber Tree 有什么不同,
function commitRoot() {
commitWork(wipRoot.child)
wipRoot = null
}
function commitWork(fiber) {
if (!fiber) {
return
}
const domParent = fiber.parent.dom
domParent.appendChild(fiber.dom)
commitWork(fiber.child)
commitWork(fiber.sibling)
}
所以我们需要一个新的变量来保存我们最后一次提交的 fiber tree ,称之为 currentRoot 。(老规矩,注释的代码是没有修改的)
function commitRoot() {
// commitWork(wipRoot.child)
currentRoot = wipRoot
// wipRoot = null
}
//function commitWork(fiber) {
// if (!fiber) {
// return
// }
// const domParent = fiber.parent.dom
// domParent.appendChild(fiber.dom)
// commitWork(fiber.child)
// commitWork(fiber.sibling)
//}
function render(element, container) {
wipRoot = {
// dom: container,
// props: {
// children: [element],
// },
alternate: currentRoot,
}
// nextUnitOfWork = wipRoot
}
//let nextUnitOfWork = null
let currentRoot = null
//let wipRoot = null
同时我们还增加了 alternate 属性给每个 fiber ,这个 alternate 用来保存旧的 fiber , 旧 fiber 是我们在上一个提交阶段,提交给 DOM 的 fiber 。
function performUnitOfWork(fiber) {
if (!fiber.dom) {
fiber.dom = createDom(fiber)
}
const elements = fiber.props.children
let index = 0
let prevSibling = null
while (index < elements.length) {
const element = elements[index]
const newFiber = {
type: element.type,
props: element.props,
parent: fiber,
dom: null,
}
if (index === 0) {
fiber.child = newFiber
} else {
prevSibling.sibling = newFiber
}
prevSibling = newFiber
index++
}
if (fiber.child) {
return fiber.child
}
let nextFiber = fiber
while (nextFiber) {
if (nextFiber.sibling) {
return nextFiber.sibling
}
nextFiber = nextFiber.parent
}
}
现在我们可以提取 performUnitOfWork 中的代码,然后创建新的 fibers 到新的 reconcileChildren 里去 。(注释的为不变的代码)
function performUnitOfWork(fiber) {
// if (!fiber.dom) {
// fiber.dom = createDom(fiber)
// }
const elements = fiber.props.children
reconcileChildren(fiber, elements)
// if (fiber.child) {
// return fiber.child
// }
// let nextFiber = fiber
// while (nextFiber) {
// if (nextFiber.sibling) {
// return nextFiber.sibling
// }
// nextFiber = nextFiber.parent
// }
}
function reconcileChildren(wipFiber, elements) {
// let index = 0
// let prevSibling = null
// while (index < elements.length) {
// const element = elements[index]
在 reconcileChildren 里面我们会将新旧 fiber 进行协调,找出新旧变化
function reconcileChildren(wipFiber, elements) {
// let index = 0
// let prevSibling = null
// while (index < elements.length) {
// const element = elements[index]
// const newFiber = {
// type: element.type,
// props: element.props,
// parent: wipFiber,
// dom: null,
// }
// if (index === 0) {
// wipFiber.child = newFiber
// } else {
// prevSibling.sibling = newFiber
// }
// prevSibling = newFiber
// index++
// }
}
我们同时遍历旧的fibre( wipFiber.alternate )的子级和要调和的元素数组。
如果我们忽略了同时遍历一个数组和一个链表所需的所有样板文件,那么剩下的就是 while 中最重要的内容: oldFiber 和 element 。
该 element 是我们要呈现给 DOM 的东西, oldFiber 是我们上次渲染的。
function reconcileChildren(wipFiber, elements) {
let index = 0
let oldFiber =
wipFiber.alternate && wipFiber.alternate.child
// let prevSibling = null
while (
index < elements.length ||
oldFiber != null
) {
const element = elements[index]
// let newFiber = null
// TODO compare oldFiber to element
//...
接下来就是比较 element 和 oldFiber 看看是否对 DOM 进行了更改。
比较这两个需要用到以下类型:
- 如果 old fiber 和 element 具有相同的 type ,我们可以保留 DOM 节点并仅使用新的 props 进行更新。
- 如果 type 不同并且有一个新元素,则意味着我们需要创建一个新的 DOM 节点。
- 如果 type 不同并且有 old fiber ,我们需要删除 old fiber 。
React 在这里使用了 keys 这样可以更好地协调,举个例子,它会检测子元素何时在元素数组中改变位置。
const sameType =
oldFiber &&
element &&
element.type == oldFiber.type
if (sameType) {
// TODO update the node
}
if (element && !sameType) {
// TODO add this node
}
if (oldFiber && !sameType) {
// TODO delete the oldFiber's node
}
当 old fiber 和 element 具有相同的 type 时,我们创建一个 new fiber ,以使 DOM 节点与 old fiber 保持一致,而 props 与 old fiber 的保持一致。
const sameType =
oldFiber &&
element &&
element.type == oldFiber.type
if (sameType) {
newFiber = {
type: oldFiber.type,
props: element.props,
dom: oldFiber.dom,
parent: wipFiber,
alternate: oldFiber,
effectTag: "UPDATE",
}
}
上面还增加了一个新属性 effectTag ,这个属性在后面的 commit 阶段会使用。
对于元素需要新的 DOM 情况,我们用 effectTag: “PLACEMENT” 来标记。
if (element && !sameType) {
newFiber = {
type: element.type,
props: element.props,
dom: null,
parent: wipFiber,
alternate: null,
effectTag: "PLACEMENT",
}
}
对于需要删除的节点,因为没有 new fiber ,所以我们给 old fiber 添加 effectTag: “DELETION” 。
if (oldFiber && !sameType) {
oldFiber.effectTag = "DELETION"
deletions.push(oldFiber)
}
这要就需要我们新增一个数组来删除需要跟踪的 fiber (注释的内容为未改变部分)。
function render(element, container) {
// wipRoot = {
// dom: container,
// props: {
// children: [element],
// },
// alternate: currentRoot,
// }
deletions = []
// nextUnitOfWork = wipRoot
}
//let nextUnitOfWork = null
//let currentRoot = null
//wipRoot = null
let deletions = null
当我们提交变化到 DOM 的时候,还需要用到这个数组。
function commitRoot() {
deletions.forEach(commitWork)
// commitWork(wipRoot.child)
// currentRoot = wipRoot
// wipRoot = null
}
接下来我们修改一下 commitWork 函数来处理新的 effectTag 。
function commitWork(fiber) {
if (!fiber) {
return
}
const domParent = fiber.parent.dom
domParent.appendChild(fiber.dom)
commitWork(fiber.child)
commitWork(fiber.sibling)
}
如果标签为 PLACEMENT ,则与之前相同,将 DOM 节点附加到 parent fiber 节点上
function commitWork(fiber) {
// if (!fiber) {
// return
// }
// const domParent = fiber.parent.dom
if (
fiber.effectTag === "PLACEMENT" &&
fiber.dom != null
) {
domParent.appendChild(fiber.dom)
}
// commitWork(fiber.child)
// commitWork(fiber.sibling)
}
如果是删除,那就删除 children 。
//if (
// fiber.effectTag === "PLACEMENT" &&
// fiber.dom != null
// ) {
// domParent.appendChild(fiber.dom)
} else if (fiber.effectTag === "DELETION") {
domParent.removeChild(fiber.dom)
}
如果是 UPDATE ,那就用改变后后 props 该更新 DOM 节点 。
} else if (
fiber.effectTag === "UPDATE" &&
fiber.dom != null
) {
updateDom(
fiber.dom,
fiber.alternate.props,
fiber.props
)
下面我们写一下 updateDom 函数。
function updateDom(dom, prevProps, nextProps) {
// TODO
}
这里面我们需要对于 新旧 fiber 的 props 有什么不同,删除已经消失的 props ,添加或更新新的 props 。
const isProperty = key => key !== "children"
const isNew = (prev, next) => key =>
prev[key] !== next[key]
const isGone = (prev, next) => key => !(key in next)
function updateDom(dom, prevProps, nextProps) {
// Remove old properties
Object.keys(prevProps)
.filter(isProperty)
.filter(isGone(prevProps, nextProps))
.forEach(name => {
dom[name] = ""
})
// Set new or changed properties
Object.keys(nextProps)
.filter(isProperty)
.filter(isNew(prevProps, nextProps))
.forEach(name => {
dom[name] = nextProps[name]
})
}
这里别忘了 React 对于事件的监听是以 on 开头的,所以这里要添加一种特殊的 props 。
const isEvent = key => key.startsWith("on")
const isProperty = key =>
key !== "children" && !isEvent(key)
//const isProperty = key => key !== "children"
//const isNew = (prev, next) => key =>
// prev[key] !== next[key]
如果事件处理发生了改变,那么就需要清除。
//function updateDom(dom, prevProps, nextProps) {
//Remove old or changed event listeners
Object.keys(prevProps)
.filter(isEvent)
.filter(
key =>
!(key in nextProps) ||
isNew(prevProps, nextProps)(key)
)
.forEach(name => {
const eventType = name
.toLowerCase()
.substring(2)
dom.removeEventListener(
eventType,
prevProps[name]
)
})
最后,我们在 updateDom 函数中添加新的监听事件。
// Add event listeners
Object.keys(nextProps)
.filter(isEvent)
.filter(isNew(prevProps, nextProps))
.forEach(name => {
const eventType = name
.toLowerCase()
.substring(2)
dom.addEventListener(
eventType,
nextProps[name]
)
})
第七步:函数组件
接下来我们需要做的是增加函数组件。
先修改一下我们刚才的样例,调用一个简单的函数组件,返回一个 h1 元素。
/** @jsx Didact.createElement */
function App(props) {
return <h1>Hi {props.name}</h1>
}
const element = <App name="foo" />
const container = document.getElementById("root")
Didact.render(element, container)
注意:如果把 jsx 转化成 js ,那么会是下面这种形式。
function App(props) {
return Didact.createElement(
"h1",
null,
"Hi ",
props.name
)
}
const element = Didact.createElement(App, {
name: "foo",
})
函数组件相比于类组件有两点不同:
- 函数组件的 fiber 里面没有 DOM 节点。
- 子节点通过运行函数得到,而不是直接通过 props 。
function performUnitOfWork(fiber) {
if (!fiber.dom) {
fiber.dom = createDom(fiber)
}
const elements = fiber.props.children
reconcileChildren(fiber, elements)
// if (fiber.child) {
// return fiber.child
// }
//...
我们需要检查 fiber type 是否为函数,然后根据类型的不同交给不同的 update 函数。在 updateHostComponent 函数中,和原来保持一致。
//function performUnitOfWork(fiber) {
const isFunctionComponent =
fiber.type instanceof Function
if (isFunctionComponent) {
updateFunctionComponent(fiber)
} else {
updateHostComponent(fiber)
}
// if (fiber.child) {
// return fiber.child
// }
// let nextFiber = fiber
// while (nextFiber) {
// if (nextFiber.sibling) {
// return nextFiber.sibling
// }
// nextFiber = nextFiber.parent
// }
//}
function updateFunctionComponent(fiber) {
// TODO
}
function updateHostComponent(fiber) {
if (!fiber.dom) {
fiber.dom = createDom(fiber)
}
reconcileChildren(fiber, fiber.props.children)
}
在 updateFunctionComponent 函数里面,我们需要调用函数来获取子节点。对照样例,下面的代码对应 fiber.type 的是 App Function 运行后,返回 h1 元素。一旦有了子节点,协调器就可以开始运行,不需要对后面的流程进行更改
function updateFunctionComponent(fiber) {
const children = [fiber.type(fiber.props)]
reconcileChildren(fiber, children)
}
针对函数组件,我们还需要修改一下 commitWork 。
function commitWork(fiber) {
if (!fiber) {
return
}
const domParent = fiber.parent.dom
if (
fiber.effectTag === "PLACEMENT" &&
fiber.dom != null
) {
domParent.appendChild(fiber.dom)
} else if (
fiber.effectTag === "UPDATE" &&
fiber.dom != null
) {
updateDom(
fiber.dom,
fiber.alternate.props,
fiber.props
)
} else if (fiber.effectTag === "DELETION") {
domParent.removeChild(fiber.dom)
}
commitWork(fiber.child)
commitWork(fiber.sibling)
}
因为函数组件没有 DOM 节点,我们需要修改两处。
- 找到 DOM 节点的父节点,我们需要沿着 fiber tree 向上移动,直到找到带有 DOM 节点的 fiber
//function commitWork(fiber) {
// if (!fiber) {
// return
// }
let domParentFiber = fiber.parent
while (!domParentFiber.dom) {
domParentFiber = domParentFiber.parent
}
const domParent = domParentFiber.dom
// if (
// fiber.effectTag === "PLACEMENT" &&
// fiber.dom != null
// ) {
domParent.appendChild(fiber.dom)
// } else if (
// fiber.effectTag === "UPDATE" &&
// fiber.dom != null
// ) {
// updateDom(
// fiber.dom,
- 在删除节点时,我们还需要继续操作,直到找到带有 DOM 节点的子节点为止
//fiber.alternate.props,
// fiber.props
// )
// } else if (fiber.effectTag === "DELETION") {
commitDeletion(fiber, domParent)
// }
// commitWork(fiber.child)
// commitWork(fiber.sibling)
//}
function commitDeletion(fiber, domParent) {
if (fiber.dom) {
domParent.removeChild(fiber.dom)
} else {
commitDeletion(fiber.child, domParent)
}
}
第八步:Hooks
最后一步,有了函数组件了,我们还需要添加 state 。
/** @jsx Didact.createElement */
function App(props) {
return <h1>Hi {props.name}</h1>
}
const element = <App name="foo" />
const container = document.getElementById("root")
Didact.render(element, container)
修改一下样例,假设我们这里是一个计数器,每点击一次, state 就 +1 。
注意:我们用 Didact.useState 来设置和更改 Count 的值。
//const Didact = {
// createElement,
// render,
useState,
//}
/** @jsx Didact.createElement */
function Counter() {
const [state, setState] = Didact.useState(1)
return (
<h1 onClick={() => setState(c => c + 1)}>
Count: {state}
</h1>
)
}
const element = <Counter />
//const container = document.getElementById("root")
//Didact.render(element, container)
下面是我们调用 useState 的地方。
function updateFunctionComponent(fiber) {
// const children = [fiber.type(fiber.props)]
reconcileChildren(fiber, children)
}
function useState(initial) {
// TODO
}
首先我们需要先设置一些初始化变量,后面会用到。
let wipFiber = null
let hookIndex = null
function updateFunctionComponent(fiber) {
wipFiber = fiber
hookIndex = 0
wipFiber.hooks = []
// const children = [fiber.type(fiber.props)]
// reconcileChildren(fiber, children)
//}
//function useState(initial) {
// // TODO
//}
上面代码中我们添加了一个 hooks 数组到 fiber 中,方便在同一组件内部多次使用 useState ,然后跟踪当前 hook 的索引。
当函数组件调用 useState 的时候,检查是否有 oldHook ,使用 hookIndex 在 alternate 中进行检查。如果有 oldHook 那就把 oldHook 的 state 复制到 newHook 当中,如果没有,就初始化 state 。
然后把 newHook 添加到 fiber 中, hook 索引 +1 返回新的 state 。
function useState(initial) {
const oldHook =
wipFiber.alternate &&
wipFiber.alternate.hooks &&
wipFiber.alternate.hooks[hookIndex]
const hook = {
state: oldHook ? oldHook.state : initial,
}
wipFiber.hooks.push(hook)
hookIndex++
return [hook.state]
}
useState 还应该返回一个函数去更新 state ,所以我们定义一个 setState 函数去接收 action ,对于样例来说, action 就是让 state 加一的函数。
然后,我们执行与 render 函数中类似的操作,设置一个新的工作为下一工作单元,然后继续下一阶段的 render 。
//wipFiber.alternate.hooks &&
// wipFiber.alternate.hooks[hookIndex]
const hook = {
// state: oldHook ? oldHook.state : initial,
queue: [],
}
const setState = action => {
hook.queue.push(action)
wipRoot = {
dom: currentRoot.dom,
props: currentRoot.props,
alternate: currentRoot,
}
nextUnitOfWork = wipRoot
deletions = []
}
// wipFiber.hooks.push(hook)
// hookIndex++
return [hook.state, setState]
//}
但是我们还没有执行这个 action ,在下次渲染组件的时候,我们会从 old hook 队列中获取所有 actions ,然后把它们逐一作用于 new hook 的 state ,所以在我们返回 state 的时候,它已经更新完毕。
//const oldHook =
// wipFiber.alternate &&
// wipFiber.alternate.hooks &&
// wipFiber.alternate.hooks[hookIndex]
// const hook = {
// state: oldHook ? oldHook.state : initial,
// queue: [],
// }
const actions = oldHook ? oldHook.queue : []
actions.forEach(action => {
hook.state = action(hook.state)
})
// const setState = action => {
大功告成,我们自己建立了一个 React 库。下面是全部代码:
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 createDom(fiber) {
const dom =
fiber.type == "TEXT_ELEMENT"
? document.createTextNode("")
: document.createElement(fiber.type)
updateDom(dom, {}, fiber.props)
return dom
}
const isEvent = key => key.startsWith("on")
const isProperty = key =>
key !== "children" && !isEvent(key)
const isNew = (prev, next) => key =>
prev[key] !== next[key]
const isGone = (prev, next) => key => !(key in next)
function updateDom(dom, prevProps, nextProps) {
//Remove old or changed event listeners
Object.keys(prevProps)
.filter(isEvent)
.filter(
key =>
!(key in nextProps) ||
isNew(prevProps, nextProps)(key)
)
.forEach(name => {
const eventType = name
.toLowerCase()
.substring(2)
dom.removeEventListener(
eventType,
prevProps[name]
)
})
// Remove old properties
Object.keys(prevProps)
.filter(isProperty)
.filter(isGone(prevProps, nextProps))
.forEach(name => {
dom[name] = ""
})
// Set new or changed properties
Object.keys(nextProps)
.filter(isProperty)
.filter(isNew(prevProps, nextProps))
.forEach(name => {
dom[name] = nextProps[name]
})
// Add event listeners
Object.keys(nextProps)
.filter(isEvent)
.filter(isNew(prevProps, nextProps))
.forEach(name => {
const eventType = name
.toLowerCase()
.substring(2)
dom.addEventListener(
eventType,
nextProps[name]
)
})
}
function commitRoot() {
deletions.forEach(commitWork)
commitWork(wipRoot.child)
currentRoot = wipRoot
wipRoot = null
}
function commitWork(fiber) {
if (!fiber) {
return
}
let domParentFiber = fiber.parent
while (!domParentFiber.dom) {
domParentFiber = domParentFiber.parent
}
const domParent = domParentFiber.dom
if (
fiber.effectTag === "PLACEMENT" &&
fiber.dom != null
) {
domParent.appendChild(fiber.dom)
} else if (
fiber.effectTag === "UPDATE" &&
fiber.dom != null
) {
updateDom(
fiber.dom,
fiber.alternate.props,
fiber.props
)
} else if (fiber.effectTag === "DELETION") {
commitDeletion(fiber, domParent)
}
commitWork(fiber.child)
commitWork(fiber.sibling)
}
function commitDeletion(fiber, domParent) {
if (fiber.dom) {
domParent.removeChild(fiber.dom)
} else {
commitDeletion(fiber.child, domParent)
}
}
function render(element, container) {
wipRoot = {
dom: container,
props: {
children: [element],
},
alternate: currentRoot,
}
deletions = []
nextUnitOfWork = wipRoot
}
let nextUnitOfWork = null
let currentRoot = null
let wipRoot = null
let deletions = null
function workLoop(deadline) {
let shouldYield = false
while (nextUnitOfWork && !shouldYield) {
nextUnitOfWork = performUnitOfWork(
nextUnitOfWork
)
shouldYield = deadline.timeRemaining() < 1
}
if (!nextUnitOfWork && wipRoot) {
commitRoot()
}
requestIdleCallback(workLoop)
}
requestIdleCallback(workLoop)
function performUnitOfWork(fiber) {
const isFunctionComponent =
fiber.type instanceof Function
if (isFunctionComponent) {
updateFunctionComponent(fiber)
} else {
updateHostComponent(fiber)
}
if (fiber.child) {
return fiber.child
}
let nextFiber = fiber
while (nextFiber) {
if (nextFiber.sibling) {
return nextFiber.sibling
}
nextFiber = nextFiber.parent
}
}
let wipFiber = null
let hookIndex = null
function updateFunctionComponent(fiber) {
wipFiber = fiber
hookIndex = 0
wipFiber.hooks = []
const children = [fiber.type(fiber.props)]
reconcileChildren(fiber, children)
}
function useState(initial) {
const oldHook =
wipFiber.alternate &&
wipFiber.alternate.hooks &&
wipFiber.alternate.hooks[hookIndex]
const hook = {
state: oldHook ? oldHook.state : initial,
queue: [],
}
const actions = oldHook ? oldHook.queue : []
actions.forEach(action => {
hook.state = action(hook.state)
})
const setState = action => {
hook.queue.push(action)
wipRoot = {
dom: currentRoot.dom,
props: currentRoot.props,
alternate: currentRoot,
}
nextUnitOfWork = wipRoot
deletions = []
}
wipFiber.hooks.push(hook)
hookIndex++
return [hook.state, setState]
}
function updateHostComponent(fiber) {
if (!fiber.dom) {
fiber.dom = createDom(fiber)
}
reconcileChildren(fiber, fiber.props.children)
}
function reconcileChildren(wipFiber, elements) {
let index = 0
let oldFiber =
wipFiber.alternate && wipFiber.alternate.child
let prevSibling = null
while (
index < elements.length ||
oldFiber != null
) {
const element = elements[index]
let newFiber = null
const sameType =
oldFiber &&
element &&
element.type == oldFiber.type
if (sameType) {
newFiber = {
type: oldFiber.type,
props: element.props,
dom: oldFiber.dom,
parent: wipFiber,
alternate: oldFiber,
effectTag: "UPDATE",
}
}
if (element && !sameType) {
newFiber = {
type: element.type,
props: element.props,
dom: null,
parent: wipFiber,
alternate: null,
effectTag: "PLACEMENT",
}
}
if (oldFiber && !sameType) {
oldFiber.effectTag = "DELETION"
deletions.push(oldFiber)
}
if (oldFiber) {
oldFiber = oldFiber.sibling
}
if (index === 0) {
wipFiber.child = newFiber
} else if (element) {
prevSibling.sibling = newFiber
}
prevSibling = newFiber
index++
}
}
const Didact = {
createElement,
render,
useState,
}
/** @jsx Didact.createElement */
function Counter() {
const [state, setState] = Didact.useState(1)
return (
<h1 onClick={() => setState(c => c + 1)}>
Count: {state}
</h1>
)
}
const element = <Counter />
const container = document.getElementById("root")
Didact.render(element, container)
结语
本篇文章除了帮助大家了解 React 的工作原理外,还可以让各位更轻松地深入 React 代码库。里面的变量名称几乎和源码保持一致。另外里面的某些理解有误的地方,烦请各位前辈指点。