react15 和 react16 在 setState 后的更新渲染解析

在 react 项目中,setState 被用于更新 state,从而实现组件重新渲染更新。经过查找阅读许多资料以及源码后,本文就来个人总结一下,简要解析 react 在 setState 后是如何更新组件的。

前言:setState 的同步和异步

  1. 异步:setState 一般情况下是异步的,由 react 的批量更新事务(ReactDefaultBatchingStrategy)控制(即 react 控制的事件,非调用 js 原生事件时是异步的),以及生命周期函数调用 setState 也不会同步更新 state。同一个函数中执行多个 setState 时会合并,并且同一个属性以最后一次 setState 的值为准。
  2. 同步:setState 在 setTimeout,setInterval 和 js 原生事件中被调用,则是同步的。

原理:同步和异步是由什么控制的? 是通过 batchedUpdates 函数中设置 isBatchingUpdates 为 true 时,则 setState 为异步更新,false 时为同步更新。

react15

先介绍事务(ReactDefaultBatchingStrategyTransaction)和 batchedUpdates 函数(用来启动事务的,启动事务后 setState 就是异步更新了),后介绍setState。

ReactDefaultBatchingStrategyTransaction 事务

setState 依靠事务进行更新,事务生命周期包含 initialize、perform、close 阶段。在开启事务后,遇到 setState 后 则将 partial state 存到组件实例的_pendingStateQueue 上, 接着调用 enqueueUpdate 排队更新方法,如下 setState 后解析。

// 批处理策略
var ReactDefaultBatchingStrategy = {
  isBatchingUpdates: false,
  batchedUpdates: function(callback, a, b, c, d, e) {
    var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates;
    ReactDefaultBatchingStrategy.isBatchingUpdates = true; //开启一次batch

    if (alreadyBatchingUpdates) {
      return callback(a, b, c, d, e);
    } else {
      // 启动事务, 将callback放进事务里执行
      return transaction.perform(callback, null, a, b, c, d, e);
    }
  },
};

batchUpdates

batchedUpdates先设置 isBatchingUpdates为true(ReactDefaultBatchingStrategy.isBatchingUpdates = true)开启事务,后将 callback函数放进事务里执行(transaction.perform(callback,…)),无论你传进去的函数是什么, 无论这个函数后续会做什么, 都会在执行完 callback(setState 的第二个参数)后调用事务的 close 方法

在 React 中,调用batchedUpdates有很多地方

第一种情况:首次渲染组件时(源码:在ReactMount.js里调用了ReactUpdates.batchedUpdates)
第二种情况:元素上或者组件上绑定了react控制的事件(非调用js原生事件),事件的监听函数中调用setState。(源码:在ReactEventListener.js里,react事件系统中的dispatchEvent函数启动了事务(调用了ReactUpdates.batchedUpdates))

重点:setState后

1、setState后

调用updater的enqueueSetState方法把需要更新的state(partial state)push进去等待队列_pendingStateQueue中, 接着调用enqueueUpdate排队更新。

2、enqueueUpdate

enqueueUpdate方法里需要判断batchingStrategy.isBatchingUpdates == true,即是否开启batch事务

  • 情况一:如果已经开启batch,然后标记当前组件为dirtyComponent, 存到dirtyComponents数组中,等到 ReactDefaultBatchingStrategy事务结束时(close)调用runBatchedUpdates批量更新所有组件
  • 情况二:方法中如果没有开启batch(或当前batch已结束,也就是说在事务的initialize或更新阶段)就调用batchedUpdates函数开启一次batch,再重新执行enqueueUpdate方法,判断isBatchingUpdates,现在为true了,标记当前组件为dirtyComponent, 存到dirtyComponents数组中, 并没有立即更新,而是继续执行后面事情,等到 ReactDefaultBatchingStrategy事务结束时(close)调用flushBatchedUpdates函数=>runBatchedUpdates函数批量更新所有组件(第三点)
// ReactBaseClasses.js :
ReactComponent.prototype.setState = function(partialState, callback) {
  this.updater.enqueueSetState(this, partialState);
  if (callback) {// 如果有callback,就将callback放入回调函数队列
    this.updater.enqueueCallback(this, callback, 'setState');
  }
};

// => ReactUpdateQueue.js:
enqueueSetState: function(publicInstance, partialState) {
    // 根据 this.setState 中的 this 拿到内部实例, 也就是组件实例
    var internalInstance = getInternalInstanceReadyForUpdate(publicInstance, 'setState');
    // 取得组件实例的_pendingStateQueue
    var queue =
      internalInstance._pendingStateQueue ||
      (internalInstance._pendingStateQueue = []);
    // 将partial state存到_pendingStateQueue
    queue.push(partialState);
    // 调用enqueueUpdate
    enqueueUpdate(internalInstance);
 }

// => ReactUpdate.js:
function enqueueUpdate(component) {
  ensureInjected(); // 注入默认策略
    
    // 如果没有开启batch(或当前batch已结束)就开启一次batch再执行, 这通常发生在异步回调中调用 setState      // 的情况
  if (!batchingStrategy.isBatchingUpdates) {
    batchingStrategy.batchedUpdates(enqueueUpdate, component);
    return;
  }
    // 如果batch已经开启就存储更新
  dirtyComponents.push(component);
  if (component._updateBatchNumber == null) {
    component._updateBatchNumber = updateBatchNumber + 1;
  }
}
3、事务结束时(close阶段)

批量更新的阶段,调用flushBatchedUpdates函数启动ReactUpdatesFlushTransaction事务负责批量更新,这个事务执行了runBatchedUpdates方法通过遍历dirtyComponents数组(在函数里ReactReconciler.performUpdateIfNecessary中调用updateComponent更新组件)进行批量更新。再结束本次batch事务(即ReactDefaultBatchingStrategy.isBatchingUpdates = false; )

var flushBatchedUpdates = function () {
  // 启动批量更新事务
  while (dirtyComponents.length || asapEnqueued) {
    if (dirtyComponents.length) {
      var transaction = ReactUpdatesFlushTransaction.getPooled();
      transaction.perform(runBatchedUpdates, null, transaction);
      ReactUpdatesFlushTransaction.release(transaction);
    }
// 批量处理callback
    if (asapEnqueued) {
      asapEnqueued = false;
      var queue = asapCallbackQueue;
      asapCallbackQueue = CallbackQueue.getPooled();
      queue.notifyAll();
      CallbackQueue.release(queue);
    }
  }
};

// ReactUpdates.js
function runBatchedUpdates(transaction) {
  var len = transaction.dirtyComponentsLength;
  // 排序保证父组件优先于子组件更新
  dirtyComponents.sort(mountOrderComparator);

  // 代表批量更新的次数, 保证每个组件只更新一次
  updateBatchNumber++;
  // 遍历 dirtyComponents
  for (var i = 0; i < len; i++) {
    var component = dirtyComponents[i];
      
    var callbacks = component._pendingCallbacks;
    component._pendingCallbacks = null;
    ...
    // 执行更新
    ReactReconciler.performUpdateIfNecessary(
      component,
      transaction.reconcileTransaction,
      updateBatchNumber,
    );
    ...
    // 存储 callback以便后续按顺序调用
    if (callbacks) {
      for (var j = 0; j < callbacks.length; j++) {
        transaction.callbackQueue.enqueue(
          callbacks[j],
          component.getPublicInstance(),
        );
      }
    }
  }
}
4、updateComponent

ReactReconciler.performUpdateIfNecessary中调用updateComponent更新组件


performUpdateIfNecessary: function (transaction) {
    if (this._pendingElement != null) {
        ReactReconciler.receiveComponent(this, this._pendingElement, transaction,                 this._context);
    } else if (this._pendingStateQueue !== null || this._pendingForceUpdate) {
        this.updateComponent(transaction, this._currentElement, this._currentElement,             this._context, this._context);
    } else {
        this._updateBatchNumber = null;
    }
}
5、updateComponent

react内部有3种不同组件:ReactCompositeComponent、ReactDOMComponent和ReactDOMTextComponent。

  • ReactCompositeComponent:re-render, 与之前 render 的 element 比较, 如果两者key && element.type 相等, 则进入下一层进行更新; 如果不等, 直接移除重新mount
  • ReactDOMComponent: render前生成新的虚拟DOM,对前后虚拟DOM进行diff算法比较(传送门:react的diff算法),找出最小更新部分,批量更新
  • ReactDOMTextComponent:直接更新Text

总结
图片

react16:加入Fiber

react16版本中加入了Fiber架构(传送门:Fiber简介)
Fiber主要分两个阶段

调度阶段(reconciliation):Fiber调度

  1. 将一个state更新任务拆分成多个时间小片,形成一个 Fiber 任务队列.
  2. 在任务队列中选出优先级高的 Fiber 执行,如果执行时间超过了deathLine,则设置为pending状态挂起状态(即执行一段时间,会跳出找Fiber任务队列中更高级的任务,如果有就放弃当前任务,即使当前任务执行了一半,可能已经经历了一些生命周期,都会被打断从来)。

渲染阶段(commit):

  1. 进入render函数,构建真实的virtualDomTree,React将其所有的变更一次性更新到DOM上。

重点:setState后

调用updater的enqueueSetState方法,传入state, callback
Component.prototype.setState = function(partialState, callback) {
  this.updater.enqueueSetState(this, partialState, callback, 'setState');
};
updater更新器中的enqueueSetState
var updater = {
  ...
  enqueueSetState(instance, partialState, callback) {
    // 获取到当前组件实例上的fiber
    const fiber = ReactInstanceMap.get(instance);
    callback = callback === undefined ? null : callback;
    // 计算当前fiber的到期时间,即计算出优先级
    const expirationTime = computeExpirationForFiber(fiber);
    const update = {
        expirationTime,
        partialState,
        callback,
        isReplace: false,
        isForced: false,
        capturedValue: null,
        next: null,
    };
    // 把更新信息任务插入fiber的update queue更新队列中
    insertUpdateIntoFiber(fiber, update);
    // 开启调度任务,进入fiber的调度阶段(reconciliation)
    scheduleWork(fiber, expirationTime);
  }
  ...
};

enqueueSetState详解:

1、get获取组件实例上的fiber

function get(key) {
  return key._reactInternalFiber;
}

2、计算到期时间/优先级computeExpirationForFiber

计算当前fiber的优先级(即过期时间),expirationTime 优先级 expirationTime 不为 1 的时候,则其值越低,优先级越高。Fiber任务的优先级:文本框输入 > 本次调度结束需完成的任务 > 动画过渡 > 交互反馈 > 数据更新 > 不会显示但以防将来会显示的任务。如下:

//用来计算fiber的到期时间,到期时间用来表示任务的优先级。
 function computeExpirationForFiber(fiber) {
    var expirationTime = void 0;
    if (expirationContext !== NoWork) {
      // 
      expirationTime = expirationContext;
    } else if (isWorking) {
      if (isCommitting) {
        //同步模式,立即处理任务,默认是1
        expirationTime = Sync;
      } else {
        //渲染阶段的更新应该与正在渲染的工作同时过期。
        expirationTime = nextRenderExpirationTime;
      }
    } else {
      //没有到期时间的情况下,创建一个到期时间
      if (fiber.mode & AsyncMode) {
        if (isBatchingInteractiveUpdates) {
          // 这是一个交互式更新
          var currentTime = recalculateCurrentTime();
          expirationTime = computeInteractiveExpiration(currentTime);
        } else {
          // 这是一个异步更新
          var _currentTime = recalculateCurrentTime();
          expirationTime = computeAsyncExpiration(_currentTime);
        }
      } else {
        // 这是一个同步更新
        expirationTime = Sync;
      }
    }
    if (isBatchingInteractiveUpdates) {
      //这是一个交互式的更新。跟踪最低等待交互过期时间。这允许我们在需要时同步刷新所有交互更新。
      if (lowestPendingInteractiveExpirationTime === NoWork || expirationTime > lowestPendingInteractiveExpirationTime) {
        lowestPendingInteractiveExpirationTime = expirationTime;
      }
    }
    return expirationTime;
  }

fiber优先级定义:

module.exports = {  
  // heigh level
  NoWork: 0, // No work is pending.
  SynchronousPriority: 1, // For controlled text inputs. 
  TaskPriority: 2, // Completes at the end of the current tick.
  AnimationPriority: 3, // Needs to complete before the next frame.
  
  // low level
  HighPriority: 4, // Interaction that needs to complete pretty soon to feel responsive.
  LowPriority: 5, // Data fetching, or result from updating stores.
  OffscreenPriority: 6, // Won't be visible but do the work in case it becomes visible.
};

3、insertUpdateIntoFiber(fiber, update)

把更新信息任务插入update queue更新队列中。确保更新队列存在,不存在则调用ensureUpdateQueues(fiber)创建一个fiber队列,调用insertUpdateIntoQueue(queue, update)将update的值更新到queue队列中。

function insertUpdateIntoFiber(fiber, update) {
  //确保更新队列存在,不存在则创建
  ensureUpdateQueues(fiber);
  //上一步已经将q1和q2队列进行了处理,定义2个局部变量queue1和queue2来保存队列信息。
  var queue1 = q1;
  var queue2 = q2;
  // 如果只有一个队列,请将更新添加到该队列并退出。
  if (queue2 === null) {
    insertUpdateIntoQueue(queue1, update);
    return;
  }

  // 如果任一队列为空,我们需要添加到两个队列中。
  if (queue1.last === null || queue2.last === null) {
    //将update的值更新到队列1和队列2上,然后退出该函数
    insertUpdateIntoQueue(queue1, update);
    insertUpdateIntoQueue(queue2, update);
    return;
  }

  // 如果两个列表都不为空,则由于结构共享,两个列表的最后更新都是相同的。所以,我们应该只追加到其中一个列表。
  insertUpdateIntoQueue(queue1, update);
  // 但是我们仍然需要更新queue2的`last`指针。
  queue2.last = update;
}

4、scheduleWork(fiber, expirationTime)

开启调度任务,进入fiber的调度阶段
scheduleWork执行流程:scheduleWork => scheduleWorkImpl => requestWork => 同步/异步 => performSyncWork => performWork => performWorkOnRoot =>
renderRoot/completeRoot => workLoop => performUnitOfWork => beginWork/completeUnitOfWork => updateClassComponent => reconcileChildrenAtExpirationTime => reconcileChildFibers => reconcileChildrenArray

scheduleWork执行流程比较长,下面讲下主要步骤(详细步骤代码):

  1. scheduleWorkImpl:
    调用scheduleWorkImpl(fiber, expirationTime, false)函数更新每个node的优先级(即将一个state更新任务拆分成多个时间小片,形成一个 Fiber 任务队列)

  2. requestWork:
    同步执行performSyncWork,异步执行scheduleCallbackWithExpiration,
    scheduleCallbackWithExpiration会调浏览器的requestidlecallback,在浏览器空闲的时候进行处理。
    expirationTime:同步执行任务下expirationTime为0,即nowork=0。不为0时,使用requestidlecallback/requestAnimationFrame异步执行任务

  3. performSyncWork 主要的任务调度:
    这里会找到高优任务先执行。
    同步任务会直接调用performWorkOnRoot进行下一步,
    异步任务也会调performWorkOnRoot,但处理不太一样
    如果有上次遗留的任务,留到空闲时运行

  4. workLoop(fiber里的判断时间片,超过deathLine,则设置为pending状态):
    异步任务在处理的时候会调用shouldYield,shouldYield会判断是不是已经超时了,超时暂时先不做。

  5. performUnitOfWork (reconcilation阶段)
    调用beginWork处理组件,针对不同组件不同处理。此过程包括dom diff(生成新的 Virtual DOM,然后通过 Diff 算法,快速找出需要更新的元素,放到更新队列中去,得到新的更新队列)。然后调用completeUnitOfWork对begin work产生的effect list进行一些处理,包括对ReactDOMComponent DOM组件的更新。

渲染阶段(commit)

进入render函数,构建真实的virtualDomTree,调用completeRoot/commitRoot,React将其所有的变更一次性更新到DOM上。

本文参考

从源码全面剖析 React 组件更新机制
React16——看看setState过程中fiber干了什么事情
react 16 渲染整理

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值