@formily/reactive源码解读【在这互联网寒冬,不如一起学习吧】

1,328 阅读5分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 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, 而是源码中定义的核心对象。)

core api.png 核心概念

根据上方的示意图,先补充下相关 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 内部的监听、调度执行机制。

那么整体的流程就可以从上面的示意图中得到:

  1. 在 tracker 函数中对 observable(可观察对象)进行读操作(read property)
  2. 进行读操作时,将该监听函数(reaction)绑定到这个属性(bind property)
  3. 当 observable 对象的属性被修改(mutate property),则通过该对象的该属性获取监听函数
  4. 触发监听函数

在接下来深入源码时,我们会对这个过程的相关细节进行探索,大致的流程就是这样啦😉。

[1]

官方文档: reactive.formilyjs.org/zh-CN/guide

[2]

sandbox 运行示例: codesandbox.io/embed/lovin…

[3]

@formily/reactive API 文档: reactive.formilyjs.org/zh-CN/api/o…