前言

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 onlyisProcessing?: 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。到此完成。

转载于:https://www.cnblogs.com/dh-dh/p/9361555.html

react16 渲染流程相关推荐

  1. 5渲染判断if_React 16 渲染流程

    学过微机的同学都应该很熟悉「中断」这个概念: CPU 正常运行程序时,内部事件或外设提出中断请求: CPU 予以响应,同时保护好 CPU 执行主程序的现场,转入调用中断服务程序: 调用完毕后恢复现场. ...

  2. 【逆向】UE4 渲染流程分析

    UE4作为当今商业引擎界的大佬,渲染和图形质量一直是首屈一指的水准,但是相对于unity来说UE4基本上是一套完整方案提供,不通过源码修改对渲染进行定制的可能性比较小,而且同时UE4这方面的文档很少, ...

  3. COCOS学习笔记--Cocos引擎渲染流程

    近期在研究Cocos引擎的渲染流程.在这里将其整个渲染流程进行一下梳理: 梳理之前我们要知道一些东西,就是我们的Cocos引擎是通过使用OpenGL的一些API来进行渲染绘制的,所以假设我们要彻底理解 ...

  4. cocos2d-x游戏引擎核心(3.x)----启动渲染流程

    (1) 首先,这里以win32平台下为例子.win32下游戏的启动都是从win32目录下main文件开始的,即是游戏的入口函数,如下: #include "main.h" #inc ...

  5. 【Android 应用开发】Paint 渲染 之 BitmapShader 位图渲染 ( 渲染流程 | CLAMP 拉伸最后像素 | REPEAT 重复绘制图片 | MIRROR 绘制反向图片 )

    文章目录 1. 位图渲染 BitmapShader 简介 ( 1 ) 位图渲染综述 ( ① 三种方式 : Shader.TileMode.CLAMP | Shader.TileMode.REPEAT ...

  6. html将页面分成三块_导航渲染流程你真的知道从输入URL到页面展示发生了什么吗?(内附思维导图)...

    导航渲染流程 通过这篇文章当你被问到从URL输入到页面展示都发生了什么的时候,基本都能对答如流,甚至可以一直深入的说,说到面试官闭麦哈哈哈~ 以下是本文的思维导图,直接拿图「点个赞」再走吧 ~ 求求了 ...

  7. OpenGL ES之基本简介和渲染流程

    简介 OpenGL ES (OpenGL for Embedded Systems) 是以⼿持和嵌入式为目标的高级3D图形应用程序编程接口(API). OpenGL ES是目前智能手机中占据统治地位的 ...

  8. [转贴]Cocos2d-x3.2与OpenGL渲染总结(一)Cocos2d-x3.2的渲染流程

    看了opengles有一段时间了,算是了解了一下下.然后,就在基本要决定还是回归cocos2dx 3.2的,看了这篇好文章,欣喜转之~ 推荐看原帖: Cocos2d-x3.2与OpenGL渲染总结(一 ...

  9. 【干货】十分钟读懂浏览器渲染流程

    在之前写过的一篇<"天龙八步"细说浏览器输入URL后发生了什么>一文中,和大家分享了从在浏览器中输入网址URL到最终页面展示的整个过程.部分读者向我反馈对于最后的浏览器 ...

最新文章

  1. IDC:预计全球人工智能支出将在四年内翻一番
  2. 4天快速入门python数据挖掘_4天快速入门Python数据挖掘
  3. Lucene in action 笔记 analysis篇
  4. PHP几个快速读取大文件例子
  5. 比尔盖茨,马斯克、霍金都告诉你:为什么要警惕人工智能(中)
  6. Oracle RESETLOGS 和 NORESETLOGS 区别说明
  7. 半双工、全双工以太网
  8. 如何在 Eclipse 中使用命令行
  9. 上海梵科信息科技有限公司
  10. fatal: ‘origin‘ does not appear to be a git repository fatal: Could not read from remote repository.
  11. Golang 结构类型
  12. python npy文件与mat文件的保存与读取
  13. 小学信息技术信息与计算机课件,小学信息技术ppt课件
  14. 记录一次pre环境OOM异常解决过程
  15. 聊聊Uber公司迁移数据库这件事
  16. Ubuntu18上基于udev实现U盘热插拔+自动化处理业务功能
  17. 转载 Lua xpcall
  18. 软件测试查漏补缺(一)——驱动开发、系统测试、测试工具
  19. 团灭了3个月的线下营销,还有希望吗?
  20. 年轻人必备的几个资源订阅号

热门文章

  1. SAP QM Multiple Specifications的使用 I
  2. 大国AI竞赛中国忙赶超,但美国仍主导全球AI芯片设计
  3. PM配置详解之二:工厂维护和客户服务中心的主数据
  4. 四种常见NLP框架使用总结
  5. 云从科技上交大提出DCMN+ 模型,在多项阅读理解数据集上成绩领先
  6. 人工智能艺术:一场前所未有的新艺术创造
  7. 人工智能不是单纯的经验和总结
  8. SAP MM 盘点事务中的序列号
  9. 综述丨七场高端报告,带你大视角看人工智能发展
  10. 斯坦福年度AI报告:人工智能全面逼近人类能力