内容简介:这是一篇讲react Fiber算法的文章,深入浅出,并且作者自己实现了Fiber的核心代码,可以很好的帮助我们理解fiber原文链接另外,建议读这篇文章之前先看一下他的另外几篇关于react的文章,本篇是建立在其之上的DIY React
这是一篇讲react Fiber算法的文章,深入浅出,并且作者自己实现了Fiber的核心代码,可以很好的帮助我们理解fiber原文链接
另外,建议读这篇文章之前先看一下他的另外几篇关于react的文章,本篇是建立在其之上的DIY React
Didact Fiber: Incremental reconciliation
github repository updated demo
Why Fiber
本文并不会展示一个完整的React Fiber,如果你想了解更多, 更多资料
当浏览器的主线程长时间忙于运行一些事情时,关键任务的执行可以能被推迟。
为了展示这个问题,我做了一个demo,为了使星球一直转动,主线程需要每16ms被调用一次,因为animation是跑在主线程上的。如果主线程被其他事情占用,假如占用了200ms,你会发现animation会发生卡顿,星球停止运行,直到主线程空闲出来运行animation。
到底是什么导致主线程如此繁忙导致不能空闲出几微秒去保持动画流畅和响应及时呢?
还记得以前实现的reconciliation code吗?一旦开始,就无法停止。如果此时主线程需要做些别的事情,那就只能等待。并且因为使用了许多递归,导致很难暂停。这就是为什么我们重写代码,用循环代替递归。
Scheduling micro-tasks
我们需要把任务分成一个个子任务,在很短的时间里运行结束掉。可以让主线程先去做优先级更高的任务,然后再回来做优先级低的任务。
我们将会需要 requestIdleCallback() 函数的帮助。它在浏览器空闲时才执行callback函数,回调函数中 deadline
参数会告诉你还有多少空闲时间来运行代码,如果剩余时间不够,那么你可以选择不执行代码,保持了主线程不会被一直占用。
const ENOUGH_TIME = 1; // milliseconds let workQueue = []; let nextUnitOfWork = null; function schedule(task) { workQueue.push(task); requestIdleCallback(performWork); } function performWork(deadline) { if (!nextUnitOfWork) { nextUnitOfWork = workQueue.shift(); } while (nextUnitOfWork && deadline.timeRemaining() > ENOUGH_TIME) { nextUnitOfWork = performUnitOfWork(nextUnitOfWork); } if (nextUnitOfWork || workQueue.length > 0) { requestIdleCallback(performWork); } } 复制代码
真正起作用的函数是performUnitOfWork。我们将会在其中写reconciliation code。函数运行一次占用很少的时间,并且返回下一次任务的信息。
为了组织这些子任务,我们将会使用fibers
The fiber data structure
我们将会为每一个需要渲染的组件创建一个fiber。 nextUnitOfWork
是对将要运行的下一个fiber的引用。 performUnitOfWork
会对fiber进行diff,然后返回下一个fiber。这个将会在后面详细解释。
fiber是啥样子的呢?
let fiber = { tag: HOST_COMPONENT, type: "div", parent: parentFiber, child: childFiber, sibling: null, alternate: currentFiber, stateNode: document.createElement("div"), props: { children: [], className: "foo"}, partialState: null, effectTag: PLACEMENT, effects: [] }; 复制代码
是一个对象啊,我们将会使用parent,child,sibling属性去构建fiber树来表示组件的结构树。
stateNode
是对组件实例的引用。他可能是DOM元素或者用户定义的类组件实例
举个例子:
在上面例子中我们可以看到将支持三种不同的组件:
host component
另外一个重要属性就是 alternate
,我们需要它是因为大多数时间我们将会有两个fiber tree。一个代表着已经渲染的dom, 我们成其为current tree 或者 old tree。另外一个是在更新(当调用setState或者render)时创建的,称其为work-in-progress tree。
work-in-progress tree不会与old tree共享任何fiber。一旦我们完成work-in-progress tree的构建和dom的改变,work-in-progress tree就变成了old tree。
所以我们使用alternate属性去链接old tree。fiber与其alternate有相同的tag,type,statenode。有时我们渲染新的组件,它可能没有alternate属性
然后,还有一个effects 列表和effectTag。当我们发现work-in-progress需要改变的DOM时,就将 effectTag
设置为 PLACEMENT
, UPDATE
, DELETION
。为了更容易知道总共有哪些需要fiber需要改变DOM,我们把所有的fiber放在effects列表里。
可能这里讲了许多概念的东西,不要担心,我们将会用行动来展示fiber。
Didact call hierarchy
为了对程序有整体的理解,我们先看一下结构示意图
我们将会从 render()
和 setState()
开始,在commitAllWork()结束
Old code
我之前告诉你我们将重构大部分代码,但在这之前,我们先回顾一下不需要重构的代码
这里我就不一一翻译了,这些代码都是在文章开头我提到的
- Element creation and JSX
- Instances, reconciliation and virtual DOM
- Components and state,这里的 Class Component需要稍微改动一下
class Component { constructor(props) { this.props = props || {}; this.state = this.state || {}; } setState(partialState) { scheduleUpdate(this, partialState); } } function createInstance(fiber) { const instance = new fiber.type(fiber.props); instance.__fiber = fiber; return instance; } 复制代码
render() & scheduleUpdate()
除了Component
,
createElement
, 我们将会有两个公共函数
render()
,
setState()
,我们已经看到
setState()
仅仅调用了
scheduleUpdate()
。
render()
和 scheduleUpdate()
非常类似,他们接收新的更新并且进入队列。
/ Fiber tags const HOST_COMPONENT = "host"; const CLASS_COMPONENT = "class"; const HOST_ROOT = "root"; // Global state const updateQueue = []; let nextUnitOfWork = null; let pendingCommit = null; function render(elements, containerDom) { updateQueue.push({ from: HOST_ROOT, dom: containerDom, newProps: { children: elements } }); requestIdleCallback(performWork); } function scheduleUpdate(instance, partialState) { updateQueue.push({ from: CLASS_COMPONENT, instance: instance, partialState: partialState }); requestIdleCallback(performWork); } 复制代码
我们将会使用 updateQueue
数组来存储等待的更新。每一次调用 render
或者 scheduleUpdate
都会将数据存储进 updateQueue
。数组里每一个数据都不一样,我们将会在 resetNextUnitOfWork()
函数中使用。
在将数据 push
存储进队列之后,我们将会异步调用 performWork()
。
performWork() && workLoop()
const ENOUGH_TIME = 1; // milliseconds function performWork(deadline) { workLoop(deadline); if (nextUnitOfWork || updateQueue.length > 0) { requestIdleCallback(performWork); } } function workLoop(deadline) { if (!nextUnitOfWork) { resetNextUnitOfWork(); } while (nextUnitOfWork && deadline.timeRemaining() > ENOUGH_TIME) { nextUnitOfWork = performUnitOfWork(nextUnitOfWork); } if (pendingCommit) { commitAllWork(pendingCommit); } } 复制代码
这里使用了我们之前看到的 performUnitOfWork
模式。
workLoop()
中判断deadline是不是有足够的时间来运行代码,如果不够,停止循环,回到 performWork()
,并且 nextUnitOfWork
还被保留为下次任务,在 performWork()
中判断是否还需要执行。
performUnitOfWork()
的作用是构建 work-in-progress tree和找到哪些需要操作DOM的改变。这种处理方式是递增的,一次只处理一个fiber。
如果 performUnitOfWork()
完成了本次更新的所有工作,则renturn值为null,并且调用 commitAllWork
改变DOM。
至今为止,我们还没有看到第一个 nextUnitOfWork
是如何产生的
resetUnitOfWork()
函数取出 updateQueue
第一项,将其转换成 nextUnitOfWork
.
function resetNextUnitOfWork() { const update = updateQueue.shift(); if (!update) { return; } // Copy the setState parameter from the update payload to the corresponding fiber if (update.partialState) { update.instance.__fiber.partialState = update.partialState; } const root = update.from == HOST_ROOT ? update.dom._rootContainerFiber : getRoot(update.instance.__fiber); nextUnitOfWork = { tag: HOST_ROOT, stateNode: update.dom || root.stateNode, props: update.newProps || root.props, alternate: root }; } function getRoot(fiber) { let node = fiber; while (node.parent) { node = node.parent; } return node; } 复制代码
如果update包含 partialState
, 就将其保存的对应fiber上,在后面会赋值给组件实例,已供render使用。
然后,我们找到old fiber树的根节点。如果update是first render调用的,root fiber将为null。如果是之后的render,root将等于 _rootContainerFiber
。如果update是因为 setState()
,则向上找到第一个没有patient属性的fiber。
然后我们将其赋值给 nextUnitOfWork
,注意,这个fiber将会是work-in-progress的根元素。
如果没有old root。stateNode将取render()中的参数。props将会是render()的另外一个参数。props中children是数组。 alternate
是 null。
如果有old root。stateNode是之前的root DOM node。props将会是newProps,如果其值不为null的话,否则就是原来的props。 alternate
就是之前的old root。
我们现在已经有了work-in-progress的根元素,让我们构造剩下的吧
performUnitOfWork()
function performUnitOfWork(wipFiber) { beginWork(wipFiber); if (wipFiber.child) { return wipFiber.child; } // No child, we call completeWork until we find a sibling let uow = wipFiber; while (uow) { completeWork(uow); if (uow.sibling) { // Sibling needs to beginWork return uow.sibling; } uow = uow.parent; } } 复制代码
performUnitOfWork()
遍历work-in-progress树
beginWork()
的作用是创建子节点的fiber。并且将第一次子节点作为fiber的child属性
如果当前fiber没有子节点,我们就调用 completeWork()
,并且返回 sibling
作为下一个 nextUnitOfWork
.
如果没有 sibling
,就继续向上操作parent fiber。直到root。
总的来说,就是先处理叶子节点,然后是其兄弟节点,然后是双亲节点。从下往上遍历。
beginWork(), updateHostComponent(), updateClassComponent()
unction beginWork(wipFiber) { if (wipFiber.tag == CLASS_COMPONENT) { updateClassComponent(wipFiber); } else { updateHostComponent(wipFiber); } } function updateHostComponent(wipFiber) { if (!wipFiber.stateNode) { wipFiber.stateNode = createDomElement(wipFiber); } const newChildElements = wipFiber.props.children; reconcileChildrenArray(wipFiber, newChildElements); } function updateClassComponent(wipFiber) { let instance = wipFiber.stateNode; if (instance == null) { // Call class constructor instance = wipFiber.stateNode = createInstance(wipFiber); } else if (wipFiber.props == instance.props && !wipFiber.partialState) { // No need to render, clone children from last time cloneChildFibers(wipFiber); return; } instance.props = wipFiber.props; instance.state = Object.assign({}, instance.state, wipFiber.partialState); wipFiber.partialState = null; const newChildElements = wipFiber.stateNode.render(); reconcileChildrenArray(wipFiber, newChildElements); } 复制代码
beginWork()
的作用有两个
reconcileChildrenArray()
因为对不同类型component的处理方式不同, 这里分成了 updateHostComponent
, updateClassComponent
两个函数。
updateHostComponennt
处理了host component 和 root component。如果fiber上没有DOM node则新建一个(仅仅是创建一个DOM节点,没有子节点,也没有插入到DOM中)。然后利用fiber props中的children去调用 reconcileChildrenArray()
updateClassComponent
处理了用户创建的class component。如果没有实例则创建一个。并且更新了props和state,这样render就是可以计算出新的children。
updateClassComponent
并不是每次都调用render函数。这有点类似于 shouldCompnentUpdate
函数。如果不需要调用render,就复制子节点。
现在我们有了 newChildElements
, 我们已经准备好去创建child fiber。
reconcileChildrenArray()
注意,这里是核心。这里创建了work-in-progress 树和决定如何更新DOM
/ Effect tags const PLACEMENT = 1; const DELETION = 2; const UPDATE = 3; function arrify(val) { return val == null ? [] : Array.isArray(val) ? val : [val]; } function reconcileChildrenArray(wipFiber, newChildElements) { const elements = arrify(newChildElements); let index = 0; let oldFiber = wipFiber.alternate ? wipFiber.alternate.child : null; let newFiber = null; while (index < elements.length || oldFiber != null) { const prevFiber = newFiber; const element = index < elements.length && elements[index]; const sameType = oldFiber && element && element.type == oldFiber.type; if (sameType) { newFiber = { type: oldFiber.type, tag: oldFiber.tag, stateNode: oldFiber.stateNode, props: element.props, parent: wipFiber, alternate: oldFiber, partialState: oldFiber.partialState, effectTag: UPDATE }; } if (element && !sameType) { newFiber = { type: element.type, tag: typeof element.type === "string" ? HOST_COMPONENT : CLASS_COMPONENT, props: element.props, parent: wipFiber, effectTag: PLACEMENT }; } if (oldFiber && !sameType) { oldFiber.effectTag = DELETION; wipFiber.effects = wipFiber.effects || []; wipFiber.effects.push(oldFiber); } if (oldFiber) { oldFiber = oldFiber.sibling; } if (index == 0) { wipFiber.child = newFiber; } else if (prevFiber && element) { prevFiber.sibling = newFiber; } index++; } } 复制代码
首先我们确定 newChildElements
是一个数组(并不像之前的diff算法,这次的算法的children总是数组,这意味着我们可以在render中返回数组)
然后,开始将old fiber中的children与新的elements做对比。还记得吗?fiber.alternate就是old fiber。new elements 来自于 props.children
(function)和 render
(Class Component)。
reconciliation算法首先diff wipFiber.alternate.child 和 elements[0],然后是 wipFiber.alternate.child.sibling 和 elements[1]。这样一直遍历到遍历结束。
- 如果
oldFiber
和element
有相同的type。就通过old fiber创建新的。注意增加了UPDATE effectTag
- 如果这两者有不同的type或者没有对应的oldFiber(因为我们新添加了子节点),就创建新的fiber。注意新fiber不会有
alternate
属性和stateNode(stateNode就会在beginWork()
中创建)。还增加了PLACEMENT effectTag
。 - 如果这两者有不同的type或者没有对应的
element
(因为我们删除了一些子节点)。我们标记old fiberDELETION
。
cloneChildFibers()
updateClassComponent
中有一个特殊情况,就是不需要render,而是直接复制fiber。
function cloneChildFibers(parentFiber) { const oldFiber = parentFiber.alternate; if (!oldFiber.child) { return; } let oldChild = oldFiber.child; let prevChild = null; while (oldChild) { const newChild = { type: oldChild.type, tag: oldChild.tag, stateNode: oldChild.stateNode, props: oldChild.props, partialState: oldChild.partialState, alternate: oldChild, parent: parentFiber }; if (prevChild) { prevChild.sibling = newChild; } else { parentFiber.child = newChild; } prevChild = newChild; oldChild = oldChild.sibling; } } 复制代码
cloneChildFibers()
拷贝了old fiber的所有的子fiber。我们不需要增加 effectTag
,因为我们确定不需要改变什么。
completeWork()
performUnitOfWork
, 当wipFiber 没有新的子节点,或者我们已经处理了所有的子节点时,我们调用 completeWork
.
function completeWork(fiber) { if (fiber.tag == CLASS_COMPONENT) { fiber.stateNode.__fiber = fiber; } if (fiber.parent) { const childEffects = fiber.effects || []; const thisEffect = fiber.effectTag != null ? [fiber] : []; const parentEffects = fiber.parent.effects || []; fiber.parent.effects = parentEffects.concat(childEffects, thisEffect); } else { pendingCommit = fiber; } } 复制代码
在 completeWork
中,我们新建了effects列表。其中包含了work-in-progress中所有包含 effecTag
。方便后面处理。最后我们将pendingCommit指向了root fiber。并且在 workLoop
中使用。
commitAllWork & commitWork
这是最后一件我们需要做的事情,改变DOM。
function commitAllWork(fiber) { fiber.effects.forEach(f => { commitWork(f); }); fiber.stateNode._rootContainerFiber = fiber; nextUnitOfWork = null; pendingCommit = null; } function commitWork(fiber) { if (fiber.tag == HOST_ROOT) { return; } let domParentFiber = fiber.parent; while (domParentFiber.tag == CLASS_COMPONENT) { domParentFiber = domParentFiber.parent; } const domParent = domParentFiber.stateNode; if (fiber.effectTag == PLACEMENT && fiber.tag == HOST_COMPONENT) { domParent.appendChild(fiber.stateNode); } else if (fiber.effectTag == UPDATE) { updateDomProperties(fiber.stateNode, fiber.alternate.props, fiber.props); } else if (fiber.effectTag == DELETION) { commitDeletion(fiber, domParent); } } function commitDeletion(fiber, domParent) { let node = fiber; while (true) { if (node.tag == CLASS_COMPONENT) { node = node.child; continue; } domParent.removeChild(node.stateNode); while (node != fiber && !node.sibling) { node = node.parent; } if (node == fiber) { return; } node = node.sibling; } } 复制代码
commitAllWork
首先遍历了所有的根root effects。
updateDomProperties()
一旦我们完成了所有的effects,就重置 nextUnitOfWork
和 pendingCommit
。work-in-progress tree就变成了old tree。并复制给 _rootContainerFiber
。 这样我们完成了更新,并且做好了等待下一次更新的准备。
更多文章请查看我的主页或者blog
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- ASP.NET Core模块化前后端分离快速开发框架介绍之3、数据访问模块介绍
- 简编漫画介绍WebAssembly
- CGroup 介绍
- CGroup 介绍
- vue初步介绍
- Microbit MicroPython 介绍
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。