Facebook 的研发能力真是惊人, Fiber
架构给 React 带来了新视野的同时,将调度一词介绍给了前端,然而这个架构实在很差懂,比起之前的 Vdom
树,新的 Fiber
树就麻烦太多。前端
能够说,React 16 和 React 15 已是技巧上的分水岭,可是得益于 React 16 的 Fiber
架构,使得 React 即便在没有开启异步的状况下,性能依旧是获得了提升。node
通过两个星期的痛苦研究,终于将 React 16 的渲染脉络摸得比较清晰,能够写文章来记录、回顾一下。react
若是你已经稍微理解了 Fiber
架构,能够直接看代码:仓库地址git
React Fiber
并非所谓的纤程(微线程、协程),而是一种基于浏览器的单线程调度算法,背后的支持 API 是大名鼎鼎的: requestIdleCallback
,获得了这个 API 的支持,咱们即可以将 React 中最耗时的部分放入其中。github
回顾 React 历年来的算法都知道,reconcilation
算法其实是一个大递归,大递归一旦进行,想要中断仍是比较很差操做的,加上头大尾大的 React 15 代码已经膨胀到了难以想象的地步,在重重压力之下,React 使用了大循环来代替以前的大递归,虽然代码变得比递归难懂了几个梯度,可是实际上,代码量比原来少了很是多(开发版本 3W 行压缩到了 1.3W 行)算法
那问题就来了,什么是 Fiber
:一种将 recocilation
(递归 diff
),拆分红无数个小任务的算法;它随时可以中止,恢复。中止恢复的时机取决于当前的一帧( 16ms
)内,还有没有足够的时间容许计算。npm
ReactDOM.render
方法,传入例如<App />
组件,React 开始运做<App />
<App />
在内部会被转换成 RootFiber
节点,一个特殊的节点,并记录在一个全局变量中,TopTree
<App />
的 RootFiber
,首先建立一个 <App />
对应的 Fiber ,而后加上 Fiber 信息,以便以后回溯。随后,赋值给以前的全局变量 TopTreerequestIdleCallback
重复第三个步骤,直到循环到树的全部节点diff
阶段,一次性将变化更新到真实 DOM
中,以防止 UI 展现的不连续性其中,重点就是 3
和 4
阶段,这两个阶段将建立真实 DOM 和组件渲染 ( render
)拆分为无数的小碎块,使用 requestIdleCallback
连续进行。在 React 15 的时候,渲染、建立、插入、删除等操做是最费时的,在 React 16 中将渲染、建立抽离出来分片,这样性能就获得了极大的提高。数组
那为何更新到真实 DOM 中不能拆分呢?理论上来讲,是能够拆分的,可是这会形成 UI 的不连续性,极大的影响体验。浏览器
以简单的组件为例子:bash
div#root
向下走,先走左子树div
有两个孩子 span
,继续走左边的span
,之下只有一个 hello
,到此,再也不继续往下,而是往上回到 span
span
有一个兄弟,所以往兄弟 span
走去span
有孩子 luy
,到此,不继续往下,而是回到 luy
的老爹 span
luy
的老爹 span
右边没有兄弟了,所以回到其老爹 div
div
没有任何的兄弟,所以回到顶端的 div#root
每通过一个 Fiber
节点,执行 render
或者 document.createElement
(或者更新 DOM
)的操做
一个 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
和兄弟 Fiber
stateNode
这个属性比较特殊,用于记录当前 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
属性,这些属性在错误边界自爆时,用于恢复当前节点。
讲了那么多的理论,你们必定是晕了,可是没办法,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
函数,咱们接下来看看
const EXPIRATION_TIME = 1 // ms async 逾期时间 let nextUnitOfWork = null let pendingCommit = null function 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
的同窗必定对这两个函数并不陌生,这两个函数其实作的就是所谓的异步调度。
performWork
函数主要作了两件事,第一件事就是拿到 deadline
进入咱们以前所谓的大循环,也就是正式进入处理新旧 Fiber
的 Diff
阶段,这个阶段比较的奇妙,咱们叫他 workLoop
阶段。workLoop
会一次处理 1 个或者多个 Fiber
,具体处理多少个,要看每一帧具体还剩下多少时间,若是一个 Fiber
消耗太多时间,那么就会等到下一帧再处理下一个 Fiber
,如此循环,遍历整个 VDOM
树。
在这里咱们注意到,若是一个
Fiber
消耗太多时间,可能会致使一帧时间的逾期,不过其实没什么问题啦,也仅仅是一帧逾期而已,对于咱们视觉上并无多大的影响。
workLoop
函数主要是三部曲:
createWorkInProgress
这个函数会构建一颗树的顶端,赋值给全局变量 nextUnitOfWork
,经过迭代的方式,不断更新 nextUnitOfWork
直到遍历完全部树的节点。performUnitOfWork
函数是第二步,不断的检测当前帧是否还剩余时间,进行 WorkInProgress
tree 的迭代WorkInProgress
tree 迭代完毕之后,调用 commitAllWork
,将全部的变动所有一次性的更新到 DOM
中,以保证 UI 的连续性全部的 Diff
和建立真实 DOM
的操做,都在 performUnitOfWork
之中,可是插入和删除是在 commitAllWork
之中。接下来,咱们逐一分析三部曲的内部操做。
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
。
// 开始遍历 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
。
而后查找当前节点的父亲节点,是否有兄弟,有就返回,当成下一个处理的节点,若是没有,就继续回溯。
整个过程用图来表示,就是:
在讨论第三部以前,咱们仍然有两个迷惑的地方:
beginWork
是如何建立孩子的completeWork
是如何收集 effect
的接下来,咱们就来一块儿看看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
.
const PLACEMENT = 1 const DELETION = 2 const UPDATE = 3 function 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
版本必定会的。
// 开始遍历 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 的 fiber function 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
阶段。
终于,咱们已经经过不断不断的调用 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 秒,就会更新,咱们简单的更新就到此结束了
再看如下调用栈,咱们的 requestIdleCallback
函数已经正确的运行了。
若是你想下载代码亲自体验,能够到 Luy 仓库中:
git clone https://github.com/Foveluy/Luy.git cd Luy npm i --save-dev npm run start
目前我能找到的全部资料都放在仓库中:资料
一开始咱们就使用了一个数组来记录 update
的信息,经过调用 requestIdleCallback
来将更新一个一个的取出来,大部分时间队列里只有一个。
取出来之后,使用从左向右遍历的方式,用链表连接一个一个的 Fiber
,并作 diff
和建立,最后一次性的 patch
到真实 DOM
中去。
如今 react 的架构已经变得极其复杂,而本文也只是将 React 的总体架构通篇流程描述了一遍,里面的细节依旧值得咱们的深究,好比,如何传递 context
,如何实现 ref
,如何实现错误边界处理,声明周期的处理,这些都是很大的话题,在接下去的文章里,我会一步一步的将这些关系讲清楚。
最后,感谢支持个人迷你框架项目:Luy ,如今正在向 Fiber
晋级!若是你喜欢,请给我一点 star🌟 表示鼓励!谢谢
若是有什么问题,能够加入咱们的学习 QQ 群: 370262116
,群里几乎全部的迷你 React
做者都在了,包括 anu
做者司徒正美, omi
做者,我等,一块儿来学习吧!