前言
这篇文章循序渐进地介绍实现以下几个概念,遵循本篇文章根本就能搞懂为啥须要 fiber,为啥须要 commit 和 phases、reconciliation 阶段等原理。本篇文章又不齐全和原文统一,这里会退出我本人的一些思考,比方通过 performUnitOfWork
解决后 fiber tree 和 element tree 的分割等。
- createElement 函数
- render 函数
- Concurrent Mode
- Fibers
- Render and Commit Phases
- Reconciliation
- Function Components
- Hooks
第一章 基本概念
以上面代码为例
// 1.jsx 语法不是非法的 js 语法
// const element = <h1 title="foo">Hello</h1>
// 2. 经 babel 等编译工具将 jsx 转换成 js,将 jsx 转换成 createElement 函数调用的形式
// const element = React.createElement(
// "h1",
// {title: "foo"},
// "Hello"
// )
// 3.React.createElement 返回的最终对象大抵如下:const element = {
type: "h1",
props: {
title: "foo",
children: "Hello",
},
}
const container = document.getElementById("root")
// ReactDOM.render(element, container)
// 4. 替换 ReactDOM.render 函数的逻辑,ReactDOM.render 大抵解决逻辑: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)
为了防止歧义,这里应用 element
示意 React elements
,node
示意实在的 DOM 元素节点。
至此,这段代码无需通过任何编译曾经可能在浏览器上跑起来了,不信你能够复制到浏览器控制台试试
这里有几点须要留神:
- 先通过
node.appendChild(text)
将子元素增加到父元素,而后再通过container.appendChild(node)
将父元素增加到容器container
中触发浏览器渲染页面。这个程序不能反过来,也就是说只有整个实在 dom 树构建实现能力增加到容器中。假如这个程序反过来,比方先执行container.appendChild(node)
,则触发浏览器回流。再执行node.appendChild(text)
又触发浏览器回流。性能极差 React.createElement
返回的最终的对象就是virtual dom
树,ReactDOM.render
依据这个virtual dom
创立实在的 dom 树
第二章 createElement 函数
以上面的代码为例
React.createElement
接管的 children 有可能是原子值,比方字符串或者数字等,React.createElement('h1', {title: 'foo'}, 'Hello')
。为了简化咱们的代码,创立一个非凡的TEXT_ELEMENT
类型将其转换成对象
React.createElement = (type, props, ...children) => {
return {
type,
props: {
...props,
children: children.map(child => {if(typeof child === 'object'){return child}
return {
type: 'TEXT_ELEMENT',
props: {
nodeValue: child,
children: [],}
}
})
}
}
}
// const element = (
// <div id="foo">
// <a>bar</a>
// <b />
// </div>
// )
// 将 jsx 转换成 js 语法
const element = React.createElement(
"div",
{id: "foo"},
React.createElement("a", null, "bar"),
React.createElement("b")
)
const container = document.getElementById("root")
ReactDOM.render(element, container)
好了,当初咱们曾经实现了一个简略的 createElement
函数,咱们能够通过一段 非凡的正文 来通知 babel 在将 jsx 转换成 js 时应用咱们本人的 createElement
函数:
const MiniReact = {createElement: (type, props, ...children) => {
return {
type,
props: {
...props,
children: children.map(child => {if(typeof child === 'object'){return child}
return {
type: 'TEXT_ELEMENT',
props: {
nodeValue: child,
children: [],}
}
})
}
}
}
}
/** @jsx MiniReact.createElement */
const element = (
<div id="foo">
<a>bar</a>
<b />
</div>
)
console.log('element======', element)
const container = document.getElementById("root")
ReactDOM.render(element, container)
第三章 render 函数
import React from 'react';
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 MiniReact = {createElement: (type, props, ...children) => {
return {
type,
props: {
...props,
children: children.map(child => {if(typeof child === 'object'){return child}
return {
type: 'TEXT_ELEMENT',
props: {
nodeValue: child,
children: [],}
}
})
}
}
},
render
}
/** @jsx MiniReact.createElement */
const element = (
<div id="foo">
<a>bar</a>
<b />
</div>
)
console.log('element======', element)
const container = document.getElementById("root")
MiniReact.render(element, container)
render
函数递归创立实在的 dom 元素,而后将各个元素 append 到其父元素中,最初整个 dom 树 append 到 root container 中,渲染实现,这个过程一旦开始,两头是无奈打断的,直到整个利用渲染实现。这也是 React16
版本以前的渲染过程
留神,只有当整个 dom 树 append 到 root container 中时,页面才会显示
第四章 Concurrent Mode
在第三章中能够看到,以后版本的 render
函数是递归构建 dom 树,最初才 append 到 root container,最终页面才渲染进去。这里有个问题,如果 dom 节点数量宏大,递归层级过深,这个过程其实是很耗时的,导致 render
函数长时间占用主线程,浏览器无奈响应用户输出等事件,造成卡顿的景象。
因而咱们须要将 render
过程拆分成小的工作单元,每执行完一个单元,都容许浏览器打断 render
过程并执行高优先级的工作,等浏览器无暇再继续执行 render
过程
如果对 requestIdleCallback
不相熟的,能够自行理解一下。实在 React 代码中并没有应用这个 api,因为有兼容性问题。因而 React 应用 scheduler package
模仿这个调度过程
参考 前端进阶面试题具体解答
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}
performUnitOfWork
接管当前工作单元,并返回下一个工作单元。工作单元能够了解为就是一个 fiber 对象节点
workLoop
循环里会循环调用performUnitOfWork
,直到所有工作单元都曾经处理完毕,或者以后帧浏览器曾经没有闲暇工夫,则循环终止。等下次浏览器闲暇工夫再接着继续执行
因而咱们须要一种数据结构,可能反对工作打断并且能够接着继续执行,很显然,链表就非常适合
第五章 Fibers
Fibers 就是一种数据结构,反对将渲染过程拆分成工作单元,实质上就是一个双向链表。这种数据结构的益处就是不便找到下一个工作单元
Fiber 蕴含三层含意:
- 作为架构来说,之前
React 15
的Reconciler
采纳递归的形式执行,数据保留在递归调用栈中,所以被称为stack Reconciler
。React 16
的Reconciler
基于Fiber 节点
实现,被称为Fiber Reconciler
- 作为动态的数据结构来说,每个
Fiber 节点
对应一个React Element
,保留了该组件的类型(函数组件 / 类组件 /html 标签)、对应的 DOM 节点信息等 - 作为动静的工作单元来说,每个
Fiber 节点
保留了本次更新中该组件扭转的状态、要执行的工作等
Fiber 的几点冷常识:
- 一个 Fiber 节点对应一个 React Element 节点,同时也是一个工作单元
- 每个 fiber 节点都有指向第一个子元素,下一个兄弟元素,父元素的指针 **
以上面代码为例:
MiniReact.render(
<div>
<h1>
<p />
<a />
</h1>
<h2 />
</div>,
container
)
对应的 fiber tree 如下:
import React from 'react';
// 依据 fiber 节点创立实在的 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
}
let nextUnitOfWork = null
// render 函数次要逻辑:// 依据 root container 容器创立 root fiber
// 将 nextUnitOfWork 指针指向 root fiber
// element 是 react element tree
function render(element, container){
nextUnitOfWork = {
dom: container,
props: {children: [element], // 此时的 element 还只是 React.createElement 函数创立的 virtual dom 树
},
}
}
function workLoop(deadline) {
let shouldYield = false
while (nextUnitOfWork && !shouldYield) {nextUnitOfWork = performUnitOfWork(nextUnitOfWork)
shouldYield = deadline.timeRemaining() < 1}
requestIdleCallback(workLoop)
}
requestIdleCallback(workLoop)
// performUnitOfWork 函数次要逻辑:// 将 element 元素增加到 DOM
// 给 element 的子元素创立对应的 fiber 节点
// 返回下一个工作单元,即下一个 fiber 节点,查找过程:// 1. 如果有子元素,则返回子元素的 fiber 节点
// 2. 如果没有子元素,则返回兄弟元素的 fiber 节点
// 3. 如果既没有子元素又没有兄弟元素,则往上查找其父节点的兄弟元素的 fiber 节点
// 4. 如果往上查找到 root fiber 节点,阐明 render 过程曾经完结
function performUnitOfWork(fiber) {
// 第一步 依据 fiber 节点创立实在的 dom 节点,并保留在 fiber.dom 属性中
if(!fiber.dom){fiber.dom = createDom(fiber)
}
// 第二步 将以后 fiber 节点的实在 dom 增加到父节点中,留神,这一步是会触发浏览器回流重绘的!!!if(fiber.parent){fiber.parent.dom.appendChild(fiber.dom)
}
// 第三步 给子元素创立对应的 fiber 节点
const children = fiber.props.children
let prevSibling
children.forEach((child, index) => {
const newFiber = {
type: child.type,
props: child.props,
parent: fiber,
dom: null
}
if(index === 0){fiber.child = newFiber} else {prevSibling.sibling = newFiber}
prevSibling = newFiber
})
// 第四步,查找下一个工作单元
if(fiber.child){return fiber.child}
let nextFiber = fiber
while(nextFiber){if(nextFiber.sibling){return nextFiber.sibling}
nextFiber = nextFiber.parent
}
}
const MiniReact = {createElement: (type, props, ...children) => {
return {
type,
props: {
...props,
children: children.map(child => {if(typeof child === 'object'){return child}
return {
type: 'TEXT_ELEMENT',
props: {
nodeValue: child,
children: [],}
}
})
}
}
},
render
}
/** @jsx MiniReact.createElement */
const element = (
<div>
<h1>
<p />
<a />
</h1>
<h2 />
</div>
)
// const element = (
// <div id="foo">
// <a>bar</a>
// <b />
// </div>
// )
console.log('element======', element)
const container = document.getElementById("root")
MiniReact.render(element, container)
这里有一点值得细品,React.createElement
返回的 element tree
和performUnitOfWork
创立的 fiber tree
有什么分割。如下图所示:
React Element Tree
是由React.createElement
办法创立的树形构造对象Fiber Tree
是依据React Element Tree
创立来的树。每个 Fiber 节点保留着实在的 DOM 节点,并且保留着对React Element Tree
中对应的Element
节点的利用。留神,Element
节点并不会保留对Fiber
节点的利用
第六章 Render and Commit Phases
第五章的 performUnitOfWork
有些问题,在第二步中咱们间接将新创建的实在 dom 节点挂载到了容器上,这样会带来两个问题:
- 每次执行
performUnitOfWork
都会造成浏览器回流重绘,因为实在的 dom 曾经被增加到浏览器上了,性能极差 - 浏览器是能够打断渲染过程的,因而可能会造成用户看到不残缺的 UI 界面
咱们须要革新一下咱们的代码,在 performUnitOfWork
阶段不把实在的 dom 节点挂载到容器上。保留 fiber tree 根节点的援用。等到 fiber tree 构建实现,再一次性提交实在的 dom 节点并且增加到容器上。
import React from 'react';
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
}
let nextUnitOfWork = null
let wipRoot = null
function render(element, container){
wipRoot = {
dom: container,
props: {children: [element], // 此时的 element 还只是 React.createElement 函数创立的 virtual dom 树
},
}
nextUnitOfWork = wipRoot
}
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)
}
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) {
// 第一步 依据 fiber 节点创立实在的 dom 节点,并保留在 fiber.dom 属性中
if(!fiber.dom){fiber.dom = createDom(fiber)
}
// 第二步 将以后 fiber 节点的实在 dom 增加到父节点中,留神,这一步是会触发浏览器回流重绘的!!!// if(fiber.parent){// fiber.parent.dom.appendChild(fiber.dom)
// }
// 第三步 给子元素创立对应的 fiber 节点
const children = fiber.props.children
let prevSibling
children.forEach((child, index) => {
const newFiber = {
type: child.type,
props: child.props,
parent: fiber,
dom: null
}
if(index === 0){fiber.child = newFiber} else {prevSibling.sibling = newFiber}
prevSibling = newFiber
})
// 第四步,查找下一个工作单元
if(fiber.child){return fiber.child}
let nextFiber = fiber
while(nextFiber){if(nextFiber.sibling){return nextFiber.sibling}
nextFiber = nextFiber.parent
}
}
const MiniReact = {createElement: (type, props, ...children) => {
return {
type,
props: {
...props,
children: children.map(child => {if(typeof child === 'object'){return child}
return {
type: 'TEXT_ELEMENT',
props: {
nodeValue: child,
children: [],}
}
})
}
}
},
render
}
/** @jsx MiniReact.createElement */
const element = (
<div>
<h1>
<p />
<a />
</h1>
<h2 />
</div>
)
// const element = (
// <div id="foo">
// <a>bar</a>
// <b />
// </div>
// )
console.log('element======', element)
const container = document.getElementById("root")
MiniReact.render(element, container)
第七章 Reconciliation
目前为止,咱们只思考增加 dom 节点到容器上这一繁多场景,更新删除还没实现。
咱们须要比照最新的 React Element Tree
和最近一次的 Fiber Tree
的差别
咱们须要给每个 fiber 节点增加一个 alternate 属性来保留旧的 fiber 节点
alternate 保留的旧的 fiber 节点次要有以下几个用处:
- 复用旧 fiber 节点上的实在 dom 节点
- 旧 fiber 节点上的 props 和新的 element 节点的 props 比照
- 旧 fiber 节点上保留有更新的队列,在创立新的 fiber 节点时执行这些队列以获取最新的页面
const children = fiber.props.children
reconcileChildren(fiber, 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++
}
}
如上代码所示:
协调过程:
- 实质上仍然是依据
新的 React Element Tree
创立新的Fiber Tree
,不过为了节俭内存开销,协调过程会判断新的 fiber 节点是否复用旧的 fiber 节点上的实在 dom 元素,如果能复用,就不须要再从头到尾全副从新创立一遍实在的 dom 元素。同时每个新 fiber 节点上还会保留着对旧 fiber 节点的援用,不便在 commit 阶段做新旧属性 props 的比照。 - 如果
old fiber.type
和new element.type
雷同,则保留旧的 dom 节点,只更新 props 属性 - 如果
type
不雷同并且有new element
,则创立一个新的实在 dom 节点 - 如果
type
不同并且有old fiber
节点,则删除该节点对应的实在 dom 节点 - 删除节点须要有个专门的数组收集须要删除的旧的 fiber 节点。因为新的 element tree 创立进去的新的 fiber tree 不存在对应的 dom,因而须要收集旧的 fiber 节点,并在 commit 阶段删除
留神,协调过程,还是以最新的 React Element Tree 为主去创立一个新的 fiber tree,只不过是新的 fiber 节点复用旧的 fiber 节点的实在 dom 元素,毕竟频繁创立实在 dom 是很耗费内存的。新的 fiber 节点还是会保留着对旧的 fiber 节点的援用,不便在 commit 阶段进行新属性和旧属性的比拟。这里会有个问题,如果新 fiber 节点保留旧 fiber 节点的援用,那么随着更新次数越来越多,旧的 fiber tree 是不是也会越来越多,如何销毁?
import React from 'react';
function createDom(fiber) {const dom = fiber.type === 'TEXT_ELEMENT' ? document.createTextNode("") : document.createElement(fiber.type)
updateDom(dom, {}, fiber.props)
return dom
}
let nextUnitOfWork = null
let wipRoot = null // 保留着对 root fiber 的援用
let currentRoot = null // 保留着当前页面对应的 fiber tree
let deletions = null
function render(element, container){
wipRoot = {
dom: container,
props: {children: [element], // 此时的 element 还只是 React.createElement 函数创立的 virtual dom 树
},
alternate: currentRoot,
}
deletions = []
nextUnitOfWork = wipRoot
}
function commitRoot(){deletions.forEach(commitWork)
commitWork(wipRoot.child)
currentRoot = wipRoot
wipRoot = null
}
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 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)
}
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 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++
}
}
function performUnitOfWork(fiber) {
// 第一步 依据 fiber 节点创立实在的 dom 节点,并保留在 fiber.dom 属性中
if(!fiber.dom){fiber.dom = createDom(fiber)
}
// 第二步 将以后 fiber 节点的实在 dom 增加到父节点中,留神,这一步是会触发浏览器回流重绘的!!!// if(fiber.parent){// fiber.parent.dom.appendChild(fiber.dom)
// }
// 第三步 给子元素创立对应的 fiber 节点
const children = fiber.props.children
// let prevSibling
// children.forEach((child, index) => {
// const newFiber = {
// type: child.type,
// props: child.props,
// parent: fiber,
// dom: null
// }
// if(index === 0){
// fiber.child = newFiber
// } else {
// prevSibling.sibling = newFiber
// }
// prevSibling = newFiber
// })
reconcileChildren(fiber, children)
// 第四步,查找下一个工作单元
if(fiber.child){return fiber.child}
let nextFiber = fiber
while(nextFiber){if(nextFiber.sibling){return nextFiber.sibling}
nextFiber = nextFiber.parent
}
}
const MiniReact = {createElement: (type, props, ...children) => {
return {
type,
props: {
...props,
children: children.map(child => {if(typeof child === 'object'){return child}
return {
type: 'TEXT_ELEMENT',
props: {
nodeValue: child,
children: [],}
}
})
}
}
},
render
}
/** @jsx MiniReact.createElement */
const container = document.getElementById("root")
const updateValue = e => {rerender(e.target.value)
}
const rerender = value => {
const element = (
<div>
<input onInput={updateValue} value={value} />
<h2>Hello {value}</h2>
</div>
)
MiniReact.render(element, container)
}
rerender("World")
第八章 Function Components
本章以上面的代码为例:
/** @jsx MiniReact.createElement */
const container = document.getElementById("root")
function App(props){return <h1>Hi { props.name}</h1>
}
const element = <App name="foo" />
MiniReact.render(element, container)
jsx 通过 babel 编译后:
function App(props) {return MiniReact.createElement("h1", null, "Hi", props.name);
}
const element = MiniReact.createElement(App, {name: "foo"});
函数组件有两点不同的中央:
- 函数组件对应的 fiber 节点没有对应的实在 dom 元素
- 须要执行函数能力获取对应的 children 节点,而不是间接从
props.children
获取
因为函数组件没有对应的 fiber 节点,因而在 commit 阶段在找父 fiber 节点对应的 dom 时,须要判断是否存在该 dom 元素
本章残缺代码:
import React from 'react';
function createDom(fiber) {const dom = fiber.type === 'TEXT_ELEMENT' ? document.createTextNode("") : document.createElement(fiber.type)
updateDom(dom, {}, fiber.props)
return dom
}
let nextUnitOfWork = null
let wipRoot = null // 保留着对 root fiber 的援用
let currentRoot = null // 保留着当前页面对应的 fiber tree
let deletions = null
function render(element, container){
wipRoot = {
dom: container,
props: {children: [element], // 此时的 element 还只是 React.createElement 函数创立的 virtual dom 树
},
alternate: currentRoot,
}
deletions = []
nextUnitOfWork = wipRoot
}
function commitRoot(){deletions.forEach(commitWork)
commitWork(wipRoot.child)
currentRoot = wipRoot
wipRoot = null
}
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 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") {// domParent.removeChild(fiber.dom)
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 workLoop(deadline) {
let shouldYield = false
while (nextUnitOfWork && !shouldYield) {nextUnitOfWork = performUnitOfWork(nextUnitOfWork)
shouldYield = deadline.timeRemaining() < 1}
if(!nextUnitOfWork && wipRoot){commitRoot()
}
requestIdleCallback(workLoop)
}
requestIdleCallback(workLoop)
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++
}
}
function performUnitOfWork(fiber) {
// 1. 函数组件对应的 fiber 节点没有实在 dom 元素
// 2. 函数组件须要运行函数获取 children
const isFunctionComponent = fiber.type instanceof Function
if(!isFunctionComponent && !fiber.dom){fiber.dom = createDom(fiber)
}
const children = isFunctionComponent ? [fiber.type(fiber.props)] : fiber.props.children
// 第二步 为每一个新的 react element 节点创立对应的 fiber 节点,并判断旧的 fiber 节点上的实在 dom 元素是否能够复用。// 节俭创立实在 dom 元素的开销
reconcileChildren(fiber, children)
// 第三步,查找下一个工作单元
if(fiber.child){return fiber.child}
let nextFiber = fiber
while(nextFiber){if(nextFiber.sibling){return nextFiber.sibling}
nextFiber = nextFiber.parent
}
}
const MiniReact = {createElement: (type, props, ...children) => {
return {
type,
props: {
...props,
children: children.map(child => {if(typeof child === 'object'){return child}
return {
type: 'TEXT_ELEMENT',
props: {
nodeValue: child,
children: [],}
}
})
}
}
},
render
}
/** @jsx MiniReact.createElement */
const container = document.getElementById("root")
function App(props){return <h1>Hi { props.name}</h1>
}
const element = <App name="foo" />
MiniReact.render(element, container)
第九章 Hooks
本章残缺代码
import React from 'react';
function createDom(fiber) {const dom = fiber.type === 'TEXT_ELEMENT' ? document.createTextNode("") : document.createElement(fiber.type)
updateDom(dom, {}, fiber.props)
return dom
}
let nextUnitOfWork = null
let wipRoot = null // 保留着对 root fiber 的援用
let currentRoot = null // 保留着当前页面对应的 fiber tree
let deletions = null
function render(element, container){
wipRoot = {
dom: container,
props: {children: [element], // 此时的 element 还只是 React.createElement 函数创立的 virtual dom 树
},
alternate: currentRoot,
}
deletions = []
nextUnitOfWork = wipRoot
}
function commitRoot(){deletions.forEach(commitWork)
commitWork(wipRoot.child)
currentRoot = wipRoot
wipRoot = null
}
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 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") {// domParent.removeChild(fiber.dom)
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 workLoop(deadline) {
let shouldYield = false
while (nextUnitOfWork && !shouldYield) {nextUnitOfWork = performUnitOfWork(nextUnitOfWork)
shouldYield = deadline.timeRemaining() < 1}
if(!nextUnitOfWork && wipRoot){commitRoot()
}
requestIdleCallback(workLoop)
}
requestIdleCallback(workLoop)
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++
}
}
function performUnitOfWork(fiber) {
// 1. 函数组件对应的 fiber 节点没有实在 dom 元素
// 2. 函数组件须要运行函数获取 children
const isFunctionComponent = fiber.type instanceof Function
if(!isFunctionComponent && !fiber.dom){fiber.dom = createDom(fiber)
}
const children = isFunctionComponent ? updateFunctionComponent(fiber) : fiber.props.children
// 第二步 为每一个新的 react element 节点创立对应的 fiber 节点,并判断旧的 fiber 节点上的实在 dom 元素是否能够复用。// 节俭创立实在 dom 元素的开销
reconcileChildren(fiber, children)
// 第三步,查找下一个工作单元
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 = []
return [fiber.type(fiber.props)]
}
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]
}
const MiniReact = {createElement: (type, props, ...children) => {
return {
type,
props: {
...props,
children: children.map(child => {if(typeof child === 'object'){return child}
return {
type: 'TEXT_ELEMENT',
props: {
nodeValue: child,
children: [],}
}
})
}
}
},
render,
useState,
}
/** @jsx MiniReact.createElement */
const container = document.getElementById("root")
function Counter(){const [state, setState] = MiniReact.useState(1)
return (<h1 onClick={() => setState(c => c + 1)}>
Count: {state}
</h1>
)
}
const element = <Counter />
MiniReact.render(element, container)