乐趣区

关于前端:Mobx-autorun-原理解析

本次分享主题为 "mobx autorun" 原理解析,次要分为以下几个局部:- 剖析 "autorun" 的应用形式;- 比照 "autorun" 与“公布订阅模式”的异同;- 实现 "autorun" 函数;

通过从 0 到 1 实现 autorun 函数当前,你能够理解以下常识:

  • autorun 与可察看对象的合作过程;
  • 为什么应用 autorun 的时候,所提供的函数会立刻执行一次?
  • 为什么 autorun 不能跟踪到异步逻辑中的可察看对象取值?

autorun 应用形式

// 申明可察看对象
const message = observable({title: 'title-01'})

/* 执行 autorun,传入监听函数 */
const dispose = autorun(() => {
    // 主动收集依赖,在依赖变更时执行注册函数
    console.log(message.title)
})

// title-01
message.title = 'title-02'
// title-02

/* 登记 autorun */
dispose()
/* 登记当前,autorun 不再监听依赖变更 */
message.title = 'title-03'

autorun 的应用流程如下:

  1. 申明可察看对象:autorun 仅会收集可察看对象作为依赖;
  2. 执行 autorun:

    • 传入监听函数并执行 autorun;
    • autorun 会主动收集函数中用到的可察看对象作为依赖;
    • autorun 返回一个登记函数,通过调用登记函数能够完结监听函数;
  3. 批改可察看对象:依赖变更,autorun 主动执行监听函数;
  4. 登记 autorun:登记之后再变更可察看对象将不再执行监听函数;

autorun VS 公布订阅模式

    通过观察 autorun 的应用形式能够看进去,autorun 与传统的“公布订阅模式”很像。接下来咱们比照下 autorun 与“公布订阅模式”的异同。

时序图

“公布订阅模式”波及如下三种流动:

  • 注册:即订阅;
  • 触发:即公布;
  • 登记:即勾销订阅;

用公布订阅者模式实现一次“注册 - 触发 - 登记”过程如下:

用 autorun 实现一次“注册 - 触发 - 登记”过程如下:

比照上述两张时序图,咱们能够得出如下论断:

  1. 开发者视角 看:

    • 在“公布订阅模式”中,开发者须要参加注册、触发和登记;
    • 在 autorun 模式中,开发者只须要参加注册和登记,触发由 autorun 主动实现;
  2. 对象视角 看:

    • 在“公布订阅模式”中,对象不参加整个过程,对象是 被动 的;
    • 在 autorun 模式中,可察看对象会参加事件的绑定和解绑,对象是 被动 的;
  3. 事件模型视角 看:

    • 在“公布订阅模式”中,事件模型作为控制器调度整个过程;
    • 在 autorun 模式中,autorun 和可察看对象协同调度整个过程;
  4. 全局视角 看:

    • “公布订阅模式”外部流程简略,但开发者应用简单;
    • autorun 模式外部流程简单,但开发者应用简略;

autorun 模式对“公布订阅模式”做了一次改良:将事件触发自动化,从而缩小开发成本。

Pros

autorun 模式相比于“公布订阅模式”有以下益处:

  • autorun 将事件触发自动化,缩小开发成本,进步开发效率;

Cons

autorun 模式相比于“公布订阅模式”有以下害处:

  • autorun 将事件触发自动化,减少了学习老本和了解老本;

如何实现 auotorun?

    依据下面的剖析咱们晓得 autorun 是“公布订阅模式”的改进版:将事件触发自动化。这种自动化是从开发者的视角看的,即开发者在每次更新对象值之后无需再手动触发一次事件模型;从对象视角看就是每次被赋值之后对象都会执行一次监听函数:

咱们能够失去“主动触发”的以下信息:

  • 触发主体:可察看对象,事件触发由可察看对象发动;
  • 触发机会:属性赋值,在可察看对象的属性被赋值时触发事件;

咱们须要解决如下问题:

  • 封装 可察看对象:让一般对象的属性具备绑定和解绑监听函数的能力;
  • 代理 对象属性的 取值 办法,在每次属性赋值时将监听函数绑定到对象属性上;
  • 代理 对象属性的 赋值 办法,在每次属性取值时执行一次监听函数;
  • 解绑 监听函数:须要提供一套机制解绑可察看对象属性上的监听函数;

封装可察看对象

【需要阐明】
    为了让对象的属性具备绑定和解绑监听函数的能力,咱们须要将一般对象封装成可察看对象:

  1. 可察看对象属性反对绑定监听函数;
  2. 可察看对象属性反对解绑监听函数;

【代码示例】
    通过调用 observable 办法能够使对象的所有属性都具备绑定和解绑事件的能力:

const message = observable({title: 'title-01'})

【方案设计】

  1. 定义一个 ObservableValue 对象,用于将对象的属性封装成可察看属性:
class ObservableValue {observers = []
    value = undefined
    constructor(value) {this.value = value}
    addObserver(observer) {this.observers.push(observer)
    }
    removeObserver(observer) {const index = this.observers.findIndex(o => o === observer)
        this.observers.splice(index, 1)
    }
    trigger() {this.observers.forEach(observer => observer())
    }
}
  1. 为了缩小对原始对象的侵入性,将 observable 扩大的性能限度在对象的一个不可枚举的 symbol 属性中:
const $mobx = Symbol("mobx administration")
function observable(instance) {const mobxAdmin = {}
    Object.defineProperty(instance, $mobx, {
        enumerable: false,
        writable: true,
        configurable: true,
        value: mobxAdmin,
    });
    ...
}
  1. 将原始对象的所有属性封装成 ObservableValue 并赋值到 mobxAdmin 中;
...
function observable(instance) {const mobxAdmin = {}
    ...
    for(const key in instance) {const value = instance[key]
        mobxAdmin[key] = new ObservableValue(value)
    }
}
  1. 将原始对象所有属性的取值和赋值都代理到 $mobx 中:
...
function observable(instance) {
    ...
    for(const key in instance) {
        Object.defineProperty(instance, key, {
            configurable: true,
            enumerable: true,
            get() {return instance[$mobx][key].value;
            },
            set(value) {instance[$mobx][key].value = value;
            },
        })
    }
    ...
}

绑定监听函数与对象

【需要阐明】
    当初咱们曾经有能力将一般对象上封装成可察看对象了。接下来咱们实现如何将监听函数绑定到可察看对象上。
【代码示例】

autorun(() => {console.log(message.title)
})

【方案设计】
    通过 autorun 的应用示例,咱们能够失去如下信息:

  1. 监听函数作为参数传递给 autorun 函数;
  2. 对象的取值操作产生在监听函数内;

咱们须要做的是在对象取值的时候将以后正在执行的监听函数绑定到对象的属性上:

...
function observable(instance) {
    ...
    for(const key in instance) {
        Object.defineProperty(instance, key, {
            ...
            get() {const observableValue = instance[$mobx][key]
                // 失去以后正在执行的监听函数
                const observer = getCurrentObserver()
                if(observer) {observableValue.addObserver(observer)
                }
                return observableValue.value;
            },
            ...
        })
    }
    ...
}

如何失去以后正在执行的监听函数?
    对象的取值代理定义在 observable 中,然而监听函数的执行却是在 autorun 中,那要如何在 observable 中拿到 autorun 的运行时信息呢🤔?

答案就是:共享变量
observable 和 autorun 都运行在 mobx 中,能够在 mobx 中定义一个共享变量治理全局状态:

共享变量
让咱们申明一个能够治理“以后正在执行的监听函数”的共享变量:

const globalState = {trackingObserver: undefined,};

让咱们应用共享变量实现监听函数与可察看对象的绑定:
设置“以后正在执行的监听函数”

function autorun(observer) {
   globalState.trackingObserver = observer
   observer()
   globalState.trackingObserver = undefined
}

剖析上述代码咱们能够晓得:

  1. 调用 autorun 当前须要立刻执行一次监听函数,用于绑定监听函数和对象;
  2. 在监听函数执行完结后会立刻革除 trackingObserver;

这两点能够别离解释 mobx 文档中的以下阐明:

  1. 当应用 autorun 时,所提供的函数总是立刻被触发一次;
  2. “过程(during)”意味着只追踪那些在函数执行时被读取的 observable。

失去并绑定“以后正在执行的监听函数”

...
function observable(instance) {
    ...
    for(const key in instance) {
        Object.defineProperty(instance, key, {
            ...
            get() {const observableValue = instance[$mobx][key]
                const observer = globalState.trackingObserver
                if(observer) {observableValue.addObserver(observer)
                }
                return observableValue.value;
            },
            ...
        })
    }
    ...
}

触发“监听函数”

...
function observable(instance) {
    ...
    for(const key in instance) {
        Object.defineProperty(instance, key, {
            ...
            set(value) {instance[$mobx][key].value = value;
                instance[$mobx][key].trigger()},
        })
    }
    ...
}

【用例测试】

const message = observable({title: "title-01",});

autorun(() => {console.log(message.title);
});

message.title = "title-02";
message.title = "title-03";

解绑监听函数与对象

【需要阐明】
    将监听函数从可察看对象上解绑,解绑当前对象赋值操作将不再执行监听函数。
【代码示例】

const dispose = autorun(() => {console.log(message.title)
})

dispose()

【方案设计】
    解绑函数从所有可察看对象的监听列表中移除监听函数:

function autorun(observer) {
    ...
    function dispose() {
        // 失去所有可察看对象
        const observableValues = getObservableValues();
        (observableValues || []).forEach(item => {item.removeObserver(observer)
        }
    }
    
    return dispose
}

如何在 autorun 中获取“所有绑定了监听函数的对象”?
    绑定监听函数的操作在 observable 中,然而解绑监听函数的操作却是在 autorun 中,那要如何在 autorun 中拿到 observable 的相干信息呢🤔?
    没错,答案还是:共享变量
    咱们之前应用的 globalState.trackingObserver 绑定的是监听函数自身,咱们能够对它进行一些封装,让它能够收集“所有绑定了监听函数的对象”。为了阐明它不再是仅仅代表监听函数,咱们将它重命名为 trackingDerivation。
共享变量

const globalState = {trackingDerivation: undefined}

封装 trackingDerivation

function autorun(observer) {
    const derivation = {observing: [],
        observer
    }
    globalState.trackingDerivation = observer
    observer()
    globalState.trackingDerivation = undefined
}

在这里咱们申明了一个 derivation 对象,它有以下属性:

  1. observing:代表所有绑定了监听函数的可察看对象;
  2. observer:监听函数;

设置“绑定了监听函数的对象”

...
function observable(instance) {
    ...
    for(const key in instance) {
        Object.defineProperty(instance, key, {
            ...
            get() {const observableValue = instance[$mobx][key]
                const derivation = globalState.trackingDerivation
                if(derivation) {observableValue.addObserver(derivation.observer)
                    derivation.observing.push(observableValue)
                }
                return observableValue.value;
            },
            ...
        })
    }
    ...
}

获取并解绑“所有绑定了监听函数的对象”

function autorun(observer) {
    const derivation = {observing: [],
        observer
    }
    ...
    function dispose() {
        const observableValues = derivation.observing;
        (observableValues || []).forEach(item => {item.removeObserver(observer)
        })
        derivation.observing = []}
    
    return dispose
}

【用例测试】

const message = observable({title: "title-01",});

const dispose = autorun(() => {console.log(message.title);
});

message.title = "title-02";
dispose()
message.title = "title-03";

参考资料

  • 残缺示例代码
退出移动版