前言

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. bootstrap-table 踩坑手记

    官方文档: http://bootstrap-table.wenzhixin.net.cn/zh-cn/documentation/ 渲染表格有两种方法: 1)data属性渲染表格 2)js代码渲染表 ...

  2. 修改oracle的字符集操作方法

    cmd环境下进行以下命令行的操作--连接sqlplus / as sysdba--命令行shutdown immediate; startup mount ALTER SYSTEM ENABLE RE ...

  3. MongoDB学习记录(四) - MongoDB的"增查改删"操作之"改"

    更新文档主要有以下几种方法: db.collection.updateOne(filter, update, options) db.collection.updateMany(filter, upd ...

  4. Sublime 中文乱码问题

    今天在Windows上使用Sublime Text 3的时候,发现一些txt文本打开以后,中文都是乱码.于是搜了一下,找到了解决方案. 步骤: 在Sublime Text里,按ctrl+`,打开Con ...

  5. H5新特性--WebStorage--WebSocke

    今天的目标 3.2:h5新特性--WebStorage localStorage  在客户端浏览器保存数据 永久保存 保存数据 localStorage [key] = value 保存数据 loca ...

  6. 招聘ETL开发工程师

    上班地点徐汇 本科以上学历 3年以上ETL开发经验熟悉Oracle数据库,精通PL  SQL开发与优化,熟悉Vertica或者GreenPlum库优先 熟悉数据库性能优化,有海量数据处理经验优先 自荐 ...

  7. INTERVAL YEAR TO MONTH数据类型

    INTERVAL YEAR TO MONTH数据类型 Oracle语法: INTERVAL 'integer [- integer]' {YEAR | MONTH} [(precision)][TO ...

  8. ios  国际化开发

    一,.xib 1.首先选中xib文件,在右边的inspector中选择对应的国际化语言,如下图

  9. 值得推荐的C/C++框架和库 (真的很强大)〔转〕

    http://m.blog.csdn.net/article/details?id=42541419

  10. java开发师笔试面试每日12题(2)

    1.Volatile和Synchronized不同点 (1).volatile只能作用于变量,使用范围较小.synchronized可以用在变量.方法.类.同步代码块等,使用范围比较广. (2).vo ...