- Published on
use-context-selector源码解读&思考
- Authors
- Name
- noodles
- 每个人的花期不同,不必在乎别人比你提前拥有
React可以通过Context来解决跨组件的属性共享问题,但是Context方案会存在性能问题,Context更新会触发使用该Context的组件渲染. React提出过Context selectors的提案,use-context-selector就通过实现Context selectors 来解决Context的性能问题.
use-context-selector简单使用
import { createContext, useContextSelector } from 'use-context-selector';
// 创建Context 这个使用方式跟React提供的createContext是一样的
const Context = createContext(null);
const Counter1 = () => {
// 通过useContextSelector获取想要的属性
const count1 = useContextSelector(Context, (v) => v[0].count1);
// 获取设置Context的函数,可以看上去有reducer的感觉
const setState = useContextSelector(Context, (v) => v[1]);
// 更新调用
const increment = () =>
setState((s) => ({
...s,
count1: s.count1 + 1,
}));
return (
<div>
<span>Count1: {count1}</span>
<button type="button" onClick={increment}>
+1
</button>
{Math.random()}
</div>
);
};
const StateProvider = ({ children }) => (
<Context.Provider value={useState({ count1: 0})}>
{children}
</Context.Provider>
);
const App = () => (
<StateProvider>
<Counter1 />
</StateProvider>
);
在上面的例子中Context的value是一个useState的返回值,这里就留下一个疑问,use-context-selector是如何实现订阅更新的呢??!!
use-context-selector原理

react的context方案中,子组件通过useContext获取Context的值和触发更新.Context的值变化的时候会触发所有使用该Context组件的更新. use-context-selector中提供Ref的方式来存储Context的值,通过订阅的方式实现更新,这样就实现了渲染的优化
Provider值生成/订阅逻辑
// 通过ref来存储context的值
const contextValue = useRef(undefined);
if (!contextValue.current) {
const listeners = new Set();
const update = (fn, options) => {
versionRef.current += 1;
const action = {
n: versionRef.current,
};
listeners.forEach((listener) => listener(action));
fn();
};
contextValue.current = {
[CONTEXT_VALUE]: {
/* "v"alue */ v: valueRef,
/* versio"n" */ n: versionRef,
// 订阅列表
/* "l"isteners */ l: listeners,
// context更新函数
/* "u"pdate */ u: update,
},
};
}
// 传给Provider的是useRef创建的contextValue
return createElement(
ProviderOrig,
{ value: contextValue.current },
children,
);
useContextSelectord订阅更新实现
const {
v: { current: value },
n: { current: version },
l: listeners,
} = contextValue;
// 通过selector从contextValue中获取想要的属性
const selected = selector(value);
// 生成useReducer 每个组件都对应自己的useReducer
// 只有selected变化的时候 当前组件才会更新
const [state, dispatch] = useReducer(
(prev, action) => {
if (!action) {
return [value, selected];
}
if ('p' in action) {
throw action.p;
}
if (action.n === version) {
if (Object.is(prev[1], selected)) {
return prev;
}
return [value, selected];
}
try {
if ('v' in action) {
if (Object.is(prev[0], action.v)) {
return prev;
}
const nextSelected = selector(action.v);
if (Object.is(prev[1], nextSelected)) {
return prev;
}
return [action.v, nextSelected];
}
} catch {
}
return [...prev];
},
[value, selected]
);
if (!Object.is(state[1], selected)) {
dispatch();
}
useIsomorphicLayoutEffect(() => {
// 添加订阅
listeners.add(dispatch);
return () => {
listeners.delete(dispatch);
};
}, [listeners]);
// 返回useReducer中的值 这个值会关联更新
return state[1];
一点思考
use-context-selector会在一些场景下用于Context的渲染优化,在看它的源码实现上也比较巧妙做到了不需要对原来的Context使用方式做较大改变就能 实现代码的升级.这点在在进行代码重构或者架构设计的时候可以借鉴.
- 在代码重构的时候去hack中间层,减少对代码大范围的改动
- 在架构设计上,分层且封装中间层逻辑,降低代码对特定库或者使用方式的依赖.