来源:Rewriting Facebook’s “Recoil” React library from scratch in 100 lines

译者:塔希

协议:CC BY-NC-SA 4.0

Atoms

Recoil 是围绕着 “atoms” 这个概念构建的。Atoms 是组成整个状态中的原子性的一部分,你可以在组件中订阅它或更改它的值。

开始,我将创建一个叫做 Atom 的类 ,用来包裹一些值 T 。我加了一些辅助方法 updatesnapshot 允许你获得或更改 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 valueconst name = context.get(NameAtom);// Do the same for AgeAtomconst 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 方法,然后调用来自基类 Statefulupdate 方法。

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行代码从零实现 Facebook 的 Recoil 库相关推荐

  1. react hooks使用_我如何使用React Hooks在约100行代码中构建异步表单验证库

    react hooks使用 by Austin Malerba 奥斯汀·马勒巴(Austin Malerba) 我如何使用React Hooks在约100行代码中构建异步表单验证库 (How I bu ...

  2. 100行代码搞定实时视频人脸表情识别(附代码)

    点击上方"小白学视觉",选择加"星标"或"置顶" 重磅干货,第一时间送达本文转自|OpenCV学堂 好就没有写点OpenCV4 + Open ...

  3. Android鬼点子 100行代码,搞定柱状图!

    最近,项目中遇到一个地方,要用到柱状图.所以这篇文章主要讲怎么搞一个柱子. 100行代码,搞定柱状图! 我的印象中柱子是这样的. 恩,简单,一个View直接放到xml,搞定! 但,设计师给的柱子是这样 ...

  4. SAP系统和微信集成的系列教程之八:100行代码在微信公众号里集成地图搜索功能

    本系列的英文版Jerry写作于2017年,这个教程总共包含十篇文章,发表在SAP社区上. 系列目录 (1) 微信开发环境的搭建 (2) 如何通过微信公众号消费API (3) 微信用户关注公众号之后,自 ...

  5. 100行代码让您学会JavaScript原生的Proxy设计模式

    面向对象设计里的设计模式之Proxy(代理)模式,相信很多朋友已经很熟悉了. 其实和Java一样,JavaScript从语言层面来讲,也提供了对代理这个设计模式的原生支持.我们用一个不到100行代码的 ...

  6. 100行代码实现最简单的基于FFMPEG+SDL的视频播放器

    简介 FFMPEG工程浩大,可以参考的书籍又不是很多,因此很多刚学习FFMPEG的人常常感觉到无从下手.我刚接触FFMPEG的时候也感觉不知从何学起. 因此我把自己做项目过程中实现的一个非常简单的视频 ...

  7. 用python画苹果的logo_简单几步,100行代码用Python画一个蝙蝠侠的logo

    转自:菜鸟学Python 简单几步,100行代码用Python画一个蝙蝠侠的logo-1.jpg (35.33 KB, 下载次数: 0) 2020-7-30 12:04 上传 蝙蝠侠作为DC漫画的核心 ...

  8. WebServer应用示例:不到100行代码玩转Siri语音控制 | ESP32轻松学(Arduino版)

    ESP32轻松学系列文章目录: ESP32 概述与 Arduino 软件准备 蓝牙翻页笔(PPT 控制器) B 站粉丝计数器 Siri 语音识别控制 LED 灯 Siri 语音识别获取传感器数据 本期 ...

  9. 100行代码实现最简单的基于FFMPEG+SDL的视频播放器(SDL1.x)

    ===================================================== 最简单的基于FFmpeg的视频播放器系列文章列表: 100行代码实现最简单的基于FFMPEG ...

  10. PONG - 100行代码写一个弹球游戏

    大家好,欢迎来到 Crossin的编程教室 ! 今天跟大家讲一讲:如何做游戏 游戏的主题是弹球游戏<PONG>,它是史上第一款街机游戏.因此选它作为我这个游戏开发系列的第一期主题. 游戏引 ...

最新文章

  1. 做项目经理到底有多爽?
  2. 如何使得自己的Python程序每行长度小于80个字符?
  3. 基于python的界面自动化测试-基于Selenium+Python的web自动化测试框架
  4. 剑指offer 算法 (知识迁移能力)
  5. L3-002 堆栈 树状数组+二分答案
  6. 【转】雷军自曝创业第一年:掏自己的钱创业成功率最高
  7. 【codevs1246】丑数,STL与取模大质数的好处
  8. Python 网页爬虫 文本处理 科学计算 机器学习 数据挖掘兵器谱 - 数客
  9. Try Microsoft AutoCollage 2008
  10. 学习 (2012.01)
  11. 我用Python又爬虫了拉钩招聘,给你们看看2019市场行情
  12. 中美线径对照表_导线截面与线径对照表
  13. kali linux暴力破解wifi密码
  14. 打印DPI如何与计算机DPI一致,ps打印尺寸怎么设置和实际纸张一致? -电脑资料
  15. 开源自动化运维平台Spug
  16. 使用ESP8266驱动TFT显示屏
  17. ajax data=text,jQuery ajax dataType值为text json探索分享
  18. 策略模式Java实现
  19. 掌握这9个单行代码技巧!你也能写出『高端』Python代码 ⛵
  20. ppt播放动画花屏-问题解决

热门文章

  1. 探秘金山隐私保险箱 (解密出加密的数据)
  2. 2021年中国工业互联网安全大赛
  3. VB中数组的大小排序解析
  4. 多文件自平衡云传输 (五)资源管理中心篇 —————— 开开开山怪
  5. linux没有cpufreq目录,Linux系统的Cpufreq
  6. File Based Optimizations(FBO,FBO焕新存储技术)介绍
  7. java --运用hhs 框架,tomcat 访问mysql 数据库 连接 失败后,自动 重新连接怎么做?
  8. tp6 thinkswoole 使用极光curl请求时报错
  9. 少儿计算机基础知识,学会这三个小知识,轻松入门少儿编程
  10. 当今世界最NB的25位大数据科学家