在两年前,我粗略地学习过
react-redux,当时不禁为其精巧的设计感到惊艳。
现在开发的过程中,已经全量地替换成了recoil,这自然也值得好好学习,recoil到底有什么好的地方
1. recoil 的 入口 root节点
packages/recoil/core/Recoil_RecoilRoot.js#121
可以看到,recoil和redux一样,都使用了 react 的上下文信息,但是 他直接使用了 ref.current 进行了数据和工具函数的存储,这样数据改变了,不会导致整个 fiberRoot 重绘
const AppContext = React.createContext<StoreRef>({current: defaultStore});
const useStoreRef = (): StoreRef => useContext(AppContext);
function RecoilRoot(props: Props): React.Node {
const {override, ...propsExceptOverride} = props;
const ancestorStoreRef = useStoreRef();
if (override === false && ancestorStoreRef.current !== defaultStore) {
// If ancestorStoreRef.current !== defaultStore, it means that this
// RecoilRoot is not nested within another.
return props.children;
}
return <RecoilRoot_INTERNAL {...propsExceptOverride} />;
}
let nextID = 0;
function RecoilRoot_INTERNAL({
initializeState_DEPRECATED,
initializeState,
store_INTERNAL: storeProp, // For use with React "context bridging"
children,
}: InternalProps): React.Node {
let storeStateRef: {current: StoreState}; // eslint-disable-line prefer-const
。。。
const replaceState = (replacer: TreeState => TreeState) => {
startNextTreeIfNeeded(storeRef.current);
// Use replacer to get the next state:
const nextTree = nullthrows(storeStateRef.current.nextTree);
let replaced;
try {
stateReplacerIsBeingExecuted = true;
replaced = replacer(nextTree);
} finally {
stateReplacerIsBeingExecuted = false;
}
if (replaced === nextTree) {
return;
}
storeStateRef.current.nextTree = replaced;
if (reactMode().early) {
notifyComponents(storeRef.current, storeStateRef.current, replaced);
}
nullthrows(notifyBatcherOfChange.current)();
};
const notifyBatcherOfChange = useRef<null | (mixed => void)>(null);
const setNotifyBatcherOfChange = useCallback(
(x: mixed => void) => {
notifyBatcherOfChange.current = x;
},
[notifyBatcherOfChange],
);
const storeRef = useRefInitOnce(
() =>
storeProp ?? {
storeID: getNextStoreID(),
getState: () => storeStateRef.current,
replaceState,
getGraph,
subscribeToTransactions,
addTransactionMetadata,
},
);
if (storeProp != null) {
storeRef.current = storeProp;
}
storeStateRef = useRefInitOnce(() =>
initializeState_DEPRECATED != null
? initialStoreState_DEPRECATED(
storeRef.current,
initializeState_DEPRECATED,
)
: initializeState != null
? initialStoreState(initializeState)
: makeEmptyStoreState(),
);
。。。
return (
<AppContext.Provider value={storeRef}>
<MutableSourceContext.Provider value={mutableSource}>
<Batcher setNotifyBatcherOfChange={setNotifyBatcherOfChange} />
{children}
</MutableSourceContext.Provider>
</AppContext.Provider>
);
}
2. atom
packages/recoil/recoil_values/Recoil_atom.js#590
作为一个数据的存储地点,atom做了大量的工作,但是跳过那一堆的条件检测以及各种辅助函数之外,可以发现,atom主要是将对应的数据以key作为唯一键 放入了全局对象当中
function atom<T>(options: AtomOptions<T>): RecoilState<T> {
const {
// @fb-only: scopeRules_APPEND_ONLY_READ_THE_DOCS,
...restOptions
} = options;
const optionsDefault: RecoilValue<T> | Promise<T> | Loadable<T> | WrappedValue<T> | T =
'default' in options
?
// $FlowIssue[incompatible-type] No way to refine in Flow that property is not defined
options.default
: new Promise(() => {});
if (isRecoilValue(optionsDefault)) {
return atomWithFallback<T>({
...restOptions,
default: optionsDefault,
// @fb-only: scopeRules_APPEND_ONLY_READ_THE_DOCS,
});
} else {
return baseAtom<T>({...restOptions, default: optionsDefault});
}
}
function baseAtom<T>(options: BaseAtomOptions<T>): RecoilState<T> {
const {key, persistence_UNSTABLE: persistence} = options;
。。。
function initAtom(
store: Store,
initState: TreeState,
trigger: Trigger,
): () => void {
liveStoresCount++;
const cleanupAtom = () => {
liveStoresCount--;
cleanupEffectsByStore.get(store)?.forEach(cleanup => cleanup());
cleanupEffectsByStore.delete(store);
};
。。。
return cleanupAtom;
}
function peekAtom(_store: Store, state: TreeState): Loadable<T> {
return (
state.atomValues.get(key) ??
cachedAnswerForUnvalidatedValue ??
defaultLoadable
);
}
function getAtom(_store: Store, state: TreeState): Loadable<T> {
if (state.atomValues.has(key)) {
// Atom value is stored in state:
return nullthrows(state.atomValues.get(key));
} else if (state.nonvalidatedAtoms.has(key)) {
。。。
const validatedValueLoadable =
validatorResult instanceof DefaultValue
? defaultLoadable
: loadableWithValue(validatorResult);
cachedAnswerForUnvalidatedValue = validatedValueLoadable;
return cachedAnswerForUnvalidatedValue;
} else {
return defaultLoadable;
}
}
function invalidateAtom() {
cachedAnswerForUnvalidatedValue = undefined;
}
function setAtom(
_store: Store,
state: TreeState,
newValue: T | DefaultValue,
): AtomWrites {
// Bail out if we're being set to the existing value, or if we're being
// reset but have no stored value (validated or unvalidated) to reset from:
if (state.atomValues.has(key)) {
const existing = nullthrows(state.atomValues.get(key));
if (existing.state === 'hasValue' && newValue === existing.contents) {
return new Map();
}
} else if (
!state.nonvalidatedAtoms.has(key) &&
newValue instanceof DefaultValue
) {
return new Map();
}
maybeFreezeValueOrPromise(newValue);
cachedAnswerForUnvalidatedValue = undefined; // can be released now if it was previously in use
return new Map().set(key, loadableWithValue(newValue));
}
// 将 atom 的 数据注册到全局对象当中
const node = registerNode(
({
key,
nodeType: 'atom',
peek: peekAtom,
get: getAtom,
set: setAtom,
init: initAtom,
invalidate: invalidateAtom,
shouldDeleteConfigOnRelease: shouldDeleteConfigOnReleaseAtom,
dangerouslyAllowMutability: options.dangerouslyAllowMutability,
persistence_UNSTABLE: options.persistence_UNSTABLE
? {
type: options.persistence_UNSTABLE.type,
backButton: options.persistence_UNSTABLE.backButton,
}
: undefined,
shouldRestoreFromSnapshots: true,
retainedBy,
}: ReadWriteNodeOptions<T>),
);
return node;
}
packages/recoil/core/Recoil_Node.js#111
所有的 atom 数据都被汇总到了nodes当中,所以对于atom来说,key必须是唯一值
const nodes: Map<string, Node<any>> = new Map();
function registerNode<T>(node: Node<T>): RecoilValue<T> {
if (nodes.has(node.key)) {
const message = `Duplicate atom key "${node.key}". This is a FATAL ERROR in
production. But it is safe to ignore this warning if it occurred because of
hot module replacement.`;
if (__DEV__) {
// TODO Figure this out for open-source
if (!isFastRefreshEnabled()) {
expectationViolation(message, 'recoil');
}
} else {
// @fb-only: recoverableViolation(message, 'recoil');
console.warn(message); // @oss-only
}
}
nodes.set(node.key, node);
const recoilValue: RecoilValue<T> =
node.set == null
? new RecoilValueClasses.RecoilValueReadOnly(node.key)
: new RecoilValueClasses.RecoilState(node.key);
recoilValues.set(node.key, recoilValue);
return recoilValue;
}
3. useRecoilState
packages/recoil/hooks/Recoil_Hooks.js
在recoil中获取对应的atom的值
function useRecoilValue<T>(recoilValue: RecoilValue<T>): T {
if (__DEV__) {
validateRecoilValue(recoilValue, 'useRecoilValue');
}
const loadable = useRecoilValueLoadable(recoilValue);
return handleLoadable(loadable, recoilValue, storeRef);
}
在
useRecoilState中
- 主要是使用了
useRecoilValueLoadable->getNodeLoadable->getNode去获取上文中提到的 全局 node 中存储的数据- 监听 atom 的变化,变化则使用
const [, forceUpdate] = useState([]);的forceUpdate强制刷新当前fibersubscribeToRecoilValue监听atom变化- 也就是在这里可以看到,因为使用了 useState,所以可以精准定位到某一个 Fiber,来进行定向的 diff 渲染
packages/recoil/hooks/Recoil_Hooks.js#243
function useRecoilValueLoadable<T>(recoilValue: RecoilValue<T>): Loadable<T> {
...
return {
TRANSITION_SUPPORT: useRecoilValueLoadable_TRANSITION_SUPPORT,
SYNC_EXTERNAL_STORE: useRecoilValueLoadable_SYNC_EXTERNAL_STORE,
MUTABLE_SOURCE: useRecoilValueLoadable_MUTABLE_SOURCE,
LEGACY: useRecoilValueLoadable_LEGACY,
}[reactMode().mode](recoilValue);
}
function useRecoilValueLoadable_LEGACY<T>(
recoilValue: RecoilValue<T>,
): Loadable<T> {
const storeRef = useStoreRef();
const [, forceUpdate] = useState([]);
const componentName = useComponentName();
const getLoadable = useCallback(() => {
if (__DEV__) {
recoilComponentGetRecoilValueCount_FOR_TESTING.current++;
}
const store = storeRef.current;
const storeState = store.getState();
const treeState = reactMode().early
? storeState.nextTree ?? storeState.currentTree
: storeState.currentTree;
// 获取 atom 的值
return getRecoilValueAsLoadable(store, recoilValue, treeState);
}, [storeRef, recoilValue]);
const loadable = getLoadable();
const prevLoadableRef = useRef(loadable);
useEffect(() => {
prevLoadableRef.current = loadable;
});
useEffect(() => {
const store = storeRef.current;
const storeState = store.getState();
// 设置一个监听 atom 变化的 回调函数
const subscription = subscribeToRecoilValue(
store,
recoilValue,
_state => {
if (!gkx('recoil_suppress_rerender_in_callback')) {
return forceUpdate([]);
}
const newLoadable = getLoadable();
if (!prevLoadableRef.current?.is(newLoadable)) {
// 监听到 变化之后,设置强制刷新
// 这里的 强制刷新 就是 const [, forceUpdate] = useState([]);
forceUpdate(newLoadable);
}
prevLoadableRef.current = newLoadable;
},
componentName,
);
if (storeState.nextTree) {
store.getState().queuedComponentCallbacks_DEPRECATED.push(() => {
prevLoadableRef.current = null;
forceUpdate([]);
});
} else {
if (!gkx('recoil_suppress_rerender_in_callback')) {
return forceUpdate([]);
}
const newLoadable = getLoadable();
if (!prevLoadableRef.current?.is(newLoadable)) {
forceUpdate(newLoadable);
}
prevLoadableRef.current = newLoadable;
}
return subscription.release;
}, [componentName, getLoadable, recoilValue, storeRef]);
return loadable;
}
function getRecoilValueAsLoadable<T>(
store: Store,
{key}: AbstractRecoilValue<T>,
treeState: TreeState = store.getState().currentTree,
): Loadable<T> {
// Reading from an older tree can cause bugs because the dependencies that we
// discover during the read are lost.
const storeState = store.getState();
....
const loadable = getNodeLoadable(store, treeState, key);
....
return loadable;
}
function getNodeLoadable<T>(
store: Store,
state: TreeState,
key: NodeKey,
): Loadable<T> {
initializeNodeIfNewToStore(store, state, key, 'get');
return getNode(key).get(store, state);
}
function getNode(key: NodeKey): Node<any> {
const node = nodes.get(key);
if (node == null) {
throw new NodeMissingError(`Missing definition for RecoilValue: "${key}""`);
}
return node;
}
4 useSetRecoilState
function useSetRecoilState<T>(recoilState: RecoilState<T>): SetterOrUpdater<T> {
if (__DEV__) {
validateRecoilValue(recoilState, 'useSetRecoilState');
}
const storeRef = useStoreRef();
return useCallback(
(newValueOrUpdater: (T => T | DefaultValue) | T | DefaultValue) => {
setRecoilValue(storeRef.current, recoilState, newValueOrUpdater);
},
[storeRef, recoilState],
);
}
function setRecoilValue<T>(
store: Store,
recoilValue: AbstractRecoilValue<T>,
valueOrUpdater: T | DefaultValue | (T => T | DefaultValue),
): void {
queueOrPerformStateUpdate(store, {
type: 'set',
recoilValue,
valueOrUpdater,
});
}
function applyAction(store: Store, state: TreeState, action: Action<mixed>) {
if (action.type === 'set') {
const {recoilValue, valueOrUpdater} = action;
const newValue = valueFromValueOrUpdater(
store,
state,
recoilValue,
valueOrUpdater,
);
const writes = setNodeValue(store, state, recoilValue.key, newValue);
for (const [key, loadable] of writes.entries()) {
writeLoadableToTreeState(state, key, loadable);
}
} else if (action.type === 'setLoadable') {
const {
recoilValue: {key},
loadable,
} = action;
writeLoadableToTreeState(state, key, loadable);
} else if (action.type === 'markModified') {
const {
recoilValue: {key},
} = action;
state.dirtyAtoms.add(key);
} else if (action.type === 'setUnvalidated') {
const {
recoilValue: {key},
unvalidatedValue,
} = action;
const node = getNodeMaybe(key);
node?.invalidate?.(state);
state.atomValues.delete(key);
state.nonvalidatedAtoms.set(key, unvalidatedValue);
state.dirtyAtoms.add(key);
} else {
recoverableViolation(`Unknown action ${action.type}`, 'recoil');
}
}