前言:
前端魔术师卡颂的react学习视频(1 搭建项目架构_哔哩哔哩_bilibili)中提到了Rodrigo Pombo的一篇react源码教程:Build your own React
本文档分组旨在翻译和记录这篇文章的学习心得,作为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
Review
下面是react应用创建最基础的代码;
首先定义了一个react元素,然后获取一个dom节点作为容器,最后将元素render到容器中;
js
const element = <h1 title="foo">Hello</h1>
const container = document.getElementById("root")
ReactDOM.render(element, container)
JSX代码通过babel这样的构建工具,转成JS代码;
js
// 替换前(JSX)
const element = <h1 title="foo">Hello</h1>
// 替换后(利用babel)
const element = React.createElement(
"h1", // tagname
{ title: "foo" }, // props
"Hello" // children
)
我们再次将React.createElement
函数的调用替换成输出的结果;
js
// 替换前
const element = React.createElement(
"h1", // tagname
{ title: "foo" }, // props
"Hello" // children
)
// 替换后(上面代码的输出结果)
// type是tagename,props是元素所有的属性键值对,children通常是一个包含更多元素的数组
const element = {
type: "h1",
props: {
title: "foo",
children: "Hello",
},
}
接下来,我们需要替换ReactDom.render
代码
js
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)
这样,我们就完全使用了js语法,实现了和使用react一样的应用程序;
Step1:creatElement
从现在开始,我们重新开始构建我们自己的 react;
这一小节,我们先实现自己的 creatElement 功能;
tips:此处使用了es6的语法知识;扩展运算符 和 剩余运算符
ES6中扩展运算符(spread)和剩余运算符(rest)详解_es 扩展运算符 英文-CSDN博客
js
// 我们使用展开语法和rest参数语法传递prop和children;
// 使用rest语法,可以保证children属性始终是数组;
function createElement(type, props, ...children) {
return {
type,
props: {
...props,
children,
},
}
}
const element = React.createElement(
"div",
{ id: "foo" },
React.createElement("a", null, "bar"),
React.createElement("b")
)
const container = document.getElementById("root")
ReactDOM.render(element, container)
children数组还可以包含string,number这样的基础文本类型,所以我们将不是对象的内容包装在一个特殊的类型元素 - TEXT-ELEMENT中;
js
function createElement(type, props, ...children) {
return {
type,
props: {
...props,
// children,
children: children.map(child =>
typeof child === "object"
? child
: createTextElement(child)
),
},
}
}
function createTextElement(text) {
return {
type: "TEXT_ELEMENT",
props: {
nodeValue: text,
children: [],
},
}
}
上面的例子中,我们仍然使用的是react的creatElement;接下来我们需要定义自己的库Didact;
js
function createElement(type, props, ...children) {
return {
type,
props: {
...props,
// children,
children: children.map(child =>
typeof child === "object"
? child
: createTextElement(child)
),
},
}
}
function createTextElement(text) {
return {
type: "TEXT_ELEMENT",
props: {
nodeValue: text,
children: [],
},
}
}
const Didact = {
createElement,
}
// 直接使用createElement创建element
const element = Didact.createElement(
"div",
{ id: "foo" },
Didact.createElement("a", null, "bar"),
Didact.createElement("b")
)
const container = document.getElementById("root")
ReactDOM.render(element, container)
我们还需要增加一行注释,当babel转义JSX的时候,使用我们定义的函数;
js
/** @jsx Didact.createElement */
// 使用jsx,需要配合babel
const element = (
<div id="foo">
<a>bar</a>
<b />
</div>
)
const container = document.getElementById("root")
ReactDOM.render(element, container)
要点总结:
我们在使用react的时候,使用的JSX语法,babel会帮我们转译成调用react.creatElement方法;
js
const element = Didact.createElement(
"div",
{ id: "foo" },
Didact.createElement("a", null, "bar"),
Didact.createElement("b")
)
creatElement 方法最后会返回一个如下的数据结构:
要点是:为基础的文本类型创建一个特殊的type(TEXT_ELEMENT),使用扩展运算符传递props,使用剩余运算符保证children始终是数组形式
js
{
type,
props: {
...props,
children,
},
}
Step2:render
上一节,我们为Didact实现了creatElement函数;
js
const container = document.getElementById("root")
ReactDOM.render(element, container)
但是最后还是使用的ReactDOM.render;
本节,我们将实现render函数;
js
const Didact = {
createElement, // 上节实现
render, // 本节实现
}
首先,我们需要使用element类型创建 DOM节点, 如果存在子节点,需要递归处理;
js
function render (elemtn, container) {
const dom = document.creatElement(element.type)
// 递归处理children
element.props.children.forEach(child =>
render(child, dom)
)
container.appendChild(dom)
}
如果是TEXT_ELEMENT元素,需要创建一个text节点;
js
function render (element, container) {
const dom =
element.type == "TEXT_ELEMENT"
? document.createTextNode("")
: document.createElement(element.type);
// 递归处理children
element.props.children.forEach(child =>
render(child, dom)
)
container.appendChild(dom)
}
为节点添加props
js
function render(element, container) {
const dom =
element.type == "TEXT_ELEMENT"
? document.createTextNode("")
: document.createElement(element.type);
// 排除掉children属性
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);
}
至此,第一节和第二节,我们获得了一个可以将jsx渲染为DOM的库:
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: []
}
};
}
function render(element, container) {
// 创建dom
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));
// 挂载dom
container.appendChild(dom);
}
const Didact = {
createElement,
render
};
/** @jsx Didact.createElement */
const element = (
<div style="background: salmon">
<h1>Hello World</h1>
<h2 style="text-align:right">from Didact</h2>
</div>
);
const container = document.getElementById("root");
Didact.render(element, container);
Step3:concurrent并发模式
第三节中,我们利用递归调用render函数来创建DOM节点,但是如果我们的元素树很大,会阻塞浏览器线程;
因此,我们将工作拆分为小单元,在我们完成每个单元任务之后,如果有其他事情需要做,会让浏览器终止渲染;
我们可以使用requestIdleCallback
制作循环,与settimeout
类似,区别在于,前者是浏览器空闲的时候,才会执行调用;
关于requestIdleCallback和settimeout,可以看这篇文档
React对于DOM的渲染已经不使用requestIdleCallback
,现在使用Scheduler
;但是原理上是类似的;
关于Scheduler原理和实现,可以看这篇文档
第八章 Concurrent Mode - Scheduler的原理与实现 - 《React 技术揭秘》 - 书栈网 · BookStack
React 的 Scheduler 的简单说明
React 为了解决 15 版本存在的问题:组件的更新是递归执行,所以更新一旦开始,中途就无法中断。当层级很深时,递归更新时间超过了16ms,用户交互就会卡顿。
React 引入了 Fiber 的架构,同时配合 Schedduler 的任务调度器,在 Concurrent(并发) 模式下可以将 React 的组件更新任务变成可中断、恢复的执行,就减少了组件更新所造成的页面卡顿。
js
let nextUnitOfWork = null
function workLoop(deadline) {
let shouldYield = false
while (nextUnitOfWork && !shouldYield) {
// 要开始使用循环,我们需要设置第一个工作单元,
// 然后编写一个 performUnitOfWork 函数,
// 该函数不仅执行工作,还返回下一个工作单元。
nextUnitOfWork = performUnitOfWork(
nextUnitOfWork
)
// requestIdleCallback 还给了我们一个截止日期参数。
// 我们可以使用它来检查浏览器需要再次控制之前我们还有多少时间。
shouldYield = deadline.timeRemaining() < 1
}
requestIdleCallback(workLoop)
}
requestIdleCallback(workLoop)
// 该函数不仅执行工作,还返回下一个工作单元。
function performUnitOfWork(nextUnitOfWork) {
// TODO
}
要点总结:
并发模式是对render递归的优化;本节简单实现了一个利用requestIdleCallback制作的workLoop,利用浏览器渲染的空闲时间来执行我们的渲染任务;
首先我们需要设置第一个工作单元,然后剩下的交给performUnitOfWork(执行工作单元)处理;
其中performUnitOfWork函数至关重要。他不仅要执行工作单元,还要返回下一个工作单元。
Step4:fibers
上一节中,我们实现了一个利用 requestIdelCallback 制作的 workLoop,其中有个关键的函数 performUnitOfWork;
那么如何组织工作单元,我们需要一个新的数据结构:fiber;
我们为每个element提供一个fiber,每个fiber都是一个工作单元;
首先,我们创建 root fiber,并将其设置为 nextUnitOfWork,剩下的工作将交给performUnitOfWork处理;
每个fiber需要做三件事情:
- 将element添加到DOM;
- 为element的children创建fiber;
- 选择下一个工作单元;
fiber数据结构的目的是为了更容易得查找下一个工作单元;每个fiber与其children,下一个sibling(兄弟姐妹),parent都有关联;
fibler查找下一个工作单元遵循以下原则:
- 如果fiber有child,下一个工作单元就是第一个child;
- 如果没有child,下一个工作单元是sibling,
- 既没有child,也没有sibling,则去找parent的sibling,也就是uncle;
- 如果parent没有sibling,则继续向上寻找parent的sibling,直到root;
- 如果到达了root,表示我们完成了所有的render工作;
接下来,我们使用代码实现以上思想:
首先,我们将创建dom独立提取成为一个函数,将render中的代码删除;
js
function creatDom (fiber) {
// 创建dom
const dom =
fiber.type == "TEXT_ELEMENT"
? document.createTextNode("")
: document.createElement(fiber.type);
// 创建props
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
function workLoop(deadline) {
let shouldYield = false
while (nextUnitOfWork && !shouldYield) {
// 要开始使用循环,我们需要设置第一个工作单元,
// 然后编写一个 performUnitOfWork 函数,
// 该函数不仅执行工作,还返回下一个工作单元。
nextUnitOfWork = performUnitOfWork(
nextUnitOfWork
)
// requestIdleCallback 还给了我们一个截止日期参数。
// 我们可以使用它来检查浏览器需要再次控制之前我们还有多少时间。
shouldYield = deadline.timeRemaining() < 1
}
requestIdleCallback(workLoop)
}
requestIdleCallback(workLoop)
// 该函数不仅执行工作,还返回下一个工作单元。
function performUnitOfWork(nextUnitOfWork) {
// TODO
}
我们在render函数中设置nextUnitOfwork为root根fiber,
当浏览器准备就绪,将调用workLoop,从root节点开始工作;
js
function render(element, container) {
nextUnitOfWork = {
dom: container, // document.getElementByid('root')
props: {
children: [element],
}
}
}
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(fiber) {
// TODO add dom node
// TODO create new fibers
// TODO return next unit of work
}
接下来,我们将聚焦在performUnitOfWork函数中
js
// 该函数不仅执行工作,还返回下一个工作单元。
function performUnitOfWork(fiber) {
// 1 add dom node
if (!fiber.dom) {
fiber.dom = creatDom(fiber)
}
if (fiber.parent) {
fiber.parent.dom.appendChildren(fiber.dom)
}
// 2 create new fibers
const elements = fiber.props.children
let index = 0
let prevSibling = null
// 遍历所有children,将fible的下一级的第一个元素设置为child
// 并依次将child中的子fible的sibling设置为下一个元素
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 {
// 上一个fiber的sibling设置为当前fible
prevSibling.sibling = newFiber
}
prevSibling = newFiber
index++
}
// 3 return next unit of work
// 如果存在child,直接return
if(fiber.child){
return fible.child
}
let nextFiber = fiber
while (nextFiber) {
// 如果存在同级fible,返回同级
if (nextFiber.sibling) {
return nextFiber.sibling
}
// 即不存在child,又不存在sibling,则回到parent节点,返回parent的sibling
nextFiber = nextFiber.parent
}
这样,我们就得到了一个可以创建fible,并且可以组织工作单元的performUnitOfWork函数;
这样,我们的并发模式完整代码就是如下:
js
function creatDom (fiber) {
// 创建dom
const dom =
fiber.type == "TEXT_ELEMENT"
? document.createTextNode("")
: document.createElement(fiber.type);
// 创建props
const isProperty = key => key !== "children";
Object.keys(fiber.props)
.filter(isProperty)
.forEach(name => {
dom[name] = fiber.props[name];
});
return dom
}
function render(element, container) {
nextUnitOfWork = {
dom: container, // document.getElementByid('root')
props: {
children: [element],
}
}
}
let nextUnitOfWork = null
// workLoop主要控制的是performUnitOfWork的执行
function workLoop(deadline) {
let shouldYield = false
while (nextUnitOfWork && !shouldYield) {
nextUnitOfWork = performUnitOfWork(
nextUnitOfWork
)
shouldYield = deadline.timeRemaining() < 1
}
requestIdleCallback(workLoop)
}
requestIdleCallback(workLoop)
// 该函数不仅执行工作,还返回下一个工作单元。
function performUnitOfWork(fiber) {
// 1 add dom node
if (!fiber.dom) {
fiber.dom = creatDom(fiber)
}
if (fiber.parent) {
fiber.parent.dom.appendChildren(fiber.dom)
}
// 2 create new fibers
const elements = fiber.props.children
let index = 0
let prevSibling = null
// 遍历所有children,将fible的下一级的第一个元素设置为child
// 并依次将child中的子fible的sibling设置为下一个元素
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 {
// 上一个fiber的sibling设置为当前fible
prevSibling.sibling = newFiber
}
prevSibling = newFiber
index++
}
// 3 return next unit of work
// 如果存在child,直接return
if(fiber.child){
return fible.child
}
let nextFiber = fiber
while (nextFiber) {
// 如果存在同级fible,返回同级
if (nextFiber.sibling) {
return nextFiber.sibling
}
// 即不存在child,又不存在sibling,则回到parent节点,返回parent的sibling
nextFiber = nextFiber.parent
}
Step5:Render 和 Commit
上一步,我们已经实现了一个performUnitOfWork函数用来组织我们的工作单元;
但是,每次处理element的时候,我们都会向DOM添加一个新的节点;在这个过程中,浏览器可能会终止我们的workLoop,这样,UI渲染将会是不完整的。
所以我们需要删除导致DOM变化部分的代码;
js
// 删除
if (fiber.parent) {
fiber.parent.dom.appendChild(fiber.dom)
}
我们还需要新增一个变量,wipRoot,来跟踪fiber树;
一旦我们完成了所有的工作,我们将整个fible树提交给DOM;所以我们还需要一个commitRoot函数,在这个函数中,我们递归的将节点附加到DOM中;
js
function commitRoot() {
commitWork(wipRoot.child)
wipRoot = null
}
// 递归处理DOM的挂载
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],
},
}
nextUnitOfWork = wipRoot
}
let nextUnitOfWork = null
let wipRoot = null
function workLoop(deadline) {
let shouldYield = false
while (nextUnitOfWork && !shouldYield) {
nextUnitOfWork = performUnitOfWork(
nextUnitOfWork
)
shouldYield = deadline.timeRemaining() < 1
}
// 如果不存在下一个节点(所有fible创建并执行完毕),提交fible树
if (!nextUnitOfWork && wipRoot) {
commitRoot()
}
requestIdleCallback(workLoop)
}
要点总结:
到此为止,我们已经实现了内容的添加;
将jsx语法通过creatElement变成react的element(包含type,props的对象),
再利用render创建wipRoot,并指定为nextUnitOfWork,
再通过requestIdleCallback执行workLoop,
workLoop负责在浏览器空闲的时候执行performUnitOfWork函数,
performUnitOfWork函数主要负责创建dom,为所有child创建fible并建立关联,根据fible机制返回下一个fible;
一旦所有的fible都执行完毕,则触发commitRoot函数,进行dom的渲染;
Step6:Reconciliation 调和
前面几节,我们已经实现了内容的添加。但是还没有更新和删除节点;
这一节我们要做的就是在我们render函数上收到的 element 与 我们提交给DOM的最后一个fible进行比较;
所以在完成commit后,我们需要保存一个 commit给DOM的最后一个fible树的reference引用,称其为currentRoot;
我们也需要将这种备份属性给到每一个fiber,这种备份属性是指向旧fiber的link,即我们在上一个commit阶段提交给DOM的fible;
js
function commitRoot() {
commitWork(wipRoot.child)
// 备份当前提交给DOM的fible
currentRoot = wipRoot
wipRoot = null
}
// 递归处理DOM的挂载
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 wipRoot = null
// 增加一个currentRoot
let currentRoot = null
我们将 preformUnitOfWork 中创建 fible 的代码提取出来,放到 reconcileChildren 函数中;
在这个函数中,我们将协调旧fiber与新elements;
js
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 (fiber,elements){
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++
}
}
接下来,我们聚焦 reconcileChildren 函数
在这个函数中,我们需要同时迭代旧fiber和新elements;这里我们只考虑oldFiber和element这两项最重要的东西;
element是我们想要渲染给DOM的内容,oldFiber是上次渲染的内容;
我们需要比较两者,看看是否需要对DOM进行更改;
每次执行reconcileChildren函数的时候,都是performUnitOfWork再次执行的时候,此时函数内的oldFiber是wipFiber的child,每执行一次while,element切换为下一个child,oldFiber切换为child的sibling,再循环一次,oldFiber切换为sibling的sibling,以此类推,完成同一层级元素的比较;
js
const sameType =
oldFiber &&
element &&
element.type == oldFiber.type
比较处理的方式如下:
-
如果oldFiber的type和新element的type相同,保留DOM节点,只更新prop
-
- 创建一个新的fiber,保留旧fiber中的DOM和element中的prop
- 添加一个新属性effectTag,并设置为 "UPDATE"
js
if (sameType) {
newFiber = {
type: oldFiber.type,
props: element.props,
dom: oldFiber.dom,
parent: wipFiber,
alternate: oldFiber,
effectTag: "UPDATE",
}
}
-
如果type不同,并且有新的element,则创建一个新的DOM节点;
-
- 将dom属性置空
- alternate关联fiber置空
- effectTag设置为"PLACEMENT"
js
newFiber = {
type: element.type,
props: element.props,
dom: null,
parent: wipFiber,
alternate: null,
effectTag: "PLACEMENT",
}
- type不同,并且有oldFiber,则删除旧节点;
js
if (oldFiber && !sameType) {
oldFiber.effectTag = "DELETION"
deletions.push(oldFiber)
}
// 在commitRoot函数中,遍历删除
deletions.forEach(commitWork)
完整的 reconcileChildren 代码:
js
function reconcileChildren(wipFiber, elements) {
// 该函数只比较wipFiber关联fiber的child(以及child的sibling)和 elements
// 每次执行这个函数,wipFiber都会按照fiber遍历的规则,切换为下一个执行单元;
// 也就是说此时的 wipFiber 就是上一次执行这个函数的时候,创建的fiber;
// wipFiber.alternate 也是在上一次执行该函数的时候进行的关联
let index = 0
// oldFiber一开始是wipFiber关联fiber的第一个子fiber
// 后面在while中通过sibling进行平移切换
let oldFiber =
wipFiber.alternate && wipFiber.alternate.child
let prevSibling = null
// 通过while,将当前wipFiber的所有子fiber创建完毕
// 并且完成了和oldFiber的比较,并打上effecttag标签
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++
}
}
下面,我们更改commitWork 函数,增加对effectTag的处理;
- 'PLACEMENT'标签,将DOM挂在到parent节点
js
if (
fiber.effectTag === "PLACEMENT" &&
fiber.dom != null
) {
domParent.appendChild(fiber.dom)
}
- 'DELETION'标签,删除DOM节点
js
else if (fiber.effectTag === "DELETION") {
domParent.removeChild(fiber.dom)
}
- 'UPDATE'标签,更新DOM的props
js
else if (
fiber.effectTag === "UPDATE" &&
fiber.dom != null
) {
updateDom(
fiber.dom,
fiber.alternate.props,
fiber.props
)
}
更新DOM操作我们单独作为一个处理函数:
js
// 先定义几个过滤器函数
// 是否是属性(排除掉children)
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) {
// 删除旧属性
Object.keys(prevProps)
.filter(isProperty)
.filter(isGone(prevProps, nextProps))
.forEach(name => {
dom[name] = ""
})
// 设置/更新新的属性值
Object.keys(nextProps)
.filter(isProperty)
.filter(isNew(prevProps, nextProps))
.forEach(name => {
dom[name] = nextProps[name]
})
}
DOM元素还有一类属性比较特殊,就是事件监听,所以,如果属性名称以on开头,我们需要特殊处理;
js
// 是否是事件
const isEvent = key => key.startsWith("on")
// 是否是属性(过滤掉children和event事件)
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) {
// 移除/更改event监听
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]
)
})
// 新增事件监听
Object.keys(nextProps)
.filter(isEvent)
.filter(isNew(prevProps, nextProps))
.forEach(name => {
const eventType = name
.toLowerCase()
.substring(2)
dom.addEventListener(
eventType,
nextProps[name]
)
})
// 删除旧属性
Object.keys(prevProps)
.filter(isProperty)
.filter(isGone(prevProps, nextProps))
.forEach(name => {
dom[name] = ""
})
// 设置/更新新的属性值
Object.keys(nextProps)
.filter(isProperty)
.filter(isNew(prevProps, nextProps))
.forEach(name => {
dom[name] = nextProps[name]
})
}
Step7:实现函数组件
下面是一个基础的函数组件
js
/** @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)
转换成js
js
function App(props) {
return Didact.createElement(
"h1",
null,
"Hi ",
props.name
)
}
const element = Didact.createElement(App, {
name: "foo",
})
我们可以看出,App这个函数组件的 fiber 并没有DOM节点,并且child并非在props中,而是在函数运行结果中;
那么我们针对函数式组件需要做特殊处理,定义两个函数
updateHostComponent函数中继续执行之前的操作(creatDom,reconcileChildren)
updateFunctionComponent函数中通过执行函数组件,得到children,再执行reconcileChildren;
js
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)
}
由于函数式组件的fiber没有dom节点,在commitWork中,子fiber的dom无法直接挂载到parentDom上,所以需要特殊处理:
- 沿着fiber向上走,找到具有DOM节点的fiber作为parent节点;
js
let domParentFiber = fiber.parent
while (!domParentFiber.dom) {
domParentFiber = domParentFiber.parent
}
const domParent = domParentFiber.dom
// 如果需要挂载的话,
domParent.appendChild(fiber.dom)
- 删除节点,需要删除parent节点中的child
js
else if (fiber.effectTag === "DELETION") {
commitDeletion(fiber, domParent)
}
function commitDeletion(fiber, domParent) {
if (fiber.dom) {
domParent.removeChild(fiber.dom)
} else {
commitDeletion(fiber.child, domParent)
}
}
Step8:实现hooks
我们将函数式组件的案例更换为经典的计数器
js
/** @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呢?
实现useState之前,我们思考一下useState该满足哪些条件;
首先,useState需要存储一些私有变量,这个私有变量可以在函数外部改变,并且不会相互污染;也就是说useState是个大闭包;
这个闭包return出去的setState可以操作闭包中的hook对象,这个hook对象需要存储state值,和一个queue队列数组。
如何保证下一次执行useState的时候,还能拿到上一次存储的state,以及最新的queue队列呢?
每次执行useState的时候,将这个hook赋值给wipFiber对象,而wipFiber始终是当前正在处理的fiber;这样,就将hook状态巧妙的存储到了fiber中,达到了持久化的效果;由于fiber的关联属性,在处理新的fiber的过程中,我们也能找到oldFiber中的hook对象;相当于通过将hook赋值给fiber,利用fiber的关联属性,持久化并传递hook;
触发setState的时候,都做了些什么呢?
当触发setState的时候,会直接往hook.queue中添加一个action,相当于操作了当前组件的fiber中的hook对象(同一个引用地址);
之后,setState重新设置nextUnitOfWork之后,woorkLoop满足执行条件,再次触发新fiber的创建流程,也就是rerender的过程;
当再次执行到该组件中的useState的时候(preformUnitOfWork > updateFunctionComponent > children = [fiber.type(fiber.props)] > useState),会从当前fiber的关联oldFiber(oldFiber在当前fiber的parent fiber中就已经建立了联系)中取出hook对象,里面有上一次渲染的state,以及setState触发添加的action,执行这个action,得到新的state,并初始化一个新的hook对象,push给当前fiber的hooks对象(存在多个useState的时候,需要通过全局的hookIndex来找到对应的hook);
hookIndex的作用是什么?
由于wipFiber的hooks数组顺序是按照执行先后顺序来的。下一次组件再次执行的时候,通过全局的hookIndex找到oldFiber中对应的oldHook,这也是为什么组件中的hooks使用不能写在条件语句或者循环体中,是为了保证hooks的顺序不会乱;
下面是具体的代码实现:
js
// 当前正在进行中的fiber
let wipFiber = null;
// 全局变量,在同一个fiber(组件)中用来在hooks找到对应hook
let hookIndex = null;
function updateFunctionComponent(fiber) {
debugger;
wipFiber = fiber;
hookIndex = 0;
// 每执行一次hooks,往这个数组里保存一份最新的state,hook:{state,queue:[]}
wipFiber.hooks = [];
// useState在此时执行
const children = [fiber.type(fiber.props)];
reconcileChildren(fiber, children);
}
function useState(initial) {
debugger;
const oldHook =
wipFiber.alternate &&
wipFiber.alternate.hooks &&
wipFiber.alternate.hooks[hookIndex];
const hook = {
state: oldHook ? oldHook.state : initial,
queue: []
};
// 执行action,更新state
const actions = oldHook ? oldHook.queue : [];
actions.forEach((action) => {
hook.state = action(hook.state);
});
const setState = (action) => {
debugger;
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];
}
有一个疑问:
为什么不直接在setState中操作hook.state的值呢?非要兜一圈,通过queue队列?
这样下一次执行useState的时候,也能从oldFiber中取到修改后的值。
总结:
到此为止,我们已经实现了几乎完整的react功能,下面是一段示例代码;
Counter2组件中使用了两次useState,用来演示hookIndex的作用
Counter作为Counter2兄弟组件,用来演示hooks,hook在wipFiber中的流转
执行环境: didact-8 (forked) - CodeSandbox
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: []
}
};
}
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) {
debugger;
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) {
debugger;
wipFiber = fiber;
hookIndex = 0;
wipFiber.hooks = [];
const children = [fiber.type(fiber.props)];
reconcileChildren(fiber, children);
}
function useState(initial) {
debugger;
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) => {
debugger;
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 Counter2() {
const [state1, setState1] = Didact.useState(3);
const [state2, setState2] = Didact.useState(2);
return (
<h2
onClick={() => {
setState1((c) => c + 3);
setState2((c) => c + 2);
}}
style="user-select: none"
>
Count: {state1} + {state2}
</h2>
);
}
function Counter() {
const [state1, setState1] = Didact.useState(1);
return (
<h1 onClick={() => setState1((c) => c + 1)} style="user-select: none">
Count: {state1}
</h1>
);
}
function App() {
return (
<div>
<Counter />
<Counter2 />
</div>
);
}
const element = <App />;
const container = document.getElementById("root");
Didact.render(element, container);