学习React Fiber理念有感

239 阅读6分钟

思考

作为前端行业框架三巨头之一的React,在Facebook和Instagram等大型应用上取得了斐然的成绩,令我不禁好奇,这样一个优秀的框架,它的设计思想和理念是怎样的呢?带着这个疑惑我去解读了React技术揭秘,在此对卡颂老师的开源分享精神表示感谢。(tip:此处以React V16标准的版本说起)

什么制约了快速响应?

从卡颂老师的揭秘中可知,我们想要实现一个快速响应的网站,通常会受到了两种情况的制约,导致我们的网站并不能如我们所愿的流畅。

  • 当遇到大量的计算时,设备性能不足而导致的掉帧,导致卡顿。(其实就是当组件树过大的时候,计算时间过长导致掉帧,CPU瓶颈)
  • 发送网络请求后,由于等待数据返回才能进一步操作导致不能快速响应。(IO的瓶颈)

CPU的瓶颈

前面说了,当项目十分庞大,组件数量繁多的情况下,就很容易遇到了CPU的瓶颈,因为这意味着你每次更新都要经历这些组件。而我们的主流浏览器的刷新率是16.6ms,并且由于JS涉及到操作dom的操作,渲染线程和JS线程注定是互斥的,所以JS的脚本执行和浏览器布局、绘制阶段是不能同时进行的。

那么也就意味着,我们想要让用户获得良好的体验,就要在16.6ms内把所有要干的事情给执行掉,也就是说需要完成以下的工作。

JS脚本执行 → 样式布局 → 样式绘制

所以如果组件树过大,你JS脚本执行的时间超过了16.6ms的话,这次刷新就没有时间去执行样式布局和样式绘制了,那么 必定会造成页面的动画无法正常渲染,致使其变得卡顿。

那么React如何去解决这个问题呢?

为了避免因组件过大导致的掉帧现象,React采用了时间切片的思想,也就是新的Fiber架构,我们将一个耗时很长的JS任务拆分为很多个小任务,并且在浏览器的每一帧的时间中,预留一些时间给JS线程(5ms) 当预留的时间不够用时,React会将控制权交还给浏览器,使其有时间去渲染UI,React则等待下一帧的时间来继续执行未完成的工作。这样的话,我们的浏览器就有充足的时间去执行样式布局和样式绘制了。也就是说我们将同步的不可控的更新过程,通过时间切片的思想将其变为了可中断的异步可控更新过程。

思考1:React怎么知道上一次JS执行到了哪里呢?

这就要说到V16的新Reconciler结构了,相比于V15的栈结构,在新的协调器中,采用的是链表的结构,每一个节点都保存着上一个节点的信息,同时也储存着和下一个节点的连接,正是因为这种特殊的结构,才使得可以其任务可以随时中断。

注意:在React 15的版本,由于其的diff采用了栈的结构,其过程类似于递归,无法中断,这使得当组件层级过深得时候,函数的调用栈过深,无法及时的返回,导致浏览器无法及时渲染UI 导致卡顿(例:当你在input框输入内容时,敲击键盘并不会显示内容,而是在某一刻突然全部蹦出,这就是因为主线程被JS占用,导致渲染线程无法执行渲染工作)

思考2:16中的更新是可中断的,那React如何解决要是中断了,DOM渲染不完全的问题呢?

由于这种链式结构的特性存在,也就是说对组件的更新过程是可以中断的,那要是在渲染到一半的时候,其DOM只更新了一半的话,会不会导致渲染不完全的情况发生呢?

我们得知在React16中,Reconciler与Renderer不再是严格同步的(就是说,不是一协调完一个就立刻通知Renderer去渲染)。而是当Scheduler将任务交给Reconciler后,Reconciler会为变化的虚拟DOM打上代表增/删/更新的标记,查询源码得知:

export const Placement = /*             */ 0b0000000000010;
export const Update = /*                */ 0b0000000000100;
export const PlacementAndUpdate = /*    */ 0b0000000000110;
export const Deletion = /*              */ 0b0000000001000;

并且,Reconciler与Renderer其工作在内存当中,只有所有的组件都完成了Reconciler的工作了之后,才会统一提交给renderer去进行渲染,由于这个过程都在内存当中执行,其更新的DOM并不会出现在界面上,所以即使是反复的中断这个过程,用户也不会看见更新不完全的DOM

img

思考:3 React新的Fiber架构会对生命周期有影响么?

通过查阅资料得知,在Fiber中更新分为两个阶段,一个是Reconciliation Phase,一个是Commit Phase。在第一阶段中,React Fiber会找出那些需要进行更新的组件,并对其打上标记,在这个过程中是可以随时中断的。而第二阶段的却是不能中断的,它会一口气将DOM给更新完。

以render函数为界,第一阶段可能会调用下面这些生命周期函数,说是“可能会调用”是因为不同生命周期调用的函数不同。

  • componentWillMount
  • componentWillReceiveProps
  • shouldComponentUpdate
  • componentWillUpdate

下面这些生命周期函数则会在第二阶段调用。

  • componentDidMount
  • componentDidUpdate
  • componentWillUnmount

img

比如说,一个低优先级的任务A正在执行,已经调用了某个组件的componentWillUpdate函数,接下来发现自己的时间分片已经用完了,于是冒出水面,看看有没有紧急任务,哎呀,真的有一个紧急任务B,接下来React Fiber就会去执行这个紧急任务B,任务A虽然进行了一半,但是没办法,只能完全放弃,等到任务B全搞定之后,任务A重头来一遍,注意,是重头来一遍,不是从刚才中段的部分开始,也就是说,componentWillUpdate函数会被再调用一次。

在V15的React中,每个生命周期函数在一个加载或者更新过程中绝对只会被调用一次;在React Fiber中,不再是这样了,第一阶段中的生命周期函数在一次加载和更新过程中可能会被多次调用

IO的瓶颈

在此我们只需要知道,网络延时是前端开发者无法解决的,因为你并不知道用户的网络情况是怎样的。我们能做的只有通过一些手段尽量的降低用户对于网络延迟的感知。比如压缩代码,压缩图片,合并资源,减少访问次数等等不一一阐述。

想要交流学习的同学,可以通过我的博客联系我哦 博客地址