recoil学习笔记

353 阅读3分钟

在两年前,我粗略地学习过 react-redux,当时不禁为其精巧的设计感到惊艳。
现在开发的过程中,已经全量地替换成了 recoil,这自然也值得好好学习,recoil到底有什么好的地方

1. recoil 的 入口 root节点

packages/recoil/core/Recoil_RecoilRoot.js#121
可以看到,recoilredux 一样,都使用了 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

  1. 主要是使用了 useRecoilValueLoadable -> getNodeLoadable -> getNode 去获取上文中提到的 全局 node 中存储的数据
  2. 监听 atom 的变化,变化则使用 const [, forceUpdate] = useState([]);forceUpdate 强制刷新当前 fiber
  3. subscribeToRecoilValue 监听 atom 变化
  4. 也就是在这里可以看到,因为使用了 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');
  }
}