type
Post
status
Published
date
Aug 16, 2018
slug
summary
或许有许多同学听说 Fiber[‘faɪbə]是因为 react16 的全新架构,也就是我们说的 React Fiber。但其实 Fiber 是一个早已有的技术,今天就让我们从 Fiber 讲起,最终帮助大家理解 React Fiber。
tags
ReactJS
Web dev
category
技术分享
icon
password
或许有许多同学听说 Fiber[’faɪbə]是因为 react16 的全新架构,也就是我们说的 React Fiber。但其实 Fiber 是一个早已有的技术,今天就让我们从 Fiber 讲起,最终帮助大家理解 React Fiber。
1.Fiber 是什么
Fiber 的中文翻译叫做纤程,听到这个名字,大家自然而然会想到进程(Process)、线程(Process)、协程(Coroutine)。没错纤程也是一种程序执行过程。我们先来复习下以上名词,从浏览器入手:
浏览器有多个进程:主进程、插件进程、GPU 进程、渲染进程(浏览器内核);
在渲染进程中有多个线程:GUI 渲染线程、JS 引擎线程、事件触发线程、定时触发器线程、异步 http 请求线程;
JS 引擎线程我们就更为熟悉了,js 的运行机制就是它说了算。更多
而 ES6 里引入的新的函数类型——生成器函数,为 javascript 带来的协程,也带来了看似同步的异步流程控制。(当然,在此之前有很多库已经带来了)
那么纤程是什么呢?很多人认为它就是协程,也有人说它是协程的一种实现方式(generator 算另一种实现)。我更偏向后者,但是具体学术定义我们不去谈,我们就以具体例子,说说它和 generator 实现的协程有什么差别。更多
2.Fiber 与 Generator 的差别
javascript 标准是没有 Fiber 的,但是强大的 node 生态难不倒我们,一个叫node-fibers的库可以满足我们的需求,然后我们上代码:
// flow-fiber.js var Fiber = require("fibers"); function call1(value) { var fiber = Fiber.current; var ret; setTimeout( v => { ret = v; fiber.run(); }, 200, value + 1 ); Fiber.yield(); return ret; } function call2(value) { var fiber = Fiber.current; var ret; setTimeout( v => { ret = v; fiber.run(); }, 100, value + 1 ); Fiber.yield(); return ret; } Fiber(function() { var value1 = 1; console.log("value1:", value1); var value2 = call1(value1); console.log("value2:", value2); var value3 = call2(value2); console.log("value3:", value3); }).run(); console.log("main"); /* * 运行结果: * value1: 1 * main * value2: 2 * value3: 3 */
上面展示了 Fiber 实现的协程,一个常见的异步流程控制,我们再看看 Generator 的实现,做个对比。
// flow-gen.js function call1(value) { setTimeout( v => { it.next(v); }, 200, value + 1 ); } function call2(value) { setTimeout( v => { it.next(v); }, 100, value + 1 ); } function* gen() { var value1 = 1; console.log("value1:", value1); var value2 = yield call1(value1); console.log("value2:", value2); var value3 = yield call2(value2); console.log("value3:", value3); } var it = gen(); // 构造迭代器 it.next(); console.log("main"); /* * 运行结果: * value1: 1 * main * value2: 2 * value3: 3 */
相同点:
- 都是顺序的,看似同步的异步流程控制风格;
- 主流程上可以暂停去做其他事,完成后再回到主流程;
不同点:
- Generator 通过 yield 关键字暂停执行;Fiber 是通过在子程序中调用
Fiber.yield()
方法暂停。
- Generator 通过调用生成器构造的迭代器的
next()
方法恢复执行;Fiber 是调用Fiber.current.run()
。
- Generator 返回结果是在
next()
函数的参数中传递;Fiber 是通过子程序的return
那么 Fiber 与 Generator 的这些不同点使得它可以做什么 Generator 做不到的事吗?我们看前面代码的升级版:
// flow-fiber-pro.js var Fiber = require("fibers"); function call1(value) { var fiber = Fiber.current; var ret; setTimeout( v => { ret = v; fiber.run(); }, 200, value + 1 ); Fiber.yield(); return ret; } function call2(value) { var fiber = Fiber.current; var ret; setTimeout( v => { ret = v; // fiber.run(); }, 100, value + 1 ); Fiber.yield(); return ret; } Fiber(function() { var value1 = 1; console.log("value1:", value1); var value2 = call1(value1); console.log("value2:", value2); var fiber = Fiber.current; setTimeout(() => { // doSomethingElse fiber.run(); }, 1000); var value3 = call2(value2); console.log("value3:", value3); }).run(); console.log("main"); /* * 运行结果: * value1: 1 * main * value2: 2 * value3: 3 */
这里我们的主流程里有一个异步任务,他可能和我们这个主流程没有任何关系,只是它的优先级高一些,需要让它先执行,于是我们没有在 call2 子程序中调用
Fiber.current.run()
,而是在主控制流程中Fiber.current.run()
,但是它依然获得了 call2 正确的返回值,不过是在 doSomethingElse 之后。但是同样情况我们用 Generator 却很难实现,因为我们虽然可以主流程上调用it.next()
,但它无法获取正确的参数,除非引入外部变量保存yield call
返回的值,但是这样的情况很多的话,就会有引入很多变量(我们要给每一个 call 函数结果做缓存,以便在主流程上恢复)。不止如此,我们可以在主流程上调用
Fiber.yield()
,中断执行,稍后在流程的某个环节再恢复,大家可以试一试。总结来说就是:在 Fiber 的控制流中,我们可以中断执行,并且可以恢复执行,之前的运算结果没有丢失;Generator 会按主流程的控制顺序强制执行下去,可以中断,但难以恢复。这也是#7942下 React 核心开发者说到 React 为什么没有采用 Generator 做异步的调度。
3.React Fiber
终于要讲我们的主角——React Fiber,前面说了那么多其实只和 React Fiber 有那么一丢丢的关系,因为 React Fiber 要解决的不单单是个异步调度的问题。
react-fiber-architecture和issue #7942这两个 React 核心开发者的文章可以帮我们很好地理解 React Fiber。React Fiber 的使命就是要解决 React 的性能痛点。
3.1 性能痛点在哪呢?
我们知道 React 分为 Reconciliation 和 Rendering 两个过程。 DOM 仅仅是 React 支持的一个渲染环境,通过 React Native 它还可以支持原生 iOS 和 Android 页面的渲染。
React 之所以能够支持如此多的渲染环境,主要是因为在设计上,reconciliation 和渲染两个过程是分离的。reconciler 做了计算两棵树差异的工作;渲染器则会使用计算得到的信息来更新实际的应用。
React Fiber 就是重写了 reconciler,它出来以后,之前的 reconciler 被称为了 Stack Reconciler,现在的则叫 Fiber Reconciler。
Stack Reconciler 的痛点在于:对树的更新方式采用对节点递归遍历的方式,这种方式是同步的,无法暂停的,假如这一次更新有 200 个节点变化,就要计算完这两百个节点的变化,然后一次性更新,在这期间浏览器那个唯一的主线程都在专心运行更新操作,无暇做其他事,假如一个节点更新要 1ms,那两百个节点就要 200ms,我们知道浏览器一帧是 16.7ms,所以这个时候就会出现明显卡顿。
过去的优化都是停留在 JavaScript 层面(Virtual DOM 的 create/diff):如减少组件的复杂度(Stateless)、减少向下 diff 的规模(SCU)、减少 diff 的成本(immutable.js)…这些都并不能解决线程的问题。React 希望通过 Fiber 重构来改变这种现状,进一步提升交互体验。
3.2 Fiber Reconciler
React 没有享受 Stack Reconciler 调度带来的优势。一个更新将会导致整个子树被立即被重新渲染。 背后驱动 Fiber 重写 React 的核心算法是为了来利用调度的优势。
为此,作者认为 Fiber Reconciler 需要做如下的事:
- 暂停任务并能够在之后恢复任务。
- 为不同的任务设置不同的优先级。
- 重新使用之前完成的任务。
- 如果不在需要则可以终止一个任务。
听起来是不是就像之前讲的 Fiber 可以做的,为了做到这些事,我们首先需要一个方式来拆分这些任务为一个个任务单元。
从某种意义上来说,这些任务单元就是 fiber。一个 fiber 是一个包含了组件及其输入输出的 JavaScript 对象。一个 fiber 代表了一个任务单元。( fiber 对象 或者看文末附录)
那任务的执行顺序是什么样的呢? element tree 会被转化成 fiber tree,但这个 fiber tree 实际上是 fiber 单链表。(component, element, instance 的区别请注意)
上图每个节点就是 element,任务就是将他们一个个转化成 fiber。
任务划分好了,如何暂停和恢复任务?在何时暂停和恢复?
我们看一段伪代码(真正代码react/packages/react-reconciler/src/ReactFiberScheduler.js的 renderRoot)
// 伪代码 function updateFiberAndView(dl) { updateView(); //更新视图,这会耗时,因此需要check时间 if (dl.timeRemaining() > 1) { var vdom = queue.shift(); var fiber = vdom, firstFiber; var hasVisited = {}; do { //深度优先遍历 var fiber = toFiber(fiber); //A处 if (!firstFiber) { firstFiber = fiber; } if (!hasVisited[fiber.uuid]) { hasVisited[fiber.uuid] = 1; //根据fiber.type实例化组件或者创建真实DOM //这会耗时,因此需要check时间 updateComponentOrElement(fiber); if (fiber.child) { //向下转换 if (dl.timeRemaining() < 1) { queue.push(fiber.child); //时间不够,放入队列 break; } fiber = fiber.child; continue; //让逻辑跑回A处,不断转换child, child.child, child.child.child } } //如果组件没有children,那么就向右找 if (fiber.sibling) { fiber = fiber.sibling; continue; //让逻辑跑回A处 } // 向上找 fiber = fiber.return; if (fiber === firstFiber || !fiber) { break; } } while (1); } if (queue.length) { requestIdleCallback(updateFiberAndView, { timeout: new Date() + 100 }); } }
我们看到这里使用了 requetIdleCallback 这个 API,这个函数的回调函数会在浏览器一帧空闲的时候执行。 这个回调函数带有一个参数 deadline,其 timeRemaining()函数是浏览器这一帧还可用的剩余时间。我们可以根据这个值来调度任务。在这一帧还有剩余时间时,执行我们的更新,时间不够了的话,就放在下一个 IdleCallback 里。
简单总结一下的话:任务就是将 element tree 转化成 fiber 对象形成的单链表,每一个 fiber 转化就是一个 unitWork;执行一次 unitWork 后,我们检查还有没有时间,这个时间是通过 IdleCallback 获得的(初始其实是 ReactDOM.render()调用它,这里会有个初始值),没有时间我们就将任务放到下一个 Idle period 由 IdleCallback 去执行。
具体总结一下:
React Fiber 把渲染/更新过程分为两个阶段:
- 可中断的 render/reconciliation 通过构造 workInProgress tree 得出 change。
- 不可中断的 commit 应用这些 DOM change。
这里主要说说第一阶段
- 如果当前节点不需要更新,直接把子节点 clone 过来,跳到 5;要更新的话打个 tag。
- 更新当前 props, state, context 等节点状态。
- 调用 should Component Update(),false 的话,跳到 5。
- 调用 render()获得新的子节点,并为子节点创建 fiber。创建过程会尽量复用现有 fiber,子节点增删也发生在这里。
- 如果没有产生 child fiber,该工作单元结束 return,并把当前节点的 sibling 作为下一个工作单元;否则把 child 作为下一个工作单元。
- 如果没有剩余可用时间了,等到下一次主线程空闲时才开始下一个工作单元;否则,立即开始做。
- 如果没有下一个工作单元了,回到了 workInProgress tree 的根节点,第 1 阶段结束,进入 pending Commit 状态。
实际上是 1->6 的工作循环(workLoop),7 是出口(completeWork),工作循环每次只做一件事,做完看要不要休息。 所以,构建 workInProgress tree 的过程就是 diff 的过程,通过 request Idle Callback 来调度执行一组任务,每完成一个任务后回来看看有没有插队的(更紧急的),每完成一组任务,把时间控制权交还给主线程,直到下一次 request Idle Callback 回调再继续构建 workInProgress tree。
3.3 生命周期大换血
由于第一阶段可以中断,之前会在第一阶段会调用的生命周期钩子方法就会有重复调用,我们挨个看一看这些可能被重复调用的函数。
- componentWillReceiveProps,即使当前组件不更新,只要父组件更新也会引发这个函数被调用,所以多调用几次没啥,安排!
- shouldComponentUpdate,这函数的作用就是返回一个 true 或者 false,不应该有任何副作用,多调用几次也无妨,安排!
- render,应该是纯函数,多调用几次无妨,安排!
- 只剩下 componentWillMount 和 componentWillUpdate 这两个函数往往包含副作用,于是,React 提供了新的钩子方法
getDerivedStateFromProps
getDerivedStateFromProps
取代了原来的 componentWillMount 与 componentWillReceiveProps 方法,而 componentWillUpdate 本来就是可有可无,以前完全是为了对称好看。getDerivedStateFromProps
让你在 render 设置新 state,你只要返回一个新对象,它就主动帮你 setState。于是整个生命周期变成了这样:
4.附录
// 一个Fiber对象作用于一个组件 export type Fiber = {| // 标记fiber类型tag.如下 tag: TypeOfWork, // fiber对应的function/class/module类型组件名. type: any, // fiber所在组件树的根组件FiberRoot对象 stateNode: any, // 处理完当前fiber后返回的fiber,即应该处理的下一个fiber // 返回当前fiber所在fiber树的父级fiber实例 return: Fiber | null, // fiber树结构相关链接 child: Fiber | null, // 子节点 sibling: Fiber | null, // 兄弟节点 index: number, // 当前处理过程中的组件props对象 pendingProps: any, // 缓存的之前组件props对象 // 当到来的pendingProps和上一个memoizedProps相等时,它意味着fiber的上一次输出可以重用,避免不必要的事务。 memoizedProps: any, // The props used to create the output. // The state used to create the output memoizedState: any, // 组件状态更新及对应回调函数的存储队列 updateQueue: UpdateQueue<any> | null, // 描述当前fiber实例及其子fiber树的数位, // 如,AsyncUpdates特殊字表示默认以异步形式处理子树, // 一个fiber实例创建时,此属性继承自父级fiber,在创建时也可以修改值, // 但随后将不可修改。 internalContextTag: TypeOfInternalContext, // 更新任务的最晚执行时间 expirationTime: ExpirationTime, // fiber的版本池,即记录fiber更新过程,便于恢复 // flush: 一个 fiber 就是把它的输出应用到屏幕上。 // work-in-progress: 一个 fiber还没有完成;从概念上讲,就是一个栈帧还没有返回。 // 在任何时候,一个组件实例至多对应有两个fibers。 // 它们是当前已经 flush 到屏幕上的fiber // 另一个是正在任务执行中的 fiber alternate: Fiber | null // Conceptual aliases // workInProgress : Fiber -> alternate The alternate used for reuse happens // to be the same as work in progress. |};
// ReactTypeOfWork.js // 优先级 module.exports = { IndeterminateComponent: 0, // Before we know whether it is functional or class FunctionalComponent: 1, ClassComponent: 2, HostRoot: 3, // Root of a host tree. Could be nested inside another node. HostPortal: 4, // A subtree. Could be an entry point to a different renderer. HostComponent: 5, HostText: 6, CoroutineComponent: 7, CoroutineHandlerPhase: 8, YieldComponent: 9, Fragment: 10 };
- 作者:Wave52
- 链接:https://vercel.wuchengran.com/article/3d2baa0a-8c74-417e-89dc-8bac896de02c
- 声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。
相关文章