【React源码学习】8 如何通过update计算state


demo继续用上次那个按钮

function App() {
  let [val, setVal] = useState(0)

  function handle(){
    setVal(val + 1)
  }

  return (
    <div className="App">
      <button onClick={() =>{handle()}}>{val}</button>
    </div>
  );
}

插入update

点击按钮后会触发一次更新, 由createUpdate创建一个update对象,明确一下update的结构

var update = {
    eventTime: eventTime,   //update发生的时间,会涉及到优先级
    lane: lane,             // 该update的优先级
    tag: UpdateState,
    payload: null,          // 传入的state
    callback: null,
    next: null              //环形链表
};

enqueueUpdate将创建的update插入到fiber.updateQueue上,看一下updateQueue的结构

{
 	baseState: fiber.memoizedState,
 	firstBaseUpdate: null,
 	lastBaseUpdate: null,
 	shared: {
 		pending: null,
 	},
    effects: null
}

baseState: 由第一个被跳过的update之间的update计算后得出的状态。在每一轮更新中,如果update优先级不够(没过期)可能会被跳过到后面的更新执行。

firstBaseUpdate: 指向上次被跳过的第一个update,如果没有跳过的update就是null

lastBaseUpdate: 如果有update被跳过,lastBaseUpdate就是上次更新update链表的最后一个update

shared.pending: 本次更新update构成的环状链表(一次更新可能产生多个udpate)

插入更新的过程就是将update插入到shared.pending环状链表上,下面是插入的代码

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

pending指向环状链表的最后一个update,插入的过程就是:1. 将新的update.next指向环状链表的第一个元素,也就是pending.next。2. 将之前的最后一个update.next指向新的update,现在新update就是最后一个update。3. 将pending指向新update

使用环状链表可以有很高的插入效率,如果是普通单向链表,插入新的update需要遍历到链表尾部。

根据update计算state

在插入update后会进入调度过程,在beginWork阶段会调用processUpdate进行state的计算

通过前面的过程可知,此时fiber上存在两个updateQueue,上次更新跳过的updateQueue和这次更新产生的updateQueue(firstBaseUpdate和shared.pending1)。

计算state前需要将新产生的update链接到上次剩下的updateQueue上,该过程在下面的代码实现。

//重置queue.shared.pending1
queue.shared.pending = null;   

// 将lastPendingUpdate.next指向null,断开环状链表
var lastPendingUpdate = pendingQueue;
var firstPendingUpdate = lastPendingUpdate.next;
lastPendingUpdate.next = null; 


//链接两个链表,将最后一个update.next指向新的链表
if (lastBaseUpdate === null) {
  firstBaseUpdate = firstPendingUpdate;
} else {
  lastBaseUpdate.next = firstPendingUpdate;
}

lastBaseUpdate = lastPendingUpdate;

总结一下这个过程:重置shared.pending -> 断开环状链表 -> 将新udpate链表链接到旧的update链表后面

    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;
  }
}

这里current fiber进行了相同的操作,参考其他的博客,这么做的意义是将updateQueue保存在current fiber上,当update被打断时防止更新丢失?关于为什么会打断更新后面会仔细写一下。

下面就涉及到了更新跳过的情况,首先看一下声明的几个变量

var newState = queue.baseState;     // 本次state计算将基于newState
var newLanes = NoLanes;
var newBaseState = null;            //如果有update被跳过,newBaseState由第一个被跳过的update之前的update计算得出,作为下一次更新的baseState。
var newFirstBaseUpdate = null;
var newLastBaseUpdate = null;
var update = firstBaseUpdate;

看一下如何处理更新,处理过程的框架是一个do while循环

var update = firstBaseUpdate;
do{
    //something
}while(true)

看一下do while里面的内容

if (!isSubsetOfLanes(renderLanes, updateLane)) {
var clone = {
    eventTime: updateEv5entTime,
    lane: updateLane,
    tag: update.tag,
    payload: update.payload,
    callback: update.callback,
    next: null
};
if (newLastBaseUpdate === null) {
    newFirstBaseUpdate = newLastBaseUpdate = clone;
    newBaseState = newState;
} else {
    newLastBaseUpdate = newLastBaseUpdate.next = clone;
} 
    newLanes = mergeLanes(newLanes, updateLane);
}

这个if里面检测一下当前update的优先级是否在本次渲染的优先级内,如果优先级低则跳过updata。跳过时将其添加到newFirstBaseUpdate内(下次更新的baseUpdate链表),而且要将newState保存在newBaseState内, 所以说baseState保存的是第一个被跳过的update之前的update计算出的state。

else {
    if (newLastBaseUpdate !== null) {
        var _clone = {
            eventTime: updateEventTime,
            lane: NoLane,
            tag: update.tag,
            payload: update.payload,
            callback: update.callback,
            next: null
        };
        newLastBaseUpdate = newLastBaseUpdate.next = _clone;
    } 
    newState = getStateFromUpdate(workInProgress, queue, update, newState, props, instance);
    var callback = update.callback;
    if (callback !== null) {
            workInProgress.flags |= Callback;
            var effects = queue.effects;
        if (effects === null) {
            queue.effects = [update];
        } else {
            effects.push(update);
        }
    }
  }

进入当前这个else说明update优先级足够,先看if (newLastBaseUpdate !== null)的情况,此时说明已经有低优先级的update被跳过了,那就应该将该update添加到newLastBaseUpdate后(lastBaseUpdate保存的是从第一个被跳过的update开始的update链表)。getStateFromUpdate会根据update及state计算出新的state(update种类不同进入不同的case,比较常见的就是UpdateState)

_assign({}, prevState, partialState);

/**
 * 
 * 这个就是普通UpdateState的合并方法,通过assign合并对象,所以调用setState时即使没有传入之前的state,旧的state也不会丢失。
 * 
 * */

看一下while的最后一部分

if (update === null) {
    pendingQueue = queue.shared.pending;
    if (pendingQueue === null) {
        break;
    } else {
        var _lastPendingUpdate = pendingQueue; 
        var _firstPendingUpdate = _lastPendingUpdate.next;
        _lastPendingUpdate.next = null;
        update = _firstPendingUpdate;
        queue.lastBaseUpdate = _lastPendingUpdate;
        queue.shared.pending = null;
    }
}

处理update时有可能产生新的update(比如typeof payload === ‘function’),所以这里检查一下queue.shared.pending, 如果产生新的update,添加到lastBaseUpdate上,重复上面的操作。

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

这里是更新计算的最后阶段,主要就是挂载被跳过的更新,将第一个被跳过的update之前的update计算出的newBaseState放在baseState上,将正常计算出的newState放在memoizedState上。后面计算state还是要基于baseState,为了保证跳过update不会影响最终的state。

summary

summary

参考

React源码阅读


Author: Maple
Reprint policy: All articles in this blog are used except for special statements CC BY 4.0 reprint polocy. If reproduced, please indicate source Maple !
  TOC