转自React技术揭秘

React15

React15架构可以分为2层:

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

Reconciler(协调器)

react是通过this.setState,this.forceUpdate,ReactDOM.renderAPI触发更新的。
每当有更新发生时,Reconciler会做如下工作:

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

Renderer(渲染器)

由于react支持跨平台,所以不同平台有不同的Renderer,浏览器的是ReactDOM
由于用递归执行,所以没办法中断,当层级很深时,递归更新时间超过了16ms,用户交互就会卡顿

state=1;
<li>{state.count}</li>//<li>1</li>
<li>{state.count*2}</li>//<li>2</li>

当点一个state+1时更新步骤:

  1. Reconciler发现1需要变为2,通知RendererRenderer更新DOM,1变为2。
  2. Reconciler发现2需要变为4,通知RendererRenderer更新DOM,2变为4。

可以看到,ReconcilerRenderer是交替工作的,当第一个li在页面上已经变化后。第二个li才进入Reconciler。就是发现改变渲染改变,改变就渲染的模式

React16

react16的架构可以分为三层:

  • Scheduler(调度器)————调度任务的优先级,高优任务优先进入Reconciler
  • Reconciler(协调器)————负责找出变化的组件,diff,又被称为render阶段,在此阶段会调用组件的render方法。
  • Renderer(渲染器)————负责将变化的组件渲染到页面上,又被称为commit阶段,就像git commit一样把render阶段的信息提交渲染到页面上。
    rendercommit阶段统称为work

Scheduler(调度器)

以浏览器是否有剩余时间作为任务中断的标准,也需要当浏览器有剩余时间时来通知到我们,类似API:requistIdCallback
就是判断浏览器有无剩余时间,如有按优先级继续执行Reconciler

Reconciler(协调器)

react15是用递归来处理虚拟DOMreact16的更新工作从递归变成可以中断的循环过程。

/** @noinline */
function workLoopConcurrent() {
  // Perform work until Scheduler asks us to yield
  while (workInProgress !== null && !shouldYield()) {
    workInProgress = performUnitOfWork(workInProgress);
  }
}

React16中,ReconcilerRenderer不是交替工作,而是当Scheduler将任务交给Reconciler后,Reconciler会将变化的虚拟DOM打上增/删/改的tag
整个SchedulerReconciler的工作都在内存中进行,只有当所有组件都完成Reconciler的工作,才会统一交给Renderer渲染

Renderer(渲染器)

Renderer根据Reconciler为虚拟DOM打的标记,同步执行对应的DOM操作。

state=1;
<li>{state.count}</li>//<li>1</li>
<li>{state.count*2}</li>//<li>2<li>

当点一个state+1时更新步骤:

  1. Scheduler接收到更新,看下有没有其它高优先更新要执行,没有的放将state.count从1变成2,交给Reconciler
  2. Reconciler接收到更新,找出需要变化的虚拟DOM,发现在1要变成2打tag:Update,又发现了2要变成4再给第二个打上tag:Update。都完了之后将打了标识的虚拟DOMRenderer
  3. Renderer接收到通知,找到打了Update标识的2个虚拟DOM,对它们执行更新DOM的操作。

2,3步可随时因为有其它高优先级任务先更新或没有剩余时间而中断,但由于2,3都是在内存中进行,不会更新页面上的DOM,所以就算反复中断,用记也不会看到更新一半的DOM

Fiber

react15及之前,Reconciler采用递归的方式创建虚拟DOM,递归不能中断,如果组件树层级很深,递归时间就多,线程释放不出来,就会造成卡顿,由于数据保存在递归栈中被称为stack Reconcilerreact16将递归的无法中断更新重构为异步的可中断更新,支持任务不同优先级,可中断与恢复,恢复后可利用之前的中间状态。每个任务更新单元为React Element对应的Fiber节点,基于Fiber节点实现叫Fiber Reconciler

Fiber的结构

function FiberNode(
  tag: WorkTag,
  pendingProps: mixed,
  key: null | string,
  mode: TypeOfMode,
) {
  // 作为静态数据结构的属性
  this.tag = tag;
  this.key = key;
  this.elementType = null;
  this.type = null;
  this.stateNode = null;

  // 用于连接其他Fiber节点形成Fiber树
  this.return = null;// 指向父级Fiber节点
  this.child = null;// 指向子Fiber节点
  this.sibling = null;// 指向右边第一个兄弟Fiber节点
  this.index = 0;

  this.ref = null;

  // 作为动态的工作单元的属性,保存本次更新造成的状态改变相关信息
  this.pendingProps = pendingProps;
  this.memoizedProps = null;
  this.updateQueue = null;
  this.memoizedState = null;
  this.dependencies = null;

  this.mode = mode;
  //保存本次更新会造成的DOM操作
  this.effectTag = NoEffect;
  this.nextEffect = null;

  this.firstEffect = null;
  this.lastEffect = null;

  // 调度优先级相关
  this.lanes = NoLanes;
  this.childLanes = NoLanes;

  // 指向该fiber在另一次更新时对应的fiber
  this.alternate = null;
}

作为架构来说,每个Fiber节点有对应的React element,多个Fiber节点是靠this.return,this.child,this.sibling3个属性连接成树的
如组件结构对应的Fiber树,用return代指父节点

function App() {
  return (
    <div>
      i am
      <span>KaSong</span>
    </div>
  )
}

image.png
render阶段依次链式执行顺序:

  1. rootFiber beginWork
  2. App Fiber beginWork
  3. div Fiber beginWork
  4. "i am" Fiber beginWork
  5. "i am" Fiber completeWork
  6. span Fiber beginWork
  7. span Fiber completeWork
  8. div Fiber completeWork
  9. App Fiber completeWork
  10. rootFiber completeWoek

双缓存Fiber

在内存中构建并直接替换的技术叫双缓存
react使用双缓存来完成Fiber树的构建与替换--对应着DOM树的创建与更新。
react中最多会同时存在2棵Fiber树。当前屏幕上显示的Fiber树称为current Fiber,正在内存构建的称为workInProgress Fiber
当内存的workInprogress Fiber树构建完成交给Renderer渲染在页面上后,应用的要节点的current指针指向workInProgress Fiber树,workInProgress Fiber树就变成了current Fiber树。
每次状态更新都会产生新的workInProgress Fiber树,通过currentworkInProgress替换,完成DOM更新。在构建workInProgress Fiber树时会尝试复用current Fiber树中已有的Fiber节点内的属性,克隆current.child作为workInProgress.child,而不需要新建workInProgres.child

就是在组件mount时,Reconciler根据JSX描述的组件内容生成组件对应的Fiber节点。在组件第一次mount的时候只有rootFiber上有插入的tag,把Reconciler生成的DOM树全部放在rootFiber下。
update时,ReconcilerJSXFiber节点,保存的数据对比,生成组件对应的Fiber节点,并根据对比结果为Fiber节点打上标记。每个执行完completeWork且存在effectTagFiber节点会被保存在effectList(只包含它的子孙节点)的单向链表中。在commit阶段只要遍历effectList就能执行所有的effect了。

diff

为了降低复杂度,reactdiff预设了3个限制:

  1. 只对同级元素进行diff,如果一个DOM节点在前后2次更新中跨越了层级,那么react就不会复用它了。
  2. 2个不同类型的元素会产生出不同的树,如果元素由div变为preact会销毁div及其子孙节点,并新建p及其子孙节点。
  3. 开发者可以通过key prop来暗示哪些子元素在不同的渲染下保持稳定,如:

    // 更新前
    <div>
      <p key="ka">ka</p>
      <h3 key="song">song</h3>
    </div>
    // 更新后
    <div>
      <h3 key="song">song</h3>
      <p key="ka">ka</p>
    </div>

    如果没有keyreact会认为div的第一个子节点由p变为h3,第二个子节点由h3变为p。符合第2条的规定,会销毁它并重建。
    但是当我们用key指明了节点前后对应关系后,react知道key==='ka'p在更新后还存在,所以DOM节点可以复用,只是需要交接下顺序。


Waxiangyu
670 声望30 粉丝