持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第1天,点击查看活动详情
@formily/reactive源码解读(1)
前言
Q: 这个库有什么地方值得我们学习呢?
A: reactive 实现了数据的响应式,而响应式在前端有重要应用。因为现前端主流框架都是使用数据驱动视图的改变,根据数据驱动的方式我们可以将这些主流框架划分为两类,一类就是以 React 为代表的 immutable 数据+纯函数模式,另一类就是 Vue 为代表的响应式数据。再以状态管理库为例,redux 使用了 immutable 数据,mobx 使用了响应式数据。
以对象这种引用值为例,应用 immutable 数据的关键是向纯函数传入当前对象,经计算后,返回的是一个新的对象。应用响应式数据的关键是创建可订阅对象,对该对象进行数据劫持。因此,通过学习这个库,可以了解响应式数据是如何创建的。
Q: 既然提到了响应式数据,那它的实现方式和 Mobx 或者 Vue 是不是一致的?
A:最核心的实现方式是一致的,都使用了 ES 语法中的 Proxy 进行数据劫持。并且,该库中基本的 API 和 Mobx 是保持一致的,通过其官方文档[1],我们知道,@formily/reactive 对 Mobx 中的 API 进行了扩展(挖个坑,后续也可以继续探讨 Vue、Mobx 中的实现方式~)。
Q:本系列的行文思路是怎样的呢?
A: 其实开源库就像是一个宝藏 🤩。不论是其运用的设计思想、实现方式,还是其工程结构、代码风格,都有可以学习的地方。在这个系列中,我们会抓大放小,保证先了解其最核心的部分。从最主要的 API 出发,讨论如果是我们来写,会怎样去实现,然后再进行源码解读,提炼设计思路。如果遇到重要的语法知识,会在稍后进行扩展。文中会摘取重要的代码片段,但强烈建议大家在源码中自行找到出处,结合上下文理解。
那么,我们开始吧 🧙🏼。
Reactive 常见API及使用方式
首先,我通过一个 demo,对 reactive 的功能有一个初步的印象。
在这个 demo 中主使用了 observable、autorun、batch、reaction
这四个 API.。
import { observable, autorun, batch, reaction } from "@formily/reactive";
// observable & autorun
const obs = observable({
a: {
b: 1
}
});
autorun(() => {
console.log("obs::", obs.a.b);
});
setTimeout(() => {
obs.a.b = 2;
}, 3000);
const shallowObs = observable.shallow({
a: {
b: 1
}
});
autorun(() => {
console.log("shalow::", shallowObs.a);
});
setTimeout(() => {
// NOT trigger the autorun
// shallowObs.a.b = 2;
shallowObs.a = { b: 3 };
}, 3000);
const refObs = observable.ref({ a: 1 });
autorun(() => {
console.log("ref::", refObs.value.a);
});
setTimeout(() => {
// trigger the autorun since the reference changed
refObs.value = { a: 2 };
}, 3000);
// reaction & batch
const obs2 = observable({
aa: 1,
bb: 2
});
const dispose = reaction(
() => {
console.log("reaction:: triggered");
return obs2.aa + obs2.bb;
},
(res) => {
console.log("reactionCallback::", res);
}
);
batch(() => {
// Not trigger the reaction since the res in reaction callback is NOT changed
obs2.aa = 2;
obs2.bb = 1;
});
obs2.aa = 4;
dispose();
sandbox 运行示例[2]
现在,对这四个基本的 API 功能可以简单总结如下:
-
observable
创建一个可监听的对象,默认是深度劫持的模式。
-
autorun
当可监听对象在 autorun 中被消费时,若可监听对象被修改,autorun 会自动运行,可以理解为监听者。
-
reaction
reaction 接收一个 tracker 函数(后续会讲到 tracker 函数的定义,BTW,autorun 中接收的也是 tracker 函数)和一个回调函数。tracker 函数部分包含类似了 autorun 的逻辑,即当监听对象被修改时触发执行。回调函数还增加了接收 tracker 返回结果,根据该结果有无变化决定是否执行的逻辑。
-
batch
将修改可监听对象的操作包装成批量执行,减少对 autorun 等监听类型函数的触发。
当然,更加完整的 API 定义及使用还是推荐在官方文档中查阅 👇。
@formily/reactive API 文档[3]
建议将官方文档中 API 的简单使用浏览一遍,这样对后面的机制才能有更好的理解呀。
然后,来看看这个库中的一些核心概念,核心概念和上面列举的 API 紧密联系,同时还透露了一部分源码的运行机制。下方这张图也是从官方文档中扒下来的,展示了观察者和订阅者之间的联系。
(注:这里的 Reaction 或者 Observable 并不是指上述具体的 API, 而是源码中定义的核心对象。)
核心概念
根据上方的示意图,先补充下相关 API。创建 Reaction 也就是创建监听者,在该库中可以使用的 API 包括:
tracker
autorun
reaction
创建 Observable 也就是创建可观察对象,在该库中可以使用的 API 主要有:
observable
model
define
makeObservable
好了,现在我们可以介绍这个 Tracker (图中的 tracker 函数)到底是什么了。其实在进行初步认识时,我们可以将其简化为使用“读”(属性访问或者说 get 操作)操作对可观察对象及其属性进行消费的函数。举个栗子,比如
import { observable, autorun } from '@formly/reactive';
const obs = observable({a:{b: {1}}})
autorun(()=>{
console.log(obs.a)
})
实际上,autorun
接收的参数就是这个 tracker 函数,在 tracker 中,我们使用 console.log
,对可观察对象 obs
进行了读操作。
由于 tracker 是由外部传入的自定义函数,Reaction 对其做一层包裹,满足 reactive 内部的监听、调度执行机制。
那么整体的流程就可以从上面的示意图中得到:
- 在 tracker 函数中对 observable(可观察对象)进行读操作(read property)
- 进行读操作时,将该监听函数(reaction)绑定到这个属性(bind property)
- 当 observable 对象的属性被修改(mutate property),则通过该对象的该属性获取监听函数
- 触发监听函数
在接下来深入源码时,我们会对这个过程的相关细节进行探索,大致的流程就是这样啦😉。
[1]
官方文档: reactive.formilyjs.org/zh-CN/guide
[2]
sandbox 运行示例: codesandbox.io/embed/lovin…
[3]
@formily/reactive API 文档: reactive.formilyjs.org/zh-CN/api/o…