-
Notifications
You must be signed in to change notification settings - Fork 12
Description
1. 前言
Mobx 是 React 的另一种经过战火洗礼的状态管理方案,和 Redux 不同的地方是 Mobx 是一个响应式编程(Reactive Programming
)库,在一定程度上可以看做没有模板的 Vue,基本原理和 Vue 一致。
Mobx 借助于装饰器的实现,使得代码更加简洁易懂。由于使用了可观察对象,所以 Mobx 可以做到直接修改状态,而不必像 Redux 一样编写繁琐的 actions 和 reducers。
Mobx 的执行流程和 Redux 有一些相似。这里借用 Mobx 官网的一张图:
简单的概括一下,一共有这么几个步骤:
- 页面事件(生命周期、点击事件等等)触发 action 的执行。
- 通过 action 来修改状态。
- 状态更新后,
computed
计算属性也会根据依赖的状态重新计算属性值。 - 状态更新后会触发
reaction
,从而响应这次状态变化来进行一些操作(渲染组件、打印日志等等)。
2. Mobx 核心概念
Mobx 的概念没有 Redux 那么多,学习和上手成本也更低。以下介绍全部基于 ES 装饰器来讲解,更多细节可以参考 Mobx 中文文档:Mobx 中文文档
对于一些实践的用法,也可以参考我一年前写的文章:Mobx 实践
本文涉及到的代码都放到了我的 GitHub 上面:simple-mobx
2.1 observable
observable 可以将接收到的值包装成可观察对象,这个值可以是 JS 基本数据类型、引用类型、普通对象、类实例、数组和映射等等等。
const list = observable([1, 2, 4]);
list[2] = 3;
const person = observable({
firstName: "Clive Staples",
lastName: "Lewis"
});
person.firstName = "C.S.";
如果在对象里面使用 get,那就是计算属性了。计算属性一般使用 get
来实现,当依赖的属性发生变化的时候,就会重新计算出新的值,常用于一些计算衍生状态。
const todoStore = observable({
// observable 属性:
todos: []
// 计算属性:
get completedCount() {
return (this.todos.filter(todo => todo.isCompleted) || []).length
}
});
更多时候,我们会配合装饰器一起使用来使用 observable
方法。
class Store {
@observable count = 0;
}
2.2 computed
想像一下,在 Redux 中,如果一个值 A 是由另外几个值 B、C、D 计算出来的,在 store 中该怎么实现?
如果要实现这么一个功能,最麻烦的做法是在所有 B、C、D 变化的地方重新计算得出 A,最后存入 store。
当然我也可以在组件渲染 A 的地方根据 B、C、D 计算出 A,但是这样会把逻辑和组件耦合到一起,如果我需要在其他地方用到 A 怎么办?
我甚至还可以在所有 connect
的地方计算 A,最后传入组件。但由于 Redux 监听的是整个 store
的变化,所以无法准确的监听到 B、C、D 变化后才重新计算 A。
但是 Mobx 中提供了 computed
来解决这个问题。正如 Mobx 官方介绍的一样,computed
是基于现有状态或计算值衍生出的值,如下面 todoList 的例子,一旦已完成事项数量改变,那么 completedCount
会自动更新。
class TodoStore {
@observable todos = []
@computed get completedCount() {
return (this.todos.filter(todo => todo.isCompleted) || []).length
}
}
2.3 reaction & autorun
autorun
接收一个函数,当这个函数中依赖的可观察属性发生变化的时候,autorun
里面的函数就会被触发。除此之外,autorun
里面的函数在第一次会立即执行一次。
const person = observable({
age: 20
})
// autorun 里面的函数会立即执行一次,当 age 变化的时候会再次执行一次
autorun(() => {
console.log("age", person.age);
})
person.age = 21;
// 输出:
// age 20
// age 21
但是很多人经常会用错 autorun,导致属性修改了也没有触发重新执行。
常见的几种错误用法如下:
- 错误的修改了可观察对象的引用
let person = observable({
age: 20
})
// 不会起作用
autorun(() => {
console.log("age", person.age);
})
person = observable({
age: 21
})
- 在追踪函数外进行间接引用
const age = person.age;
// 不会起作用
autorun(() => {
console.log('age', age)
})
person.age = 21
reaction
则是和 autorun
功能类似,但是 autorun
会立即执行一次,而 reaction
不会,使用 reaction 可以在监听到指定数据变化的时候执行一些操作,有利于和副作用代码解耦。reaction 和 Vue 中的 watch
非常像。
// 当todos改变的时候将其存入缓存
reaction(
() => toJS(this.todos),
(todos) => localStorage.setItem('mobx-react-todomvc-todos', JSON.stringify({ todos }))
)
看到 autorun 和 reaction 的用法后,也许你会想到,如果将其和 React 组件结合到一起,那不就可以实现很细粒度的更新了吗?
没错,Mobx-React 就是来解决这个问题的。
2.4 observer
Mobx-React 中提供了一个 observer
方法,这个方法主要是改写了 React 的 render
函数,当监听到 render
中依赖属性变化的时候就会重新渲染组件,这样就可以做到高性能更新。
@observer
class App extends Component {
@observable count = 0;
@action
increment = () => {
this.count++;
}
render() {
<h1 onClick={this.increment}>{ this.count }</h1>
}
}
3. mobx 原理
从上面的一些用法来看,很明显实现上用到了 Object.defineProperty
或者 Proxy
。当 autorun 第一次执行的时候会触发依赖属性的 getter
,从而收集当前函数的依赖。
const person = observable({ name: 'tom' })
autorun(function func() {
console.log(person.name)
})
在 autorun 里面相当于做了这么一件事情。
person.watches.push(func)
当依赖属性触发 setter
的时候,就会将所有订阅了变化的函数都执行一遍,从而实现了数据响应式。
person.watches.forEach(watch => watch())
Mobx 的实现原理很简单,整体上和 Vue 比较像,简单来说就是这么几步:
- 用
Object.defineProperty
或者Proxy
来拦截observable
包装的对象属性的get/set
。 - 在
autorun
或者reaction
执行的时候,会触发依赖状态的get
,此时将autorun
里面的函数和依赖的状态关联起来。也就是我们常说的依赖收集。 - 当修改状态的时候会触发
set
,此时会通知前面关联的函数,重新执行他们。
// 使用 `Object.defineProperty` 或者 `Proxy` 来代理这个对象
let person = observable({
age: 20
});
autorun(function F () {
console.log("age", person.age); // 收集 person.age 的依赖,将 F 放到一个观察队列里面
});
person.age = 21; // 修改 age 时触发 set,从队列里面取出 F,重新执行
3.1 observable
observable
的源码实现在 api/observable.ts 文件中,主要是在 createObservable
函数里面。
function createObservable(v: any, arg2?: any, arg3?: any) {
// @observable someProp;
if(isStringish(arg2)) {
storeAnnotation(v, arg2, observableAnnotation)
return
}
// already observable - ignore
if (isObservable(v)) return v
// plain object
if (isPlainObject(v)) return observable.object(v, arg2, arg3)
// Array
if (Array.isArray(v)) return observable.array(v, arg2)
// Map
if (isES6Map(v)) return observable.map(v, arg2)
// Set
if (isES6Set(v)) return observable.set(v, arg2)
// other object - ignore
if (typeof v === "object" && v !== null) return v
// anything else
return observable.box(v, arg2)
}
这段代码里面对数据类型进行了判断,调用不同的函数,这里主要以 object
的情况为例,返回的是 observable.object(v, arg2, arg3)
。
observable.object
的实现在 observableFactories
里面,这里有判断是否使用 Proxy
,如果用 Proxy
,就走 asDynamicObservableObject
这个方法。
const observableFactories: IObservableFactory = {
object<T = any>(
props: T,
decorators?: AnnotationsMap<T, never>,
options?: CreateObservableOptions
): T {
return extendObservable(
globalState.useProxies === false || options?.proxy === false
? asObservableObject({}, options)
: asDynamicObservableObject({}, options),
props,
decorators
)
},
} as any
这里主要看 extendObservable 方法,它在 extendobservable.ts 文件里面。
export function extendObservable<A extends Object, B extends Object>(
target: A,
properties: B,
annotations?: AnnotationsMap<B, never>,
options?: CreateObservableOptions
): A & B {
const descriptors = getOwnPropertyDescriptors(properties)
const adm: ObservableObjectAdministration = asObservableObject(target, options)[$mobx]
startBatch()
try {
ownKeys(descriptors).forEach(key => {
adm.extend_(
key,
descriptors[key as any],
// must pass "undefined" for { key: undefined }
!annotations ? true : key in annotations ? annotations[key] : true
)
})
} finally {
endBatch()
}
return target as any
}
关键代码在 adm.extend_
里面,传入了对象的 key
、descriptors[key]
。这里的 adm
是根据 target 创建的一个 ObservableObjectAdministration
实例。
extend_(
key: PropertyKey,
descriptor: PropertyDescriptor,
annotation: Annotation | boolean,
proxyTrap: boolean = false
): boolean | null {
if (annotation === true) {
annotation = this.defaultAnnotation_
}
if (annotation === false) {
return this.defineProperty_(key, descriptor, proxyTrap)
}
assertAnnotable(this, annotation, key)
const outcome = annotation.extend_(this, key, descriptor, proxyTrap)
if (outcome) {
recordAnnotationApplied(this, annotation, key)
}
return outcome
}
extend_
里面调用了 annotation.extend_
方法,这个 annotation
比较关键,可以看到 annotation = this.defaultAnnotation_
这句,按照 defaultAnnotation_
-> getAnnotationFromOptions
-> createAutoAnnotation
-> observableAnnotation.extend_
这个链路找下去发现最后调用的是 observableAnnotation.extend_
。
function extend_(
adm: ObservableObjectAdministration,
key: PropertyKey,
descriptor: PropertyDescriptor,
proxyTrap: boolean
): boolean | null {
assertObservableDescriptor(adm, this, key, descriptor)
return adm.defineObservableProperty_(
key,
descriptor.value,
this.options_?.enhancer ?? deepEnhancer,
proxyTrap
)
}
这里就是根据 key
和 value
来定义了 observable
的属性,看下 defineObservableProperty_
做了些什么。
defineObservableProperty_(
key: PropertyKey,
value: any,
enhancer: IEnhancer<any>,
proxyTrap: boolean = false
): boolean | null {
try {
startBatch();
const cachedDescriptor = getCachedObservablePropDescriptor(key)
const descriptor = {
configurable: globalState.safeDescriptors ? this.isPlainObject_ : true,
enumerable: true,
get: cachedDescriptor.get,
set: cachedDescriptor.set
}
// Define
if (proxyTrap) {
if (!Reflect.defineProperty(this.target_, key, descriptor)) {
return false
}
} else {
defineProperty(this.target_, key, descriptor)
}
const observable = new ObservableValue(
value,
enhancer,
__DEV__ ? `${this.name_}.${key.toString()}` : "ObservableObject.key",
false
)
this.values_.set(key, observable)
// Notify (value possibly changed by ObservableValue)
this.notifyPropertyAddition_(key, observable.value_)
} finally {
endBatch()
}
return true
}
主要有两部分,一个是根据 key
来获取 cachedDescriptor
,将其设置为 defineProperty
的 descriptor
,这里的 get/set
就是之后 Mobx 的拦截规则。
另一个是创建一个 ObservableValue
实例,将其存入 this.values_
里面。
这里的 cachedDescriptor.get
最终也是调用了 this.values_.get(key)!.get()
,也就是 ObservableValue
里面的 get
方法。
public get(): T {
this.reportObserved()
return this.dehanceValue(this.value_)
}
这个 reportObserved
最终会调到 observable.ts 文件里面,它将当前的这个 ObservableValue
实例放到了 derivation.newObserving_
上面,通过 derivation.unboundDepsCount_
进行了映射。
export function reportObserved(observable: IObservable): boolean {
checkIfStateReadsAreAllowed(observable)
const derivation = globalState.trackingDerivation
if (derivation !== null) {
/**
* Simple optimization, give each derivation run an unique id (runId)
* Check if last time this observable was accessed the same runId is used
* if this is the case, the relation is already known
*/
if (derivation.runId_ !== observable.lastAccessedBy_) {
observable.lastAccessedBy_ = derivation.runId_
// get 的时候将 observable 存到 newObserving_ 上面
derivation.newObserving_![derivation.unboundDepsCount_++] = observable
if (!observable.isBeingObserved_ && globalState.trackingContext) {
observable.isBeingObserved_ = true
observable.onBO()
}
}
return true
} else if (observable.observers_.size === 0 && globalState.inBatch > 0) {
queueForUnobservation(observable)
}
return false
}
至此,整个 observable
的流程就分析清楚了,如下图所示:
3.2 autorun
autorun
是触发 get
的地方,它里面的函数会在依赖的数据发生变化的时候执行。它的源码在 autorun.ts 文件里面。
function autorun(
view: (r: IReactionPublic) => any,
opts: IAutorunOptions = EMPTY_OBJECT
): IReactionDisposer {
const name: string =
opts?.name ?? (__DEV__ ? (view as any).name || "Autorun@" + getNextId() : "Autorun")
const runSync = !opts.scheduler && !opts.delay
let reaction: Reaction
if (runSync) {
// normal autorun
reaction = new Reaction(
name,
function (this: Reaction) {
this.track(reactionRunner)
},
opts.onError,
opts.requiresObservable
)
} else {
const scheduler = createSchedulerFromOptions(opts)
// debounced autorun
let isScheduled = false
reaction = new Reaction(
name,
() => {
if (!isScheduled) {
isScheduled = true
scheduler(() => {
isScheduled = false
if (!reaction.isDisposed_) reaction.track(reactionRunner)
})
}
},
opts.onError,
opts.requiresObservable
)
}
function reactionRunner() {
view(reaction)
}
reaction.schedule_()
return reaction.getDisposer_()
}
可以看到,这里会创建一个 Reaction
的实例,将我们的函数 view
传到 reaction.track
里面,然后一起传给 Reaction
的构造函数,等待合适的时机再去执行这个函数。在 Mobx 里面其实都是通过 reaction.schedule_
来调度执行的。
schedule_() {
if (!this.isScheduled_) {
this.isScheduled_ = true
globalState.pendingReactions.push(this)
runReactions()
}
}
这里将这个 Reaction 实例放到 pendingReactions
里面,然后执行了 runReactions
。这里的 reactionScheduler
可能是为了以后实现其他功能留下的一个口子。
let reactionScheduler: (fn: () => void) => void = f => f()
export function runReactions() {
// Trampolining, if runReactions are already running, new reactions will be picked up
if (globalState.inBatch > 0 || globalState.isRunningReactions) return
reactionScheduler(runReactionsHelper)
}
function runReactionsHelper() {
globalState.isRunningReactions = true
const allReactions = globalState.pendingReactions
let iterations = 0
while (allReactions.length > 0) {
if (++iterations === MAX_REACTION_ITERATIONS) {
allReactions.splice(0) // clear reactions
}
let remainingReactions = allReactions.splice(0)
for (let i = 0, l = remainingReactions.length; i < l; i++)
remainingReactions[i].runReaction_()
}
globalState.isRunningReactions = false
}
在 runReactionsHelper
里面会遍历我们的 pendingReactions
数组,执行里面的 reaction
实例的 runReaction_
方法。
runReaction_() {
if (!this.isDisposed_) {
startBatch()
this.isScheduled_ = false
const prev = globalState.trackingContext
globalState.trackingContext = this
if (shouldCompute(this)) {
this.isTrackPending_ = true
try {
this.onInvalidate_()
} catch (e) {
this.reportExceptionInDerivation_(e)
}
}
globalState.trackingContext = prev
endBatch()
}
}
这里面的 onInvalidate_
其实就是刚刚的 reaction.track
方法。然后来看下 reaction.track
的实现。
track(fn: () => void) {
if (this.isDisposed_) {
return
// console.warn("Reaction already disposed") // Note: Not a warning / error in mobx 4 either
}
startBatch()
const notify = isSpyEnabled()
let startTime
this.isRunning_ = true
const prevReaction = globalState.trackingContext // reactions could create reactions...
globalState.trackingContext = this
const result = trackDerivedFunction(this, fn, undefined)
globalState.trackingContext = prevReaction
this.isRunning_ = false
this.isTrackPending_ = false
if (this.isDisposed_) {
clearObserving(this)
}
if (isCaughtException(result)) this.reportExceptionInDerivation_(result.cause)
if (__DEV__ && notify) {
spyReportEnd({
time: Date.now() - startTime
})
}
endBatch()
}
它会在 trackDerivedFunction
里面调用刚刚的 view 函数(autorun
包裹的那个函数),我们知道在执行 view 函数的时候,如果里面依赖了被 observable
包裹对象的属性,那么就会触发属性的 get 方法,也就回到了刚刚分析 observable
的 reportObserved
里面。它会将 observable
挂载到 derivation.newObserving_
上面。
function trackDerivedFunction<T>(derivation: IDerivation, f: () => T, context: any) {
const prevAllowStateReads = allowStateReadsStart(true)
changeDependenciesStateTo0(derivation)
// 初始化 derivation.newObserving_
derivation.newObserving_ = new Array(derivation.observing_.length + 100)
derivation.unboundDepsCount_ = 0
derivation.runId_ = ++globalState.runId
const prevTracking = globalState.trackingDerivation
globalState.trackingDerivation = derivation
globalState.inBatch++
let result
if (globalState.disableErrorBoundaries === true) {
// 这里触发了 observableValue.get,继而执行了 reportObserved
// derivation.newObserving_[derivation.unboundDepsCount_++] = observer;
result = f.call(context)
} else {
try {
result = f.call(context)
} catch (e) {
result = new CaughtException(e)
}
}
globalState.inBatch--
globalState.trackingDerivation = prevTracking
bindDependencies(derivation)
warnAboutDerivationWithoutDependencies(derivation)
allowStateReadsEnd(prevAllowStateReads)
return result
}
到了这里,你会发现在 newObserving
上面已经收集到了 view 函数依赖的属性,这里的 derivation
实际上就是前面的那个 Reaction 实例。
然后又执行了 bindDependencies
函数,它就是将 Reaction 实例和 observable
关联起来的。
function addObserver(observable: IObservable, node: IDerivation) {
observable.observers_.add(node)
if (observable.lowestObserverState_ > node.dependenciesState_)
observable.lowestObserverState_ = node.dependenciesState_
}
到了这一步,我们已经可以从每个对象属性的 observers_
上面获取到需要通知变更的函数了。只要在 set 的时候从 observers_
取出来、遍历、执行就行了。
在 set 阶段依然走的是 reaction.schedule_
这个方法去调度的,重复我们最开始执行 autorun 的那一步。
4. 从零实现
知道原理之后,那我们就可以来一步步去实现这个 Mobx,这里将会以装饰器写法为例子。
4.1 observable
前面在讲解装饰器的时候说过,装饰器函数一般接收三个参数,分别是目标对象、属性、属性描述符。
我们都知道,被 observable 包装过的对象,其属性也是可观察的,也就是说需要递归处理其属性。
其次,由于需要收集依赖的方法,某个方法可能依赖了多个可观察属性,相当于这些可观察属性都有自己的订阅方法数组。
const cat = observable({name: "tom"})
const mice = observable({name: "jerry"})
autorun(function func1 (){
console.log(`${cat.name} and ${mice.name}`)
})
autorun(function func2(){
console.log(mice.name)
})
以上面这段代码为例,可观察对象 cat
只有 func1
一个订阅方法,而 mice
则有 func1
和 func2
两个方法。
cat.watches = [func1]
mice.watches = [func1, func2]
可见 observable
在调用的时候,使用了类似 id
之类的来对不同调用做区分。
我们可以先简单地实现一下 observable 方法。首先,实现一个可以根据 id 来区分的类。
let observableId = 0
class Observable {
id = 0
constructor(v) {
this.id = observableId++;
this.value = v;
}
}
然后我们要实现一个装饰器,这个方法支持拦截属性的 get/set
。这个 observable
装饰器其实就是利用了 Observable
这个类。
function observable(target, name, descriptor) {
const v = descriptor.initializer.call(this);
const o = new Observable(v);
return {
enumerable: true,
configurable: true,
get: function() {
return o.get();
},
set: function(v) {
return o.set(v);
}
};
};
然后我们要来实现 Observable
类里面的 get/set
,但这个 get/set
和 autorun
也是密切相关的。
get/set
做了什么呢?get
会在 autorun
执行的时候,将传给 autorun
的函数依赖收集到 id
相关的数组里面。
而 set
则是会触发数组中相关函数的执行。
let observableId = 0
class Observable {
id = 0
constructor(v) {
this.id = observableId++;
this.value = v;
}
set(v) {
this.value = v;
dependenceManager.trigger(this.id);
}
get() {
dependenceManager.collect(this.id);
return this.value;
}
}
4.2 依赖收集autorun
前面讲了,autorun
会立即执行一次,并且会将其函数收集起来,存到和 observable. id
相关的数组中去。那么 autorun
就是一个收集依赖、执行函数的过程。实际上,在执行函数的时候,就已经触发了 get
来做了收集。
import dependenceManager from './dependenceManager'
export default function autorun(handler) {
dependenceManager.beginCollect(handler);
handler(); // 触发 get,执行了 dependenceManager.collect()
dependenceManager.endCollect();
}
接着我们来实现一下 dependenceManager
的几个方法,首先定义一个 DependenceManager
类。
再来拆解一下 DependenceManager
中的方法,如下:
- beginCollect: 用一个全局变量保存着函数依赖。
- collect: 当执行
get
的时候,根据传入的id
,将函数依赖放入数组中。 - endCollect: 清除刚开始的函数依赖,以便于下一次收集。
- trigger: 当执行
set
的时候,根据id
来执行数组中的函数依赖。
class DependenceManager {
Dep = null;
_store = {};
beginCollect(handler) {
DependenceManager.Dep = handler
}
collect(id) {
if (DependenceManager.Dep) {
this._store[id] = this._store[id] || {}
this._store[id].watchers = this._store[id].watchers || []
this._store[id].watchers.push(DependenceManager.Dep);
}
}
endCollect() {
DependenceManager.Dep = null
}
trigger(id) {
const store = this._store[id];
if(store && store.watchers) {
store.watchers.forEach(s => {
s.call(this);
});
}
}
}
export default new DependenceManager()
至此,我们已经完成了一个完整的 Mobx 骨架,可以做一些有意思的事情了。写个简单的例子来试试:
class Counter {
@observable count = 0;
increment() {
this.count++;
}
}
const counter = new Counter()
autorun(() => {
console.log(`count=${counter.count}`)
})
counter.increment()
可以看到 autorun
里面的函数被执行了两次,说明我们已经实现了一个简单的 Mobx。但这个 Mobx 还有很多问题,比如不支持更深层的对象,也监听不到数组等等。
4.3 对深层对象和数组的处理
在前面我们讲解 Proxy
和 Object.defineProperty
的时候就已经说过,由于 Object.defineProperty
的问题,无法监听到新增加的项,因此对于动态添加的属性或者下标就无法进行监听。
据尤雨溪所讲,在 Vue 里面由于考虑性能就放弃了监听数组的下标变化。而在 Mobx4 中使用了比较极端的方式,那就是不管数组中有多少项,都是用一个长度 1000 的数组来存放,去监听这 1000 个下标变化,可以满足大多数场景。
但是在未来的 Vue3 和已面世的 Mobx5 中,都已经使用 Proxy 来实现对数组的拦截了。这里我们使用 Proxy 来增加对数组的监听。
class Observable {
set(v) {
// 如果是数组,那么就对数组做特殊的处理
if(Array.isArray(v)) {
this._wrapArray(v);
} else {
this.value = v;
}
dependenceManager.trigger(this.id);
}
_wrapArray(arr) {
this.value = new Proxy(arr, {
set(obj, key, value) {
obj[key] = value;
// 如果是修改数组项的值,那么就触发通知
if(key !== 'length') {
dependenceManager.trigger(this.id);
}
return true;
}
});
}
现在已经可以监听到数组的变化了,那么用一个例子试试吧。
class Store {
@observable list = [1, 2, 3];
increment() {
this.list[0]++
}
}
const store = new Store()
autorun(() => {
console.log(store.list[0])
})
store.list[0]++;
原本预想中会打印两次,结果却只打印了一个1,这是为什么?来翻查一下前面 Observable.set
的代码会发现,我们只拦截了对象最外层属性的变化,如果有更深层的就拦截不到了。如果对 list
重新赋值就会生效,但修改 list
中第一项就不会生效。
所以我们还需要对对象深层属性做个递归包装,这也是 Mobx 中 observable
的功能之一。
- 首先,我们来判断包裹的属性是否为对象。
- 如果是个对象,那么就遍历其属性,对属性值创建新的
Observable
实例。 - 如果属性也是个对象,那么就进行递归,重复步骤1、2。
function createObservable(target) {
if (typeof target === "object") {
for(let property in target) {
if(target.hasOwnProperty(property)) {
const observable = new Observable(target[property]);
Object.defineProperty(target, property, {
get() {
return observable.get();
},
set(value) {
return observable.set(value);
}
});
createObservable(target[property])
}
}
}
}
这个 createObservable
方法,我们只需要在 observable
装饰器里面执行就行了。
function observable(target, name, descriptor) {
const v = descriptor.initializer.call(this);
createObservable(v)
const o = new Observable(v);
return {
enumerable: true,
configurable: true,
get: function() {
return o.get();
},
set: function(v) {
createObservable(v)
return o.set(v);
}
};
};
继续使用上面那个例子,会发现成功打印出来了1、2,说明我们实现的这个简单的 Mobx 即支持拦截数组,又支持拦截深层属性。
4.4 computed
我们都知道 computed
有三个特点,分别是:
computed
是个 get 方法,会缓存上一次的值computed
会根据依赖的可观察属性重新计算- 依赖了
computed
的函数也会被重新执行
其实 computed
和 observable
的实现思路类似,区别在于 computed
需要收集两次依赖,一次是 computed
依赖的可观察属性,一次是依赖了 computed
的方法。
首先,我们来定义一个 computed
的,这个方法依然是个装饰器。
function computed(target, name, descriptor) {
const getter = descriptor.get; // get 函数
const computed = new Computed(target, getter);
return {
enumerable: true,
configurable: true,
get: function() {
return computed.get();
}
};
}
接下来实现这个 Computed
类,这个类的实现方式和 Observable
差不多。
let id = 0
class Computed {
constructor(target, getter) {
this.id = id++
this.target = target
this.getter = getter
}
get() {
dependenceManager.collect(this.id);
}
}
在执行 get 方法的时候,就会去收集依赖了当前 computed
的方法。我们还需要去收集当前 computed
依赖的属性。
这里是不是可以利用前面的 dependenceManager.beginCollect
呢?没错,我们可以用 dependenceManager.beginCollect
来收集到重新计算的 get
函数,并且通知依赖了 computed
的方法。
registerReComputed() {
if(!this.hasBindAutoReCompute) {
this.hasBindAutoReCompute = true;
dependenceManager.beginCollect(this._reCompute, this);
this._reCompute();
dependenceManager.endCollect();
}
}
reComputed() {
this.value = this.getter.call(this.target);
dependenceManager.trigger(this.id);
}
最终,我们的实现是这样的:
let id = 0
class Computed {
constructor(target, getter) {
this.id = id++
this.target = target
this.getter = getter
}
registerReComputed() {
if(!this.hasBindAutoReCompute) {
this.hasBindAutoReCompute = true;
dependenceManager.beginCollect(this._reCompute, this);
this._reCompute();
dependenceManager.endCollect();
}
}
reComputed() {
this.value = this.getter.call(this.target);
dependenceManager.trigger(this.id);
}
get() {
this.registerReComputed();
dependenceManager.collect(this.id);
return this.value;
}
}
4.5 observer
而 observer 的实现比较简单,就是利用了 React 的 render
方法执行进行依赖收集,我们可以在 componentWillMount
里面注册 autorun
。
function observer(target) {
const componentWillMount = target.prototype.componentWillMount;
target.prototype.componentWillMount = function() {
componentWillMount && componentWillMount.call(this);
autorun(() => {
this.render();
this.forceUpdate();
});
};
}
至此,我们已经实现了一个完整的 Mobx
库,可以看到 Mobx
的实现原理比较简单,使用起来也没有 Redux 那么繁琐。