likes
comments
collection
share

React源码核心系列之状态更新原理🤪🤪🤪

作者站长头像
站长
· 阅读数 21

ReactDOM.createRoot(...)之后都经历了些什么? 迷迷糊糊的讲解了 JSXFiber 树的结构,那么这篇文章我们将来详细讲解一下整个 React 生命周期的运行流程。

什么是Fiber

Fiber 出现之前,react 会递归所有的 vdom 节点,如果 DOM 节点过多,会导致其他事件影响滞后例如点击事件,输入事件等等,造成了页面卡顿。

为了解决这一问题,react 引入了 fiber 这种数据结构,将更新渲染耗时长的大任务,分为许多的小片。每个小片的任务执行完成后,都先去执行其他高优先级的任务,并且能做到中断渲染和恢复。

这样 JavaScript 的主线程就不会被 react 独占,虽然任务执行的总时间不变,但是页面能够及时响应高优先级任务,显得不会卡顿了。

fiber 分片模式下,浏览器主线程能够定期被释放,去执行一些动画之类的,保证了渲染的帧率,为我们提供了一种跟踪、调度、暂停和中止工作的便捷方式,保证了页面的性能和流畅度。

fiber数据结构

React 中,每一个 React 元素对应一个 fiber 对象,那么每个 Fiber 对象有如下的属性,具体如下代码所示:

function FiberNode(
  tag: WorkTag,
  pendingProps: mixed,
  key: null | string,
  mode: TypeOfMode,
) {

  // Instance
  this.tag = tag;// 对应组件的类型
  this.key = key;// key属性
  this.elementType = null;// 元素类型
  this.type = null;// 函数组件或者类组件
  this.stateNode = null;// 真实 DOM 节点

  // Fiber
  this.return = null;// 指向父节点
  this.child = null;// 指向子节点
  this.sibling = null;// 指向兄弟节点
  this.index = 0;// 如果没有兄弟节点为0,如果是父节点下的子节点是数组,结合key做diff

  this.ref = null;// ref 实例

  this.pendingProps = pendingProps;// 新的props
  this.memoizedProps = null;// 旧的props
  this.updateQueue = null;// fiber上的更新队列
  this.memoizedState = null;// 对应memoizedProps,上次渲染的state
  this.dependencies = null;
  this.mode = mode;// 表示当前组件下的子组件的渲染方式
  // Effects
  this.flags = NoFlags; // 用于记录fiber的状态(删除、新增、替换等)
  this.subtreeFlags = NoFlags;// 当前子节点的副作用状态
  this.deletions = null;// 删除的子节点的fiber

  this.lanes = NoLanes; // 优先级
  this.childLanes = NoLanes;

  this.alternate = null;// current树和workInprogress树之间的相互引用
}

updateQueue

在上面的属性当中,我们看到了 this.updateQueue = null;,它是 fiber 对象上的更新队列,每次更新都会创建一个 Update 对象,这个 Update 对象的结果如下所示:

export type Update<State> = {|
  eventTime: number,
  lane: Lane,
  tag: 0 | 1 | 2 | 3,
  payload: any,
  callback: (() => mixed) | null,

  next: Update<State> | null,
|};

从上面的数据结构可以简单了解到,Update 是用来描述当前的对象操作的一些基本信息:时间(eventTime)、优先级(lane)、回调函数(callback)、组件的类型(tag)以及下一个更新对象(next)。

initializeUpdateQueue

当调用 createFiberRoot(...) 函数创建 fiber 的时候,会在该函数内部自动调用 initializeUpdateQueue 函数,该函数的主要作用是为 fiber.updateQueue 属性上添加一个对象,具体代码如下所示:

export function initializeUpdateQueue<State>(fiber: Fiber): void {
  const queue: UpdateQueue<State> = {
    baseState: fiber.memoizedState,
    firstBaseUpdate: null,
    lastBaseUpdate: null,
    shared: {
      pending: null,
      interleaved: null,
      lanes: NoLanes,
    },
    effects: null,
  };
  fiber.updateQueue = queue;
}

enqueueUpdate

在上面的的这个命名当中是一个队列,但它并不是一个队列,而是一个链表结构,pending 永远指向最后一个 update 更新对象,第一个进入的 update 对象是排在第一个,最后一个 update 更新对象的 next 指针指向第一个,具体插入步骤如下图所示:

React源码核心系列之状态更新原理🤪🤪🤪

前者是刚开始只有一个更新时,当有新的更新插入时,会像后者一样,以此类推,形成一个单循环链表,具体代码如下所示:

export function enqueueUpdate<State>(
  fiber: Fiber,
  update: Update<State>,
  lane: Lane,
): FiberRoot | null {
  const updateQueue = fiber.updateQueue;
  if (updateQueue === null) {
    // fiber 被卸载时
    return null;
  }

  // 返回一个对象 {interleaved:null, lanes:0, pending:null}
  const sharedQueue: SharedQueue<State> = (updateQueue: any).shared;

  // pending 永远指向最后一个更新
  if (isUnsafeClassRenderPhaseUpdate(fiber)) {
    const pending = sharedQueue.pending;
    if (pending === null) {
      // 第一次更新创建一个循环单链表
      update.next = update;
    } else {
      // 如果更新队列不为空,取出第一个更新
      update.next = pending.next;
      pending.next = update;
    }
    sharedQueue.pending = update;
    return unsafe_markUpdateLaneFromFiberToRoot(fiber, lane);
  } else {
    return enqueueConcurrentClassUpdate(fiber, sharedQueue, update, lane);
  }
}

enqueueUpdate调用

当我们首次渲染的时候,或者通过调用 setState 方法,都会调用 classComponentUpdater 对象下的 enqueueSetState(...) 方法,在该方法里面调用了 enqueueUpdate(fiber, update, lane),接下来我们通过控制台打印输出看看这个 update 的值是怎么样变化的,首先我们编写这样的页面组件:

import React, { Component } from "react";

class App extends Component {
  constructor() {
    super();
    this.state = {
      count: 1,
    };
  }

  render() {
    return (
      <div>
        <h2>count的值为 {this.state.count}</h2>
        <h1
          onClick={() => {
            this.setState({ count: this.state.count + 1 });
            this.setState({ count: this.state.count + 2 });
            this.setState({ count: this.state.count + 3 });
          }}
        >
          点击按钮
        </h1>
      </div>
    );
  }
}

export default App;

当首次渲染时,通过控制台打印 update 对象有如下所示:

React源码核心系列之状态更新原理🤪🤪🤪

首次渲染的值我们看完了,那么我们再看看通过 setState 后的 update 对象的是怎么改变的吧:

React源码核心系列之状态更新原理🤪🤪🤪

React18 之后,为了提高性能,避免每次调用 setState 都进行一次更新,避免 render 函数会被频繁调用,所以它采用的方法是批量更新,而 Update 做成单链表的主要原因也是这样,只需利用一次指针的查找便能查找到最后一个 update 对象,同时插入新的 update 对象也非常方便。

UpdateQueue 如何更新

当调度任务依次执行时,会调用 processUpdateQueue 函数计算最终的 state,并且会根据 lane 来区分执行的时候有先后顺序,对应的是 processUpdateQueue 的先后执行顺序。

processUpdateQueue

  • 首先保存了链表的第一个节点和最后一个节点,它们分别是 firstBaseUpdatelastBaseUpdate;
  • 保存最后一次的更新 queue.shared.pending 赋值为变量 pendingQueue;
  • 如果 pendingQueue 不为空,记录第一个和最后一个 update,并将单循环链表设置为单链表;
  • 将待更新的第一个 update 对象添加到更新队列中的第一个更新元素,将最后一个添加到更新队列中的最后一个更新元素;
  • 判断 workInProgress.alternate 是否为空;
  • 获取当前的 currentQueue,在 React 的更新流程中,如果是 ClassComponent 或者 HostRoot,currentQueue 原理永远不为空:
    1. 首次渲染情况下:

React源码核心系列之状态更新原理🤪🤪🤪

2. setState情况下:

React源码核心系列之状态更新原理🤪🤪🤪

  • 获取到当前队列中的最后一个更新 currentLastBaseUpdate,如果它不等于第一个并且不等于空,则又将其设置为循环链表;
  • 如果 firstBaseUpdate 不为空,则将 queue.baseState 赋值给 newState,例如通过点击按钮后,会保存上一次的状态 {count: 1};
  • 进入 do...while(...) 循环;
  • 通过 isSubsetOfLanes 函数用于判断当前更新的优先级(updateLane)是否满足更新队列的优先级;
  • 如果优先级不足则跳过,并将其存放在 newFirstBaseUpdatenewLastBaseUpdate 中,会将处理好的 newState 存放于 newBaseState 中,更新并升级 update 对象的优先级,让其能在下一次遍历时能够执行;
  • 如果优先级足够并且判断 newLastBaseUpdate 不为空,如果不为空说明之前有被跳过的 uodate 对象,则将现在这个 upodate 对象放入到 newLastBaseUpdate 中并将被推迟的 update 对象的优先级设置为 Nolane 等级;
  • 调用 getStateFromUpdate 计算新的 state,并将其赋值在 newState 中;
  • 如果调用 setState 时候传入有回调函数,那么会标记当前 fibercallback并存放在 flags 中;
  • 继续遍历下一个 update 对象,检查 queue.shared.pending 是否有更新,然后把剩余的推入到 lastBaseUpdate 中,当 pendingQueue 执行完成后退出循环;
  • 如果 newLastBaseUpdate 为空,说明没有被延迟的 update,会把整个队列的计算结构的 newState 赋值给 queue.baseState;
  • 更新 workInProgress 节点的优先级,更新策略为如果没有优先级被跳过,则意味着本次将 update 都处理完了, lanes 清空,否则将低优先级 update 的优先级升级;
  • 更新 workInProgress节点上的 memoizedState;

processUpdateQueue 函数的具体代码如下所示:

export function processUpdateQueue<State>(
  workInProgress: Fiber,
  props: any,
  instance: any,
  renderLanes: Lanes
): void {
  // This is always non-null on a ClassComponent or HostRoot
  const queue: UpdateQueue<State> = (workInProgress.updateQueue: any);

  hasForceUpdate = false;
  let firstBaseUpdate = queue.firstBaseUpdate;

  let lastBaseUpdate = queue.lastBaseUpdate;

  let pendingQueue = queue.shared.pending;
  if (pendingQueue !== null) {
    // 如果队列不为空,则把 fiber 中的 queue pending 指针置位空
    queue.shared.pending = null;
    // 获取最后一个更新
    const lastPendingUpdate = pendingQueue;
    // 因为是循环链表,获取 lastPendingUpdate.next 就是头节点
    const firstPendingUpdate = lastPendingUpdate.next;
    // 将其设置为单链表
    lastPendingUpdate.next = null;
    if (lastBaseUpdate === null) {
      firstBaseUpdate = firstPendingUpdate;
    } else {
      lastBaseUpdate.next = firstPendingUpdate;
    }
    lastBaseUpdate = lastPendingUpdate;

    const current = workInProgress.alternate;
    if (current !== null) {
      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;
      }
    }
  }
  // These values may change as we process the queue.
  if (firstBaseUpdate !== null) {
    // 创建副本变量
    let newState = queue.baseState;
    console.log(newState);
    let newLanes = NoLanes;
    let newBaseState = null;
    let newFirstBaseUpdate = null;
    let newLastBaseUpdate = null;
    let update = firstBaseUpdate;
    do {
      // 获取 update 的优先级,判断是否需要执行更新
      const updateLane = update.lane;
      const updateEventTime = update.eventTime;
      // 更新队列的第一个 update 的优先级低于 renderLanes
      if (!isSubsetOfLanes(renderLanes, updateLane)) {
        // 由于优先级低于 renderLanes,不执行该 update 的更新
        const clone: Update<State> = {
          eventTime: updateEventTime,
          lane: updateLane,
          tag: update.tag,
          payload: update.payload,
          callback: update.callback,
          next: null,
        };
        // 本次没有更新的 update,会优先放到下一次去判断要不要更新
        if (newLastBaseUpdate === null) {
          newFirstBaseUpdate = newLastBaseUpdate = clone;
          newBaseState = newState;
        } else {
          newLastBaseUpdate = newLastBaseUpdate.next = clone;
        }
        // 更新 update 的 lane,往里塞入了满足条件的优先级,这样下次遍历到时才能执行
        newLanes = mergeLanes(newLanes, updateLane);
      } else {
        if (newLastBaseUpdate !== null) {
          const clone: Update<State> = {
            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
        );
        const callback = update.callback;
        if (
          callback !== null &&
          update.lane !== NoLane
        ) {
          workInProgress.flags |= Callback;
          const effects = queue.effects;
          if (effects === null) {
            queue.effects = [update];
          } else {
            effects.push(update);
          }
        }
      }
      update = update.next;
      if (update === null) {
        pendingQueue = queue.shared.pending;
        if (pendingQueue === null) {
          break;
        } else {
          const lastPendingUpdate = pendingQueue;
          const firstPendingUpdate =
            ((lastPendingUpdate.next: any): Update<State>);
          lastPendingUpdate.next = null;
          update = firstPendingUpdate;
          queue.lastBaseUpdate = lastPendingUpdate;
          queue.shared.pending = null;
        }
      }
    } while (true);

    if (newLastBaseUpdate === null) {
      newBaseState = newState;
    }
    queue.baseState = ((newBaseState: any): State);
    queue.firstBaseUpdate = newFirstBaseUpdate;
    queue.lastBaseUpdate = newLastBaseUpdate;
    const lastInterleaved = queue.shared.interleaved;
    if (lastInterleaved !== null) {
      let interleaved = lastInterleaved;
      do {
        newLanes = mergeLanes(newLanes, interleaved.lane);
        interleaved = ((interleaved: any).next: Update<State>);
      } while (interleaved !== lastInterleaved);
    } else if (firstBaseUpdate === null) {
      queue.shared.lanes = NoLanes;
    }
    markSkippedUpdateLanes(newLanes);
    workInProgress.lanes = newLanes;
    workInProgress.memoizedState = newState;
  }
}

getStateFromUpdate

在上面的函数中有调用过 getStateFromUpdate(...) 函数,在开始之前我们先通过控制台打印一下这些参数传进来的值分别是什么?

下面的图片中显示的是首次渲染的输出结果

React源码核心系列之状态更新原理🤪🤪🤪

下面的图片中显示的是调用 setState 的输出结果

React源码核心系列之状态更新原理🤪🤪🤪

prevState 存放的是上一次更新的值,而 update 对象是本次更新的信息。

在这个函数里面主要有 ReplaceStateCaptureUpdateUpdateStateForceUpdate 四种类型更新,分别可以翻译为替换更新、捕获性更新、setState更新、强制性更新。

下面来对这四个更新进行讲解,它们分别是:

  • UpdateState: 判断 partialState 是否函数,如果是函数则直接调用后获取返回结果(你也可以将其理解为将 setState 从异步变为同步),否则直接把 partialState 作为 newState 的一部分,最后与原来的 state 使用 assign({}, prevState, partialState) 进行合并;
  • ReplaceState: 与 UpdateState 的流程相似,但是最后不会做合并,而是直接把结果作为新的 state 返回;

参考文章

总结

从下载 React 源码到现在,过了大概半个月时间,满脑子天天都是 React 源码,现在可以放一放咯,准备春招实习去了。

不妨大胆一点,去做你想做的事情,成为你想称为的人🤗🤗🤗