文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

React源码state计算流程和优先级实例解析

2022-11-13 19:29

关注

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件事情

根据组件实例获取其 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;
}

属性的含义如下:

将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 是一个环状链表。

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

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函数主要做了三件事情

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

变量解释

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

enqueueSetState 产生的 update对象 环形链表

当前 Fiber 节点中 updateQueue 对象中的属性,代表当前组件整个更新队列链表的首尾节点

shared.pending 剪开后的产物,分别代表新产生的 update对象 链表的首尾节点,最终会合并到 currentFiber 和 workInProgress 两棵树的更新队列尾部

newState计算过程会得到,只要存在低优先级的 update 对象,这两个变量就会有值。这两个变量会赋值给 workInProgress 的 baseUpdate,作为下一轮更新 update对象 链表的首尾节点

构造本轮更新的 updateQueue

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

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

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

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

为什么要做这两件事情?

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

  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 = firstPendingUpdate;
    } else {
      lastBaseUpdate.next = firstPendingUpdate;
    }
    lastBaseUpdate = lastPendingUpdate;
    // 5. 将 pendingupdate 合并到 currentFiber树的 baseUpdate
    var current = workInProgress.alternate;
    if (current !== null) {
      var currentQueue = current.updateQueue;
      var currentLastBaseUpdate = currentQueue.lastBaseUpdate;
      if (currentLastBaseUpdate !== lastBaseUpdate) {
        if (currentLastBaseUpdate === null) {
          currentQueue.firstBaseUpdate = firstPendingUpdate;
        } else {
          currentLastBaseUpdate.next = firstPendingUpdate;
        }
        currentQueue.lastBaseUpdate = lastPendingUpdate;
      }
    }
  }

源码看起来很多,但本质上只做了一件事,从源码中可以看出这部分主要就是把 shared.pending 剪开,拿到我们的 pendingUpdate,再把 pendingUpdate 合并到本轮更新和 currentFiber 节点的 baseUpdate 中。

计算 newState

在这部分的源码中,除了计算 newState,还有另外一个重要工作是,构造下一轮更新用的 updateQueue

到这里可能会有疑问,为什么需要构造下轮更新的 updateQueue,本轮更新我们把 shared.pending 里面的对象遍历计算完,再把 state 更新,下轮更新进来再根据这个 state 计算不行好了吗?

如果没有高优先级任务打断机制,确实是不需要在这里构造下轮更新的 updateQueue,因为每轮更新我们只会依赖当前的 state 和 shared.pending

打断机制下,低优先级任务重启后的执行,需要依赖完整的更新队列才能保证 state 的连续性和正确性。下面我举个例子

state = { count: 0 }
componentDidMount() {
    const button = this.buttonRef.current
    // 低优先级任务
    setTimeout(() => this.setState({ count: 1 }), 1000)
    // 高优先级任务
    setTimeout(() => button.click(), 1040)
}
handleButtonClick = () => {
    this.setState( prevState => {
      return { count: prevState.count + 2 }
    } )
}

我们期望能实现的效果是 0 -> 2 -> 3,需求如下:

知道了需求,我们可以大概列一下实现思路:

上面说的需求和实现思路在 react 的源码中实现其实是非常简单的,但要理解其中的含义可能需要费点功夫,下面可以看看我改动过后的源码,可以直接从 do...while 开始看

  function cloneUpdate(update) {
      return {
          eventTime: update.eventTime,
          lane: update.lane,
          tag: update.tag,
          payload: update.payload,
          callback: update.callback,
          next: null
      };
  }
  if (firstBaseUpdate !== null) {
    var newState = queue.baseState;
    var newBaseState = null;
    var newFirstBaseUpdate = null;
    var newLastBaseUpdate = null;
    var update = firstBaseUpdate;
    // 遍历 updateQueue
    do {
      var updateLane = update.lane;
      var updateEventTime = update.eventTime;
      // 校验当前 update 对象够不够优先级
      if (!isSubsetOfLanes(renderLanes, updateLane)) {
        // 优先级不够,我们需要从当前 update 对象开始重新构造一个更新队列
        var clone = cloneUpdate(update)
        if (newLastBaseUpdate === null) {
          newFirstBaseUpdate = newLastBaseUpdate = clone;
          // 当前的 newState 就作为下轮更新的 baseState 使用
          newBaseState = newState;
        } else {
          newLastBaseUpdate = newLastBaseUpdate.next = clone;
        }
      } else {
        // 优先级够
        if (newLastBaseUpdate !== null) {
          // newLastBaseUpdate 不为空,就代表存在优先级不够的 update 对象
          var _clone = cloneUpdate(update)
          // 为保证状态连续性,即使当前 update 对象优先级足够,也要被放到 updateQueue 中
          newLastBaseUpdate = newLastBaseUpdate.next = _clone;
        }
        // 计算newState
        newState = getStateFromUpdate(workInProgress, queue, update, newState, props, instance);
      }
      update = update.next;
    } while (update);

逻辑如下:

优先级不够

优先级足够

这里 newState 的计算逻辑很简单

更新 workInProgress 节点

更新 workInProgress 节点属性的逻辑不多,主要就是把 newBaseState、newBaseUpate 赋值给 workInProgress 节点,作为下一轮更新的 baseState 和更新队列使用

if (newLastBaseUpdate === null) {
  newBaseState = newState;
}
queue.baseState = newBaseState;
queue.firstBaseUpdate = newFirstBaseUpdate;
queue.lastBaseUpdate = newLastBaseUpdate;
workInProgress.memoizedState = newState;

总结

看到上面的原理解析是不是很复杂,我们可以忽略所有的实现细节,回归现象本质,state计算就是遍历 update对象 链表根据 payload 得到新的state。在此前提下,因为优先级机制,打断之后会还原 workInProgress 节点,从而会引起 update对象 丢失问题 和 state计算连续性问题。解决这两个问题才是我们上面说的复杂的实现细节

update对象丢失问题

为什么会丢失

我们知道高优先级任务进来会打断低优先级任务的执行,打断之后会将当前的 workInProgress 节点还原为开始的状态,也就是可以理解为会将 workInProgress 树还原为当前页面所渲染的 currentFiber 节点。当 workInProgress 节点还原之后,我们本来存在 workInProgress 中的 updateQueue 属性也会被重置,那就意味着低优先级的 update 对象会丢失。

上面说到的,setState产生的新 update对象 是会放在 currentFiber 节点上也是这个原因,如果 setState 产生的新 update对象 放到 workInProgress 上,只要 workInProgress 被还原,这些 update对象 就会丢失

如何解决

我们在 processUpdateQueue 函数的开始阶段,将新产生的 update 对象,也就是 shared.pending 中的值,合并到 currentFiber( workInProgress.alternate ) 节点的 firstBaseUpdatelastBaseUpdate。具体规则如下

还原 workInProgress 节点执行的函数是 prepareFreshStack,里面会用 currentFiber 节点的属性覆盖 workInProgress 节点,从而实现还原功能。所以就算 workInProgress 节点被重置,我们只要把 update对象 合并到 currentFiber 节点上,还原的时候依然会存在于新的 workInProgress 节点

state计算的连续性

问题现象

我们上面说到,低优先级任务重启,不能覆盖高优先级任务计算得到的值,且需要根据低优先级任务计算得到的newState,作为高优先级的baseState再去执行一次高优先级任务。什么意思呢这是?

state = { count: 0 }
componentDidMount() {
    const button = this.buttonRef.current
    // 低优先级任务 - AUpate
    setTimeout(() => this.setState({ count: 1 }), 1000)
    // 高优先级任务 - BUpdate
    setTimeout(() => button.click(), 1040)
}
handleButtonClick = () => {
    this.setState( prevState => {
      return { count: prevState.count + 2 }
    } )
}

上面代码所产生的update对象如下

AUpate = { lane: 低, payload: 1 }
BUpdate = { lane: 高, payload: state => ({ count: state.count + 2 }) }

这也就决定了我们要用队列的形式去存储所有 update对象。update对象的存储顺序决定了state计算的前后依赖性,从而保证状态的连续性和准确性

明确很重要的一点,优先级高低只会影响某个 update对象 是否会提前执行,不会影响最终的 state 结果。最终的 state 结果还是由更新队列中 update对象 的顺序决定的

如何解决

我们看到 processUpdateQueue 中有两部分都是在构造更新队列的

这样无论是什么优先级,只要按顺序构造出更新队列,我就能计算出正确的newState,同时利用队列的性质,保证 update对象 间 state计算 的连续性

以上就是React源码state计算流程和优先级实例解析的详细内容,更多关于React state计算流程优先级的资料请关注编程网其它相关文章!

阅读原文内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     807人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     351人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     314人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     433人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     221人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-前端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯