[译]-100行代码从零实现 Facebook 的 Recoil 库
来源: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 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
方法,然后调用来自基类 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行代码从零实现 Facebook 的 Recoil 库相关推荐
- react hooks使用_我如何使用React Hooks在约100行代码中构建异步表单验证库
react hooks使用 by Austin Malerba 奥斯汀·马勒巴(Austin Malerba) 我如何使用React Hooks在约100行代码中构建异步表单验证库 (How I bu ...
- 100行代码搞定实时视频人脸表情识别(附代码)
点击上方"小白学视觉",选择加"星标"或"置顶" 重磅干货,第一时间送达本文转自|OpenCV学堂 好就没有写点OpenCV4 + Open ...
- Android鬼点子 100行代码,搞定柱状图!
最近,项目中遇到一个地方,要用到柱状图.所以这篇文章主要讲怎么搞一个柱子. 100行代码,搞定柱状图! 我的印象中柱子是这样的. 恩,简单,一个View直接放到xml,搞定! 但,设计师给的柱子是这样 ...
- SAP系统和微信集成的系列教程之八:100行代码在微信公众号里集成地图搜索功能
本系列的英文版Jerry写作于2017年,这个教程总共包含十篇文章,发表在SAP社区上. 系列目录 (1) 微信开发环境的搭建 (2) 如何通过微信公众号消费API (3) 微信用户关注公众号之后,自 ...
- 100行代码让您学会JavaScript原生的Proxy设计模式
面向对象设计里的设计模式之Proxy(代理)模式,相信很多朋友已经很熟悉了. 其实和Java一样,JavaScript从语言层面来讲,也提供了对代理这个设计模式的原生支持.我们用一个不到100行代码的 ...
- 100行代码实现最简单的基于FFMPEG+SDL的视频播放器
简介 FFMPEG工程浩大,可以参考的书籍又不是很多,因此很多刚学习FFMPEG的人常常感觉到无从下手.我刚接触FFMPEG的时候也感觉不知从何学起. 因此我把自己做项目过程中实现的一个非常简单的视频 ...
- 用python画苹果的logo_简单几步,100行代码用Python画一个蝙蝠侠的logo
转自:菜鸟学Python 简单几步,100行代码用Python画一个蝙蝠侠的logo-1.jpg (35.33 KB, 下载次数: 0) 2020-7-30 12:04 上传 蝙蝠侠作为DC漫画的核心 ...
- WebServer应用示例:不到100行代码玩转Siri语音控制 | ESP32轻松学(Arduino版)
ESP32轻松学系列文章目录: ESP32 概述与 Arduino 软件准备 蓝牙翻页笔(PPT 控制器) B 站粉丝计数器 Siri 语音识别控制 LED 灯 Siri 语音识别获取传感器数据 本期 ...
- 100行代码实现最简单的基于FFMPEG+SDL的视频播放器(SDL1.x)
===================================================== 最简单的基于FFmpeg的视频播放器系列文章列表: 100行代码实现最简单的基于FFMPEG ...
- PONG - 100行代码写一个弹球游戏
大家好,欢迎来到 Crossin的编程教室 ! 今天跟大家讲一讲:如何做游戏 游戏的主题是弹球游戏<PONG>,它是史上第一款街机游戏.因此选它作为我这个游戏开发系列的第一期主题. 游戏引 ...
最新文章
- 做项目经理到底有多爽?
- 如何使得自己的Python程序每行长度小于80个字符?
- 基于python的界面自动化测试-基于Selenium+Python的web自动化测试框架
- 剑指offer 算法 (知识迁移能力)
- L3-002 堆栈 树状数组+二分答案
- 【转】雷军自曝创业第一年:掏自己的钱创业成功率最高
- 【codevs1246】丑数,STL与取模大质数的好处
- Python 网页爬虫 文本处理 科学计算 机器学习 数据挖掘兵器谱 - 数客
- Try Microsoft AutoCollage 2008
- 学习 (2012.01)
- 我用Python又爬虫了拉钩招聘,给你们看看2019市场行情
- 中美线径对照表_导线截面与线径对照表
- kali linux暴力破解wifi密码
- 打印DPI如何与计算机DPI一致,ps打印尺寸怎么设置和实际纸张一致? -电脑资料
- 开源自动化运维平台Spug
- 使用ESP8266驱动TFT显示屏
- ajax data=text,jQuery ajax dataType值为text json探索分享
- 策略模式Java实现
- 掌握这9个单行代码技巧!你也能写出『高端』Python代码 ⛵
- ppt播放动画花屏-问题解决
热门文章
- 探秘金山隐私保险箱 (解密出加密的数据)
- 2021年中国工业互联网安全大赛
- VB中数组的大小排序解析
- 多文件自平衡云传输 (五)资源管理中心篇 —————— 开开开山怪
- linux没有cpufreq目录,Linux系统的Cpufreq
- File Based Optimizations(FBO,FBO焕新存储技术)介绍
- java --运用hhs 框架,tomcat 访问mysql 数据库 连接 失败后,自动 重新连接怎么做?
- tp6 thinkswoole 使用极光curl请求时报错
- 少儿计算机基础知识,学会这三个小知识,轻松入门少儿编程
- 当今世界最NB的25位大数据科学家