React源码分析7-state计算流程和优先级

setState执行之后会发生什么

setState 执行之后,会执行一个叫 enqueueSetState 的方法,这个主要作用是创建 Update 对象和发起调度,可以看下这个函数的逻辑

enqueueSetState: function (inst, payload, callback) {
   
    // 1. inst是组件实例,从组件实例中拿到当前组件的Fiber节点
    var fiber = get(inst);
    var eventTime = requestEventTime();
    var lane = requestUpdateLane(fiber);
    // 2.1 根据更新发起时间、优先级、更新的payload创建一个update对象
    var update = createUpdate(eventTime, lane);
    update.payload = payload;

    // 2.2 如果 setState 有回调,顺便把回调赋值给 update 对象的 callback 属性
    if (callback !== undefined && callback !== null) {
   
      update.callback = callback;
    }

    // 3. 将 update 对象关联到 Fiber 节点的 updateQueue 属性中
    enqueueUpdate(fiber, update);
    // 4. 发起调度
    var root = scheduleUpdateOnFiber(fiber, lane, eventTime);
}

从上面源码可以清晰知道,setState 调用之后做的4件事情

  1. 根据组件实例获取其 Fiber 节点
  2. 创建 Update 对象
  3. Update 对象关联到 Fiber 节点的 updateQueue 属性中
  4. 发起调度

根据组件实例获取其 Fiber 节点

其实就是拿组件实例中的 _reactInternals 属性,这个就是当前组件所对应的 Fiber 节点

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

题外话:react利用双缓存机制来完成 Fiber 树的构建和替换,也就是 currentworkInProgress 两棵树,那 enqueueSetState 里面拿的是那棵树下的 Fiber 节点呢?

答案是:current树下的Fiber节点。具体的原理在下面update对象丢失问题再说明

创建update对象

function createUpdate(eventTime, lane) {
   
  var update = {
   
    eventTime: eventTime,
    lane: lane,
    tag: UpdateState,
    payload: null,
    callback: null,
    next: null
  };
  return update;
}

属性的含义如下:

  • eventTime:update对象创建的时间,用于ensureRootIsScheduled计算过期时间用
  • lane:此次更新的优先级
  • payload:setState的第一个参数
  • callback:setState的第二个参数
  • next:连接的下一个 update 对象

Update对象关联到Fiber节点的updateQueue属性

这里执行的是 enqueueUpdate 函数,下面是我简化过后的逻辑

function enqueueUpdate(fiber, update) {
   
    var updateQueue = fiber.updateQueue;
    var sharedQueue = updateQueue.shared;
    var pending = sharedQueue.pending;

    if (pending === null) {
   
      update.next = update;
    } else {
   
      update.next = pending.next;
      pending.next = update;
    }
    sharedQueue.pending = update;
}

可以看到这里的逻辑主要是将 update 对象放到 fiber 对象的 updateQueue.shared.pending 属性中, updateQueue.shared.pending 是一个环状链表。

那为什么需要把它设计为一个环状链表?我是这样理解的

  1. shared.pending 存放的是链表的最后一个节点,那么在环状链表中,链表的最后一个节点的next指针,是指向环状链表的头部节点,这样我们就能快速知道链表的首尾节点
  2. 当知道首尾节点后,就能很轻松的合并两个链表。比如有两条链表a、b,我们想要把 b append到 a 的后面,可以这样做
const lastBPoint = bTail
const firstBPoint = bTail.next
lastBPoint.next = null
aTail.next = firstBPoint
aTail = lastBPoint

后面即使有c、d链表,同样也可以用相同的办法合并到a。react 在构建 updateQueue 链表上也用了类似的手法,新产生的 update 对象通过类似上面的操作合并到 updateQueue 链表,相关参考视频讲解:进入学习

发起调度

enqueueUpdate 末尾,执行了 scheduleUpdateOnFiber 函数,该方法最终会调用 ensureRootIsScheduled 函数来调度react的应用根节点。

当进入 performConcurrentWorkOnRoot 函数时,就代表进入了 reconcile 阶段,也就是我们说的 render 阶段。render 阶段是一个自顶向下再自底向上的过程,从react的应用根节点开始一直向下遍历,再从底部节点往上回归,这就是render阶段的节点遍历过程。

这里我们需要知道的是,在render阶段自顶向下遍历的过程中,如果遇到组件类型的Fiber节点,我们会执行 processUpdateQueue 函数,这个函数主要负责的是组件更新时 state 的计算

processUpdateQueue做了什么

processUpdateQueue函数主要做了三件事情

  1. 构造本轮更新的 updateQueue,并缓存到 currentFiber 节点中
  2. 循环遍历 updateQueue,计算得到 newState,构造下轮更新的 updateQueue
  3. 更新 workInProgress 节点中的 updateQueuememoizedState 属性

这里的 updateQueue 并不指代源码中 Fiber 节点的 updateQueue,可以理解为从 firstBaseUpdatelastBaseUpdate 的整条更新队列。这里为了方便描述和理解,直接用 updateQueue 替代说明。

变量解释

因为涉及的变量比较多,processUpdateQueue 函数的逻辑看起来并不怎么清晰,所以我先列出一些变量的解释方便理解

  • shared.pending:
    • enqueueSetState 产生的 update对象 环形链表
  • first/lastBaseUpdate:-- 下面我会用 baseUpdate 代替
    • 当前 Fiber 节点中 updateQueue 对象中的属性,代表当前组件整个更新队列链表的首尾节点
  • first/lastPendingUpdate:下面我会用 pendingUpdate 代替
    • shared.pending 剪开后的产物,分别代表新产生的 update对象 链表的首尾节点,最终会合并到 currentFiber 和 workInProgress 两棵树的更新队列尾部
  • newFirst/LastBaseUpdate:下面我会用 newBaseUpdate 代替
    • newState计算过程会得到,只要存在低优先级的 update 对象,这两个变量就会有值。这两个变量会赋值给 workInProgress 的 baseUpdate,作为下一轮更新 update对象 链表的首尾节点
  • baseState:newState 计算过程依赖的初始 state
  • memoizedState:当前组件实例的 state,processUpdateQueue 末尾会将 newState 赋值给这个变量

构造本轮更新的 updateQueue

上面我们说到 shared.pendingenqueueSetState 产生的 update对象 环形链表,在这里我们需要剪断这个环形列表取得其中的首尾节点,去组建我们的更新队列。那如何剪断呢?

shared.pending 是环形链表的尾部节点,它的下一个节点就是环形链表的头部节点,参考上一小节我们提到的链表合并操作。

var lastPendingUpdate = shared.pending;
var firstPendingUpdate = lastPendingUpdate.next;
lastPendingUpdate.next = null;

这样就能剪断环形链表,拿到我们想要的新的 update 对象 —— pendingUpdate。接着我们要拿着这个 pendingUpdate 做两件事情:

  1. pendingUpdate 合并到当前Fiber节点的更新队列
  2. pendingUpdate 合并到 currentFiber树 中对应 Fiber节点 的更新队列

为什么要做这两件事情?

  1. 第一个是解决状态连续性问题,当出现多个 setState 更新时,我们要确保当前 update对象 的更新是以前一个 update对象 计算出来的 state 为前提。所以我们需要构造一个更新队列,新的 update对象 要合并到更新队列的尾部,从而维护state计算的连续性
  2. 第二个是解决 update 对象丢失问题。在 shared.pending 被剪开之后,shared.pending会被赋值为null,当有高优先级任务进来时,低优先级任务就会被打断,也就意味着 workInProgress 树会被还原,shared.pending 剪开之后得到的 pendingUpdate 就会丢失。这时就需要将 pendingUpdate 合并到 currentFiber树 的更新队列中

接下来可以大致看一下这一部分的源码

  var queue = workInProgress.updateQueue;
  var firstBaseUpdate = queue.firstBaseUpdate;
  var lastBaseUpdate = queue.lastBaseUpdate;

  // 1. 先拿到本次更新的 update对象 环形链表
  var pendingQueue = queue.shared.pending;

  if (pendingQueue !== null) {
   
    // 2. 清空pending
    queue.shared.pending = null;

    var lastPendingUpdate = pendingQueue;
    var firstPendingUpdate = lastPendingUpdate.next;
    // 3. 剪开环形链表
    lastPendingUpdate.next = null;

    // 4. 将 pendingupdate 合并到 baseUpdate
    if (lastBaseUpdate === null) {
   
      firstBaseUpdate <
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值