前言
最近总爱翻这种巧而精的库的源码,一个是代码量小,容易调试懂,另一个是实现思路巧,能学到不少东西。那么今天,我们翻谁的源码呢?如题,那就是mobx的。
mobx作为一个状态管理库,小巧而轻便,门槛低,官方文档更是没几个字,api数量不多,去掉一些怎么用到的when、flow这些,更是少了。而且它不止学习成本低,它使用起来也非常简单。正因为这些,它广受开发者们的喜爱。
不过,今天我们并不是来学习它如何使用的,而是学习它都要做什么以及如何做的。所以下文不会有mobx的入门相关的内容
,我们今天是来讲它的原理
的。
实现一个简单版的mobx
依我看来,只要掌握了autorun和observable这个两个api,基本就是了解整个mobx的实现思路了,从而实现它的其他api都是易如反掌,所以下面我们就来实现一个这两个api。
先看个简单例子:
js
import { observable, autorun } from 'mobx';
let obj = { name: 1 };
// observable可以把一个普通对象变成可观察对象
let proxyObj = observable(obj);
// 通过autorun可以创建一个响应,类似useEffect,创建时执行一次,里面的值改变时也会自动执行
autorun(() => {
console.log(proxyObj.name);
})
proxyObj.name = 2; // 此时autorun会执行,打印2
proxyObj.name = 3; // 此时autorun会执行,打印3
如代码中的注释,autorun在创建时会执行一次,后面如果改变了autorun里用到的可观察对象,那么autorun都会再次执行,就像代码中连续两次修改proxyObj.name一样,autorun就跟着自动执行了两次。
看到这里,其实不难看出,mobx就是在所用到的可观察对象的set上做了手脚,当改变了可观察对象的值(即是调用了set),那么就执行一下相关的autorun。
不过在讲这个autorun的实现之前,我们要先知道可观察对象也就是observable都做了什么。
observable
observable其实就是双向绑定的实现,不过mobx有意思的地方在于,它给每一个需要双向绑定的(或者说成为可观察对象)的数据都安排了一个管家,我们想要访问或者改写这个可观察对象,都是要通过这个管家的。
实现过双向绑定的应该知道,不同数据类型的双向绑定的实现方式其实稍有不同,所以本文不讲解observable中对所有数据类型的实现,只讲对Object的实现。
那么我们就写下这样一段代码:
js
import { object } from './observableobject';
function isObject(value) {
return value !== null && typeof value === 'object';
}
function observable(v) {
if (isObject) {
return object(v);
}
}
export default observable;
从代码可以看出,observable的逻辑都在object这个方法中。
前排警告:以下代码逻辑非常绕
object这个方法涉及到的逻辑非常复杂,这边先看一下文字版逻辑顺序,再直接看代码,相信各位都能看懂。
以以下这段代码为例
js
let obj = { name: 1 };
// observable可以把一个普通对象变成可观察对象
let proxyObj = observable(obj);
- 1、创建一个空对象{}(下文都称之为obj2),并将他转成一个Proxy,并且设定一个独特的捕获器(下文会讲这个捕获器)
- 2、生成一个管家,
管家类(类名为ObservableObjectAdministration)
的一个实例(即是 new ObservableObjectAdministration),下文都称这个实例为adm。并给这个管家生成一个独有的名字,这个名字的生成规则为ObservableObject@ + 一个自增的id,保证了唯一性。 - 3、给obj2增加一个不可改写的属性,属性名为Symbol('mobx administration'),这个属性名我们称之为$mobx,属性值为adm。
- 4、遍历obj(即是代码中的{ name: 1 }这个对象)的自有属性,都执行一次adm的extend方法。
到这先暂停,我们来看一下这个管家类。
管家类的所有代码都附上来了,但我们先把目光放在extend和defineObservableProperty上。
js
class ObservableObjectAdministration {
// target就是那个obj2
// values是一个new Map()
// name是管家的名字,即ObservableObject@ + 一个自增的id
constructor(target, values, name) {
this.target = target;
this.values = values; // 存放属性的信息
this.name = name;
}
get(key) {
return this.target[key];
}
set(key, value) {
if (this.values.has(key)) {
return this.setObservableValue(key, value);
}
}
extend(key, descriptor) {
this.defineObservableProperty(key, descriptor.value);
}
getObservableValue(key) {
return this.values.get(key).get();
}
setObservableValue(key, value) {
const observableValue = this.values.get(key);
observableValue.setNewValue(value);
return true;
}
defineObservableProperty(key, value) {
const descriptor = {
configurable: true,
enumerable: true,
get() {
// 注意,这里的this指向的是obj2
return this[$mobx].getObservableValue(key);
},
set() {
// 注意,这里的this指向的是obj2
return this[$mobx].setObservableValue(key, value);
}
}
Object.defineProperty(this.target, key, descriptor);
this.values.set(key, new ObservableValue(value));
}
}
看完上述代码,我们可以看出,extend方法其实就是将obj上面的属性都赋值给adm的target和values,并且会将值做一次加工,就是new ObservableValue(value)这里。
注意:由于复杂类型的引用原因,其实这里的target和obj2是一样的,给target赋值,其实也就是给obj2赋值了。
这里贴一下ObservableValue这个类的代码,先不用关心里面都做了什么。
js
class ObservableValue {
constructor(value) {
this.value = value;
this.observers = new Set(); // 此可观察值的监听者,或者说是观察者
}
get() {
reportObserved(this);
return this.value;
}
setNewValue(newValue) {
this.value = newValue;
propagateChanged(this);
}
}
好!铺垫到这里,还记得上文第一步生成obj2时谈到的捕获器吗?这里我们就来聊聊他。
obj2生成转成Proxy可以看成以下代码。
js
// 就是获取到obj2身上的adm
function getAdm(target) {
return target[$mobx];
}
const objectProxyTraps = {
// get方法改写成adm的get,也就是从adm上的values读取值
get(target, name) {
return getAdm(target).get(name);
},
// 同理,set方法改写成adm的set,也就是执行adm的set方法
set(target, name, value) {
return getAdm(target).set(name, value);
}
};
const obj2 = new Proxy({}, objectProxyTraps);
其实上面这个转成Proxy的代码,一句话就能说清楚,就是当从obj2身上读和写属性时,都改成走adm的对应方法。
那么好,整个observable的最后一步做什么呢?
- 5、返回obj2。
对的,就是这么简单,把obj2返回回去。
那么经过了这么一大堆的逻辑处理,现在都成什么样子了呢?
举个例子,假设我们想要读取obj的name属性,会发生以下这样的步骤:
读取obj.name -> 执行obj2.get() -> 从obj2上获取到adm -> 执行adm.get(name) -> 读取到adm.target.get(name)
总的来说,交给observable的数据,他们都成了少爷,不但被分配了一个专属管家,而且任何事情都会交给管家做。
autorun
上面的observable的逻辑是非常绕的,如果看不太懂也没关系,不会很影响看懂autorun。
autorun的设计思想是很好理解的。
我们在定义autorun时,autorun会执行一次,而当执行到可观察对象的时候,必定会执行可观察对象的get方法(也就是读取属性值)。那么我们就可以在这个get方法上做一下文章,通过这个get方法让这个属性绑定上这个autorun。
而当我们改变绑定过autorun的属性的值的时候,我们就让绑定的autorun们都执行一遍即可。
主要思路我们清楚了,那么下面来看看实现步骤:
- 1、定义一个全局变量globalState
- 2、定义autorun时,生成一个独有的autorun的名字并且生成一个叫Reaction(下文有完整代码)的类的实例(下文成reaction),将reaction赋值给globalState.trackingDerivation
- 3、执行定义autorun时传入的函数
- 4、每当执行可观察对象的get方法时,就将自身push到globalState.trackingDerivation(即是reaction)的observing数组中
- 5、函数执行完毕后,将globalState.trackingDerivation置为null
- 6、遍历observing,将自身(即是reaction)都push进他们的observers数组中。
上面这些步骤就是一个双向绑定的过程。可观察对象自身有个observers数组,而reaction自身有个observing数组,通过全局变量globalState桥接,互相将自己都push进了各自的数组中,实现了双向绑定。
有了这一个双向绑定之后,只要我们改了可观察对象的值的时候,依次执行一下observers里的reaction就好了。
完整代码
- observable.js
js
import { object } from './observableobject';
import { isObject } from './utils';
function observable(v) {
if (isObject) {
return object(v);
}
}
export default observable;
- observableobject.js
js
import { getNextId, addHiddenProp, $mobx, getAdm, globalState } from './utils';
class ObservableValue {
constructor(value) {
this.value = value;
this.observers = new Set(); // 此可观察值的监听者,或者说是观察者
}
get() {
reportObserved(this);
return this.value;
}
setNewValue(newValue) {
this.value = newValue;
propagateChanged(this);
}
}
function propagateChanged(observableValue) {
const { observers } = observableValue;
observers.forEach(observer => {
observer.runReaction();
})
}
function reportObserved(observableValue) {
const trackingDerivation = globalState.trackingDerivation;
if (trackingDerivation) {
trackingDerivation.observing.push(observableValue);
}
}
class ObservableObjectAdministration {
constructor(target, values, name) {
this.target = target;
this.values = values; // 存放属性的信息
this.name = name;
}
get(key) {
return this.target[key];
}
set(key, value) {
if (this.values.has(key)) {
return this.setObservableValue(key, value);
}
}
extend(key, descriptor) {
this.defineObservableProperty(key, descriptor.value);
}
getObservableValue(key) {
return this.values.get(key).get();
}
setObservableValue(key, value) {
const observableValue = this.values.get(key);
observableValue.setNewValue(value);
return true;
}
defineObservableProperty(key, value) {
const descriptor = {
configurable: true,
enumerable: true,
get() {
return this[$mobx].getObservableValue(key);
},
set() {
return this[$mobx].setObservableValue(key, value);
}
}
Object.defineProperty(this.target, key, descriptor);
this.values.set(key, new ObservableValue(value));
}
}
function asObservableObject(target) {
const name = `ObservableObject@${getNextId()}`;
const adm = new ObservableObjectAdministration(
target, new Map(), name,
);
addHiddenProp(target, $mobx, adm);
return target;
}
const objectProxyTraps = {
get(target, name) {
return getAdm(target).get(name);
},
set(target, name, value) {
return getAdm(target).set(name, value);
}
};
function asDynamicObservableObject(target) {
asObservableObject(target);
const proxy = new Proxy(target, objectProxyTraps);
return proxy;
}
function extendObservable(proxyObject, properties) {
const descriptors = Object.getOwnPropertyDescriptors(properties);
const adm = proxyObject[$mobx];
Reflect.ownKeys(descriptors).forEach(key => {
adm.extend(key, descriptors[key]);
})
return proxyObject;
}
export function object(target) {
const dynamicObservableObject = asDynamicObservableObject({});
return extendObservable(dynamicObservableObject, target);
}
- autorun.js
js
import { getNextId } from "./utils";
import Reaction from './reaction';
function autorun(view) {
const name = 'Autorun@' + getNextId();
const reaction = new Reaction(
name,
function () {
this.track(view);
}
);
reaction.schedule();
}
export default autorun;
- reaction
js
import { globalState } from "./utils";
export default class Reaction {
constructor(name, onInvalidate) {
this.name = name;
this.onInvalidate = onInvalidate;
this.observing = []; // 表示观察到了哪些可观察变量
}
track(fn) {
globalState.trackingDerivation = this;
fn.call();
globalState.trackingDerivation = null;
bindDependencies(this);
}
schedule() {
globalState.pendingReaction.push(this);
runReactions();
}
runReaction() {
this.onInvalidate();
}
}
function bindDependencies(derivation) {
const { observing } = derivation;
observing.forEach((observableValue) => {
observableValue.observers.add(derivation);
});
}
function runReactions() {
const allReactions = globalState.pendingReaction;
let reaction;
while (reaction = allReactions.shift()) {
reaction.runReaction();
}
}
- utils.js
js
export const $mobx = Symbol('mobx administration');
export function isObject(value) {
return value !== null && typeof value === 'object';
}
let mobxGuid = 0;
export function getNextId() {
return ++mobxGuid;
}
export function addHiddenProp(obj, propName, value) {
Object.defineProperty(obj, propName, {
enumerable: false,
writable: true,
configurable: false,
value,
});
}
export function getAdm(target) {
return target[$mobx];
}
export const globalState = {
pendingReaction: [],
trackingDerivation: null,
}
mobx-react
既然都理解了mobx的原理了,那么这里顺带提一嘴mobx-react的一些原理。
在我们使用mobx-react的时候,他的效果是什么?
很简单,就是当可观察对象的值变的时候,组件自己自动
重新渲染一下。注意是自动,这个自动二字有没非常熟悉?对了!那就是autorun。我们只要将setState({})传给autorun就能自己实现一个mobx-react的效果了。
不过,实际上,mobx-react是直接用Reaction来做的。这个Reaction和autorun里用到的Reaction就是同一个。
这里就实现一个useObserver好了。
js
import React, { useState } from 'react';
import { Reaction } from 'mobx';
export function useObserver(fn) {
// 仅仅是为了得到一个强行更新组件的函数
const [, setState] = useState({});
const forceUpdate = () => setState({});
let reaction = new Reaction('observer', forceUpdate);
let rendering;
reaction.track(() => {
rendering = fn();
}, forceUpdate);
return rendering;
}
结尾
本文主要讲了mobx的observable和autorun的原理和实现,然后通过autorun引出了mobx-react的useObserver的原理和实现。
读懂了本文,不但可以收获到mobx和mobx-react的基本原理,还能明白双向绑定的原理和应用,而且大量的类的使用,对于写工具库也是非常有帮助的。
以下附上代码仓库,仓库中的代码不仅只实现了mobx-react的useObserver,还有另外几个api,感兴趣的可以拉下来看看。
最后:创作不易,给个小赞啦~