前言
useSyncExternalStore 算是一个非常重要的 hook api,但是平时用的不多,对于三方ui组件来说,不可谓不重要,例如前一篇介绍的类似于 redux 的 zustand 就是以他为核心做出来的,并延续扩展成最后的状态
如果我们了解了 useSyncExternalStore 后,必要的时候可以调整缩减一些三方代码库,作为我们的基础组件库,都是没问题的
这篇文章结合上篇文章 zustand轻量级状态管理库 来介绍,并参考 zustand 的部分源码来介绍 useSyncExternalStore 的实际场景
useSyncExternalStore 基本介绍
useSyncExternalStore 是一个让我们不用 setState 就能更新状态的一个 api,常见用到三方UI代码库中,平常基本上很很少用,其有下面几个参数
- subscribe 订阅函数,组件订阅时调用该函数,当状态发生改变时,需要调用 subscribe 里面的 onStoreChange 参数方法
forceStoreRerender(fiber)强制重新渲染页面,一般提前保存起来必要的时候调用 - getSnapshot 状态快照函数 state,获取状态使用
- getServerSnapshot 服务端状态快照 state,在服务端渲染场景使用时需要,需要和 Snapshot 快照一致
export function useSyncExternalStore<Snapshot>(
subscribe: (onStoreChange: () => void) => () => void,
getSnapshot: () => Snapshot,
getServerSnapshot?: () => Snapshot,
): Snapshot;
基础使用
定义状态快照 state
let store = {
x: 0,
y: 0
}
加入 subscribe,使用 useSyncExternalStore 如下所示
ps:订阅时触发 subscribe 函数,其返回一个参数为回调函数指针 onStoreChange,调用其可以理解触发渲染(一般保存起来在合适时机更新state调用,以触发渲染),结果返回销毁函数,当订阅组件销毁时移除监听
useSyncExternalStore(
(onStoreChange) => {
//以最常见的窗口监听为例
//假设窗口调整了,设置窗口监听回调
window.addEventListener("resize", (e) => {
store = {
x: e.currentTarget.outerWidth,
y: e.currentTarget.outerHeight,
};
//调用 onStoreChange 可以强制触发渲染
onStoreChange();
});
return () => {
//组件销毁后,移除窗口监听回调
window.removeEventListener("resize", callback);
};
}, // subscribe
() => store, //getSnapshot
() => store //getServerSnapshot
);
ps:即使看了基础使用也很抽象,毕竟这么直接用的很少,后面会以三方组件为案例介绍
useSyncExternalStore 结合 zustand 介绍理解使用
zustand 就是基于 useSyncExternalStore 完成的状态订阅更新的,下面通过其一步步了解 useSyncExternalStore
zustand 有两个核心文件 react.js、vanilla.js,一个基础调用,一个是状态更新加工厂
了解前,先回顾下基础使用
zustand基础使用
//store
export const useStore = create((set) => ({
count: 0,
increase: () => set((state) => ({ count: state.count + 1 })),
decrease: () => set((state) => ({ count: state.count - 1 })),
reset: () => set({ count: 0 }),
setCount: (count) => set({ count }),
}));
//component
const { count, increase, decrease, reset, setCount } = useStore()
...
react.js
先看入口函数 react.js,我们使用的是其 create 方法
//react.js
var React = require('react');
var vanilla = require('zustand/vanilla');
const identity = (arg) => arg;
//核心代码,可以看到其三个参数都是来自于 api,也就是 vanilla.js 中介绍的内容
function useStore(api, selector = identity) {
const slice = React.useSyncExternalStore(
api.subscribe,
() => selector(api.getState()),
() => selector(api.getInitialState())
);
React.useDebugValue(slice);
return slice;
}
//创建 store 新的 store
const createImpl = (createState) => {
// 通过 vanilla 初始话基本参数,也就是作为加工厂
const api = vanilla.createStore(createState);
//创建新带selector 的 store(这里不多介绍,看使用就知道是可以加工state的),并合并api 参数以便以外面直接使用
const useBoundStore = (selector) => useStore(api, selector);
Object.assign(useBoundStore, api);
return useBoundStore;
};
//我们的入口函数,不多说,案例默认传递了一个 set 方法就是 createState
const create = (createState) => createState ? createImpl(createState) : createImpl;
exports.create = create;
exports.useStore = useStore;
vanilla.js
上面的 react.js 引入了该核心加工文件,useSyncExternalStore 的参数都是来自于这里
其中比较重要的是订阅功能, 通过 subscribe 方法将触发渲染的回调参数 onStoreChange 放到 Listeners 中,当重新 setState 时,调用 onStoreChange 触发渲染
//vanilla.js
const createStoreImpl = (createState) => {
//声明 state、listeners
let state;
// listeners 在 subscribe 添加监听,重新 set 更新state 时,触发所有 listener以更新内容
//使用了 set,访问效率更高
const listeners = /* @__PURE__ */ new Set();
//声明 setState,里面包含了对比了新老 stare,支持使用 replace替换state
//新老内容不一致时调用保存好的 onStoreChange 强制渲染
const setState = (partial, replace) => {
//查看是否是函数,可以调用
const nextState = typeof partial === "function" ? partial(state) : partial;
//比较state是否发生改变,包括类型
if (!Object.is(nextState, state)) {
const previousState = state;
//更新新的 stare
state = (replace != null ? replace : typeof nextState !== "object" || nextState === null) ? nextState : Object.assign({}, state, nextState);
//触发所有监听回调
listeners.forEach((listener) => listener(state, previousState));
}
};
// getSnapshot、getServerSnapshot 的参数,他们是一样的
const getState = () => state;
const getInitialState = () => initialState;
//组件订阅回调,当订阅时回调一次,保存到我们的 listeners 中,更新 set 时,重新渲染内容
const subscribe = (listener) => {
//添加到监听
listeners.add(listener);
//组件释放时,移除监听
return () => listeners.delete(listener);
};
//设置 api 用返回给外部
const api = { setState, getState, getInitialState, subscribe };
//调用createState,也就是我们的初始set方法,初始化默认 state
const initialState = state = createState(setState, getState, api);
return api;
};
//和前面的 createImpl 类似,传递了我们默认的 setState 的初始函数
const createStore = (createState) => createState ? createStoreImpl(createState) : createStoreImpl;
exports.createStore = createStore;
最后
就介绍到这里吧,本身也不是很复杂的工具,通过其能让我们更理解 zustand 的使用,在封装组件时,也能更加得心应手