大家好,这里是梦兽编程,更多知识专栏关注梦兽编程梦兽编程。
相信2023年的今天大家都听闻过Zustand
这一类的新型状态库,它可以完全脱离React传统状态库以来上下文进行通信。Zustand非常的轻量实现代码只有仅仅200行代码左右就可以实现。
只要你会fp(函数式编程规范中的观察值模型) + React 18 提供一个hooks就能完成这一项功能。
观察者模型
视频可以看观察者模式在函数式编程有多简单实现?
class派写法和fp派写法自己选一个就好了,概念都差不多的
zustand 中是如何实现的。
在vanilla.ts
源码中,我们可以看到这么一段代码,这里我把必要的代码移除,让代码更加清晰。
ini
// vanilla.ts
const createStoreImpl = createState => {
let state;
const listeners = new Set();
const setState = (partial, replace) => {
state = newState;
listeners.forEach(listener => listener(state));
};
const getState = () => state;
const subscribe = listener => {
listeners.add(listener);
return () => {
listeners.delete(listener);
};
};
const api = { setState, getState, subscribe };
// 因为我们传进来的createState是一个 (setState,getState)=> ({})
// 所以这里我们就可以subscribe给后面的React做铺垫
state = createState(setState, getState, api);
return api;
};
export const createStore = createState =>
createState ? createStoreImpl(createState) : createStoreImpl;
如何使用
scss
// 创建
const store = createStoreImpl({ count: 0 });
// 更新
store.setState({ count: 1 });
// 订阅
const unsubscribe = store.subscribe((state) => {
console.log('State changed:', state);
});
store.setState({ count: 2 }); // 触发订阅的回调函数
unsubscribe(); // 取消订阅
store.setState({ count: 3 }); // 不会触发订阅的回调函数
store.destroy(); // 销毁这个store
它是如何更新React的?
我们都知道React是一个单向绑定的ui框架。它不像Ng2,vue2,修改值就能viewer就会更新这种mvvm。在react中希望更新viewer的操作交给开发者自己控制,这也是为什么在一个业务中你可以不用调试就可以猜到那里出问题(前提是你不用mbox)。
回想一下我们使用react,是不是经常需要这么做。
scss
const [state,setState] = useState(0);
setState(1)
// renderer...
那问题来了,现在这种Zustand把状态丢给一个外部变量进行管理的状况库。是如何更新React的viewer?它没有Mbox这类可以在上下文中进行更新。多亏React18 带来的 新Api useSyncExternalStore
,如果你使用React 18已经pr进去了,如果使用的16-18之间的版本。use-sync-external-store
需要这个依赖包
我们想看一个简单的例子,看看官方是如何使用用的。
javascript
// This is an example of a third-party store
// that you might need to integrate with React.
// If your app is fully built with React,
// we recommend using React state instead.
let nextId = 0;
let todos = [{ id: nextId++, text: 'Todo #1' }];
let listeners = [];
export const todosStore = {
addTodo() {
todos = [...todos, { id: nextId++, text: 'Todo #' + nextId }]
emitChange();
},
subscribe(listener) {
listeners = [...listeners, listener];
return () => {
listeners = listeners.filter(l => l !== listener);
};
},
getSnapshot() {
return todos;
}
};
function emitChange() {
for (let listener of listeners) {
listener();
}
}
import { useSyncExternalStore } from 'react';
import { todosStore } from './todoStore.js';
export default function TodosApp() {
// 最主要的是 todosStore subscribe 和 getSnapshot 的实现
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>
</>
);
}
typescript
import useSyncExternalStoreExports from 'use-sync-external-store/shim/with-selector'
const {useSyncExternalStoreWithSelector} = useSyncExternalStoreExports
const createImpl = (createState) => {
// api 就是去获取一个 上面的 createStoreImpl 一个观察者对象
const api = typeof createState === 'function'
? createStore(createState)
: createState
const useBoundStore = (selector, equalityFn) =>
useStore(api, selector, equalityFn)
Object.assign(useBoundStore, api)
return useBoundStore
}
export const create = (createState) => createState
? createImpl(createState)
: createImpl
export function useStore<TState, StateSlice>(
api: WithReact<StoreApi<TState>>,
selector: (state: TState) => StateSlice = api.getState as any,
equalityFn?: (a: StateSlice, b: StateSlice) => boolean
) {
// 想想上面的例子 所以我们的在 set的时候就能通知到React需要去做Render Viewer了
const slice = useSyncExternalStoreWithSelector(
api.subscribe,
api.getState,
api.getServerState || api.getState,
selector,
equalityFn
)
useDebugValue(slice)
return slice
}
为什么React18需要提供这么一个API?
为了解决并发模型下tearing的问题,还不知道什么是tearing,可以谷歌一下。这个概念在国外的程序员已经讨论很久了。
并发渲染是很棒的,但是对于依赖于外部存储的库来说,可能会出现 tearing 问题。 tearing 是指用户可以看到的视觉不一致,即 UI 对于相同的状态显示多个值。通过比较同步渲染和并发渲染的过程,我们可以了解 tearing 在并发渲染中发生的区别。
useSyncExternalStore是React 18中为解决这个问题而引入的新钩子。该钩子基本上接收两个函数作为参数。
结语
看完这个文章,你也可以写出一个轻量级的状态库。嘻嘻是不是很开心呢?这里是梦兽编程期待在下一篇文章中再次见到你!感谢你的阅读。
截屏2023-08-17 23.44.00.png
我的B站视频号更多视频动态。
截屏2023-08-18 00.02.24.png
本文使用 markdown.com.cn 排版