前言

react升级到16之后,架构发生了比较大的变化,现在不看,以后怕是看不懂了,react源码看起来也很麻烦,也有很多不理解的地方。

大体看了一下渲染过程。

react16架构的变化

react api的变化就不说了。react架构从stack变到了“fiber”。

最大的变化就是支持了任务帧,把各个任务都增加了优先级,同步和异步。比如用户输入input是优先级比较高的,它可以打断低优先级的任务。

比如再处理dom diff的时候耗时严重,fiber任务处理大概会有50ms的帧时长,超过这个时间就会先去看看有没高优任务去做。然后回来做低优先级任务。

  • 优先级高的任务可以中断低优先级的任务。然后再重新,注意是重新执行优先级低的任务。
  • 还增加了异步任务,调用requestIdleCallback api,浏览器空闲的时候执行。(不过用户操作默认是同步的,暂时还没开放这个特性)
  • dom diff树变成了链表,一个dom对应两个fiber(一个链表),对应两个队列,这都是为找到被中断的任务,重新执行而设计的。

设计成链表之后就更方便了。递归改循环,结点关系更好维护了,看下图就一目了然。

渲染流程

代码太多,就不贴出来了,整理了一个图。这里就对图说下流程。(以下是个人理解,欢迎指正)

这篇源码分析还是挺不错的(http://zxc0328.github.io/2017/09/28/react-16-source/)

调用setState后,其实是调用了this.updater.enqueueSetState,这个函数首先从全局拿到React组件实例对应的fiber,然后拿到了fiber的优先级。最后调用了addUpdate向队列中推入需要更新的fiber,并调用scheduleUpdate触发调度器调度一次新的更新。

setState其实是把任务推到队列里,然后调用调度器处理更新

addUpdate

然后看下addUpdate,其实是把任务封装成update,然后把update推入updateQueue,

type Update = {
priorityLevel: PriorityLevel,
partialState: PartialState<any, any>,
callback: Callback | null,
isReplace: boolean,
isForced: boolean,
isTopLevelUnmount: boolean,
next: Update | null,
}; type UpdateQueue = {
first: Update | null,
last: Update | null,
hasForceUpdate: boolean,
callbackList: null | Array<Callback>, // Dev only
isProcessing?: boolean,
};

每个react 结点都有2个fiber链表,一个叫current fiber,一个叫alternate fiber,而每个链表又对应两个updateQueue。

而currentFiber.alternate = alternateFiber; alternateFiber.alternate = currentFiber。通过alternate属性连接起来。初始化的时候,alternate fiber是current fiber 的clone。

处理diff的时候,操作的是alternateFiber,处理完diff,让currentFiber = alternateFiber;这样一个处理就完成了。

如果被高优插入,current queue的存在就可以做备份,需要继续处理。

insertUpdate函数,将update按优先级插入这两个queue。

到此,一般工作做完了。另一半就是react的核心,调度算法了。

scheduleUpdate

fiber的调度是一个链表,从当前的结点,一直遍历到根结点(每个fiber都有fiber.return,它的值是它的父元素)。fiber的调度是从根结点进行的。遍历过程中会更新父结点的pendingWorkPriority(当前fiber的优先级)。标志这个结点上等待更新的事务的优先级。

当遍历到HostRoot(根结点)时,开始真正的调度工作。

会根据任务的优先级来决定是不是执行这次更新,

  • 如果优先级为sychronousPriority(比如用户输入), 就执行同步更新,立马执行。
  • 如果是TaskPriority,它的执行一般是在batchUpdate里面进行更新。(比如我回调里执行setSate,它的任务优先级就是TaskPriority),它会在dispatch的callback里面等callback执行完再执行更新。这样是对应setstate的batchUpdate。后面再讲这部分。如果是TaskPriority,直接return。
  • 如果不是这两个优先级的话,表明这次更新是异步的,那就在浏览器空闲的时间处理它。调用的是scheduleDeferredCallback,会根据平台注入函数,比如浏览器的api是requestIdleCallback,如果浏览器不支持react还有个polyfill。

我们继续跟着同步任务处理走,同步任务调用performWork:

performWork

这里会开始任务的处理,调度完成。

performWork的作用就是“刷新”待更新队列,执行待更新的事务。

处理逻辑主循环是workLoop,也会调用一次scheduleDeferredCallback,未来处理一次更新,来完成workLoop未完成的任务。

workLoop

如果没有alternate fiber首先会根据update queue创建fiber,进入的是createWorkInProgress函数。createWorkInProgress 这个函数会构建一颗树的顶端,赋值给全局变量 nextUnitOfWork ,通过迭代的方式,不断更新 nextUnitOfWork 直到遍历完所有树的节点。

然后就会处理帧任务,进入函数首要处理就是看下上次处理有没有没被处理的任务,然后根据时间片处理任务。

如果任务在deadline之前没有commit,就会被标记pendingCommit,workLoop首先会处理pendingCommit。

接下来是处理逻辑:

  • 如果下一个update是同步的,就调用performUnitOfWork进行reconcilation,最后调用commitAllWork把它渲染到dom。
  • 解决完同步的任务,这时候看下时间片,有没有超时,如果没有,调用performUnitOfWork执行异步任务。
  • 如果还有时间就commitAllWork,如果没有时间就留到下一帧提交。

react16 将任务的处理分成reconcilation和render,reconcilation包括dom diff,处理react组件的属性和钩子等。单独把render到dom抽出来,以便跨平台。

一次更新是同步还是异步是由优先级决定的,异步渲染默认是关闭的。用户代码的优先级是同步的。

performUnitOfWork

performUnitOfWork是执行的reconcilation阶段,它又拆分成beginWork和complete,beginWork做的是产出effect list,complete做的是处理react实例的生命周期和处理属性等操作。

effect list是dom diff的产出,其实是被打过tag的diff数组。render阶段会根据effect list render到dom

beginWork

beginWork就会根据不同的fiber tag做不同的处理,比如ClassComponent(React组件实例)调用updateClassComponent,hostComponent(真实dom)调用updateHostComponent。

updateClassComponent

如果为空,初始化一个react组件,调用组件的componentWillMount,如果新老props不一样,调用componentWillReceiveProps。这时候会检测shouldComponentUpdate,如果为true,就调用实例的render渲染出children,然后调用reconcileChildren对dom进行diff。

reconcileChildren其实是调用的reconcileChildrenArray。进行大名鼎鼎的dom diff操作。

updateHostComponent

真实dom直接进行reconcileChildrenArray dom diff

reconcileChildrenArray(dom diff)

React的reconcile算法采用的是层次遍历,然后在层次上面进行简化的两端比较法,因为fiber树是单链表结构,没有子节点数组这样的数据结构。也就没有可以供两端同时比较的尾部游标。所以React的这个算法是一个简化的两端比较法,只从头部开始比较。

从头部遍历。第一次遍历新数组,对上了,新老index都++,比较新老数组哪些元素是一样的,(通过updateSlot,比较key),如果是同样的就update。第一次遍历完了,如果新数组遍历完了,那就可以把老数组中剩余的fiber删除了。

如果老数组完了新数组还没完,那就把新数组剩下的都插入。

如果这些情况都不是,就把所有老数组元素按key放map里,然后遍历新数组,插入老数组的元素,这是移动的情况。

最后再删除没有被上述情况涉及的元素

completeUnitOfWork

completeUnitOfWork是reconcile的complete阶段,主要是更新props和调用生命周期方法等等。

主要的逻辑是调用completeWork完成收尾,然后将当前子树的effect list插入到HostRoot的effect list中(这块主要是收集effect tag到顶端的effect,然后直接render,不需要遍历链表了)

completeWork

这里也是根据不同的fiber tag进行对应的处理。

主要是HostComponent的处理,检查结点是不是需要更新,如果需要就打个tag,标记为update的side-effect。更新新老ref。

commitAllWork

这块就是render到dom的操作了。根据dom diff产生的effect list进行dom的增删改。还会调用一些生命周期,比如删除组件调用componentWillUnmount。

如果effect发生在ClassComponent上,调用组件的componentDidMount

componentDidUpdate。

在render完暴露钩子以便调用包括

componentDidMount、componentDidUpdate和componentWillUnmount

到这全部流程就结束了。

带来的问题

  • render是不会被打断的,可以被打断的只有reconcilation阶段,而且被打断之后,低优先级的任务是重新执行,这就导致了reconcilation阶段的一些操作会被多次执行。

    reconcilation阶段的react生命周期函数不能保证只被调用一次,所以相关逻辑需要整理。

  • 如果高优先级的任务一直存在,那么低优先级的任务则永远无法进行,组件永远无法继续渲染。

  • 因为以上两个问题,到现在为止react16还不敢直接开启默认渲染。

fiber对象

为帮助理解,这里写一下fiber对象。

const Fiber = {
tag: HOST_COMPONENT,
type: 'div',
return: parentFiber,
child: childFiber,
sibling: null,
alternate: currentFiber,
stateNode: document.createElement('div') | instance,
props: { children: [], className: 'foo' },
partialState: null,
effectTag: PLACEMENT,
effects: [],
firstEffect: null,
lastEffect: null
}

return,child,sibling都标识着该结点的下个结点的连接,链表就是通过这3个属性遍历的。

effectTag 和 effects 这两个属性为的是记录每个节点 Diff 后需要变更的状态,比如删除,移动,插入,替换,更新等

alternate就是之前提过的连接两个fiber 链表的属性,通过这个属性获得两个链表

stateNode,用于记录当前 fiber 所对应的真实 DOM 结点 或者 当前虚拟组件的实例,这么做的原因第一是为了实现 Ref ,第二是为了实现 DOM 的跟踪。

firstEffect 、lastEffect 等是用来保存中断前后 effect 的状态,用户中断后恢复之前的操作。

tag:

export type TypeOfWork = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10;

export const IndeterminateComponent = 0; // 尚不知是类组件还是函数式组件
export const FunctionalComponent = 1; // 函数式组件
export const ClassComponent = 2; // Class类组件
export const HostRoot = 3; // 组件树根组件,可以嵌套
export const HostPortal = 4; // 子树. Could be an entry point to a different renderer.
export const HostComponent = 5; // 标准组件,如地div, span等
export const HostText = 6; // 文本
export const CallComponent = 7; // 组件调用
export const CallHandlerPhase = 8; // 调用组件方法
export const ReturnComponent = 9; // placeholder(占位符)
export const Fragment = 10; // 片段

fiber优先级

同步任务包括SynchronousPriority和TaskPriority,剩下的异步任务会在接下来的几帧完成。

{
NoWork: 0, // No work is pending.
SynchronousPriority: 1, // For controlled text inputs. Synchronous side-effects.
TaskPriority: 2, // Completes at the end of the current tick.
HighPriority: 3, // Interaction that needs to complete pretty soon to feel responsive.
LowPriority: 4, // Data fetching, or result from updating stores.
OffscreenPriority: 5, // Won't be visible but do the work in case it becomes visible.
}

QA

fiber的异步任务挂起和恢复?任务调度的粒度?

任务的调度是在workLoop中进行的,workLoop首先会判断有没有之前没有commitAll的(这块其实在做渲染),没有的话就看下时间,执行performUnitOfWork。(这里还会判断一下如果没有下个工作的结点了,就判断一下再commitAll)。这是挂起的过程,直接return掉。粒度显而易见就是每个performUnitOfWork函数,每个结点的performUnitOfWork就是最小工作粒度。

任务的恢复是在调用workLoop之后,判断一下之前的任务有没有被return。然后调用scheduleDeferredCallback来执行一下workLoop来解决之前没被处理的事务。

如何进行批量的setstate处理?异步setstate?

比如

 handleClick = (e) => {
e.stopPropagation();
this.setState({
title: 'click2'
})
this.setState({
title: 'click3'
})
this.setState({
title: 'click4'
})
}

触发 dispatchEvent 回调函数的处理过程中,会执行到 batchedUpdates。

function batchedUpdates(fn, a) {
var previousIsBatchingUpdates = isBatchingUpdates; // 批量处理
isBatchingUpdates = true;
try {
return fn(a); // // 此过程中可能改变state所以需要再performWork
} finally {
isBatchingUpdates = previousIsBatchingUpdates;
if (!isBatchingUpdates && !isRendering) {
performWork(Sync, null);
}
}
}

isBatchingUpdates被置为true,这三个 setState 都不能立即执行performWork。

if (isBatchingUpdates) {
if (isUnbatchingUpdates) {
nextFlushedRoot = root;
nextFlushedExpirationTime = Sync;
performWorkOnRoot(nextFlushedRoot, nextFlushedExpirationTime);
}
return;
}
if (expirationTime === Sync) {
performWork(Sync, null);
} else {
scheduleCallbackWithExpiration(expirationTime);
}

而会在这个callback执行之后调用performWork,这样就完成了batch 处理。

为什么settimeout可以跳过batchUpdate呢?

setTimeout 的回调函数须等 dispatchEvent 函数执行完,也就是要等到 performWork 执行,然而在 performWork 中, nextFlushedRoot 为 null , while 循环无法进行。

function performWork () {
while (nextFlushedRoot !== null && nextFlushedExpirationTime !== NoWork && (minExpirationTime === NoWork || nextFlushedExpirationTime <= minExpirationTime) && !deadlineDidExpire) {
performWorkOnRoot(nextFlushedRoot, nextFlushedExpirationTime);
// Find the next highest priority work.
findHighestPriorityRoot();
}
}

执行 setTimeout 的回调函数时, isBatchingUpdates 已经变为 false,所以每次 setState 都会触发 performWork 。

回顾

这里总结一下react的渲染流程:把新state push 到2个fiber queue里面,通过alternate queue构建一个alternate fiber之后所有的tag都打在这个fiber上面,遍历到根结点,从根结点进行dom diff的打tag处理。打完tag,收集tag(收集到顶端的currentFiber.effect上),收集的tag列表(effect List)。到此进入render过程,直接根据effect List操作dom。到此完成。

react16 渲染流程的更多相关文章

  1. NGUI渲染流程

    1 渲染流程 NGUI的渲染流程其实就是把Widget组件生成Mesh所需要的缓存数据,然后生成对应的DrallCall组合对应数据,生成渲染需要的Mesh数据,提交渲染. Widget(数据) UI ...

  2. cocos2d-x渲染流程

    Cocos2Dx之渲染流程 发表于8个月前(2014-08-08 22:46)   阅读(3762) | 评论(2) 17人收藏此文章, 我要收藏 赞2 如何快速提高你的薪资?-实力拍“跳槽吧兄弟”梦 ...

  3. NGUI 渲染流程深入研究 (UIDrawCall UIGeometry UIPanel UIWidget)

    上图是一个简要的NGUI的图形工作流程,UIGeometry被UIWidget实例化之后,通过UIWidget的子类,也就是UISprit,UILabel等,在OnFill()函数里算出所需的Geom ...

  4. webview渲染流程

    文档标记说明 ################# 消息边界 +++++++++++++++++ 区域分隔 $$$$$$$$$$$$$$$$$ 线程边界 ~~~~~~~~~~~~~~~~~ 进程边界 - ...

  5. D3D渲染流程--转载

    http://www.cnblogs.com/ixnehc/articles/1282350.html 先从最基础的写起吧,关于Device的渲染流程. D3D9的Device就是D3D给我们提供的一 ...

  6. 【Stage3D学习笔记续】山寨Starling(三):Starling核心渲染流程

    这篇文章我们剔除Starling的Touch事件体系和动画体系,专门来看看Starling中的渲染流程实现,以及其搭建的显示列表结构. 由于Starling是模仿Flash的原生显示列表,所以我们可以 ...

  7. Ogre内部渲染流程分析系列

    come from:http://blog.csdn.net/weiqubo/article/details/6956005 要理解OGRE引擎,就要理解其中占很重要位置的 Renderable接口, ...

  8. mapbox.gl源码解析——基本架构与数据渲染流程

    加载地图 Mapbox GL JS是一个JavaScript库,使用WebGL渲染交互式矢量瓦片地图和栅格瓦片地图.WebGL渲染意味着高性能,MapboxGL能够渲染大量的地图要素,拥有流畅的交互以 ...

  9. Cocos2dx开发之运行与渲染流程分析

    学习Cocos2dx,我们都知道程序是由 AppDelegate 的方法 applicationDidFinishLaunching 开始,在其中做些必要的初始化,并创建运行第一个 CCScene 即 ...

随机推荐

  1. git 取消对某个文件的跟踪

    git rm --cached <fileName> 例如 : git add 后,不想提交在status里的.idea/vcs.xml.操作如下:

  2. Linux学习历程——SUID、SGID、SBIT简介

    一.SUID.SGID.SBIT简介 SUID:对一个可执行文件,不是以发起者身份来获取资源,而是以可执行文件的属主身份来执行.SGID:对一个可执行文件,不是以发起者身份来获取资源,而是以可执行文件 ...

  3. NTFS权限和共享权限的区别

    共享权限 共享权限有三种:完全控制.更改.读取 共持本地安全性.换句话说,他在同一台计算机上以不同用户名登录,对硬盘上同一文件夹可以有不同的访问权限. 注意:NTFS权限对从网络访问和本机登录的用户都 ...

  4. Linux(Manjaro) - IntelliJ IDEA (JetBrains) 字体模糊解决方法

    Linux(Manjaro) - IntelliJ IDEA 字体模糊解决方法 解决方法非常简单, 只要安装 JetBrains 提供的 jre 即可 使用 Octopi 或者 pacman 安装名为 ...

  5. mysql 分组内 排序

    mysql 分组内 排序 类似于 sqlserver over partition by   因为mysql中木有sqlserver over partition by这个函数,要从sqlserver ...

  6. VUE的一个数据绑定与页面刷新相关的bug

    1.场景: N层嵌套的循环查询业务场景,框架是vue.其中在最后一层查完之后,还需要查其中每一项的两个属性,类型都是列表.查完之后将其赋值给一个变量用于页面展示.代码如下: (1)异常代码: getS ...

  7. 折半、快排、插入排序的Java实现

    插入排序 import java.util.Arrays; public class InsertionSort { /** * 对数组里面进行插入排序 * 参数1 数组 * 参数2 数组大小 */ ...

  8. VSCode 配置并使用less

    1.安装node.js 2.安装less npm install less -g 3.安装VS插件 Easy LESS 4.编辑保存.less文件,会在对应的目录下生成对应的.css文件

  9. Linux 下操作Mysql指令的总结 远程连接的设置

    参考博客:https://www.cnblogs.com/liaocheng/p/4243579.html (常用命令) https://www.cnblogs.com/zhangzhu/archiv ...

  10. 关于win7+VS2017环境下的opencv-contirb配置的一个坑

    问题出现背景: 由于课题需要用到SURF detector, 我依照网上的一下教程,把opencv-contrib的配置了一遍.但是,当我写了一个小demo来测试模块是否能正常使用的时候,程序能正常编 ...