React Fiber 架构原理之3 —— “更新”到底是个啥

701 阅读7分钟

前面两篇,我们基本清楚了 Fiber 的调度机制和 Fiber 树的构建机制。但这一切的前提都是,我们通过某种方式触发了“更新”。这篇我们将回到开端,研究研究“更新”。

在 React 中,我们有几种行为会触发更新:

this.setState this.forceUpdate useState useReducer

前两个来自 Class Component,后两个来自 Function Component,分开看。

Part 1 Class Component 的更新

入口

在 ReactBaseClasses 中有 setState 的实现,把传入的 partialState 传入 updater 的 enqueueSetState:

// ReactBaseClasses.js
Component.prototype.setState = function(partialState, callback) {
  this.updater.enqueueSetState(this, partialState, callback, 'setState');
};

在 Fiber 下,enqueueSetState 由 ReactFiberClassComponent 实现,先 createUpdate 创建 Update,再 enqueueUpdate 把 Update 加入队列,最后通过 scheduleWork 发起调度。

// ReactFiberClassComponent.js
enqueueSetState(inst, payload, callback) {
  const fiber = getInstance(inst);
  const update = createUpdate(expirationTime);
  update.payload = payload;
  enqueueUpdate(fiber, update);
  scheduleWork(fiber, expirationTime);
},

Update 数据结构

createUpdate 返回这样一个新建的 Update 对象:

{
  expirationTime: expirationTime,
  tag: UpdateState,
  payload: null,
  callback: null,
  next: null,
  nextEffect: null,
};

这里有两个关键属性:payload 和 callback,记录触发当前更新的 partialState 和回调,也就是 setState 传入的东西。

创建的 Update 会继续通过 enqueueUpdate 加入 Fiber 节点的 Update 队列。先来一张图说明 Fiber、Update、Update 队列之间的关系。

  • 未处理的 Update 对象,会以「单向链表」的形式组织起来,Update 间以 next 相连。
  • Update Queue 是这个链表的入口,通过 firstUpdate、lastUpdate 连接到链表头尾,并挂在 Fiber 节点的 updateQueue 上。

在 createUpdateQueue 方法中,我们可以看到完整的 Update Queue 结构:

export function createUpdateQueue<State>(baseState: State): UpdateQueue<State> {
  const queue: UpdateQueue<State> = {
    baseState,
    firstUpdate: null,
    lastUpdate: null,
    firstCapturedUpdate: null,
    lastCapturedUpdate: null,
    firstEffect: null,
    lastEffect: null,
    firstCapturedEffect: null,
    lastCapturedEffect: null,
  };
  return queue;
}

处理新 Update

那要怎样把一个 Update 加入 Update Queue 呢?只要接到 lastUpdate 的 next 上,并更改 lastUpdate 指向:

function appendUpdateToQueue<State>(queue: UpdateQueue<State>, update: Update<State>) {
  if (queue.lastUpdate === null) {
    queue.firstUpdate = queue.lastUpdate = update;
  } else {
    queue.lastUpdate.next = update;
    queue.lastUpdate = update;
  }
}

enqueueUpdate 兼容 workInProgress 树

现在回到 enqueueUpdate,要通过上面的方法把 Update 放到 Queue 中。

那问题来了,在任一时刻,Fiber 架构下可能存在两棵 Fiber 树(参考React Fiber 架构原理:关于 Fiber 树的一切 - 知乎)——当前 Fiber 所在的 current 树和正在构建的 workInProgress 树。这时候又要怎么处理呢?

情况一:没有workInProgress 树关联节点

很容易判断 workInProgress 上对应节点不存在:fiber.alternate === null。不存在的话操作很简单了,检查 fiber.updateQueue 是否存在,然后更新:

queue1 = fiber.updateQueue;
if (queue1 === null) {
  queue1 = fiber.updateQueue = createUpdateQueue(fiber.memoizedState);
}
appendUpdateToQueue(queue1, update);

情况二:有 workInProgress 树关联节点

这时我们要保证 workInProgress 树上关联节点的 Update Queue 也被同步修改。首先还是保证两边的 updateQueue 都存在:

queue1 = fiber.updateQueue;
queue2 = alternate.updateQueue;
if (queue1 === null) {
  if (queue2 === null) {
    queue1 = fiber.updateQueue = createUpdateQueue(fiber.memoizedState);
    queue2 = alternate.updateQueue = createUpdateQueue(alternate.memoizedState);
  } else {
    queue1 = fiber.updateQueue = cloneUpdateQueue(queue2);
  }
} else {
  if (queue2 === null) {
    queue2 = alternate.updateQueue = cloneUpdateQueue(queue1);
  }
}

然后插入 Update:

appendUpdateToQueue(queue1, update);
appendUpdateToQueue(queue2, update);

Update 创建入队后,再回到最外层的 enqueueSetState 函数,在里面执行 scheduleWork 发起调度。

Part 2 Function Component 的更新

对于 Function Component,我们是通过 useState、useReducer 这样的 hooks 管理状态并发起更新的:

function MyComponent () {
  const [ a, setA ] = useState(1);
  ...setA(2)
};

hook 和 Fiber 的关系

Function Component 自己是没有状态的,它的状态来自于每次执行函数时,useState 返回的内容。

每个 useState 对应一个 hook 对象,当 Function Component 首次渲染时,会把所有调用到的 useState 对应的 hook 对象,以链表的形式挂载到对应 Fiber 节点的 memoizedState 上。(hook 的实现细节不展开讨论)

hook 通过返回值向函数组件提供状态和改变状态并触发更新的方法。

hook 上的 Update

因此,一个 hook 要自己管理自己的“a”和“setA”,在代码里称作 memoizedState 和 dispatch。在每个 useState 第一次被调用时(也就是 Function Component 的 mount 阶段),hook 被创建:

function mountState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
  const hook = mountWorkInProgressHook();
  hook.memoizedState = hook.baseState = initialState;
  const queue = (hook.queue = {
    last: null,
    dispatch: null,
    lastRenderedReducer: basicStateReducer,
    lastRenderedState: (initialState: any),
  });
  const dispatch: Dispatch<BasicStateAction<S>> = (queue.dispatch = (dispatchAction.bind(
    null,
    ((currentlyRenderingFiber: any): Fiber),
    queue,
  ): any));
  return [hook.memoizedState, dispatch];
}

和 Class Component 的 Fiber 类似,hook 上构建了这样的数据结构来管理 Update:

这里和前面 Class Component Fiber 上的 Update 有两点不同:

  • Update 不是单向链表,而是单项循环链表,最后的一个 Update 会通过 next 指向第一个
  • 因此,queue 不必同时指向首尾,而是只通过 last 指向最后一个 Update

处理新 Update

前面代码,我们看到 dispatch 最终是由一个 dispatchAction 实现的,这个方法 bind 了当前 Fiber 节点和当前 hook 的 queue,另外 setA 传入的值会出现在第三个参数上:

function dispatchAction<S, A>(
  fiber: Fiber,
  queue: UpdateQueue<S, A>,
  action: A,
)

dispatchAction 首先创建一个 Update 对象 update,然后给它插到 queue 里:

const update: Update<S, A> = {
  expirationTime,
  action,
  eagerReducer: null,
  eagerState: null,
  next: null,
};
const last = queue.last;
if (last === null) {
  update.next = update;
} else {
  const first = last.next;
  if (first !== null) {
    update.next = first;
  }
  last.next = update;
}
queue.last = update;

这个是符合前面结构的,看图:

最后,在传入的 fiber 上发起调度:

scheduleWork(fiber, expirationTime);

Part 3 render 中的更新获取

前面两章我们知道了,更新操作会构造一个 Update,放到更新队列里(Class Component 在 Fiber 上,Function Component 在 Hook 上)。最后发起 scheduleWork。

但这个 scheduleWork 是异步调度的(参考React Fiber 架构原理 —— 自底向上盘一盘 Scheduler - 知乎),也就是最终发起 render 的时候,队列里可能已经“攒”了好几个 Update。所以接下来看下,在 render 阶段,更新队列是如何拿到最终更新值的。

入口

调度发起后,会经由 workLoop - performUnitOfWork 到 beginWork。 ——React Fiber 架构原理 —— 自底向上盘一盘 Scheduler - 知乎 beginWork 会根据 fiber 节点的 tag 分发到不同的 updateXXX 函数。 ——React Fiber 架构原理:关于 Fiber 树的一切 - 知乎

这里对应 Class Component 和 Function Component 的分别是两个函数:updateClassComponent 和 updateFunctionComponent。

Class Component 的更新合并

updateClassComponent 调 updateClassInstance,再调 processUpdateQueue 更新 memoizedState,最终赋值给实例的 state:

function updateClassInstance( current: Fiber, workInProgress: Fiber, ctor: any, newProps: any ): boolean {
  const instance = workInProgress.stateNode;
  // ... 省略:一些属性处理
  if (updateQueue !== null) {
    processUpdateQueue( workInProgress, updateQueue, newProps, instance, renderExpirationTime);
    newState = workInProgress.memoizedState;
  }
  // ... 省略:shouldUpdate 相关处理
  instance.state = newState;
}

显然这个 processUpdateQueue 是个清洗 update queue 并更新 memoizedState 的方法。

update queue 要怎么清洗呢?假设我们有如下 Fiber,memorizedState 初始值是{ a:1, b:2, c:3 }。三个排队的 Update 分别修改了 a、b、a。我们只要从头遍历,逐个合并,就能得到最终的 { a:12, b:22, c:3 }。期间的局部修改、覆盖都合预期。

processUpdateQueue 代码是这么写的(片段):

let update = queue.firstUpdate;
let resultState = queue.baseState;
while (update !== null) {
  resultState = getStateFromUpdate(...);
  update = update.next;
}
queue.baseState = newBaseState;
queue.lastUpdate = null;
queue.firstUpdate = null;
workInProgress.memoizedState = resultState;

这里 getStateFromUpdate 抽出来是因为要处理几种不同的 update,但我们 setState 用的是UpdateState。根据参数类型,返回更新的对象就好:

switch (update.tag) {
  case UpdateState: {
    const payload = update.payload;
    let partialState;
    if (typeof payload === 'function') {
      partialState = payload.call(instance, prevState, nextProps);
    } else {
      partialState = payload;
    }
    return Object.assign({}, prevState, partialState);
  }
}

到这里,fiber 的 update queue 合并完成,memoizedState 更到最新,可以继续 render 了。

Function Component 的更新合并

Hook 的处理相对简单。协调过程中,会从 updateFunctionComponent 调到 renderWithHooks,这里面有个“换挡”机制。会根据 current 情况判断是从新 mount 还是在旧的基础上 update。然后切换不同的 ReactCurrentDispatcher。

ReactCurrentDispatcher.current = nextCurrentHook === null ? HooksDispatcherOnMount : HooksDispatcherOnUpdate;

在首次调用时,直接赋值给 memoizedState:

function mountState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
  const hook = mountWorkInProgressHook();
  hook.memoizedState = hook.baseState = initialState;
  // ... 省略
}

update 阶段的 useState 调用会走到 updateState - updateReducer,这里仍然是即时通过 reducer 计算并修改了 memoizedState,再把最新的值作为第一个参数返回给 Function 用于渲染。

// updateReducer
let newState = hook.memoizedState;
let update = firstRenderPhaseUpdate;
do {
  const action = update.action;
  newState = reducer(newState, action);
  update = update.next;
} while (update !== null);
hook.memoizedState = newState;
return [hook.memoizedState, dispatch];

因此,当你在 render 时候调用 useState 取值时,取到的一定是最新的。

Part Z 总结

本篇比较简短,主要讨论了 React 里的“更新”。

  • 更新可以通过 setState、useState 等方式触发,分别来自 Class Component、Function Component。
  • 两种组件的更新数据结构有所不同,但整体上都是一个 Update 对象、通过链表挂在 Fiber 或者 Hook 上。
  • Update 合并的过程就是链表遍历的过程,其目标是获得最新的 state。这个过程,对类组件来说发生在 render 阶段,对 Hook 来说发生在 dispatch 调用阶段。

React Fiber 原理系列

  1. React Fiber 架构原理:关于 Fiber 树的一切 - 知乎
  2. React Fiber 架构原理 —— 自底向上盘一盘 Scheduler - 知乎
  3. React Fiber 架构 —— “更新”到底是个啥