前言
react 在进行组件渲染时,从 setState 开始到渲染完成整个过程是同步的(“一气呵成”)。如果需要渲染的组件比较庞大,js 执行会占据主线程时间较长,会导致页面响应度变差,使得 react 在动画、手势等应用中效果比较差。
所以在 react v15 之前使用它做一些动画效果或大数据量处理渲染之类的都可能会出现卡顿等性能问题。
react v15 是函数调用栈,递归的方式,从父节点(Virtual DOM)开始遍历,以找出不同。将所有的 Virtual DOM 遍历完成后,reconciler 才能给出当前需要修改真实 DOM 的信息,并传递给 renderer,进行渲染,然后屏幕上才会显示此次更新内容。对于特别庞大的 vDOM 树来说,reconciliation 过程会很长(大于浏览器渲染帧的时间 16ms),在这期间,主线程是被 js 占用的,因此任何交互、布局、渲染都会停止,给用户的感觉就是页面被卡住了。
破解 JavaScript 中同步操作时间过长的方法其实很简单——分片。
把一个耗时长的任务分成很多小片,每一个小片的运行时间很短,虽然总时间依然很长,但是在每个小片执行完之后,都给其他任务一个执行的机会,这样唯一的线程就不会被独占,其他任务依然有运行的机会。
React Fiber 把更新过程碎片化,每执行完一段更新过程,就把控制权交还给 React 负责任务协调的模块,看看有没有其他紧急任务要做,如果没有就继续去更新,如果有紧急任务,那就去做紧急任务。
维护每一个分片的数据结构,就是 Fiber。
React Fiber
在 React Fiber 中,一次更新过程会分成多个分片完成,所以完全有可能一个更新任务还没有完成,就被另一个更高优先级的更新过程打断,这时候,优先级高的更新任务会优先处理完,而低优先级更新任务所做的工作则会完全作废,然后等待机会重头再来。
因为一个更新过程可能被打断,所以 React Fiber 一个更新过程被分为两个阶段(Phase):第一个阶段 协调阶段 和第二阶段 提交阶段。
在第一阶段协调阶段,React Fiber 会找出需要更新哪些 DOM,这个阶段是可以被打断的;但是到了第二阶段 提交阶段,那就一鼓作气把 DOM 更新完,绝不会被打断。
这两个阶段大部分工作都是 React Fiber 做,和我们相关的也就是生命周期函数。
以 render 函数为界,第一阶段可能会调用下面这些生命周期函数,说是“可能会调用”是因为不同生命周期调用的函数不同。
第一阶段:
componentWillMount
componentWillReceiveProps
shouldComponentUpdate
componentWillUpdate
第二阶段:
componentDidMount
componentDidUpdate
componentWillUnmount
因为第一阶段的过程会被打断而且“重头再来”,就会造成意想不到的情况。
例如已经执行到某个 Fiber 内的生命周期函数 componentWillUpdate,但由于已经到了一个渲染帧的时间,需要把控制权让出,让浏览器查看是否有任务需要执行,若有则废弃当前 Fiber,执行浏览器的任务,等浏览器让出控制权后重新执行这个 Fiber,所以才会出现二次执行生命周期 componentWillUpdate 的情况。
因此第一阶段中的生命周期函数在一次加载和更新过程中可能会被多次调用!
但某些使用者在使用生命周期函数时会做一些奇怪的操作上,既违背这个生命周期提供的意义,如果出现二次调用就会出现各种 BUG,所以 react 把某些生命周期在过渡版本提示为废弃钩子,componentWillMount,componentWillReceiveProps,componentWillUpdate。
fiber 执行单元
fiber 的实现方式为时间分片 + 链表结构,一个组件实例对应有一个 fiber fiber 其实就是虚拟 Dom。
fiber 维护了一个分片的数据结构,就是可终止可恢复的链表式数据结构。
如一个<Card />
组件的 fiber:
1 | // FiberNode 结构如下: |
通过 child, slibing, return 的结构关联,可知它是一个单链表结构。
时间分片
实现时间分片的目的只是为了在做一些大批量 DOM 渲染时,让客户端不要给客户出现卡顿的不流畅现象。
而使用浏览器提供的 API 是最好的,requestIdleCallback 和 requestAnimationFrame,如果需要兼容低版本浏览器,可以使用 setTimeout 模拟,但可能会出现一瞬间的白屏情况。
setTimeout 闪屏现象
在 js 引擎里,setTimeout 的事件处理函数会先等待到目标时间后执行,然后进入事件队列里等待主线程的空闲,因此 setTimeout 的实际执行时间可能会比其设定的时间晚一些。
那么我们在 setTimeout 设置的时间与浏览器的渲染时间很大可能是不一致的,而且很大可能是 setTimeout 被主线程处理比渲染要晚一些,导致 setTimeout 里的渲染内容只能在下一帧渲染时被渲染,这是丢帧现象。
例子
- 没有用时间分片
1 | // 记录任务开始时间 |
十万条数据渲染时会出现很明显的卡顿白屏等现象。
- requestAnimationFrame 实现时间分片
1 | //需要插入的容器 |
在操作时很顺畅,体验很好。
宝藏资料
这可能是最通俗的 React Fiber(时间分片) 打开方式
React Fiber
React Fiber 是什么
fiber
React Fiber 初探