Rewriting Recoil from scratch
recoil是facebook编写的一个库,它之所以诞生是因为人体工程学、context的性能问题和useState。这是一个非常聪明的库,几乎每个人都会找到它的用途——如果你想了解更多,请查看这段解释视频。
刚开始我被图论和recoil惊到了,但渐渐的理解后,感觉也没那么特别了。也许我也可以实现一个类似的东西。
我自己实现的版本和recoil完全不一样。
Atoms:原子
Recoil是基于原子概念构建的。原子是一些在组件中可以订阅和可以更新的状态片段。
首先,创建一个包含T值的Atom类,这个类包含update和snapshot方法可以更新和获取T值。
class Atom<T> {
constructor(private value: T) {}
update(value: T) {
this.value = value;
}
snapshot(): T {
return this.value;
}
}
为了能够监听状态的改变,需要使用监听者模式进行进行处理。这种模式在RxJs中很常见,但是在这里我要从头实现一个简单的同步版本。
为了知道谁在监听状态,使用一个Set存放callbacks。Set或者Hash Set 是一个可以存储每一项数据都唯一的数据结构。在Javascript中特别容易转换成数组,有添加和删除存储项的便捷方法。
通过subscribe添加监听者,并返回一个Disconnecter,Disconnecter定一个disconnect方法用于取消监听。当组件卸载时,就可以取消状态的监听。
接下来,增加一个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 {
li: () => {
this.listeners.delete(callback);
},
};
}
}
接下来,添加一个emit方法。此方法循环遍历每个侦听器,并为它们提供状态中的当前值。最后,我更新update方法以在设置状态时发出新值。
最后,当状态发生变化时,通过update方法emit新值给所有的监听者。
是时候用到react组件上了,为此我写了一个useCoiledValue的hook。
这个hook返回atom的当前状态,并可以监听状态变化,状态变化后重新渲染组件,组件卸载后取消监听。
这里有一个比较奇怪的hook updateState,通过设置一个新的引用对象出发组件重新渲染。这个小小的hack可以使得组件重新渲染。
export function useCoiledValue<T>(value: Atom<T>): T {
const [, updateState] = useState({});
useEffect(() => {
const { disconnect } = value.subscribe(() => updateState({}));
return () => disconnect();
}, [value]);
return value.snapshot();
}
接下来增加一个useCoiledState方法,和useState特别像,可以设置当前的状态值,也可以更新值。
export function useCoiledState<T>(atom: Atom<T>): [T, (value: T) => void] {
const value = useCoiledValue(atom);
return [value, useCallback((value) => atom.update(value), [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版本的“computed values”或“reducers”。用他们自己的话来说:
selector表示一段派生状态。您可以将派生状态视为将状态传递给纯函数的输出,纯函数以某种方式修改给定状态。
Recoil中的selector的api特别简洁,您使用名为get的方法创建一个对象,该方法返回的值就是您的状态值,在get方法中,您可以订阅其他状态,并且每当它们更新时,您的选择器也会更新。
在我们的例子中,我将把get方法重命名为generator。我之所以这样称呼它,是因为它本质上是一个工厂函数,应该根据管道中输入的内容生成状态的下一个值。

在代码中,我们可以使用以下类型签名捕获此generate方法。
type SelectorGenerator<T> = (context: GeneratorContext) => T;
对于不熟悉Typescript的同学简单介绍一下,这定义了一个函数,这个函数的参数是context,返回值是T,这个T就是Selecter的内部状态。
那么GeneratorContext对象是用来做什么的呢?
selectors使用其他状态生成自己的状态,这个外部状态称为依赖:dependencies
当调用GeneratorContext的get方法时,会添加一个依赖,每当依赖项更新时,selector也会更新。
下面是创建selector的generate大概函数:
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.`;
};
写完generate函数后,让我们创建Selector 类。此类应接受generate函数作为构造函数参数,并在类上使用getDep方法返回依赖的原子的值。
您可能会注意到,在构造函数中,我编写了super(未定义为any)。这是因为super必须是派生类构造函数的第一行。为了更好理解,在这种情况下,您可以将未定义视为未初始化的内存。
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);
}
}
此选择器仅适用于生成一次状态。为了对依赖的更改做出反应,我们需要订阅它们。
为此,让我们更新getDep方法以订阅依赖项,并调用updateSelector方法。为了确保选择器每次更改只更新一次,让我们使用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有一些创建原子和选择器的辅助功能。由于大多数JavaScript开发人员认为类是邪恶的,因此它们将帮助掩盖我们的暴行。
一个用于创建原子。。。
export function atom<V>(
value: { key: string; default: V }
): Atom<V> {
return new Atom(value.default);
}
还有一个用于创建选择器。。。
export function selector<V>(value: {
key: string;
get: SelectorGenerator<V>;
}): Selector<V> {
return new Selector(value.get);
}
哦,还记得以前的UseColedValue hook吗?让我们更新它以接受选择器
export function useCoiledValue<T>(value: Stateful<T>): T {
const [, updateState] = useState({});
useEffect(() => {
const { disconnect } = value.subscribe(() => updateState({}));
return () => disconnect();
}, [value]);
return value.snapshot();
}
到这里就完成了,完整代码请参考:"recoil-clone" Github repository.
结论
我曾经读到一段话:所有好的软件都应该足够简单,任何人都可以在需要时重写它。Recoil有很多我在这里还没有实现的功能,但看到这样一个简单直观的设计,可以手动实现,令人兴奋。
在您决定在生产中推出我的盗版Recoil之前,请确保您考虑以下事项:
选择器没有取消监听atoms。这意味着当你停止使用它们时,它们会导致内存泄漏。
React引入了一个名为useMutableSource的hook。如果您使用的是最新版本的React,则应使用此选项,而不是UseColedValue中的setState。
选择器和原子仅在重新渲染之前对状态进行浅比较。在某些情况下,将此更改为深比较可能更有意义的
Recoil为每个原子和选择器使用一个key字段,该字段用作名为“应用程序范围观察”的功能的元数据。尽管没有使用它来保持对API的理解,但我还是包含了它。
Recoil在选择器中支持异步,这将是一项艰巨的任务,因此我已确保将其排除在外
除此之外,希望我已经向您展示了,在决定状态管理解决方案时,您不必总是看库。通常情况下,你无法设计出完全符合你的解决方案的东西——毕竟Recoil就是这样产生的。
写完这篇文章后给介绍一个jotai库,这是一个与我的Recoil非常相似的特性,支持异步!
https://bennetthardwick.com/recoil-from-scratch/?spm=ata.21736010.0.0.f2dc2a44vBswcq