在之前用 React 写一些小项目的时候有时也需要用到状态管理,但是用 Redux 有一种"大材小用"感觉,就在寻找有没有很精巧的实现,最好是响应式的,最后搜索了一番在 Github 发现了两个满足我需求的库:
jsx
// valtio
import resso from 'resso';
import { proxy, useSnapshot } from 'valtio';
const state = proxy({ count: 0, text: 'hello' });
function Counter() {
const snap = useSnapshot(state);
return (
<div>
{snap.count}
<button onClick={() => ++state.count}>+1</button>
</div>
);
}
// resso
const store = resso({ count: 0, text: 'hello' });
function App() {
const { count } = store; // data used in UI → must destructure at top first 🥷
return (
<>
{count}
<button onClick={() => (store.count += 1)}>+</button>
</>
);
}
结合官方给出的文档示例还是最终在项目用了 resso,所以就有了这篇文章,同时作者在知乎也专门写了一篇文章介绍他的思路实现resso,世界上最简单的 React 状态管理器。有兴趣小伙伴可以专门去瞅瞅。
不过在阅读源码实现,需要先了解两个 API:
- useSyncExternalStore
- ReactDOM.unstable_batchedUpdates
useSyncExternalStore
这个 API 可能你完全陌生,事实上我也是,后面专门去官方文档查阅了一下 useSyncExternalStore,它的作用可以简单理解为订阅外部的 store,例如我们在使用 redux 之类的时候,它可能不是通过 useState 的形式而是在外部维护了一个 store,然后结合发布订阅模式来实现数据的更新。
下面用官方给出的切换离线和在线 hooks 例子快速看下这个 api
js
import { useSyncExternalStore } from 'react';
function getSnapshot() {
return navigator.onLine;
}
function subscribe(callback) {
window.addEventListener('online', callback);
window.addEventListener('offline', callback);
return () => {
window.removeEventListener('online', callback);
window.removeEventListener('offline', callback);
};
}
function ChatIndicator() {
const isOnline = useSyncExternalStore(subscribe, getSnapshot);
// ...
}
这里每次 online 或者 offline 发生变化执行回调函数,而每次执行 getSnapshot 如果返回值不同,就会让 React 重新渲染组件。
每次对比 getSnapshot 返回值使用的是 Object.is。
unstable_batchedUpdates
之所以介绍这个 API 其实和性能优化有关,在 React18 之前对于非同步代码和不是 react 事件处理函数 setState 不会进行批处理,例如下面代码:
jsx
import React, { useEffect, useState } from 'react';
const App: React.FC = () => {
console.log('App组件渲染了!');
const [count1, setCount1] = useState(0);
const [count2, setCount2] = useState(0);
useEffect(() => {
document.body.addEventListener('click', () => {
setCount1((count) => count + 1);
setCount2((count) => count + 1);
});
// 在原生js事件中不会进行批处理
}, []);
return (
<>
<div>count1: {count1}</div>
<div>count2: {count2}</div>
</>
);
};
export default App;
这里每次点击组件都会渲染两次,共计输出六次 console.log 信息。
但是有什么办法可以改变呢?下面用 ReactDOM.unstable_batchedUpdates 重写上面这个例子
jsx
import React, { useEffect, useState } from 'react';
const App: React.FC = () => {
console.log('App组件渲染了!');
const [count1, setCount1] = useState(0);
const [count2, setCount2] = useState(0);
useEffect(() => {
document.body.addEventListener('click', () => {
ReactDOM.unstable_batchedUpdates(() => {
setCount1((count) => count + 1);
setCount2((count) => count + 1);
});
});
}, []);
return (
<>
<div>count1: {count1}</div>
<div>count2: {count2}</div>
</>
);
};
export default App;
这样就会每次更新状态组件只渲染一次了,最后提示一下 unstable_batchedUpdates 是同步代码。
实现思想
在代码阅读之前有必要说下它的设计实现思想是啥子。
在使用 redux 的时候,我们更新可能是通过 dispatch
这样的一个函数来完成,但是有没有其他方式呢?ES6 引入了 proxy 可以对对象的值进行劫持,每次更新都可以获取到对应的 key 和 value。
事实上在 Vue2 中也可以通过监听属性的 seter 来实现监听,虽然效果不完整。
试着定义一个 store
js
export const store = { a: 123, b: 456 };
在组件中分别使用
js
// 组件 A
const {a} = store;
// 组件B
const {a,b} = store;
更新方式也很简单,直接更改 store 属性就行,例如:
js
// 组件 A 和 B 都得到更新
store.b = 'b';
那么要怎么实现上述我们定义的这样一个使用方式呢?
作者是通过 proxy 每次 get 的时候自动把对应的 key 值保存到 map 键名,而对应的 setState 作为 value,例如在组件 A 和 B 它们可能是这样储存的。
js
// 组件 A
const [a, setA] = useState(store.a);
// 组件 B
const [a, setA] = useState(store.a);
const [b, setB] = useState(store.b);
// map
const listenerMap = {
a: [setA, setA],
b: [setB],
};
之后监听修改的属性,如果有更新直接调用对应 key 下的 setState 就可以做到更新。
js
// 假设更新 A
listenerMap.a.forEach((setA) => setA(store.a));
源码分析
作者是通过 TypeScript 来实现的,但是这里关注具体的实现,对于类型直接删除了,如果有需要小伙伴可以自行去阅读。
因为代码量不是很大,所以这里直接通过贴代码+注释的形式来进行讲解。
js
// 这里因为是状态库,需要考虑不同的 React 版本,React18 可以直接 import React 来获取 useSyncExternalStore
import { useSyncExternalStore } from 'use-sync-external-store/shim';
// 判断是否为开发环境
const __DEV__ = process.env.NODE_ENV !== 'production';
// 判断是否为对象, {} 这样的,通用的 map、set 之类的对象都统统不可以
const isObj = (val) => {
return Object.prototype.toString.call(val) === '[object Object]';
};
// 这里是一个标识符后面讲解
let isGetStateInMethod = false;
let run = (fn) => {
fn();
};
const resso = (obj) => {
// 初始检查
if (__DEV__ && !isObj(obj)) {
throw new Error('object required');
}
/*
* 它储存的形式为 {[key:string]: Set()}
*/
const state = {};
/*
* 它储存的形式为 {key: Function}
*/
const methods = {};
Object.keys(obj).forEach((key) => {
const initVal = obj[key];
// 给 methods 添加属性
if (initVal instanceof Function) {
methods[key] = (...args) => {
isGetStateInMethod = true;
const res = initVal(...args);
isGetStateInMethod = false;
return res;
};
return;
}
// 给 state 添加属性
const listeners = new Set();
state[key] = {
// 添加 getSnapshot 到 listeners
subscribe: (listener) => {
listeners.add(listener);
return () => listeners.delete(listener);
},
// 数据快照
getSnapshot: () => obj[key],
// 更新方法
setSnapshot: (val) => {
if (val !== obj[key]) {
obj[key] = val;
run(() => listeners.forEach((listener) => listener()));
}
},
useSnapshot: () => {
return useSyncExternalStore(
state[key].subscribe,
state[key].getSnapshot,
state[key].getSnapshot
);
},
};
});
const setState = (key, val) => {
// 只更新初始 obj 定义属性
if (key in obj) {
if (key in state) {
// 判断是属性更新还是函数调用更新
const newVal = val instanceof Function ? val(obj[key]) : val;
// 调用上文的 setSnapshot
state[key].setSnapshot(newVal);
} else if (__DEV__) {
throw new Error(`\`${key}\` is a method, can not update`);
}
} else if (__DEV__) {
throw new Error(`\`${key}\` is not initialized in store`);
}
};
return new Proxy(() => undefined, {
get: (_target, key) => {
if (key in methods) {
return methods[key];
}
// 重点讲下这段代码,首先判断是否为 state,也就是上文中初始传递的对象非函数部分
if (key in state) {
// 这里判断是否 isGetStateInMethod 主要是因为可能存在在 methods 中来引用 store 中的属性,所以如果是这种情况直接返回
if (isGetStateInMethod) {
return obj[key];
}
// 这里从外部拿到对应的值返回,如果不是在最外层使用(React 限制,不可以在回调之类的函数内使用use之类方法,这里直接返回 obj 下的值)
try {
return state[key].useSnapshot();
} catch (err) {
return obj[key];
}
}
if (__DEV__) {
if (key !== 'prototype' && key !== 'name' && key !== 'displayName') {
throw new Error(`\`${key}\` is not initialized in store`);
}
}
},
// 这里监听属性变动,例如这种形式调用更新 store.count = 60;
set: (_target, key, val) => {
setState(key, val);
return true;
},
/*
* apple是指函数调用,官方文档是支持下面这种形式来更新的
* store({ count: 60, text: 'world' });
* 参数分别是目标对象、目标对象的上下文对象(this)和目标对象的参数数组
* 这里之所以解构成 [firstArg, oneAction] 形式是因为,除了上面这种方式还可以
store('count', (prev) => prev + 1);
store((prev) => ({
count: prev.count + 1,
text: prev.text === 'hello' ? 'world' : 'hello',
}));
*/
apply: (_target, _thisArg, [firstArg, oneAction]) => {
// store('count', (prev) => prev + 1);
if (typeof firstArg === 'string') {
setState(firstArg, oneAction);
return;
}
// store({ count: 60, text: 'world' });
if (isObj(firstArg)) {
const newObj = firstArg;
Object.keys(newObj).forEach((key) => {
setState(key, newObj[key]);
});
return;
}
// store((prev) => ({
// count: prev.count + 1,
// text: prev.text === 'hello' ? 'world' : 'hello',
// }));
if (typeof firstArg === 'function') {
const newObj = firstArg(obj);
Object.keys(newObj).forEach((key) => {
setState(key, newObj[key]);
});
}
},
});
};
// 这里是给用户暴露配置,例如上面说的,你想要在异步代码中执行批量更新 unstable_batchedupdates
resso.config = ({ batch }) => {
run = batch;
};
export default resso;
最后
这是一篇很早之前就想写的文章,一直拖到现在才完成,最后如果有理解错误或者文笔错误欢迎指出,同时有可以内推的岗位欢迎滴滴。
文章参考: