title: 【重识前端】React源码阅读(二)2000字告诉你Fiber设计动机
date: 2021-04-05 10:08:31
tags: [react]
category: [重拾前端]
cover: /image/cover/react.png
前言
上一篇挖的坑几乎都是Fiber
有关的,本来想先学习一下Hooks
,发现也和FIber
密切相关了,决定先搞定一下Fiber
。不了解的可以查看我的主页。Fiber
架构React
写了几年时间,一篇文章肯定是讲不完的,我决定分为四个阶段介绍。
- 动机(或者说初衷
- 初始化
- render
- 更新
看了网上的文章大都浅尝辄止,就写了一些简单的介绍,我也照猫画虎然后去面试,被打回了。比如几个问题,如果你能答的不错说明你的Fiber
知识还不错。
- 都说
Fiber
是有优先级,有打断任务的机制,但是JavaScript
他是单线程的,在计算的时候如何打断呢? - 数据结构为什么选择了链表作为数据结构?别的不可以吗?比如,数组?
- 为什么有的时候
render
会执行多次? Fiber
架构一定是异步的吗?Fiber
树和虚拟DOM树有什么区别?
这些都是我面试遇到的问题…太深了,网上的文章几乎没有提到,所以我决定自己来学习一下,然后分享并记录我的学习过程。如果有错误的地方,还请一起讨论。有任何问题都可以添加我的v:「DerrickTel」 和我讨论。
麻烦写一下备注,谢谢。
前置小知识
进程和线程
多线程可以并行处理任务,但是线程是不能单独存在的,它是由进程来启动和管理的。那什么又是进程呢?
一个进程就是一个程序的运行实例。详细解释就是,启动一个程序的时候,操作系统会为该程序创建一块内存,用来存放代码、运行中的数据和一个执行任务的主线程,我们把这样的一个运行环境叫进程。
因为不是核心,所以我这里给一个简单的例子帮助理解:
进程是火车头,线程是火车的车厢。没有火车头,车厢无法自己运行。一个火车头可以拉多个车厢。
单线程的JavaScript和多线程的浏览器
大家第一次接触到JavaScript听到最多的恐怕就是JavaScript 是单线程的,更有甚者会知道为什么JavaScript要是单线程的。
试想如果渲染线程和 JavaScript 线程同时在工作,那么渲染结果必然是难以预测的:比如渲染线程刚绘制好的画面,可能转头就会被一段 JavaScript 给改得面目全非。这就决定了JavaScript 线程和渲染线程必须是互斥的:这两个线程不能够穿插执行,必须串行。当其中一个线程执行时,另一个线程只能挂起等待。
所以JavaScript是单线程的。那为什么浏览器又是多线程的?因为在早期的单线程浏览器会碰到几个问题:
- 不稳定
- 比如一个Web视频或Web游戏,这些可能都需要强大的插件来帮助解决问题。但是插件最容易出现BUG,一个BUG崩溃了,那就会导致整个浏览器的崩溃
- 不流畅
- 就一个线程,如果被一个任务占据了大量的事情,甚至是死循环,也会让浏览器处于停滞状态
- 不安全
- 如果单线程的插线在运行时,是一个病毒或者恶意程序,你的电脑将面临巨大的风险
所以浏览器就需要多线程来处理这些遗留问题,并且可以保证速度的同时保证安全。比如网络线程,可以把数据都弄清楚了,等待JavaScript来提取,就好像洗衣服,我放洗衣店洗,我吃完饭来拿就好了。不需要我自己手动的去洗。但是也会带来几个问题
- 更多的系统资源占用。
- 更复杂的浏览器架构。(不是本文重点
单线程的JavaScript有自己的运行机制「事件循环」。可以在我的主业查看文章进行阅读。
React的卡顿
在React15
及之前的版本。都是进行两颗树的比较,然后找到需要更新的地方,最后更新到真实的DOM树上。而这个算法是在数据结构中的两棵树的比较中优化而来。也从原本 O(n3) 的 Diff 时间复杂度优化到了前无古人的 O(n)。我们称之为「 Stack Reconciler 」。在很长的一段时间风光无限。
但是,随着时间的推移和业务复杂度的提升,React 曾经被人们津津乐道的 Stack Reconciler 也渐渐在体验方面显出疲态。
为什么会出现乏力的感觉?
因为从本质上来说,栈调和机制下的 Diff 算法,其实是树的深度优先遍历的过程。而树的深度优先遍历,总是和递归脱不了关系。
递归就是除非碰到了他的返回条件,否则,将无线的循环下去。这个问题最致命的就是,无法停止。加入足够大,将会出现「页面卡死」。
FIber的出现
首先Fiber的翻译是:「纤维」。
维基百科:「纤维(英语:fiber)是指由连续或不连续的细丝组成的物质。」
简单来说就是非常的细的东西,也就是对整个渲染过程进行纤维级别的控制。
Fiber如何解决问题
Fiber是对整个React的调度算法的重写,又是一个React自创的数据结构,里面保存了一个虚拟DOM的节点,也保存了需要更新的状态,以及副作用。
我们知道,用户在使用的时候最怕的是什么?没反应,也就是「页面卡死」。而我们玩游戏觉得卡是因为他一秒钟的帧数少于60帧,所以我们觉得卡,而浏览器的页面也一样。React只需要一秒钟能够保证60帧的流畅出现就可以了,用户就不觉得「页面卡死」了。
所以Fiber的任务肯定是,在保证至少60帧每秒的情况下,才去做别的事情。
Fiber架构核心:“可中断”“可恢复”“优先级”
Fiber所做的这一系列操作都是为了用户的丝滑体验。
想象一下,打断现在非常冗长的计算,通过优先级的判断,优先渲染页面或者别的什么。然后再恢复冗长的计算。是不是脑中基本有一个模糊的概念了?
在进一步,React其实为很多任务进行了切片,这些每一个细小的片,我们可以成为「Fiber」。在执行完一个「Fiber」的时候,会检查队列中最高优先级的任务。如果有比目前的这个优先级高的,先把数据和状态保存在「Fiber」节点上,然后进行那个「更高优先级任务」。之后再检查,如果没有就恢复任务。
在 render 阶段,一个庞大的更新任务被分解为了一个个的工作单元,这些工作单元有着不同的优先级,React 可以根据优先级的高低去实现工作单元的打断和恢复。由于 render 阶段的操作对用户来说其实是“不可见”的,所以就算打断再重启,对用户来说也是 0 感知。但是,工作单元(也就是任务)的重启将会伴随着对部分生命周期的重复执行,这些生命周期是:
-
componentWillMount
-
componentWillUpdate
-
shouldComponentUpdate
-
componentWillReceiveProps
其中 shouldComponentUpdate 的作用是通过返回 true 或者 false,来帮助我们判断更新的必要性,一般在这个函数中不会进行副作用操作,因此风险不大。
而 “componentWill” 开头的三个生命周期,则常年被开发者以各种各样的姿势滥用,是副作用的“重灾区”。
思考
之前的问题,现在是否有的已经有答案了?有的还没有?欢迎讨论
- 都说
Fiber
是有优先级,有打断任务的机制,但是JavaScript
他是单线程的,在计算的时候如何打断呢? - 数据结构为什么选择了链表作为数据结构?别的不可以吗?比如,数组?
- 为什么有的时候
render
会执行多次? Fiber
架构一定是异步的吗?Fiber
树和虚拟DOM树有什么区别?
索引
https://zh-hans.reactjs.org/docs