Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

从零实现 Mobx:深入理解 Mobx 原理 #54

Open
yinguangyao opened this issue Jun 27, 2021 · 3 comments
Open

从零实现 Mobx:深入理解 Mobx 原理 #54

yinguangyao opened this issue Jun 27, 2021 · 3 comments

Comments

@yinguangyao
Copy link
Owner

yinguangyao commented Jun 27, 2021

1. 前言

Mobx 是 React 的另一种经过战火洗礼的状态管理方案,和 Redux 不同的地方是 Mobx 是一个响应式编程(Reactive Programming)库,在一定程度上可以看做没有模板的 Vue,基本原理和 Vue 一致。

Mobx 借助于装饰器的实现,使得代码更加简洁易懂。由于使用了可观察对象,所以 Mobx 可以做到直接修改状态,而不必像 Redux 一样编写繁琐的 actions 和 reducers。

code.png-50.7kB

Mobx 的执行流程和 Redux 有一些相似。这里借用 Mobx 官网的一张图:

image.png-238kB

简单的概括一下,一共有这么几个步骤:

  1. 页面事件(生命周期、点击事件等等)触发 action 的执行。
  2. 通过 action 来修改状态。
  3. 状态更新后,computed 计算属性也会根据依赖的状态重新计算属性值。
  4. 状态更新后会触发 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,导致属性修改了也没有触发重新执行。
常见的几种错误用法如下:

  1. 错误的修改了可观察对象的引用
let person = observable({
    age: 20
})
// 不会起作用
autorun(() => {
    console.log("age", person.age);
})
person = observable({
    age: 21
})
  1. 在追踪函数外进行间接引用
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())

image.png-101.5kB

Mobx 的实现原理很简单,整体上和 Vue 比较像,简单来说就是这么几步:

  1. Object.defineProperty 或者 Proxy 来拦截 observable 包装的对象属性的 get/set
  2. autorun 或者 reaction 执行的时候,会触发依赖状态的 get,此时将 autorun 里面的函数和依赖的状态关联起来。也就是我们常说的依赖收集。
  3. 当修改状态的时候会触发 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_ 里面,传入了对象的 keydescriptors[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
    )
}

这里就是根据 keyvalue 来定义了 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,将其设置为 definePropertydescriptor,这里的 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 方法,也就回到了刚刚分析 observablereportObserved 里面。它会将 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 则有 func1func2 两个方法。

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/setautorun 也是密切相关的。
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 中的方法,如下:

  1. beginCollect: 用一个全局变量保存着函数依赖。
  2. collect: 当执行 get 的时候,根据传入的 id,将函数依赖放入数组中。
  3. endCollect: 清除刚开始的函数依赖,以便于下一次收集。
  4. 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 对深层对象和数组的处理

在前面我们讲解 ProxyObject.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 的功能之一。

  1. 首先,我们来判断包裹的属性是否为对象。
  2. 如果是个对象,那么就遍历其属性,对属性值创建新的 Observable 实例。
  3. 如果属性也是个对象,那么就进行递归,重复步骤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 有三个特点,分别是:

  1. computed 是个 get 方法,会缓存上一次的值
  2. computed 会根据依赖的可观察属性重新计算
  3. 依赖了 computed 的函数也会被重新执行

其实 computedobservable 的实现思路类似,区别在于 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 那么繁琐。

@switers-wang
Copy link

autorun收集依赖好理解。因为它肯定会自动执行一次。所以在第一次执行的时候 通过get 收集依赖就好了。
reaction不会执行。怎么第一次收集依赖的呀

@yinguangyao
Copy link
Owner Author

autorun收集依赖好理解。因为它肯定会自动执行一次。所以在第一次执行的时候 通过get 收集依赖就好了。 reaction不会执行。怎么第一次收集依赖的呀

@switers-wang reaction 有两个传参,第一个传参的回调会立即执行一次,所以还是收集到了依赖

@loading-99
Copy link

loading-99 commented Jul 28, 2024

学习过程中遇见了三个问题:
1.目前NodeJS(版本:16.20.0)对装饰器还是不支持的,运行代码可以使用babel进行打包,插件@babel/plugin-proposal-decorators
2.代码中,Computed 类和Observable 类的id无法区分,无法实现Computed类的回调注册。
3.computed 的实现存在问题, registerReComputed中不能直接调用reComputed,会导致死循环, 直接this.value = this.getter.call(this.target)即可,死循环原因在于dependenceManager.trigger(this.id)执行时会递归调用compted实例的get方法。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants