useSyncExternalStore使用及原理

271 阅读4分钟

注:这个hook使用了发布订阅的订阅器即subscribe

一、使用

1.ts声明

export function useSyncExternalStore<Snapshot>(
    // onStoreChage可以理解为用例里面todosStore.subscribe函数入参listener;
    // onStoreChage对应react运行时函数subscribeToStore的handleStoreChange变量
    subscribe: (onStoreChange: () => void) => () => void,
    getSnapshot: () => Snapshot,
    getServerSnapshot?: () => Snapshot,
): Snapshot;

注:useSyncExternalStore hook返回类型是Snapshot;根据useSyncExternalStore的第二个入参getSnapshotts类型也能自动推导出是getSnapshot函数返回值类型Snapshot

2.用例解析

官方用例

TSX文件

import { useSyncExternalStore } from 'react';
import { todosStore } from './todoStore.js';

export default function TodosApp() {
  // 添加订阅,返回Snapshot(快照)
  const todos = useSyncExternalStore(todosStore.subscribe, todosStore.getSnapshot);
  return (
    <>
      <button onClick={() => todosStore.addTodo()}>Add todo</button>
      <hr />
      <ul>
        {todos.map(todo => (
          <li key={todo.id}>{todo.text}</li>
        ))}
      </ul>
    </>
  );
}

Store文件(第三方store)

一个简单的发布订阅

// 这是一个第三方 store 的例子,
let nextId = 0;
let todos = [{ id: nextId++, text: 'Todo #1' }];
let listeners = [];

export const todosStore = {
  // 改变state
  addTodo() {
    todos = [...todos, { id: nextId++, text: 'Todo #' + nextId }]
    emitChange();
  },
  // 发起订阅有react触发
  subscribe(listener) {
    // push进监听队列
    listeners = [...listeners, listener];
    // return出一个移除订阅
    return () => {
      listeners = listeners.filter(l => l !== listener);
    };
  },
  // 获取Snapshot(快照)
  getSnapshot() {
    return todos;
  }
};
// 发布订阅触发监听事件
function emitChange() {
  for (let listener of listeners) {
    listener();
  }
}

二、原理

image.png

1.挂载阶段关键函数

这里忽略服务端渲染代码

mountSyncExternalStore

function mountSyncExternalStore<T>(
    // useSyncExternalStore hook的第一个参数
    subscribe: (() => void) => () => void,
    // useSyncExternalStore hook的第二个参数,读取store中的Snapshot
    getSnapshot: () => T,
  ): T {
    const fiber = currentlyRenderingFiber;
    const hook = mountWorkInProgressHook();
    // 执行 getSnapshot 函数,读取store中的Snapshot。类比用例中todosStore.getSnapshot
    nextSnapshot = getSnapshot();
    // Unless we're rendering a blocking lane, schedule a consistency check.
    // Right before committing, we will walk the tree and check if any of the
    // stores were mutated.
    //
    // We won't do this if we're hydrating server-rendered content, because if
    // the content is stale, it's already visible anyway. Instead we'll patch
    // it up in a passive effect.
    const root: FiberRoot | null = getWorkInProgressRoot();

    if (root === null) {
        throw new Error(
            'Expected a work-in-progress root. This is a bug in React. Please file an issue.',
        );
    }

    if (!includesBlockingLane(root, renderLanes)) {
        pushStoreConsistencyCheck(fiber, getSnapshot, nextSnapshot);
    }


    // Read the current snapshot from the store on every render. This breaks the
    // normal rules of React, and only works because store updates are
    // always synchronous.
    // 缓存从外部store Snapshot,重新渲染时可以从hooks链表上获取当前hook的Snapshot
    hook.memoizedState = nextSnapshot;
    // 这个init对象很重要,存储了当前Snapshot和useSyncExternalStore的第二个参数getSnapshot函数
    const inst: StoreInstance<T> = {
        value: nextSnapshot,
        getSnapshot,
    };
    hook.queue = inst;

    // Schedule an effect to subscribe to the store.
    // 挂载effect,订阅外部的store;subscribeToStore触发订阅;mountEffect类比useEffect第一次执行
    mountEffect(subscribeToStore.bind(null, fiber, inst, subscribe), [subscribe]);

    // Schedule an effect to update the mutable instance fields. We will update
    // this whenever subscribe, getSnapshot, or value changes. Because there's no
    // clean-up function, and we track the deps correctly, we can call pushEffect
    // directly, without storing any additional state. For the same reason, we
    // don't need to set a static flag, either.
    // TODO: We can move this to the passive phase once we add a pre-commit
    // consistency check. See the next comment.
    fiber.flags |= PassiveEffect;
    pushEffect(
        HookHasEffect | HookPassive,
        updateStoreInstance.bind(null, fiber, inst, nextSnapshot, getSnapshot),
        undefined,
        null,
    );
    // 这里返回Snapshot,用例中todos
    return nextSnapshot;
}

subscribeToStore

function subscribeToStore(fiber, inst, subscribe) {
  const handleStoreChange = () => {
    // The store changed. Check if the snapshot changed since the last time we
    // read from the store.
    // inst的值=>: const inst: StoreInstance<T> = { value: nextSnapshot, getSnapshot, };
    if (checkIfSnapshotChanged(inst)) {
      // Force a re-render.
      // 强制重新渲染
      forceStoreRerender(fiber);
    }
  };
  // Subscribe to the store and return a clean-up function.
  // 订阅store
  return subscribe(handleStoreChange);
}

checkIfSnapshotChanged

function checkIfSnapshotChanged(inst) {
  const latestGetSnapshot = inst.getSnapshot;
  const prevValue = inst.value;
  try {
    const nextValue = latestGetSnapshot();
    // 判断前后值是否一样
    return !is(prevValue, nextValue);
  } catch (error) {
    return true;
  }
}

2.更新阶段的关键函数

updateSyncExternalStore

function updateSyncExternalStore<T>(
  subscribe: (() => void) => () => void,
  getSnapshot: () => T,
): T {
  const fiber = currentlyRenderingFiber;
  const hook = updateWorkInProgressHook();
  // Read the current snapshot from the store on every render. This breaks the
  // normal rules of React, and only works because store updates are
  // always synchronous.
  const nextSnapshot = getSnapshot();
  // 获取旧的Snapshot
  const prevSnapshot = hook.memoizedState;
  const snapshotChanged = !is(prevSnapshot, nextSnapshot);
  if (snapshotChanged) {
    // 更新hook memoizedState
    hook.memoizedState = nextSnapshot;
    // 标记更新
    markWorkInProgressReceivedUpdate();
  }
  const inst = hook.queue;
  // 根据subscribe内存地址是否变化重新执行useEffect
  updateEffect(subscribeToStore.bind(null, fiber, inst, subscribe), [
    subscribe,
  ]);

  // Whenever getSnapshot or subscribe changes, we need to check in the
  // commit phase if there was an interleaved mutation. In concurrent mode
  // this can happen all the time, but even in synchronous mode, an earlier
  // effect may have mutated the store.
  if (
    inst.getSnapshot !== getSnapshot ||
    snapshotChanged ||
    // Check if the susbcribe function changed. We can save some memory by
    // checking whether we scheduled a subscription effect above.
    (workInProgressHook !== null &&
      workInProgressHook.memoizedState.tag & HookHasEffect)
  ) {
    fiber.flags |= PassiveEffect;
    pushEffect(
      HookHasEffect | HookPassive,
      updateStoreInstance.bind(null, fiber, inst, nextSnapshot, getSnapshot),
      undefined,
      null,
    );

    // Unless we're rendering a blocking lane, schedule a consistency check.
    // Right before committing, we will walk the tree and check if any of the
    // stores were mutated.
    const root: FiberRoot | null = getWorkInProgressRoot();

    if (root === null) {
      throw new Error(
        'Expected a work-in-progress root. This is a bug in React. Please file an issue.',
      );
    }

    if (!includesBlockingLane(root, renderLanes)) {
      pushStoreConsistencyCheck(fiber, getSnapshot, nextSnapshot);
    }
  }
  // 返回新的Snapshot
  return nextSnapshot;
}

3.组件卸载取消订阅

这部分很简单;可以理解为组件卸载执行useEffect return的函数,即取消订阅

mountEffect(subscribeToStore.bind(null, fiber, inst, subscribe), [subscribe]);
updateEffect(subscribeToStore.bind(null, fiber, inst, subscribe), [subscribe]);
function subscribeToStore(fiber, inst, subscribe) {
  const handleStoreChange = () => {
    // The store changed. Check if the snapshot changed since the last time we
    // read from the store.
    if (checkIfSnapshotChanged(inst)) {
      // Force a re-render.
      forceStoreRerender(fiber);
    }
  };
  // Subscribe to the store and return a clean-up function.
  // 订阅store,返回取消订阅
  return subscribe(handleStoreChange);
}