1
本次分享主题为 "mobx autorun" 原理解析,主要分为以下几个部分:
- 分析 "autorun" 的使用方式;
- 对比 "autorun" 与“发布订阅模式”的异同;
- 实现 "autorun" 函数;

After implementing the autorun function from 0 to 1, you can understand the following knowledge:

  • The collaboration process between autorun and observable objects;
  • Why is the provided function executed once when autorun is used?
  • Why can't autorun track the values of observable objects in asynchronous logic?

How to use 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'

The use process of autorun is as follows:

  1. Declare observable objects: autorun will only collect observable objects as dependencies;
  2. Execute autorun:

    • Pass in the monitor function and execute autorun;
    • autorun will automatically collect the observable objects used in the function as a dependency;
    • autorun returns a logout function, the monitoring function can be ended by calling the logout function;
  3. Modify the observable object: depending on the change, autorun automatically executes the monitoring function;
  4. Log off autorun: After logging off, changing the observable object will no longer execute the monitoring function;

Autorun VS publish and subscribe mode

By observing how autorun is used, it can be seen that autorun is very similar to the traditional "publish and subscribe model". Next, let's compare the similarities and differences between autorun and the "publish and subscribe model".

Timing diagram

The "publish and subscribe model" involves the following three activities:

  • Register: subscribe;
  • Trigger: release;
  • Cancellation: that is, cancel the subscription;

Use the publish subscriber model to implement a "registration-trigger-deregistration" process as follows:

The process of implementing a "registration-trigger-logout" with autorun is as follows:

Comparing the above two timing diagrams, we can draw the following conclusions:

  1. From developer perspective see:

    • In the "publish and subscribe model", developers need to participate in registration, triggering and cancellation;
    • In the autorun mode, developers only need to participate in registration and deregistration, and the trigger is automatically implemented by autorun;
  2. From the object perspective :

    • In the "publish and subscribe mode", the object does not participate in the whole process, the object is passive ;
    • In the autorun mode, the observable object will participate in the binding and unbinding of the event, and the object is actively ;
  3. From event perspective model see:

    • In the "publish and subscribe model", the event model acts as a controller to schedule the entire process;
    • In autorun mode, autorun and observable objects coordinate to schedule the entire process;
  4. From global perspective see:

    • The internal process of "publishing and subscribing mode" is simple, but it is complicated for developers to use;
    • The internal process of autorun mode is complicated, but it is simple for developers to use;

The autorun mode has made an improvement to the "publish and subscribe mode": automatic event triggering, thereby reducing development costs.

Pros

Compared with the "publish and subscribe mode", the autorun mode has the following advantages:

  • Autorun automatically triggers events, reduces development costs, and improves development efficiency;

Cons

Compared with the "publish and subscribe mode", the autorun mode has the following disadvantages:

  • Autorun automates the triggering of events, which increases the cost of learning and understanding;

How to realize auotorun?

Based on the above analysis, we know that autorun is an improved version of the "publish and subscribe model": it automates event triggering. This kind of automation is from the developer's perspective, that is, the developer does not need to manually trigger the event model after each update of the object value; from the object's perspective, the object will execute a listener function every time it is assigned:

We can get the following information of "auto trigger":

  • Trigger subject: the observable object, the event trigger is initiated by the observable object;
  • Trigger timing: attribute assignment, an event is triggered when the attribute of the observable object is assigned;

We need to solve the following problems:

  • Encapsulate observable object : Let the properties of ordinary objects have the ability to bind and unbind monitoring functions;
  • agent object attributes value method, each time the listener attribute assignment function bound to the object properties;
  • assignment object attribute of the agent of 161c00de7b7241 executes a monitoring function every time the attribute value is taken;
  • unbind monitor function on the properties of the observable object;

Encapsulate observable objects

【Statement of needs】
In order to make the properties of the object have the ability to bind and unbind the monitoring function, we need to encapsulate the ordinary object into an observable object:

  1. Observable object properties support binding monitoring functions;
  2. The properties of observable objects support unbinding monitoring functions;

[Code example]
By calling the observable method, all properties of the object have the ability to bind and unbind events:

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

【Design】

  1. Define an ObservableValue object to encapsulate the properties of the object into observable properties:
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. In order to reduce the intrusion of the original object, the function of observable extension is limited to a non-enumerable symbol attribute of the object:
const $mobx = Symbol("mobx administration")
function observable(instance) {
    const mobxAdmin = {}
    Object.defineProperty(instance, $mobx, {
        enumerable: false,
        writable: true,
        configurable: true,
        value: mobxAdmin,
    });
    ...
}
  1. Encapsulate all attributes of the original object into ObservableValue and assign them to mobxAdmin;
...
function observable(instance) {
    const mobxAdmin = {}
    ...
    for(const key in instance) {
        const value = instance[key]
        mobxAdmin[key] = new ObservableValue(value)
    }
}
  1. The value and assignment of all attributes of the original object are delegated to $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;
            },
        })
    }
    ...
}

Binding monitoring functions and objects

【Statement of needs】
Now we have the ability to encapsulate ordinary objects into observable objects. Next we implement how to bind the listener function to the observable object.
[Code example]

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

【Design】
Through the usage example of autorun, we can get the following information:

  1. The monitor function is passed to the autorun function as a parameter;
  2. The value operation of the object occurs in the monitoring function;

What we need to do is to bind the currently executing monitoring function to the object's property when the object is getting its value:

...
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;
            },
            ...
        })
    }
    ...
}

How to get the currently executing monitoring function?
The value proxy of the object is defined in the observable, but the execution of the monitoring function is in the autorun. How do you get the runtime information of the autorun in the observable?

The answer is: shared variable
Both observable and autorun run in mobx. You can define a shared variable in mobx to manage the global state:

shared variable
Let us declare a shared variable that can manage the "monitoring function currently being executed":

const globalState = {
  trackingObserver: undefined,
};

Let us use shared variables to bind the listener function to the observable object:
sets the "monitoring function currently being executed"

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

Analyzing the above code, we can know:

  1. After calling autorun, you need to execute a monitoring function immediately to bind the monitoring function and the object;
  2. The trackingObserver will be cleared immediately after the execution of the monitoring function ends;

These two points can respectively explain the following instructions in the mobx document:

  1. When using autorun, the provided function is always triggered once immediately;
  2. "During" means to track only those observables that are read when the function is executed.

gets and binds the "monitoring function currently being executed"

...
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;
            },
            ...
        })
    }
    ...
}

triggers the "monitoring function"

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

[Use case test]

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

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

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

Unbind monitoring functions and objects

【Statement of needs】
Unbind the monitoring function from the observable object. After unbinding, the object assignment operation will no longer execute the monitoring function.
[Code example]

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

dispose()

【Design】
The unbinding function removes the listening function from the listening list of all observable objects:

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

How to get "all objects bound to monitor function" in autorun?
The operation of binding the monitoring function is in the observable, but the operation of unbinding the monitoring function is in the autorun. How to get the relevant information of the observable in autorun 🤔?
Yes, the answer is still: shared variable
The globalState.trackingObserver we used before is bound to the monitoring function itself. We can encapsulate it so that it can collect "all objects bound to the monitoring function". In order to show that it is no longer just a monitoring function, we renamed it to trackingDerivation.
shared variable

const globalState = {
    trackingDerivation: undefined
}

package trackingDerivation

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

Here we declare a derivation object, which has the following properties:

  1. observing: on behalf of all observable objects bound to the monitoring function;
  2. observer: monitor function;

set "object bound to listener function"

...
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;
            },
            ...
        })
    }
    ...
}

obtains and unbinds "all objects bound to listener functions"

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

[Use case test]

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

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

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

Reference


xh4722
240 声望11 粉丝