前言
Facebook 的研发能力真是惊人, Fiber 架构给 React 带来了新视野的同时,将调度一词介绍给了前端,然而这个架构实在不好懂,比起以前的 Vdom 树,新的 Fiber 树就麻烦太多。
可以说,React 16 和 React 15 已经是技巧上的分水岭,但是得益于 React 16 的 Fiber 架构,使得 React 即使在没有开启异步的情况下,性能依旧是得到了提高。
经过两个星期的痛苦研究,终于将 React 16 的渲染脉络摸得比较清晰,可以写文章来记录、回顾一下。
如果你已经稍微理解了 Fiber 架构,可以直接看代码:仓库地址
什么是 React Fiber ?
React Fiber 并不是所谓的纤程(微线程、协程),而是一种基于浏览器的单线程调度算法,背后的支持 API 是大名鼎鼎的: requestIdleCallback ,得到了这个 API 的支持,我们便可以将 React 中最耗时的部分放入其中。
回顾 React 历年来的算法都知道,reconcilation 算法实际上是一个大递归,大递归一旦进行,想要中断还是比较不好操作的,加上头大尾大的 React 15 代码已经膨胀到了不可思议的地步,在重重压力之下,React 使用了大循环来代替之前的大递归,虽然代码变得比递归难懂了几个梯度,但是实际上,代码量比原来少了非常多(开发版本 3W 行压缩到了 1.3W 行)
那问题就来了,什么是 Fiber :一种将 recocilation (递归 diff ),拆分成无数个小任务的算法;它随时能够停止,恢复。停止恢复的时机取决于当前的一帧( 16ms )内,还有没有足够的时间允许计算。
React 16 前后的大小图
React 异步渲染流程图
image
用户调用
ReactDOM.render方法,传入例如<App />组件,React 开始运作<App /><App />在内部会被转换成RootFiber节点,一个特殊的节点,并记录在一个全局变量中,TopTree拿到
<App />的RootFiber,首先创建一个<App />对应的 Fiber ,然后加上 Fiber 信息,以便之后回溯。随后,赋值给之前的全局变量 TopTree使用
requestIdleCallback重复第三个步骤,直到循环到树的所有节点最后完成了
diff阶段,一次性将变化更新到真实DOM中,以防止 UI 展示的不连续性
其中,重点就是 3 和 4 阶段,这两个阶段将创建真实 DOM 和组件渲染 ( render )拆分为无数的小碎块,使用 requestIdleCallback 连续进行。在 React 15 的时候,渲染、创建、插入、删除等操作是最费时的,在 React 16 中将渲染、创建抽离出来分片,这样性能就得到了极大的提升。
那为什么更新到真实 DOM 中不能拆分呢?理论上来说,是可以拆分的,但是这会造成 UI 的不连续性,极大的影响体验。
递归变成了循环
image
以简单的组件为例子:
从顶端的
div#root向下走,先走左子树div有两个孩子span,继续走左边的来到
span,之下只有一个hello,到此,不再继续往下,而是往上回到span因为
span有一个兄弟,因此往兄弟span走去兄弟
span有孩子luy,到此,不继续往下,而是回到luy的老爹spanluy的老爹span右边没有兄弟了,因此回到其老爹divdiv没有任何的兄弟,因此回到顶端的div#root
每经过一个 Fiber 节点,执行 render 或者 document.createElement (或者更新 DOM )的操作
Fiber 数据结构
一个 Fiber 数据结构比较复杂
const Fiber = {
tag: HOST_COMPONENT,
type: 'div', return: parentFiber,
child: childFiber,
sibling: null,
alternate: currentFiber,
stateNode: document.createElement('div') | instance,
props: { children: [], className: 'foo' },
partialState: null,
effectTag: PLACEMENT,
effects: []
}这是一个比较完整的 Fiber object,他复杂的原因是因为一个 Fiber 就代表了一个「正在执行或者执行完毕」的操作单元。这个概念不是那么好理解,如果要说得简单一点就是:以前的 VDOM 树节点的升级版。让我们介绍几个关键属性:
由「 递归改循环 」我们可以得知,当我们循环的遍历树到达底部时,需要回到其父节点,那么对应的就是
Fiber中的return属性(以前叫parent)。child和sibling类似,代表这个Fiber的子Fiber和兄弟FiberstateNode这个属性比较特殊,用于记录当前Fiber所对应的真实DOM节点 或者 当前虚拟组件的实例,这么做的原因第一是为了实现Ref,第二是为了实现DOM的跟踪tag属性在新版的React中一共有 14 种值,分别代表了不同的JSX类型。effectTag和effects这两个属性为的是记录每个节点Diff后需要变更的状态,比如删除,移动,插入,替换,更新等...
alternate 属性我想拿出来单独说一下,这个属性是 Fiber 架构新加入的属性。我们都知道,VDOM 算法是在更新的时候生成一颗新的 VDOM 树,去和旧的进行对比。在 Fiber 架构中,当我们调用 ReactDOM.render 或者 setState 之后,会生成一颗树叫做:work-in-progress tree,这一颗树就是我们所谓的新树用来与我们的旧树进行对比,新的树和旧的树的 Fiber 是完全不一样的,此时,我们就需要 alternate 属性去链接新树和旧树。
司徒正美的研究中,一个 Fiber 和它的 alternate 属性构成了一个联婴体,他们有共同的 tag ,type,stateNode 属性,这些属性在错误边界自爆时,用于恢复当前节点。
开始写代码:Component 构造函数
讲了那么多的理论,大家一定是晕了,但是没办法,Fiber 架构已经比之前的简单 React 要复杂太多了,因此不可能指望一次性把 Fiber 的内容全部理解,需要反复多看。
当然,结合代码来梳理,思路旧更加清晰了。我们在构建新的架构时,老的 Luy 代码大部分都要进行重构了,先来看看几个主要重构的地方:
export class Component { constructor(props, context) { this.props = props this.context = context this.state = this.state || {} this.refs = {} this.updater = {}
}
setState(updater) {
scheduleWork(this, updater)
}
render() { throw 'should implement `render()` function'
}
}
Component.prototype.isReactComponent = true这就是
React.Component的代码构造函数中,我们都进两个参数,一个是外部的
props,一个是context内部有
state,refs,updater,updater用于收集setState的信息,便于之后更新用。当然,在这个版本之中,我并没有使用。setState函数也并没有做队列处理,只是调用了scheduleWork这个函数Component.prototype.isReactComponent = true,这段代码表饰着,如果一个组件的类型为function且拥有isReactComponent,那么他就是一个有状态组件,在创建实例时需要用new,而无状态组件只需要fn(props,context)调用
const tag = { HostComponent: 'host', ClassComponent: 'class', HostRoot: 'root', HostText: 6, FunctionalComponent: 1}const updateQueue = []export function render(Vnode, Container, callback) {
updateQueue.push({ fromTag: tag.HostRoot, stateNode: Container, props: { children: Vnode }
})
requestIdleCallback(performWork) //开始干活}export function scheduleWork(instance, partialState) {
updateQueue.push({ fromTag: tag.ClassComponent, stateNode: instance, partialState: partialState
})
requestIdleCallback(performWork) //开始干活}我们定义了一个全局变量 updateQueue 来记录我们所有的更新操作,每当 render 和 scheduleWork (setState) 触发时,我们都会往 updateQueue 中 push 一个状态,然后,进而调用大名鼎鼎的 requestIdleCallback 进行更新。在这里与之前的 react 15 最大不同是,更新阶段和首次渲染阶段得到了统一,都是使用了 updateQueue 进行更新。
实际上这里还有优化的空间,就是多次 setState 的时候,应该合并成一次再进行 requestIdleCallback 的调用,不过这并不是我们的目标,我们的目标是搞懂 Fiber 架构。requestIdleCallback 调用的是 performWork 函数,我们接下来看看
performWork 函数
const EXPIRATION_TIME = 1 // ms async 逾期时间let nextUnitOfWork = nulllet pendingCommit = nullfunction performWork(deadline) {
workLoop(deadline) if (nextUnitOfWork || updateQueue.length > 0) {
requestIdleCallback(performWork) //继续干
}
}function workLoop(deadline) { if (!nextUnitOfWork) { //一个周期内只创建一次
nextUnitOfWork = createWorkInProgress(updateQueue)
} while (nextUnitOfWork && deadline.timeRemaining() > EXPIRATION_TIME) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork)
} if (pendingCommit) { //当全局 pendingCommit 变量被负值
commitAllwork(pendingCommit)
}
}熟悉 requestIdleCallback 的同学一定对这两个函数并不陌生,这两个函数其实做的就是所谓的异步调度。
requestIdleCallback 用法
performWork 函数主要做了两件事,第一件事就是拿到 deadline 进入我们之前所谓的大循环,也就是正式进入处理新旧 Fiber 的 Diff 阶段,这个阶段比较的奇妙,我们叫他 workLoop 阶段。workLoop 会一次处理 1 个或者多个 Fiber ,具体处理多少个,要看每一帧具体还剩下多少时间,如果一个 Fiber 消耗太多时间,那么就会等到下一帧再处理下一个 Fiber ,如此循环,遍历整个 VDOM 树。
在这里我们注意到,如果一个
Fiber消耗太多时间,可能会导致一帧时间的逾期,不过其实没什么问题啦,也仅仅是一帧逾期而已,对于我们视觉上并没有多大的影响。
workLoop 函数主要是三部曲:
createWorkInProgress这个函数会构建一颗树的顶端,赋值给全局变量nextUnitOfWork,通过迭代的方式,不断更新nextUnitOfWork直到遍历完所有树的节点。performUnitOfWork函数是第二步,不断的检测当前帧是否还剩余时间,进行WorkInProgresstree 的迭代当
WorkInProgresstree 迭代完毕以后,调用commitAllWork,将所有的变更全部一次性的更新到DOM中,以保证 UI 的连续性
所有的 Diff 和创建真实 DOM 的操作,都在 performUnitOfWork 之中,但是插入和删除是在 commitAllWork 之中。接下来,我们逐一分析三部曲的内部操作。
第一步:createWorkInProgress
export function createWorkInProgress(updateQueue) { const updateTask = updateQueue.shift() if (!updateTask) return
if (updateTask.partialState) { // 证明这是一个setState操作
updateTask.stateNode._internalfiber.partialState = updateTask.partialState
} const rootFiber =
updateTask.fromTag === tag.HostRoot
? updateTask.stateNode._rootContainerFiber
: getRoot(updateTask.stateNode._internalfiber) return { tag: tag.HostRoot, stateNode: updateTask.stateNode, props: updateTask.props || rootFiber.props, alternate: rootFiber // 用于链接新旧的 VDOM
}
}function getRoot(fiber) { let _fiber = fiber while (_fiber.return) {
_fiber = _fiber.return
} return _fiber这个函数的主要作用就是构建 workInProgress 树的顶端并赋值给全局变量 nextUnitOfWork。
首先,我们先从 updateQueue 中获取一个任务对象 updateTask 。随后,进行判断是否是更新阶段。然后获取 workInProgress 树的顶端。如果是第一次渲染, RootFiber 的值是空的,因为我们并没有构建任何的树。
最后,我们将返回一个 Fiber 对象,这个 Fiber 对象的标识符( tag )是 HostRoot 。
第二步:performUnitOfWork
// 开始遍历function performUnitOfWork(workInProgress) { const nextChild = beginWork(workInProgress) if (nextChild) return nextChild // 没有 nextChild, 我们看看这个节点有没有 sibling
let current = workInProgress while (current) { //收集当前节点的effect,然后向上传递
completeWork(current) if (current.sibling) return current.sibling //没有 sibling,回到这个节点的父亲,看看有没有sibling
current = current.return
}
}我们调用 performUnitOfWork 处理我们的 workInProgress 。
整个函数做的事情其实就是一个左遍历树的过程。首先,我们调用 beginWork ,获得一个当前 Fiber 下的第一个孩子,如果有直接返回出去给 nextUnitOfWork ,当作下一个处理的节点;如果没有找到任何孩子,证明我们已经到达了树的底部,通过下面的 while 循环,回到当前节点的父节点,将当前 Fiber 下拥有 Effect 的孩子全部记录下来,以便于之后更新 DOM 。
然后查找当前节点的父亲节点,是否有兄弟,有就返回,当成下一个处理的节点,如果没有,就继续回溯。
整个过程用图来表示,就是:
image
在讨论第三部之前,我们仍然有两个迷惑的地方:
beginWork是如何创建孩子的completeWork是如何收集effect的接下来,我们就来一起看看
beginWork
function beginWork(currentFiber) { switch (currentFiber.tag) { case tag.ClassComponent: { return updateClassComponent(currentFiber)
} case tag.FunctionalComponent: { return updateFunctionalComponent(currentFiber)
} default: { return updateHostComponent(currentFiber)
}
}
}function updateHostComponent(currentFiber) { // 当一个 fiber 对应的 stateNode 是原生节点,那么他的 children 就放在 props 里
if (!currentFiber.stateNode) { if (currentFiber.type === null) { //代表这是文字节点
currentFiber.stateNode = document.createTextNode(currentFiber.props)
} else { //代表这是真实原生 DOM 节点
currentFiber.stateNode = document.createElement(currentFiber.type)
}
} const newChildren = currentFiber.props.children return reconcileChildrenArray(currentFiber, newChildren)
}function updateFunctionalComponent(currentFiber) { let type = currentFiber.type let props = currentFiber.props const newChildren = currentFiber.type(props) return reconcileChildrenArray(currentFiber, newChildren)
}function updateClassComponent(currentFiber) { let instance = currentFiber.stateNode if (!instance) { // 如果是 mount 阶段,构建一个 instance
instance = currentFiber.stateNode = createInstance(currentFiber)
} // 将新的state,props刷给当前的instance
instance.props = currentFiber.props
instance.state = { ...instance.state, ...currentFiber.partialState } // 清空 partialState
currentFiber.partialState = null
const newChildren = currentFiber.stateNode.render() // currentFiber 代表老的,newChildren代表新的
// 这个函数会返回孩子队列的第一个
return reconcileChildrenArray(currentFiber, newChildren)
}beginWork 其实是一个判断分支的函数,整个函数的意思是:
判断当前的
Fiber是什么类型,是class的走class分支,是stateless的走stateless,是原生节点的走原生分支如果没有
stateNode,则创建一个stateNode如果是
class,则创建实例,调用render函数,渲染其儿子;如果是原生节点,调用DOM API创建原生节点;如果是stateless,就执行它,渲染出VDOM节点最后,走到最重要的函数,
recocileChildrenArray函数,将其每一个孩子进行链表的链接,进行diff,然后返回当前Fiber之下的第一个孩子
我们来看看比较重要的 classComponent 的构建流程
function updateClassComponent(currentFiber) { let instance = currentFiber.stateNode if (!instance) { // 如果是 mount 阶段,构建一个 instance
instance = currentFiber.stateNode = createInstance(currentFiber)
} // 将新的state,props刷给当前的instance
instance.props = currentFiber.props
instance.state = { ...instance.state, ...currentFiber.partialState } // 清空 partialState
currentFiber.partialState = null
const newChildren = currentFiber.stateNode.render() // currentFiber 代表老的,newChildren代表新的
// 这个函数会返回孩子队列的第一个
return reconcileChildrenArray(currentFiber, newChildren)
}function createInstance(fiber) { const instance = new fiber.type(fiber.props)
instance._internalfiber = fiber return instance
}如果是首次渲染,那么组件并没有被实例话,此时我们调用 createInstance 实例化组件,然后将当前的 props 和 state 赋值给 props 、state ,随后我们调用 render 函数,获得了新儿子 newChildren。
渲染出新儿子之后,来到了新架构下最重要的核心函数 reconcileChildrenArray .
reconcileChildrenArray
const PLACEMENT = 1const DELETION = 2const UPDATE = 3function placeChild(currentFiber, newChild) { const type = newChild.type if (typeof newChild === 'string' || typeof newChild === 'number') { // 如果这个节点没有 type ,这个节点就可能是 number 或者 string
return createFiber(tag.HostText, null, newChild, currentFiber, PLACEMENT)
} if (typeof type === 'string') { // 原生节点
return createFiber(tag.HOST_COMPONENT, newChild.type, newChild.props, currentFiber, PLACEMENT)
} if (typeof type === 'function') { const _tag = type.prototype.isReactComponent ? tag.CLASS_COMPONENT : tag.FunctionalComponent return { type: newChild.type, tag: _tag, props: newChild.props, return: currentFiber, effectTag: PLACEMENT
}
}
}function reconcileChildrenArray(currentFiber, newChildren) { // 对比节点,相同的标记更新
// 不同的标记 替换
// 多余的标记删除,并且记录下来
const arrayfiyChildren = arrayfiy(newChildren) let index = 0
let oldFiber = currentFiber.alternate ? currentFiber.alternate.child : null
let newFiber = null
while (index < arrayfiyChildren.length || oldFiber !== null) { const prevFiber = newFiber const newChild = arrayfiyChildren[index] const isSameFiber = oldFiber && newChild && newChild.type === oldFiber.type if (isSameFiber) {
newFiber = { type: oldFiber.type, tag: oldFiber.tag, stateNode: oldFiber.stateNode, props: newChild.props, return: currentFiber, alternate: oldFiber, partialState: oldFiber.partialState, effectTag: UPDATE
}
} if (!isSameFiber && newChild) {
newFiber = placeChild(currentFiber, newChild)
} if (!isSameFiber && oldFiber) { // 这个情况的意思是新的节点比旧的节点少
// 这时候,我们要将变更的 effect 放在本节点的 list 里
oldFiber.effectTag = DELETION
currentFiber.effects = currentFiber.effects || []
currentFiber.effects.push(oldFiber)
} if (oldFiber) {
oldFiber = oldFiber.sibling || null
} if (index === 0) {
currentFiber.child = newFiber
} else if (prevFiber && newChild) { // 这里不懂是干嘛的
prevFiber.sibling = newFiber
}
index++
} return currentFiber.child
}这个函数做了几件事
将孩子
array化,这么做能够使得react的render函数返回数组currentFiber是新的workInProgress上的一个节点,是属于新的VDOM树 ,而此时,我们必须要找到旧的VDOM树来进行比对。那么在这里,Alternate属性就起到了关键性作用,这个属性链接了旧的VDOM,使得我们能够获取原来的VDOM接下来我们进行对比,如果新的节点的
type与原来的相同,那么我们将新建一个Fiber,标记这个Fiber为UPDATE如果新的节点的
type与原来的不相同,那我们使用PALCEMENT来标记他如果旧的节点数量比新的节点少,那就证明,我们要删除旧的节点,我们把旧节点标记为
DELETION,并构建一个effect list记录下来当前遍历的是组件的第一个孩子,那么我们将他记录在
currentFiber的child字段中当遍历的不是第一个孩子,我们将 新建的
newFiber用链表的形式将他们一起推入到currentFiber中返回当前
currentFiber下的第一个孩子
看着比较啰嗦,但是实际上做的就是构建链表和 diff 孩子的过程,这个函数有很多优化的空间,使用 key 以后,在这里能提高很多的性能,为了简单,我并没有对 key 进行操作,之后的 Luy 版本一定会的。
completeWork: 收集 effectTag
// 开始遍历function performUnitOfWork(workInProgress) { const nextChild = beginWork(workInProgress) if (nextChild) return nextChild // 没有 nextChild, 我们看看这个节点有没有 sibling
let current = workInProgress while (current) { //收集当前节点的effect,然后向上传递
completeWork(current) if (current.sibling) return current.sibling //没有 sibling,回到这个节点的父亲,看看有没有sibling
current = current.return
}
}//收集有 effecttag 的 fiberfunction completeWork(currentFiber) { if (currentFiber.tag === tag.classComponent) { // 用于回溯最高点的 root
currentFiber.stateNode._internalfiber = currentFiber
} if (currentFiber.return) { const currentEffect = currentFiber.effects || [] //收集当前节点的 effect list
const currentEffectTag = currentFiber.effectTag ? [currentFiber] : [] const parentEffects = currentFiber.return.effects || []
currentFiber.return.effects = parentEffects.concat(currentEffect, currentEffectTag)
} else { // 到达最顶端了
pendingCommit = currentFiber
}
}这个函数做了两件事,第一件事情就是收集当前 currentFiber 的 effectTag ,将其 append 到父 Fiber 的 effectlist 中去,通过循环一层一层往上,最终到达顶端 currentFiber.return === void 666 的时候,证明我们到达了 root ,此时我们已经把所有的 effect 收集到了顶端的 currentFiber.effect 上,并把它赋值给 pendingCommit ,进入 commitAllWork 阶段。
第三步:commitAllWork
终于,我们已经通过不断不断的调用 requestIdleCallback 和 大循环,将我们的所有变更都找出来放在了 workInProgress tree 里,我们接下来就要做最后一步:将所有的变更一次性的变更到真实 DOM 中,注意,这个阶段里我们不再运行创建 DOM 和 render ,因此,虽然我们一次性变更所有的 DOM ,但是性能来说并不是太差。
function commitAllwork(topFiber) {
topFiber.effects.forEach(f => {
commitWork(f)
})
topFiber.stateNode._rootContainerFiber = topFiber
topFiber.effects = []
nextUnitOfWork = null
pendingCommit = null}我们直接拿到 TopFiber 中的 effects list ,遍历,将变更全部打到 DOM 中去,然后我们将全局变量清理干净。
function commitWork(effectFiber) { if (effectFiber.tag === tag.HostRoot) { // 代表 root 节点没什么必要操作
return
} // 拿到parent的原因是,我们要将元素插入的点,插在父亲的下面
let domParentFiber = effectFiber.return
while (domParentFiber.tag === tag.classComponent || domParentFiber.tag === tag.FunctionalComponent) { // 如果是 class 就直接跳过,因为 class 类型的fiber.stateNode 是其本身实例
domParentFiber = domParentFiber.return
} //拿到父亲的真实 DOM
const domParent = domParentFiber.stateNode if (effectFiber.effectTag === PLACEMENT) { if (effectFiber.tag === tag.HostComponent || effectFiber.tag === tag.HostText) { //通过 tag 检查是不是真实的节点
domParent.appendChild(effectFiber.stateNode)
} // 其他情况
} else if (effectFiber.effectTag == UPDATE) { // 更新逻辑 只能是没实现
} else if (effectFiber.effectTag == DELETION) { //删除多余的旧节点
commitDeletion(effectFiber, domParent)
}
}function commitDeletion(fiber, domParent) {
let node = fiber while (true) { if (node.tag == tag.classComponent) {
node = node.child continue
}
domParent.removeChild(node.stateNode) while (node != fiber && !node.sibling) {
node = node.return
} if (node == fiber) { return
}
node = node.sibling
}
}这一部分代码是最好理解的了,就是做的是删除和插入或者更新 DOM 的操作,值得注意的是,删除操作依旧使用的链表操作。
最后来一段测试代码:
import React from './Luy/index'import { Component } from './component'import { render } from './vdom'class App extends Component {
state = { info: true
} constructor(props) { super(props)
setTimeout(() => { this.setState({ info: !this.state.info
})
}, 1000)
}
render() { return ( <div>
<span>hello</span>
<span>luy</span>
<div>{this.state.info ? 'imasync' : 'iminfo'}</div>
</div>
)
}
}
render(<App />, document.getElementById('root'))我们来看看动图吧!当节点 mount 以后,过了 1 秒,就会更新,我们简单的更新就到此结束了
image
image
再看以下调用栈,我们的 requestIdleCallback 函数已经正确的运行了。
如果你想下载代码亲自体验,可以到 Luy 仓库中:
git clone https://github.com/Foveluy/Luy.gitcd Luy npm i --save-dev npm run start
目前我能找到的所有资料都放在仓库中:资料
回顾本文几个重要的点
一开始我们就使用了一个数组来记录 update 的信息,通过调用 requestIdleCallback 来将更新一个一个的取出来,大部分时间队列里只有一个。
取出来以后,使用从左向右遍历的方式,用链表链接一个一个的 Fiber ,并做 diff 和创建,最后一次性的 patch 到真实 DOM 中去。
现在 react 的架构已经变得极其复杂,而本文也只是将 React 的整体架构通篇流程描述了一遍,里面的细节依旧值得我们的深究,比如,如何传递 context ,如何实现 ref ,如何实现错误边界处理,声明周期的处理,这些都是很大的话题,在接下去的文章里,我会一步一步的将这些关系讲清楚。
作者:Floveluy
链接:https://www.jianshu.com/p/748f8c49944a
共同学习,写下你的评论
评论加载中...
作者其他优质文章






