前言

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. map获取数字与int比较

    已知 map.get("id")为数字,如:123问题 id.equals(123) 结果为false而使用 int id = (Integer)map.get("id& ...

  2. MySQL忘记密码怎么修改密码

    MySQL的 root 帐号密码默认为空,经常都有修改密码后忘记密码的事.如果忘记了root 帐号密码,那该怎么修改密码呢?这里有一个可行的方法,就是在MySQL安全模式下(跳过权限检查)修改密码的方 ...

  3. CommonsChunkPlugin VS SplitChunksPlugin

    等了好久终于等到你, webpack团队人员卧薪尝胆五个多月的时间终于带来的webpack4.0,个人觉得webpack4带来的最大优化便是对于懒加载块拆分的优化,删除了CommonsChunkPlu ...

  4. JAVA获取微信小程序openid和获取公众号openid,以及通过openid获取用户信息

    一,首先说明下这个微信的openid 为了识别用户,每个用户针对每个公众号会产生一个安全的OpenID,如果需要在多公众号.移动应用之间做用户共通,则需前往微信开放平台,将这些公众号和应用绑定到一个开 ...

  5. 浅谈nodejs和php

    现在,Web开发公司和开发人员可以选择多种技术栈来构建Web应用程序.早期网络发展,不同的技术被用于前端和后端开发.但是,随着Node.js的发布,布局发生了变化,因为它允许开发人员使用 JavaSc ...

  6. JQuery跳出each循环的方法

    一.jquery each循环,要实现break和continue的功能: break----用return false; continue --用return ture; 二.jquery怎么跳出当 ...

  7. 2019.03.29 bzoj5463: [APIO2018] 铁人两项(圆方树+树形dp)

    传送门 题意简述:给你一张无向图,问你满足存在从a−>b−>ca->b->ca−>b−>c且不经过重复节点的路径的有序点对(a,b,c)(a,b,c)(a,b,c) ...

  8. git撤销commit-hard

    场景: 不小心commit了一个不应该commit的修改,但是还没有push,想撤销那个commit 命令: a)git log b)git reset --hard commit_id 具体步骤如下 ...

  9. 分享几个写 demo 的思路

    好久没有动笔,最近发现了一个新的写 demo 的思路,非常有意思.仔细一想,自己仿佛积累了不少写 demo 的思路和想法,总结一下,抛砖引玉. 本文所说 demo 主要分以下三种: 本地 demo 外 ...

  10. ubuntu16 gitlab的简单安装

    1.安装好ubuntu的ssh服务,使用xshell登录虚拟机 2.下载安装包:  wget -c https://downloads-packages.s3.amazonaws.com/ubuntu ...