从零写一个Recoil(翻译)

简介: Rewriting Recoil from scratchrecoil是facebook编写的一个库,它之所以诞生是因为人体工程学、context的性能问题和useState。这是一个非常聪明的库,几乎每个人都会找到它的用途——如果你想了解更多,请查看这段解释视频。刚开始我被图论和recoil惊到了,但渐渐的理解后,感觉也没那么特别了。也许我也可以实现一个类似的东西。我自己实现的版本和recoil

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

目录
相关文章
|
传感器 机器学习/深度学习 数据采集
【航迹关联】基于NNDA、PDA、JPDA三种算法实现航迹关联附matlab代码
【航迹关联】基于NNDA、PDA、JPDA三种算法实现航迹关联附matlab代码
|
JavaScript 前端开发
__proto__和prototype的区别
`prototype`和`__proto__`虽然都与JavaScript的原型继承和对象关系密切相关,但它们的定义、所属对象、作用和功能等方面存在着明显的区别。理解它们之间的区别对于深入掌握JavaScript的面向对象编程和原型链机制非常重要
|
5月前
|
前端开发 Java 数据库连接
帮助新手快速上手的 JAVA 学习路线最详细版涵盖从入门到进阶的 JAVA 学习路线
本Java学习路线涵盖从基础语法、面向对象、异常处理到高级框架、微服务、JVM调优等内容,适合新手入门到进阶,助力掌握企业级开发技能,快速成为合格Java开发者。
706 3
|
人工智能 数据可视化 JavaScript
NodeTool:AI 工作流可视化构建器,通过拖放节点设计复杂的工作流,集成 OpenAI 等多个平台
NodeTool 是一个开源的 AI 工作流可视化构建器,通过拖放节点的方式设计复杂的工作流,无需编码即可快速原型设计和测试。它支持本地 GPU 运行 AI 模型,并与 Hugging Face、OpenAI 等平台集成,提供模型访问能力。
777 14
NodeTool:AI 工作流可视化构建器,通过拖放节点设计复杂的工作流,集成 OpenAI 等多个平台
|
API 数据安全/隐私保护 开发者
使用MechanicalSoup进行网页自动化交互
使用MechanicalSoup进行网页自动化交互
179 2
|
jenkins 持续交付 开发者
自动化部署:使用Jenkins和Docker实现持续集成与交付
【8月更文挑战第31天】本文旨在为读者揭示如何通过Jenkins和Docker实现自动化部署,从而加速软件开发流程。我们将从基础概念讲起,逐步深入到实际操作,确保即使是初学者也能跟上步伐。文章将提供详细的步骤说明和代码示例,帮助读者理解并应用这些工具来优化他们的工作流程。
|
并行计算 监控 搜索推荐
使用 PyTorch-BigGraph 构建和部署大规模图嵌入的完整教程
当处理大规模图数据时,复杂性难以避免。PyTorch-BigGraph (PBG) 是一款专为此设计的工具,能够高效处理数十亿节点和边的图数据。PBG通过多GPU或节点无缝扩展,利用高效的分区技术,生成准确的嵌入表示,适用于社交网络、推荐系统和知识图谱等领域。本文详细介绍PBG的设置、训练和优化方法,涵盖环境配置、数据准备、模型训练、性能优化和实际应用案例,帮助读者高效处理大规模图数据。
338 5
|
存储 JavaScript 前端开发
🚀超级加速:轻松发现开源项目的终极秘籍🎁
本文介绍了8种方法,帮助开发者轻松找到适合自己的开源项目。通过利用如 GitHub Trending、Good First Issues 和 OpenSauced 等平台,读者可以有效地筛选和参与开源项目,提升自己的技术能力与社交网络。开源不仅是技术贡献,更是个人成长与机会的宝贵途径。无论是新手还是有经验的开发者,这些资源都能助你一臂之力,让你在开源社区中茁壮成长。
376 1