Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

React批量(异步)更新以及同步更新原理 #5

Open
lizuncong opened this issue Apr 30, 2022 · 0 comments
Open

React批量(异步)更新以及同步更新原理 #5

lizuncong opened this issue Apr 30, 2022 · 0 comments

Comments

@lizuncong
Copy link
Owner

React 更新过程相关的代码都在 ReactFiberWorkLoop.js 文件中

批处理(异步更新)机制简述

React 源码中,通过全局变量 executionContext 控制 React 执行上下文,指示 React 开启同步或者异步更新。executionContext 一开始被初始化为 NoContext,因此 React 默认是同步更新的。

当我们在合成事件中调用 setState 时:

const onBtnClick = () => {
  debugger;
  setCount(1);
  setCount(2);
};
<button onClick={onBtnClick}>{count}</button>;

实际上合成事件会调用 batchedEventUpdates(onBtnClick),将我们的函数 onBtnClick 拦截一层。batchedEventUpdates 实现如下:

function batchedUpdates(fn, a) {
  var prevExecutionContext = executionContext; // 保存原来的值
  executionContext |= EventContext;
  try {
    return fn(a); // 调用我们的合成事件逻辑onBtnClick
  } finally {
    executionContext = prevExecutionContext; // 函数执行完成恢复成原来的值

    if (executionContext === NoContext) {
      // Flush the immediate callbacks that were scheduled during this batch
      resetRenderTimer();
      flushSyncCallbackQueue();
    }
  }
}

const batchedEventUpdates = batchedUpdates;

可以看出该方法在执行时会更改 executionContext 指示 React 异步更新。这也是为什么我们在合成事件中多次调用 setState,而 React 只会更新一次的原因。函数执行完成,executionContext 又会恢复成原来的值。如果我们的 setState 逻辑是在 setTimeout 中,当合成事件执行完毕,此时 executionContext 恢复成原来的值, setTimeout 中的 setState 就变成了同步更新

React17 版本中提供了一个 unstable_batchedUpdates API,如果我们希望在 setTimeout 等异步任务中开启批量更新,则可以使用这个方法包裹一下我们的业务代码。

exports.unstable_batchedUpdates = batchedUpdates;

更新队列 syncQueue

React 使用 syncQueue 维护一个更新队列。syncQueue 数组存的是 performSyncWorkOnRootperformSyncWorkOnRoot 这个方法从根节点开始更新

function scheduleSyncCallback(callback) {
  if (syncQueue === null) {
    syncQueue = [callback];

    // 开始调度,其实这部分逻辑相当于queueMicrotask(flushSyncCallbackQueueImpl),让更新在
    // 下一个微任务中执行
    immediateQueueCallbackNode = Scheduler_scheduleCallback(
      Scheduler_ImmediatePriority,
      flushSyncCallbackQueueImpl
    );
  } else {
    // 注意这里不需要再开启一个新的微任务!!
    syncQueue.push(callback);
  }

  return fakeCallbackNode;
}
// flushSyncCallbackQueueImpl简单实现如下:
function flushSyncCallbackQueueImpl() {
  syncQueue.forEach((cb) => cb());
  syncQueue = null;
}

scheduleSyncCallback 函数中如果 syncQueuenull,则初始化一个数组,开启一个微任务调度。而如果 syncQueue 不为 null,则添加进更新队列,此时不需要再重新开启一个微任务调度

如果 executionContext === NoContext 则直接刷新 syncQueue

function scheduleUpdateOnFiber(fiber, lane, eventTime) {
  // 省略前面的代码
  if (executionContext === NoContext) {
    resetRenderTimer();
    flushSyncCallbackQueue();
  }
  // 省略后面的代码
}

批量更新场景

在合成事件等 React 能够接管的场景中,setState批量更新的。点击按钮,查看控制台可以发现只打印了一次:

render====== 2

const Counter = () => {
  const [count, setCount] = useReducer(reducer, 0);
  console.log("render======", count);
  return (
    <button
      onClick={() => {
        debugger;
        setCount(1);
        setCount(2);
      }}
    >
      {count}
    </button>
  );
};

同步更新场景

setTimeoutPromise回调异步任务 场景中,setState同步更新的。点击按钮,查看控制台可以发现打印了两句话:

render====== 1

render====== 2

const Counter = () => {
  const [count, setCount] = useReducer(reducer, 0);
  console.log("render======", count);
  return (
    <button
      onClick={() => {
        setTimeout(() => {
          debugger;
          setCount(1);
          setCount(2);
        }, 0);
      }}
    >
      {count}
    </button>
  );
};

批量更新机制主流程源码

onClick 函数里加一行 debugger。点击按钮,开始 debug。首先执行的是 dispatchAction 函数,但是如果我们追溯函数调用栈,可以发现实际上是会先执行合成事件相关的函数:

image

合成事件调用了 batchedEventUpdates,此时 executionContext 已经被设置为批量更新

image

回到 dispatchAction 方法中,这个方法主要是构造更新队列,然后调用 scheduleUpdateOnFiber 开始调度更新,异步 or 同步更新的逻辑主要在这个函数的流程中!!scheduleUpdateOnFiber 主要流程如下:

const SyncLane = 1;
const SyncLanePriority = 15;
const NoContext = 0;
let executionContext = NoContext;
let syncQueue = [];
const scheduleUpdateOnFiber = (fiber, lane, eventTime) => {
  const root = markUpdateLaneFromFiberToRoot(fiber);
  if (lane === SyncLane) {
    // 开始创建一个任务,从根节点开始进行更新
    ensureRootIsScheduled(root);
    // 如果当前的executionContext执行上下文环境是NoContext(非批量)
    if (executionContext === NoContext) {
      // 需要注意,我们在ensureRootIsScheduled函数中,将flushSyncCallbackQueue放在了微任务中去执行,
      // 但是如果executionContext是同步更新的话,这里会直接调用flushSyncCallbackQueue开始更新任务,更新完成后
      // flushSyncCallbackQueue会清空syncQueue

      flushSyncCallbackQueue();
    }
  }
};
function ensureRootIsScheduled(root) {
  const newCallbackPriority = returnNextLanesPriority();
  const existingCallbackPriority = root.callbackPriority;

  if (existingCallbackPriority === newCallbackPriority) {
    // The priority hasn't changed. We can reuse the)
    return;
  }

  if (newCallbackPriority === SyncLanePriority) {
    newCallbackNode = scheduleSyncCallback(
      performSyncWorkOnRoot.bind(null, root)
    );
  }
  root.callbackPriority = newCallbackPriority;
}

// 其实就是把performSyncWorkOnRoot函数添加到队列里,在下一个微任务里面执行
function scheduleSyncCallback(callback) {
  if (syncQueue === null) {
    syncQueue = [callback]; // Flush the queue in the next tick, at the earliest.

    immediateQueueCallbackNode = Scheduler_scheduleCallback(
      Scheduler_ImmediatePriority,
      flushSyncCallbackQueue
    );
  } else {
    syncQueue.push(callback);
  }
}
// flushSyncCallbackQueue简单实现如下:
function flushSyncCallbackQueue() {
  syncQueue.forEach((cb) => cb());
  syncQueue = null;
}

performSyncWorkOnRoot 从根节点开始更新,这个不属于本节内容。

当我们点击按钮,从合成事件派发到 React 从当前 fiber 节点开始调度更新,并且决定是异步或者同步更新的主要流程如下图:

image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant