前言

React实现可以粗划为两部分:reconciliation(diff阶段)和 commit(操作DOM阶段)。在 v16 之前,reconciliation 简单说就是一个自顶向下递归算法,产出需要对当前DOM进行更新或替换的操作列表,一旦开始,会持续占用主线程,中断操作却不容易实现。当JS长时间执行(如大量计算等),会阻塞样式计算、绘制等工作,出现页面脱帧现象。所以,v16 进行了一次重写,迎来了代号为Fiber的异步渲染架构。


Fiber

Fiber核心是实现了一个基于优先级和requestIdleCallback的循环任务调度算法。它包含以下特性:(参考:fiber-reconciler)

  • reconciliation阶段可以把任务拆分成多个小任务
  • reconciliation阶段可随时中止或恢复任务
  • 可以根据优先级不同来选择优先执行任务

从其特性可看出,Fiber核心是更换了reconciliation阶段的运作。那么,问题来了:

  • 为什么对reconciliation阶段进行拆分,commit阶段呢?

    reconciliation阶段包含的主要工作是对current tree 和 new tree 做diff计算,找出变化部分。进行遍历、对比等是可以中断,歇一会儿接着再来。

    commit阶段是对上一阶段获取到的变化部分应用到真实的DOM树中,是一系列的DOM操作。不仅要维护更复杂的DOM状态,而且中断后再继续,会对用户体验造成影响。在普遍的应用场景下,此阶段的耗时比diff计算等耗时相对短。

    所以,Fiber选择在reconciliation阶段拆分

  • 如何拆分呢?

首先,我们可以通过 A Cartoon Intro to Fiber中的一张图来看:

  1. 用户调用ReactDOM.render传入组件,React创建Element树;
  2. 在第一次渲染时,创建vdom树,用来维护组件状态和dom节点的信息(如List/Button/Item等)。当后续操作如render或setState时需要更新,通过diff算出变化的部分;
  3. 根据变化的部分更新vdom树、调用组件生命周期函数等,同步应用到真实的DOM节点中。

在第二阶段,Fiber是把render/update分片,拆解成多个小任务来执行,每次只检查树上部分节点,做完此部分后,若当前一帧(16ms)内还有足够的时间就继续做下一个小任务,时间不够就停止操作,等主线程空闲时再恢复。

这种停止/恢复操作,需要记录上下文信息。而当前只记录单一dom节点的vDom tree 是无法完成的,

Fiber引入了fiber tree,是用来记录上下文的vDom tree,可以理解为升级版的钢铁侠。

fiber tree上一个节点的结构大致有:

export type Fiber = {
tag: TypeOfWork, // 类型
type: 'div',
return: Fiber|null, // 父节点
child: Fiber|null, // 子节点
sibling: Fiber|null, // 兄弟节点
alternate: Fiber|null, //diff出的变化记录在这个fiber上
.....
};

完整树结构可参见 ReactFiber

所以,Fiber是根据一个fiber节点(VDOM节点)来拆分,以fiber node为一个任务单元,一 个组件实例都是一个任务单元。任务循环中,每处理完一个fiber node,可以中断/挂起/恢复。

  • 基于requestIdleCallback和优先级任务调度

1.requestIdleCallback

  window.requestIdleCallback(callback[, options])

浏览器提供的requestIdleCallback API中的Cooperative Scheduling可以让浏览器在空闲时间执行回调(开发者传入的方法),在回调参数中可以获取到当前帧(16ms)剩余的时间。利用这个信息可以合理的安排当前帧需要做的事情,如果时间足够,那继续做下一个任务,如果时间不够就歇一歇,调用requestIdleCallback来获知主线程不忙的时候,再继续做任务。

2.不同的任务分配不同的优先级,Fiber根据任务的优先级来动态调整任务调度,先做高优先级的任务

       {
NoWork: 0, // No work is pending.
SynchronousPriority: 1, // 文本输入框
TaskPriority: 2, // 当前调度正执行的任务
AnimationPriority: 3, // 动画过渡
HighPriority: 4, // 用户交互反馈
LowPriority: 5, // 数据的更新
OffscreenPriority: 6, // 预估未来需要显示的任务
}

3.任务调度的过程是:

  1. 在任务队列中选出高优先级的fiber node执行,调用requestIdleCallback获取所剩时间,若执行时间超过了deathLine,或者突然插入更高优先级的任务,则执行中断,保存当前结果,修改tag标记一下,设置为pending状态,迅速收尾并再调用一个requestIdleCallback,等主线程释放出来再继续
  2. 恢复任务执行时,检查tag是被中断的任务,会接着继续做任务或者重做

一个任务单元执行结束或挂起,会调用基于requestIdleCallback的调度器,返回一个新的任务队列继续进行上述过程。

  • 如何任务循环

    上面我们有介绍fiber node的属性存放上下文信息的指向。

  • child:指向当前节点的子节点。当此节点任务结束后,如果child存在,就开始子节点的任务

  • sibling: 当子树已遍历到底部,回到父节点时,会拿到父节点的兄弟节点进行任务

  • return: 当当前节点没有子节点时,会向上回到父节点

  • alternate

    在调用render或setState后,会克隆出一个镜像fiber,diff产生出的变化会标记在镜像fiber上。而alternate就是链接当前fiber tree和镜像fiber tree, 用于断点恢复。

    • workInProgress tree

      React中对其描述如下:

    work-in-progress

    A fiber that has not yet completed; conceptually, a stack frame which has not yet returned.The alternate of the current fiber is the work-in-progress, and the alternate of the work-in-progress is the current fiber.

    当前fiber节点的alternate属性指向workInProgress节点,对应workInProgress节点的alternate属性指向当前fiber节点。

    上面alternate中说到镜像fiber tree就是workInProgress tree。

    workInProgress tree上每个节点都有一个effect list,用来存放需要更新的内容。此节点更新完毕会向子节点或邻近节点合并 effect list。

    任务循环的过程如下:

    1. 找到高优先级的待处理的节点
    2. 检查当前节点是否需要更新,不需要的话,直接clone子节点,直接到5
    3. 打个tag标记,更新自己(组件更新props,context等,DOM节点记下DOM change)
    4. 通过render获取子节点,生成子节点的workInProgress节点
    5. 若没有产生子节点,归并diff出的不同部分effect list(包含DOM change)到父节点
    6. 把子节点或兄弟节点作为待处理任务,准备进入下一个任务循环。若已经回到了workInProgress tree的根节点,则任务循环结束

    通过每个节点更新结束时向上归并effect list来收集任务结果,reconciliation结束后,根节点的effect list里记录了包括DOM change在内的所有side effect

    通过一段代码,我们来看看如何进行任务循环:

    export class Home extend React.component<HomeProps, any> {
    componentWillReceiveProps(nextProps: HomeProps) {}
    componentDidMount() {}
    componentDidUpdate() {}
    componentWillUnmount() {}
    .....
    render() {
    return (
    <div className="top">
    <span>ZZ</span>
    <button>click</button>
    </div>
    )
    }
    }
    ReactDom.render(<Home />, document.querySelector(selectors: '#hostRoot'))

    以它为例创建fiber tree:

  1. 从根节点 #hostRoot出发,走到它的child节点 div.top
  2. div.top有两个子节点span和button ,先从左子树span开始
  3. span只有一个子节点 ZZ,到达树的底部后原路返回
  4. 返回到span时发现其有一个兄弟节点 button, 走去button
  5. button有一个子节点 click, 同样到达树的底部后原路返回到button
  6. button的右边没有兄弟了(若有,则继续重复4-5),返回到父节点 div.top
  7. 最后回到根节点 #hostRoot

再次setState或render时,构建workInProgress tree:

  1. 先把根节点 #hostRoot克隆出来,并用child属性指向它在fiber tree中的子节点div.top
  2. 把div.top从fiber tree中克隆出来,需要更新的话,加一个tag标志
  3. 更新div.top节点的状态,属性的指向等,并通过render获取到它的新子节点
  4. 尽量复用旧子节点span或button来创建新子节点的workInProgress结构
  5. 把新节点button做为下一个任务,调用requestIdleCallback获取当前帧所剩余的时间,如果还足够,就继续button的任务,重复2-4,否则,就等主线程空闲后再开始循环
  6. 当子节点submit中没有child属性的指向,表示已到达底部。会把diff时产生的effect list会merge回return属性指向的父节点button上
  7. 当父节点button没有兄弟节点时,会一直向上return回根节点,并把每个节点上产生的effect-list合并到根节点上。任务循环结束

我们再来看下源码中的reconcilation阶段:

  • workLoop阶段

首先获取到下一个任务的执行优先级,调用一个performUnitOfWork函数进入reconcilation阶段的工作, 分为beginWork和completeWork

1、beginWork

beginWork中只是处理节点的不同tag属性对应不同的更新方法。如 updateClassComponent方法对应的是tag类型是React组件实例。

reconcileChildren 实现对新老节点的diff。其内部主要是调用 reconcileChildFibers 方法。

React中diff是基于同层级节点的,节点的移动/插入/删除等操作都是在fiber tree的同一层级中进行。

2、 completeWork

completeWork的工作主要是通过新老节点的prop或tag等,收集节点的effect-list。然后向上一层一层循环,merge每个节点的effect-list,当到达根节点#hostRoot时,节点上包含所有的effect-list。并把effect-list传给pendingcommit,进入commit阶段。


  • Fiber对生命周期的影响
  // reconciliation阶段
componentWillMount
componentWillReceiveProps
shouldComponentUpdate
componentWillUpdate
--------------------------------
// commit阶段
componentDidMount
componentDidUpdate
componentWillUnmount

在我们之前的文章中也介绍过,在reconciliation阶段,生命周期函数会被多次调用,开发者慎重调用这个阶段的生命周期。

Reactv16.3已经开始了废弃计划:

  • 16.3版本:引入带UNSAFE_前缀的3个生命周期函数UNSAFE_componentWillMount,UNSAFE_componentWillReceiveProps和UNSAFE_componentWillUpdate,这个阶段新旧6个函数都能用。新引入生命周期函数getDerivedStateFromProps和getSnapshotBeforeUpdate,用来代替componentWillReceiveProps和componentWillUpdate
  • 16.3+版本:警告componentWillMount,componentWillReceiveProps和componentWillUpdate即将过时,这个阶段新旧6个函数也都能用,只是旧的在DEV环境会报Warning
  • 17.0版本:正式废弃componentWillMount,componentWillReceiveProps和componentWillUpdate,这个阶段只有新的带UNSAFE_前缀的3个函数能用,旧的不会再触发

本文只是分析了reconciliation阶段,不涉及commit阶段,感兴趣的同学可以研究下。

浅谈React16框架 - Fiber的更多相关文章

  1. 手撸ORM浅谈ORM框架之基础篇

    好奇害死猫 一直觉得ORM框架好用.功能强大集众多优点于一身,当然ORM并非完美无缺,任何事物优缺点并存!我曾一度认为以为使用了ORM框架根本不需要关注Sql语句如何执行的,更不用关心优化的问题!!! ...

  2. 手撸ORM浅谈ORM框架之Add篇

    快速传送 手撸ORM浅谈ORM框架之基础篇 手撸ORM浅谈ORM框架之Add篇 手撸ORM浅谈ORM框架之Update篇 手撸ORM浅谈ORM框架之Delete篇 手撸ORM浅谈ORM框架之Query ...

  3. 手撸ORM浅谈ORM框架之Update篇

    快速传送 手撸ORM浅谈ORM框架之基础篇 手撸ORM浅谈ORM框架之Add篇 手撸ORM浅谈ORM框架之Update篇 手撸ORM浅谈ORM框架之Delete篇 手撸ORM浅谈ORM框架之Query ...

  4. 手撸ORM浅谈ORM框架之Delete篇

    快速传送 手撸ORM浅谈ORM框架之基础篇 手撸ORM浅谈ORM框架之Add篇 手撸ORM浅谈ORM框架之Update篇 手撸ORM浅谈ORM框架之Delete篇 手撸ORM浅谈ORM框架之Query ...

  5. 手撸ORM浅谈ORM框架之Query篇

    快速传送 手撸ORM浅谈ORM框架之基础篇 手撸ORM浅谈ORM框架之Add篇 手撸ORM浅谈ORM框架之Update篇 手撸ORM浅谈ORM框架之Delete篇 手撸ORM浅谈ORM框架之Query ...

  6. 【SSH学习笔记】浅谈SSH框架

    说在前面 本学期我们有一门课叫做Java EE,由陈老师所授,主要讲的就是Java EE 中的SSH框架. 由于陈老师授课风格以及自己的原因导致学了整整一学期不知道在讲什么,所以才有了自己重新学习总结 ...

  7. 浅谈angular框架

    最近新接触了一个js框架angular,这个框架有着诸多特性,最为核心的是:MVVM.模块化.自动化双向数据绑定.语义化标签.依赖注入,以上这些全部都是属于angular特性,虽然说它的功能十分的强大 ...

  8. 浅谈可扩展性框架:MEF

    之前在使用Prism框架时接触到了可扩展性框架MEF(Managed Extensibility Framework),体验到MEF带来的极大的便利性与可扩展性. 此篇将编写一个可组合的应用程序,帮助 ...

  9. 13.Object-C--浅谈Foundation框架常用的结构体

    ------- android培训.iOS培训.期待与您交流! ---------- 昨天学习了Foundation框架中常用的结构体,下面我简单的总结一下,如果错误麻烦请留言指正,谢谢! Found ...

随机推荐

  1. Mac使用笔记大全

    1.mac中,怎么直接在当前文件夹打开终端? 步骤:(1)在键盘-快捷键-服务,勾选“新建位于文件夹位置的终端窗口”.(2)然后在需要打开终端的文件夹上,右键,“新建位于文件夹位置的终端窗口”即可. ...

  2. mysql 数据库的备份和还原

    1. 逻辑备份 (和存储引擎无关) mysqldump -uroot -p schoolDB TSubject > /mysqlbackup/schoolDB.TSubject.sql  (备份 ...

  3. 关于Hbuild引用mui常用代码块以及部分控件.

    MUI-最接近原生APP体验的高性能前端框架, 追求性能体验,是我们开始启动MUI项目的首要目标,轻量必然是重要特征: MUI不依赖任何第三方JS库,压缩后的JS和CSS文件仅有100+K和60+K, ...

  4. CSS绝对定位的原点:是在border上、padding上还是在content上?

    用了那么久的绝对定位,却一直没在意一个问题,就是绝对定位的原点,究竟是在盒模型的哪一处.今天想到这个问题,直接搜索没有找到标准文档,也没有搜索到相关的问题,于是决定自己动手实现一下看看,并把这个结果发 ...

  5. Python Day 10

    阅读目录: 函数 函数的使用 函数的分类 函数的返回值 ##函数 ##函数的定义-----what?----什么是函数: # 函数:完成 特定 功能的代码块,作为一个整体,对其进行特定的命名,该名字就 ...

  6. temp--内蒙农信(环境)

    规章制度篇: 1, 内蒙农信办公地址:    呼和浩特市赛罕区内蒙古自治区农村信用社联合社(陶利街) 农金大厦1201室. 2, 电子版蓝底照片(办饭卡,自己充钱) , 行里面吃饭标准  早餐8元.午 ...

  7. Unity加载AB资源

    using System.Collections; using System.Collections.Generic; using UnityEngine; public class AssetBun ...

  8. mysql 模糊查询条件带‘%’问题

  9. ajax轮询与长轮询

      刚刚网了关于轮询的知识,必须拿到自己这里来做个备份了! 其实以前用ajax轮询做个及时数据更新的,只是当时做了不知道那个就是轮询. 首先我们什么时候会想到用轮询技术呢? 一般而言,最多的是及时信息 ...

  10. RabbitMq相关

    RabbitMq 通过通过IP,Port等参数创建connection对象,然后实际上通信用的是channel,channel的建立基于connection RPC 调用: RPCClient通过ch ...