React绝地反杀之万字长文彻底搞懂渲

北京荨麻疹主治医院 http://baidianfeng.39.net/a_zhiliao/210116/8595950.html

转前端好多年了,平时接触最多的框架就是React。在熟悉了其用法之后,避免不了想深入了解其实现原理,网上相关源码分析的文章挺多的,但是总感觉不如自己阅读理解来得深刻。于是话了几个周末去了解了一下常用的流程。也是通过这篇文章将自己的个人理解分享出来。

在具体的源码流程分析之前,根据个人理解,结合网上比较好的文章,先来分析一些概念性的东西。后续再分析具体的流程逻辑。

React15架构分层

React15版本(Fiber以前)整个更新渲染流程分为两个部分:

Reconciler(协调器);负责找出变化的组件Renderer(渲染器);负责将变化的组件渲染到页面上Reconciler

在React中可以通过setState、forceUpdate、ReactDOM.render来触发更新。每当有更新发生时,Reconciler会做如下工作:

调用组件的render方法,将返回的JSX转化为虚拟DOM将虚拟DOM和上次更新时的虚拟DOM对比通过对比找出本次更新中变化的虚拟DOM通知Renderer将变化的虚拟DOM渲染到页面上Renderer

在对某个更新节点执行玩Reconciler之后,会通知Renderer根据不同的"宿主环境"进行相应的节点渲染/更新。

React15的缺陷

React15的diff过程是递归执行更新的。由于是递归,一旦开始就"无法中断"。当层级太深或者diff逻辑(钩子函数里的逻辑)太复杂,导致递归更新的时间过长,Js线程一直卡主,那么用户交互和渲染就会产生卡顿。看个例子:count-demo

buttonclickbuttonli1li-lililili-li4lili3li-li6li

当点击button后,列表从左边的1、、3变为右边的、4、6。每个节点的更新过程对用户来说基本是同步,但实际上他们是顺序遍历的。具体步骤如下:

点击button,触发更新Reconciler检测到需要变更为,则立刻通知Renderer更新DOM。列表变成、、3Reconciler检测到需要变更为,通知Renderer更新DOM。列表变成、4、3Reconciler检测到需要变更为,则立刻通知Renderer更新DOM。列表变成、4、6

从此可见Reconciler和Renderer是交替工作的,当第一个节点在页面上已经变化后,第二个节点再进入Reconciler。由于整个过程都是同步的,所以在用户看来所有节点是同时更新的。如果中断更新,则会在页面上看见更新不完全的新的节点树!

假如当进行到第步的时候,突然因为其他任务而中断当前任务,导致第3、4步无法进行那么用户就会看到:

buttonclickbuttonli1li-lililili-lilili3li-li3li

这种情况是React绝对不希望出现的。但是这种应用场景又是十分必须的。想象一下,用户在某个时间点进行了输入事件,此时应该更新input内的内容,但是因为一个不在当前可视区域的列表的更新导致用户的输入更新被滞后,那么给用户的体验就是卡顿的。因此React团队需要寻找一个办法,来解决这个缺陷。

React16架构分层

React15架构不能支撑异步更新以至于需要重构,于是React16架构改成分为三层结构:

Scheduler(调度器);调度任务的优先级,高优任务优先进入ReconcilerReconciler(协调器);负责找出变化的组件Renderer(渲染器);负责将变化的组件渲染到页面上Scheduler

React15对React16提出的需求是Diff更新应为可中断的,那么此时又出现了两个新的两个问题:中断方式和判断标准;

React团队采用的是合作式调度,即主动中断和控制器出让。判断标准为超时检测。同时还需要一种机制来告知中断的任务在何时恢复/重新执行。React借鉴了浏览器的requestIdleCallback接口,当浏览器有剩余时间时通知执行。

由于一些原因React放弃使用rIdc,而是自己实现了功能更完备的polyfill,即Scheduler。除了在空闲时触发回调的功能外,Scheduler还提供了多种调度优先级供任务设置。

Reconciler

在React15中Reconciler是递归处理VirtualDOM的。而React16使用了一种新的数据结构:Fiber。VirtualDOM树由之前的从上往下的树形结构,变化为基于多向链表的"图"。

更新流程从递归变成了可以中断的循环过程。每次循环都会调用shouldYield()判断当前是否有剩余时间。源码地址。

functionworkLoopConcurrent(){//PerformworkuntilSchedulerasksustoyieldwhile(workInProgress!==null!shouldYield()){workInProgress=performUnitOfWork(workInProgress);}}

前面有分析到React15中断执行会导致页面更新不完全,原因是因为Reconciler和Renderer是交替工作的,因此在React16中,Reconciler与Renderer不再是交替工作。当Scheduler将任务交给Reconciler后,Reconciler只是会为变化的VirtualDOM打上代表增/删/更新的标记,而不会发生通知Renderer去渲染。类似这样

exportconstPlacement=/**/0b0;exportconstUpdate=/**/0b0;exportconstPlacementAndUpdate=/**/0b0;exportconstDeletion=/**/0b0;

只有当所有组件都完成Reconciler的工作,才会统一交给Renderer进行渲染更新。

Renderer(Commit)

Renderer根据Reconciler为VirtualDOM打的标记,同步执行对应的渲染操作。对于我们在上一节使用过的例子,在React16架构中整个更新流程为:

setState产生一个更新,更新内容为:state.count从1变为更新被交给Scheduler,Scheduler发现没有其他更高优先任务,就将该任务交给ReconcilerReconciler接到任务,开始遍历VirtualDOM,判断哪些VirtualDOM需要更新,为需要更新的VirtualDOM打上标记Reconciler遍历完所有VirtualDOM,通知RendererRenderer根据VirtualDOM的标记执行对应节点操作

其中步骤、3、4随时可能由于如下原因被中断:

有其他更高优先任务需要先更新当前帧没有剩余时间

由于Scheduler和Reconciler的工作都在内存中进行,不会更新页面上的节点,所以用户不会看见更新不完全的页面。

Diff原则

React的Diff是有一定的前提假设的,主要分为三点:

DOM跨层级移动的情况少,对VirtualDOM树进行分层比较,两棵树只会对同一层次的节点进行比较。不同类型的组件,树形结构不一样。相同类型的组件树形结构相似同一层级的一组子节点操作无外乎更新、移除、新增,可以通过唯一ID区分节点

无论是JSX格式还是React.createElement创建的React组件最终都会转化为VirtualDOM,最终会根据层级生成相应的VirtualDOM树形结构。React15每次更新会成新的VirtualDOM,然后通递归的方式对比新旧VirtualDOM的差异,得到对比后的"更新补丁",最后映射到真实的DOM上。React16的具体流程后续会分析到

源码分析

React源码非常多,而且16以后的源码一直在调整,目前Github上最新源码都是保留xxx.new.js与xxx.old.js两份代码。react源码是采用Monorepo结构来进行管理的,不同的功能分在不同的package里,唯一的坏处可能就是方法地址索引起来不是很方便,如果不是对源码比较熟悉的话,某个功能点可能需要通过关键字全局查询然后去一个个排查。开始之前,可以先阅读下官方的这份阅读指南

因为源码实在是太多太复杂了,所有我这里尽可能的最大到小,从面到点的一个个分析。大致的流程如下:

首先得知道通过JSX或者createElement编码的代码到底会转成啥然后分析应用的入口ReactDOM.render接着进一步分析setState更新的流程最后再具体分析Scheduler、Reconciler、Renderer的大致流程

触发渲染更新的操作除了ReactDOM.render、setState外,还有forceUpdate。但是其实是差不多的,最大差异在于forceUpdate不会走shouldComponentUpdate钩子函数。

数据结构Fiber

开始正式流程分析之前,希望你对Fiber有过一定的了解。如果没有,建议你先看看这则视频。然后,先来熟悉下ReactFiber的大概结构。

exporttypeFiber={//任务类型信息;//比如ClassComponent、FunctionComponent、ContextProvidertag:WorkTag,key:null

string,//reactElement.type的值,用于reconciliation期间的保留标识。elementType:any,//fiber关联的function/classtype:any,//any类型!!一般是指Fiber所对应的真实DOM节点或对应组件的实例stateNode:any,//父节点/父组件return:Fiber

null,//第一个子节点child:Fiber

null,//下一个兄弟节点sibling:Fiber

null,//变更状态,比如删除,移动effectTag:SideEffectTag,//用于链接新树和旧树;旧-新,新-旧alternate:Fiber

null,//开发模式mode:TypeOfMode,//...};FiberRoot

每一次通过ReactDom.render渲染的一棵树或者一个应用都会初始化一个对应的FiberRoot对象作为应用的起点。其数据结构如下ReactFiberRoot。

typeBaseFiberRootProperties={//Thetypeofroot(legacy,batched,concurrent,etc.)tag:RootTag,//root节点,ReactDOM.render()的第二个参数containerInfo:any,//持久更新会用到。react-dom是整个应用更新,用不到这个pendingChildren:any,//当前应用root节点对应的Fiber对象current:Fiber,//当前更新对应的过期时间finishedExpirationTime:ExpirationTime,//已经完成任务的FiberRoot对象,在


转载请注明:http://www.aierlanlan.com/cyrz/563.html

  • 上一篇文章:
  •   
  • 下一篇文章: