浅谈 recoil 体验

1,946 阅读2分钟

recoil 是什么

简单的说,就是数据流管理工具,emm,社区关于这类的轮子已经够多了,为什么 fb 还要亲自下场弄这个? 一开始我也是这种心态,所以也是快速把玩了一下,但是在后面深入了解之后,发现事情没有这么简单。可以抛开官网提到的很多特性,例如写法更像 react、方便 tree shaking 等等,其实最重要的是,为了支持 react 后续的 cocurrent 模式 甚至是后续的其他新特性。

拿个小李子来说,

const textState = atom({
  key: 'textState', // unique ID (with respect to other atoms/selectors)
  default: Promise.resolve("text")// default value (aka initial value)
});
// const textStateCopy = atom(textState)
const charCountState = selector({
  key: 'charCountState', // unique ID (with respect to other atoms/selectors)
  get: ({get}) => {
    return new Promise((r) => {
      setTimeout(() => {
        r("try recoil")
      }, 2000);
    })
  },
});
function CharacterCount() {
  const count = useRecoilValue(charCountState);

  return <>Character Count: {count}</>;
}

function TextInput() {
    const text = useRecoilValue(textState);
  
    return (
      <div>
        Echo: {text}
      </div>
    );
}

export default function CharacterCounter() {
    return (
      <div>
        <TextInput />
        <CharacterCount />
      </div>
    );
}
function App() {
  return (
    <RecoilRoot>
      <Suspense fallback={<h1>loading</h1>}>
        <CharacterCounter />
      </Suspense>
    </RecoilRoot>
  );
}

看到没有,天然支持 suspense,里面关键的两个点,atom 和 selector 都支持 返回 loadable,有个小问题就是 atom 也是支持 promise 的,不过它内部是通过包一层 selector 去支持,内部判断传入的default 值是 promise 还是 recoilvalue ,从而决定是否用 selector 包一层,但是在我写上面代码的时候,报错了,emm, recoil 内部忘记在把 selector require进来了,于是顺手还蹭了pr

loadable

什么是 loadable ? 无论是 atom 还是 selector ,返回的都是 loadable ,贴个 react 官网中 suspense 的例子, 这是一个 loadable 的经典示例,根据 promise 具体的状态来决定返回什么值

function wrapPromise(promise) {
  let status = "pending";
  let result;
  let suspender = promise.then(
    r => {
      status = "success";
      result = r;
    },
    e => {
      status = "error";
      result = e;
    }
  );
  return {
    read() {
      if (status === "pending") {
        throw suspender;
      } else if (status === "error") {
        throw result;
      } else if (status === "success") {
        return result;
      }
    }
  };
}

虽然在 recoil 里面 loadable 的数据结构有所不同,但是大致都一样, 当然开发者也可以选择是由框架处理还是返回给自身去处理

function handleLoadable<T>(loadable: Loadable<T>, atom, storeRef): T {
  // We can't just throw the promise we are waiting on to Suspense.  If the
  // upstream dependencies change it may produce a state in which the component
  // can render, but it would still be suspended on a Promise that may never resolve.
  if (loadable.state === 'hasValue') {
    return loadable.contents;
  } else if (loadable.state === 'loading') {
    const promise = new Promise(resolve => {
      storeRef.current.getState().suspendedComponentResolvers.add(resolve);
    });
    throw promise;
  } else if (loadable.state === 'hasError') {
    throw loadable.contents;
  } else {
    throw new Error(`Invalid value of loadable atom "${atom.key}"`);
  }
}

recoil 的依赖收集

依赖收集,有两个概念,分为 upstream 和 downstream。

downstream

大白俗,下游。举个例子来说,selector 1 用到了 atom 1 和 selector 2 去衍生数据, 那么 node 1 和 selector 2 的下游就是 selector1

upstream

还是大白俗,上游。atom1 和 selector2 就是 selector1 的上游

了解这个有什么用呢? 对于 selector1 来说, 当 atom1 或者 selector 2 更新的时候,会让通知让 selector2 进行更新,然后把值缓存下来。

在 recoil 中, 无论 atom 还是 selector,都是注册到成一个node,treestate 中会保存他们对于 key 的依赖,包括 组件下游、 node 下游 和 node 上游,这也是为什么要求 key 唯一的原因。正是有了这些东西,在 atom 还是 selector 更新的时候, 能准确地去更新组件,例如,atom1 更新了,那么我们我们会把 atom1 的 下游 node 全部找出来, 再把这些 node 的 下游组件找到,然后立即执行或者延迟执行注册的更新函数。

更新组件有时序,'now' 和 'enqueue',从字面就知道是立刻调用更新函数还是塞入queuedComponentCallbacks队列里面更新。那内部更新函数长啥样呢。const [_, forceUpdate] = useState([]); 没错,就是长这个样子, 这个 state 的作用,就是为了 forceUpdate 我们的组件更新。

关于 reactish 和 tree shaking

reactish ,这个很明显了,用的时候基本跟我们写内置 hook 差不多,而不用思维跳跃方式去用社区不同数据的方式去思考怎么构建数据流

tree shaking,这个嘛,每个 node 都是单独声明的,而不是像 redux 或者 mobx 那样耦合在一个对象里面,这种松耦合的方式,也是 vue3 现在做的,比较高级的说法是 runtime tree shaking。

为什么要 unique key

无论是 atom 和 selector,注册的时候都需要唯一 key,为什么呢?key 是 node 唯一标识, 像上面说的依赖和更新函数,都是跟 key 挂上钩的,甚至 atom 和 selector 调用之后返回,如果打印出来,你可以看到就是存在 挂着 key 的对象,如果 key 重复了,会报错(虽然现在 recoil 把报错注释掉了),但是会把原来的 node 节点 替代掉。但是像前面说的 atom 对 promise 和 recoilvalue,内部会用 selector 包一层 ,其中返回的 selector 的 key 会变成 ${options.key}__withFallback

recoil 里面的编程小技巧

  1. ?? 这个是 es 新出的特性, 具体用法可以去查下
  2. 创建空对象,class DefaultValue {} const DEFAULT_VALUE: DefaultValue = new DefaultValue();, 通过 Insanceof DefaultValue 去做判断。

总结

这两天基本把 recoil 内部的主要流程翻了一遍,有人说很像 mobx,难道是收集依赖? loadable 的天然支持, 对于后续 react cocurrent 模式的可谓说很重要,还有就是 hook 的书写方式,再也不用思考什么是 action 或者 Observer了,:逃)