Skip to content

Update

在 React 中对于组件 state 状态的计算不是简简单单的根据 setState 返回的值是多少就直接更新 instance.state 的,而是为支持异步可中断更新的机制 和 渲染优先级(低优先级的更新可能被高优先级的打断)的能力将其包装成一个 链表的格式 存储在组件的 Fiber.updateQueue 或者 Fiber.memorizedState 中。

那么为什么要这样设计呐?

  1. 解决在组件函数默认情况下, this.state 状态是不可变的,其是异步更新的
js
class UpdateComponent extends React.Component {
  constructor(props) {
    super(props);
  }
  state = {
    count: "",
  };

  onChange = () => {
    console.log(this.state.count); // 输出 ""
    this.setState((preState) => {
      return {
        count: preState.count + "A",
      };
    });
    console.log(this.state.count); // 输出 "" 这个时候 state 还没有更新

    this.setState((preState) => {
      return {
        count: preState.count + "B",
      };
    });
    console.log(this.state.count); // 输出 ""
  };

  render() {
    return <div onClick={this.onChange}></div>;
  }
}

那么是如何保持在 setState 中 this.state 状态的不可变性的呐?

  1. 如果在一个低优先级任务处理中间的 setState 中发现高优先级的任务,而被打断,那么下次重新执行的时候 怎么完整的执行整个低优先级的更新流程

如 一个低优先级的离散任务拖拽中会执行 ABC 三个任务,然后在执行到 B 这个任务的时候,发现有一个高优先级的任务,这个时候就会打断 B 这个任务,然后执行高优先级的任务,这个时候出现 ABDC 的情况,还是 DABC , ABCD

js
class UpdateComponent extends React.Component {
  constructor(props) {
    super(props);
    this.buttonRef = React.createRef();
  }

  state = {
    count: "",
  };

  handleButtonClick = () => {
    this.setState((prevState) => {
      return { count: prevState.count + "D" };
    });
  };

  onBeginClickTask = () => {
    const button = this.buttonRef.current;
    setTimeout(() => button.click(), 600);
  };

  onDragHandler = () => {
    this.setState((preState) => {
      return {
        count: preState.count + "A",
      };
    });
    this.setState((preState) => {
      return {
        count: preState.count + "B",
      };
    });
    this.setState((preState) => {
      return {
        count: preState.count + "C",
      };
    });
  };

  render() {
    return (
      <div>
        当前值: {this.state.count}
        <button ref={this.buttonRef} onClick={this.handleButtonClick}>
          增加D
        </button>
        <button onClick={this.onBeginClickTask}>开始</button>
        <div>
          <div
            style={{ height: 100, width: 100, backgroundColor: "red" }}
            id="drag-element"
            draggable={true}
            onDrag={this.onDragHandler}
          >
            拖拽
          </div>
        </div>
      </div>
    );
  }
}

如果在执行拓展的时候执行到 B 这个的时候 ,正好触发了 按钮的 click 事件, 这个是高优先级的任务,ABC 任务会被打断,加了一个 D,这时候如何保持 ABC 的流程,而不是 ABDC, 具体就是由以下 processUpdateQueue 函数处理的

答案

在执行 ABC 函数的时候,其函数本身的执行是不可以中断的,只是在执行到 B 的 setState 的时候,进行例行的 ensureRootIsScheduled(root, eventTime); 进行任务调度,判断是否存在更高优先级的任务,这个时候判断到有更高优先级的 D 任务在 任务队列里面了,那么就会通过 scheduleCallback中执行requestHostTimeout将一个宏任务推到浏览器的任务队列中,那么在 onDragHandler 执行完成后,发现任务队列存在任务就挂起当前渲染 Update 的向下执行,从而执行 D 这个更高优先级的任务,然后执行完成后,继续执行之前被打断的 Update,从而保证了 ABC 的执行流程。

这里面涉及到很多调度机制,在这边我们去看一下 在 ClassComponent 中如何维护整个执行的更新联调

对于第一个问题,我们可以看一下 setState 函数的源码, 其主要是创建一个 update 对象,然后将其添加到 FiberNode.updateQueue.share.pending 链表中, 而不会真正的执行 修改 state 的操作。

具体的修改操作是组件的渲染阶段(beginWork) 中调用 processUpdateQueue 函数,这个函数会根据 FiberNode.updateQueue 中的 update 链表来更新组件的 state。

如上述例子一

触发 Update 的方法

  • ReactDOM.render

  • this.setState

  • this.forceUpdate

  • useState —— FunctionComponent

  • useReducer —— FunctionComponent

可以看出主要由以下三种组件触发的 HostRootFunctionComponent , ClassComponent , 由于函数式编程的差异,所以 HostRoot, ClassComponent使用的同一套 Update 队列(FiberNode.updateQueue) , FunctionComponent 使用的是 FiberNode.memoizedState去维持更新 Update 的顺序问题,下面我们看一下 ClassComponent 的 updateQueue

ClassComponent 的 UpdateQueue 结构

UpdateQueue 的结构如下

js
/**
 * 初始化一个 UpdateQueue 类型的对象
 * @param {*} fiber
 */
export function initializeUpdateQueue<State>(fiber: Fiber): void {
  const queue: UpdateQueue<State> = {
    // 当前的 state 对象 , 即当前组件的 this.state 对象
    baseState: fiber.memoizedState,
    // -- 保存的被打断的历史 update 的值
    firstBaseUpdate: null,
    lastBaseUpdate: null,

    shared: {
      // 保存的当前执行的完成后 update 链表
      pending: null,
      //  保存当前执行完成后 产生的 lane
      lanes: NoLanes,
    },
    // 保存所有的 update中产生副作用的 update 对象 , 即存在 callback 的update
    effects: null,
  };
  fiber.updateQueue = queue;
}

ClassComponent的 update 对象

this.setState(xx) 的时候,其核心调用的是 enqueueSetState函数,然后创建一个 update 对象

js
const update = createUpdate(eventTime, lane);
update.payload = payload;
if (callback !== undefined && callback !== null) {
  if (__DEV__) {
    warnOnInvalidCallback(callback, "setState");
  }
  update.callback = callback;
}

那么 update 对象具体的结构有哪些

js
const update: Update<*> = {
  eventTime,
  lane,

  tag: UpdateState,
  payload: null,
  callback: null,

  next: null,
};
  • eventTime

requestEventTime() 创建的一个事件时间

  • lane

requestUpdateLane(fiber) 生成一个 事件的 优先级 lane

  • tag

Update 的类型,具体可以看下面 Update 的类型

  • payload

事件的负载,记录了 setState 的更新的最新数据

  • callback

更新的回调函数,其会产生副作用,并存储到 updateQueue.effects 中

  • next

存储了下一个 setState 的 update 对象

UpdateQueue 创建流程

从前面我们知道 React 通过一套双缓存 Fiber 树的机制去维护整个 React 的更新,其中

  • 代表当前页面状态的 current Fiber 树
  • 代表正在执行的 workInProgress Fiber 数

这样同一个节点 FiberNode 就存在两个 updateQueue 对象,其中

  • current.updateQueue 保存的是之前生成的 update 数据,
  • workInProgress.updateQueue 保存的是当前执行生成的 update 数据。

在 commit 阶段完成页面渲染后,workInProgress Fiber 树变为 current Fiber 树,workInProgress Fiber 树内 Fiber 节点的 updateQueue 就变成 current updateQueue。

所以在执行拖拽事件 onDragHandler() 的时候,其经历了三次 setState 流程,产生三个 update 对象并添加到当前执行的 阶段完成页面渲染后,workInProgress.updateQueue 对象。

然后在执行完成

processUpdateQueue

那么如上述例子在 触发了一次拓展事件 onDragHandler() 的时候,其经历了三次 setState 流程,并产生 以下 updateQueue 对象

shell
updateQueue = {
    share : {
        pending : update<A>
                  update<A>.next => update<B>
                                    update<B>.next => update<C>
                                                      update<C>.next => updateQueue.share.pending
    }
}

然后再 processUpdateQueue 的时候就经历一下流程

  1. 将当前 queue.shared.pending 产生的 update 环装链表 剪短,并连接在 updateQueue.lastBaseUpdate后面
js
// 1. 将当前执行流程生成的 update 队列拼接到 updateQueue.firstBaseUpdate 和 updateQueue.lastBaseUpdate 中形成双向链表
// The pending queue is circular. Disconnect the pointer between first
// and last so that it's non-circular.
const lastPendingUpdate = pendingQueue;
const firstPendingUpdate = lastPendingUpdate.next;
lastPendingUpdate.next = null;
// Append pending updates to base queue
if (lastBaseUpdate === null) {
  firstBaseUpdate = firstPendingUpdate;
} else {
  lastBaseUpdate.next = firstPendingUpdate;
}
lastBaseUpdate = lastPendingUpdate;

这样上面的结果就变成

updateQueue = {
    firstBaseUpdate : update<A>
                            update<A>.next => update<B>
                                                update<B>.next => update<C>
                                                                  update<C>.next =
    lastBaseUpdate   <=============================================================
}
  1. 将当前的 update 链表指向 current.firstBaseUpdate , 并将 current.lastBaseUpdate.next指向当前的 firstBaseUpdate

如果正好触发了 click 事件,那么就不会插入到浏览器中,而是进行 update<D> 的处理, 这时候正好之前的都放在 current.updateQueue.firstBaseUpdate 上面了,那么这一次就将这个接到 workInProgress.updateQueue.lastBaseUpdate 并将 current.updateQueue.lastBaseUpdate 指向 workInProgress.updateQueue.lastBaseUpdate

updateQueue = {
    firstBaseUpdate : update<A>
                            update<A>.next => update<B>
                                                update<B>.next => update<C>
                                                                  update<C>.next => update<D>
                                                                                    update<D>.next =>
    lastBaseUpdate   <===============================================================================
}

源码如下

js
// 2. 将之前执行的 updateQueue.firstBaseUpdate 拼接到当前 updateQueue.firstBaseUpdate.next , 并将当前 updateQueue.lastBaseUpdate 指向旧的 lastBaseUpdate
const current = workInProgress.alternate;
if (current !== null) {
  // This is always non-null on a ClassComponent or HostRoot
  const currentQueue: UpdateQueue<State> = (current.updateQueue: any);
  const currentLastBaseUpdate = currentQueue.lastBaseUpdate;
  if (currentLastBaseUpdate !== lastBaseUpdate) {
    if (currentLastBaseUpdate === null) {
      currentQueue.firstBaseUpdate = firstPendingUpdate;
    } else {
      currentLastBaseUpdate.next = firstPendingUpdate;
    }
    currentQueue.lastBaseUpdate = lastPendingUpdate;
  }
}
  1. 处理 update

这一步是循环遍历链表,并判断是否是第一个低优先级的 update,那么这时候后面所有的 update 都会存放到一个新的 newFirstBaseUpdate => newLastBaseUpdate 链表中, 对于存在于当前更新通道的 update , 其都会根据 update.tag 进行不同的处理,具体看 getStateFromUpdate

getStateFromUpdate