背景
自从React16版本更新了Hook用法,同时引入了新的Fiber架构去重构整个渲染和事件处理过程,React团队引入Hook是为了更好剥离业务代码,让开发能更加友好的抽象代码,达到低耦合的函数组件目的,那么重构Diff算法,引入Fiber架构是为了什么呢? 其实只是为了能够一个目标快速响应
,原先Diff算法时间复杂度为O(n3) O(n^3)O(n3) ,最后经过Fiber重构达到了O(n)O(n)O(n),这里面具体有什么门道,值得我们去深入研究一下。
问题
在了解Fiber架构之前,我们需要对原有React16之前版本是有什么问题,才需要引入Fiber架构去解决该问题?
React15及以前的版本采用的是Stack Reconciler(栈协调器)架构,使用同步递归方式去创建虚拟DOM,一旦进入创建过程,就无法中断,如果创建过程超过16ms,用户就会出现页面卡顿感觉。具体可以参考下图:
因此,从网上搜索了一下React15及以前的版本反馈,的主要问题有如下几个:
- React的动画效果表现不佳
- React在有大量DOM节点渲染卡顿
为什么
为什么会出现卡顿的情况,主要原因如下:
1.JavaScript是单线程,与渲染线程互斥,当其中一个线程执行时,另一个线程只能挂起等待。
2.Stack Reconciler 栈协调器某个任务是长期占用JavaScript主线程
前置知识
为了更好了解Fiber架构设计,需要提前了解一些前置知识,每个知识点其实都需要深入了解,这里只是简单描述,主要有以下几点:
- 单线程的 JavaScript 与多线程的浏览器
- React生命周期
- React虚拟DOM
单线程的 JavaScript 与多线程的浏览器
在我们学习前端知识的时候,有个结论是: 单线程的 JavaScript 与多线程的浏览器
。
一个完整的web网页在浏览器显示和交互的进程(chrome为主),需要涉及到线程主要以下几个部分:
GUI 渲染线程
,负责渲染浏览器界面HTML元素,当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会执行。JavaScript引擎线程
,JS内核,负责处理Javascript脚本程序。 一直等待着任务队列中任务的到来,然后解析Javascript脚本,运行代码。定时触发器线程
,定时器setInterval与setTimeout所在线程,为什么要单独弄个线程处理定时器?是因为JavaScript引擎是单线程的, 如果处于阻塞线程状态就会影响记计时的准确事件触发线程
,用来控制事件轮询,JS引擎自己忙不过来,需要浏览器另开线程协助异步http请求线程
,在XMLHttpRequest
或fetch
在连接后是通过浏览器新开一个线程请求, 将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件放到 JavaScript引擎的处理队列中等待处理。这里需要注意XMLHttpRequest
和fetch
的区别,fetch
是w3c标准化后一个专门提供给开发调用发起http的API接口,XMLHttpRequest是一个非标准化的Http请求对象,主要是可以发起http请求获取XML数据。
上述就是浏览器的多线程,然后单线程的JavaScript通常指的是JavaScript引擎线程
,为什么需要单线程?因为多线程可能会出现各种UI交互冲突问题。因此了解单线程JS需要注意几点:
- GUI线程和JS引擎是互斥的,当JS引擎执行时GUI线程会被挂起,GUI更新则会被保存在一个队列中等到JS引擎线程空闲时立即被执行。
- JS 引擎只是任意的 JS 代码按需执行的环境,是其他线程调用触发JS引擎执行JS代码,比如:一个按钮点击触发事件,接着调用js引擎执行等
JS 引擎工作流程图如下:
React 生命周期
为了更好了解React Fiber架构,我们需要对比React15和React16的生命周期,具体如下:
React15的生命周期
在15版本的时候,一个完整的组件生命周期如下(按照执行顺序):
- constructor(),组件的构造函数,用来初始化state
- componentWillMount(),初始化渲染前时调用
- componentDidMount(),初始化渲染后调用
- componentWillReceiveProps(),父组件修改组件的props时会调用
- render(),每次渲染时候会调用
- componentWillUpdate(),组件更新前调用
- shouldComponentUpdate(),组件更新时调用,主要判断组件要不要更新
- componentDidUpdate(),组件更新后调用
- componentWillUnmount(),组件卸载时调用
按照不同时期,执行过程是不一样,具体可以见React的生命周期更改相关文章。
React16生命周期
相比较React15,16版本基于Fiber架构主要对更新周期的函数做了调整,整个生命周期如下:
- constructor(),组件的构造函数,用来初始化state
- getDerivedStateFromProps(),初始化/更新时调用,使用 props 来派生/更新 state。
- componentDidMount(),初始化渲染后调用
- shouldComponentUpdate(),
- render(),每次渲染时候会调用
- shouldComponentUpdate(),组件更新时调用,主要判断组件要不要更新
- getSnapshotBeforeUpdate(),返回值会作为第三个参数给到 componentDidUpdate。它的执行时机是在 render 方法之后,真实 DOM 更新之前。可以同时获取到更新前的真实 DOM 和更新前后的 state&props 信息。
- componentDidUpdate(),组件更新后调用,从 getSnapshotBeforeUpdate 获取到的值
- componentWillUnmount(),组件卸载时调用
对比一下,React 16 废弃的是哪些生命周期:
- componentWillMount;
- componentWillUpdate;
- componentWillReceiveProps
这些生命周期的共性,就是它们都处于 render 阶段,都可能重复被执行,而且由于这些 API 常年被滥用,它们在重复执行的过程中都存在着不可小觑的风险。
为什么废弃这些生命周期,因为引用了Fiber架构,render 阶段是允许暂停、终止和重启的。这就导致 render 阶段的生命周期都是有可能被重复执行的。
React 虚拟DOM
虚拟 DOM(Virtual DOM)本质上是JS 和 DOM 之间的一个映射缓存,它在形态上表现为一个能够描述 DOM 结构及其属性信息的 JS 对象。
记住两个点:
- 虚拟 DOM 是 JS 对象
- 虚拟 DOM 是对真实 DOM 的描述
虚拟DOM出现react生命周期的两个节点:
1.挂载阶段,React 将结合 JSX 的描述,构建出虚拟 DOM 树,然后通过 ReactDOM.render 实现虚拟 DOM 到真实 DOM 的映射
2.更新阶段,页面的变化在作用于真实 DOM 之前,会先作用于虚拟 DOM,虚拟 DOM 将在 JS 层借助算法先对比出具体有哪些真实 DOM 需要被改变,然后再将这些改变作用于真实 DOM,这里就需要DOM Diff算法。
为什么需要虚拟DOM?并不是因为虚拟DOM有更高的性能,而是因为虚拟 DOM 的优越之处在于,它能够在提供更爽、更高效的研发模式(也就是函数式的 UI 编程方式)的同时,仍然保持一个还不错的性能。解决了以下问题:
1.研发体验/研发效率的问题,解决以往模板和数据,需要重复调整的问题
2.跨平台的问题,从web、小程序、app等,一套虚拟DOM,结合不同渲染逻辑,满足各类跨端场景
而在虚拟DOM这一块,Fiber架构的引入,最大的调整就是虚拟DOM更新中的diff算法,由于分片渲染,不需要一次将diff执行,可以分批计算从而减少diff算法的复杂度。