来源:Rewriting Facebook's "Recoil" React library from scratch in 100 lines
译者:塔希
协议:CC BY-NC-SA 4.0
Atoms
Recoil 是围绕着 “atoms” 这个概念构建的。Atoms 是组成整个状态中的原子性的一部分,你可以在组件中订阅它或更改它的值。
开始,我将创建一个叫做 Atom 的类 ,用来包裹一些值 T 。我加了一些辅助方法 update 和 snapshot 允许你获得或更改 Atom 的值。
class Atom<T> {
constructor(private value: T) {}
update(value: T) {
this.value = value;
}
snapshot(): T {
return this.value;
}
}
为了能够监听到状态的变化,你需要使用观察者模式。这种模式常见于像 RxJS 这样的库,不过我们将从头开始写一个简单同步的版本,方便使用理解。
为了知道是谁在监听状态,我使用 Set 来存储用于监听的回调函数。一个 Set (或 Hash Set) 是一个存储独一无二值的数据结构。在 JavaScript 中,它可以很容易的转变成数组,并且带有一些富有帮助性的方法来高效的添加或移除值。
通过 subscrible 方法我们可以增加一个监听者。 subscrible 方法返回一个 Disconnecter - 一个带有停止监听方法的接口。 这个方法的调用时机是在当一个 React 组件被卸载,你不再想监听状态变化时。
接下来,一个叫做 emit 的方法被添加了。这个方法会遍历所有的监听函数,将当前存储的值传递给他们。
最终,我们重写了 update 方法,当新的值被设置时,我们会执行 emit 操作。
type Disconnecter = { disconnect: () => void };
class Atom<T> {
private listeners = new Set<(value: T) => void>();
constructor(private value: T) {}
update(value: T) {
this.value = value;
this.emit();
}
snapshot(): T {
return this.value;
}
emit() {
for (const listener of this.listeners) {
listener(this.snapshot());
}
}
subscribe(callback: (value: T) => void): Disconnecter {
this.listeners.add(callback);
return {
disconnect: () => {
this.listeners.delete(callback);
},
};
}
}
呼!
是时候将 atom 和 React 组件连接在一起了。为了做到这点,我创建一个叫 useCoiledValue 的 hook。(听起来很熟悉?)
这个 hook 会返回 atom 当前存储的状态值,并监听其变化,当状态值变化时进行重渲染。当这个 hook 被卸载时,它会清除掉开始设置的监听函数。
关于 updateState 这个 hook 可能会有点奇怪。通过 updateState 设置一个新({})的引用,React 会重新渲染这个组件。这种手段可能有点 hack ,不过却是简单有效的方式来保证组件一定会被重渲染(当 atom 的值更新时)。
export function useCoiledValue<T>(value: Atom<T>): T {
const [, updateState] = useState({});
useEffect(() => {
const { disconnect } = value.subscribe(() => updateState({}));
return () => disconnect();
}, [value]);
return value.snapshot();
}
接下来,我添加了一个 useCoiledState 方法。它的 API 很像 useState - 它会给你 atom 当前存储的最新值并允许你进行重新设置。
export function useCoiledState<T>(atom: Atom<T>): [T, (value: T) => void] {
const value = useCoiledValue(atom);
return [value, useCallback((value) => atom.update(value), [atom])];
}
现在,我们已经这些理解了这些 hooks,是时间来研究下 Selectors 了。在那之前,我们先将之前的代码重构下。
和 atom 一样,一个 selector 可以看作是一个带有状态的值。为了能够实现 selector 时简单点,我先将大部分逻辑从 Atom 中抽出到一个叫做 Stateful 的基类中。
class Stateful<T> {
private listeners = new Set<(value: T) => void>();
constructor(private value: T) {}
protected _update(value: T) {
this.value = value;
this.emit();
}
snapshot(): T {
return this.value;
}
subscribe(callback: (value: T) => void): Disconnecter {
this.listeners.add(callback);
return {
disconnect: () => {
this.listeners.delete(callback);
},
};
}
}
class Atom<T> extends Stateful<T> {
update(value: T) {
super._update(value);
}
}
接着来!
Selectors
Selector 是 Recoil 版本的 “计算属性” 或 "reducers". 用他们的原话讲
一个选择器代表着一份派生状态。你可以将派生状态视为某种状态传递纯函数,在其内部进行修改然后输出的结果
Recoil 中的 selectors 的 API 很简单,你创建一个带有 get 方法的对象, get 的返回值就是你当前的状态值。在 get 方法内部,你可以订阅其他的状态值,当他们更新时,你的 selector 同样会更新。
在我们版本中,我将 get 方法重命名成了 generator。我之所以这样称呼它,是因为本质上 generator 是一个工厂函数,能够根据流入的状态生产出新的状态值。
在代码中,我们用下面这个函数签名来标注 generator 方法
type SelectorGenerator<T> = (context: GeneratorContext) => T;
针对那些对不熟悉 Typescript 的人解释下,这这是一个接受环境对象(GeneratorContext)作为参数,返回 T 类型值的函数。这个返回值会成为 selector 内部存储的状态值。
GeneratorContext 对象的作用是什么?
它使得 selectors 可以访问其他的状态值,并基于他们计算出自己的状态值。从现在开始,我们将这些其他状态值称呼为 “依赖”
interface GeneratorContext {
get: <V>(dependency: Stateful<V>) => V
}
任何时候有人调用了 GeneratorContext 上的 get 方法,都会把被访问的状态值作为依赖加入到依赖数组中。这意味这,当任何一个依赖项更新时, selector 同样会更新。
下面是一个创建 selector 的生产函数的样子
function generate(context) {
// Register the NameAtom as a dependency
// and get it's value
const name = context.get(NameAtom);
// Do the same for AgeAtom
const age = context.get(AgeAtom);
// Return a new value using the previous atoms
// E.g. "Bob is 20 years old"
return `${name} is ${age} years old.`;
};
先把生产函数放到一边,我们来一个 Selector 类。这个类应该接受一个生产函数作为构造函数的参数,然后使用类上的 getDep 方法取得所依赖的 Atom 们存储的值。
你可能注意到我写的构造函数里的 super(undefined as any). 这是因为 super 关键字必须作为派生类构造函数的第一行。如果有助理解,这里你可以认为 undefined 表示着未初始化的内存.
export class Selector<T> extends Stateful<T> {
private getDep<V>(dep: Stateful<V>): V {
return dep.snapshot();
}
constructor(
private readonly generate: SelectorGenerator<T>
) {
super(undefined as any);
const context = {
get: dep => this.getDep(dep)
};
this.value = generate(context);
}
}
这个 selector 仅仅能生产状态值一次。为了能够在依赖变化时对状态值进行更新,我们需要订阅这些依赖。
为了做到这点,我们来升级下 getDep 方法来订阅依赖和调用updateSelector 方法。为了确保 selector 对于每一次依赖的变更只更新一次,我们将这些依赖放到 Set 里来追踪。
updateSelector 方法和先前例子的构造函数很相似。它创建一个 GeneratorContext ,执行 generate 方法,然后调用来自基类 Stateful 的 update 方法。
export class Selector<T> extends Stateful<T> {
private registeredDeps = new Set<Stateful>();
private getDep<V>(dep: Stateful<V>): V {
if (!this.registeredDeps.has(dep)) {
dep.subscribe(() => this.updateSelector());
this.registeredDeps.add(dep);
}
return dep.snapshot();
}
private updateSelector() {
const context = {
get: dep => this.getDep(dep)
};
this.update(this.generate(context));
}
constructor(
private readonly generate: SelectorGenerator<T>
) {
super(undefined as any);
const context = {
get: dep => this.getDep(dep)
};
this.value = generate(context);
}
}
快要完成了。Recoil 有一些辅助函数来帮助创建 atoms 和 selectors。因为大部分 JavaScript 开发者认为类是邪恶的,所以他们可以帮助掩盖我们的逆天大罪。
一个用于创建 atom
export function atom<V>(
value: { key: string; default: V }
): Atom<V> {
return new Atom(value.default);
}
一个用于创建 selector
export function selector<V>(value: {
key: string;
get: SelectorGenerator<V>;
}): Selector<V> {
return new Selector(value.get);
}
对了,还记得先前的 useCoiledValue hook 吗?我们对它重构下,使其同样能够接受 selectors:
export function useCoiledValue<T>(value: Stateful<T>): T {
const [, updateState] = useState({});
useEffect(() => {
const { disconnect } = value.subscribe(() => updateState({}));
return () => disconnect();
}, [value]);
return value.snapshot();
}
就这样!我们完成了!🎉
表扬下自己吧!
这就完了?
为了简洁(和能够使用"100行"作为标题吸引点击率)起见,我决定省略评论,测试和例子。如果你想要更详尽的解释(或想要尝试下例子),这些东西都在我的 “recoil-clone” Github 仓库 里。
这里同样有个实例网站,你可以进行尝试
结论
我曾经读到过,所有的好软件都应该足够简单,当任何人想要重写的时候就可以进行重写。 Recoil 依然有很多我没实现的特性,但令人兴奋的是,如此的简洁和直观的设计能够合理的被手动实现出来。
在你决定将我的山寨 Recoil 投入生产环境前,确保你已经知道下列这些事情:
- Selectors 不会取消对 atoms 的监听。这意味着当你不再使用他们时,会造成内存泄漏。
- React 已经介绍了一个叫
useMutableSource的 hook 。 如果你正在使用最新版本的 React ,你应该使用它来替换useCoiledValue中的setState。 - Selectors 和 Atoms 仅仅会在重新渲染前做一个浅比较。在有些场景下,也许使用深比较比较合理。
- Recoil 使用
key属性了标识每一个 atom 或 selector ,其被用作元数据用于支持 “应用范围的状态观察” 。我这里使用它的目的是为了保持 API 相似。 - Recoil 在 selectors 里支持 async 函数,这将是一个艰巨的任务,所以我将这个特性排除在外了。
除此之外,希望我已经向你展示了在决定使用什么状态管理解决方案时,你不必总是需要依赖第三方库。那样做的话,您往往无法找出完全适合您的解决方案的-毕竟这就是 Recoil 的诞生方式。
🏆 掘金技术征文|双节特别篇