background

We all know the difference between watch and computed in use. So let’s dig a little deeper and see how they differ in implementation principles.

Implementation principle of Watch

  • Type: {[key: string] : string | Function | Object | Array}

  • Detail: an object whose key is the expression to observe and whose value is the corresponding callback function. The value can also be a method name, or an object that contains options. The Vue instance will call $watch() at instantiation time, iterating through each property of the Watch object.

1. Initialize the Watch

export function initState(vm) {
  // Initialize watch on vm
  if (opts.watch) {
    initWatch(vm, opts.watch);
}

function initWatch(vm, watch) {
// Ignore the case where the value is the method name for now
  for (key in watch) {
    let handler = watch[key];
    if (Array.isArray(handler)) {
      for (let i = 0; i < handler.length; i++) { createWatcher(vm, key, handler[i]); }}else{ createWatcher(vm, key, handler); }}}function createWatcher(vm, key, handler) {
  return vm.$watch(key, handler);
}
// Extension methods on the Vue prototype are added in the form of mixins.
export function stateMixin(Vue) {
    Vue.prototype.$watch = function(key, handler, options = {}) {
        options.user = true; 
        // tag this.user to distinguish between rendering watcher and user watcher
        new Watcher(this, key, handler, options); }}Copy the code

2. The watcher

  1. The expressionkey/exprOrFnTo a function for later callsthis.getter
  2. this.getterValue of callObject.definePropertythegetIn the methoddep.dependCollect currentwatcher
  3. For the first time,new WatcherWhen calledgetMethod to save the initialthis.value=this.get(). Second user updaterun()Call againgetMethod to save the new valuenewValue=this.get()And executes the callback function.
class Watcher {
    constructor(vm, exprOrFn, callback, options) {
        // ...
        this.user = !! options.user
        if(typeof exprOrFn === 'function') {this.getter = exprOrFn; 
        }else{
            this.getter = function (){ // Convert an expression to a function
                let path = exprOrFn.split('. ');
                let obj = vm;
                for(let i = 0; i < path.length; i++){ obj = obj[path[i]]; }returnobj; }}this.value = this.get(); // Log the initial value to the value property
    }
    get() {
        pushTarget(this); // Save the user-defined watcher
        const value = this.getter.call(this.vm); // Execute function (dependent collection)
        popTarget(); / / remove the watcher
        return value;
    }
  
    run(){
        let value = this.get();    // Get a new value
        let oldValue = this.value; // Get the old value
        this.value = value;
        if(this.user){ // If the user watcher calls the callback passed in by the user
            this.callback.call(this.vm,value,oldValue)
        }
    }
}
Copy the code

Implementation principle of computed Data

  • type:{ [key: string]: Function | { get: Function, set: Function } }
  • detailed: Calculation properties are not executed by default. The result of the evaluated property is cached unless dependent on the responsepropertyChange is what recalculates. Note that if a dependency (such as a non-reactive property) is outside the instance scope, the calculated property is not updated.

1. Initialize computed

Create a Watcher for each attribute key for computed

export function initState(vm) {
  // Initialize computed on the VM
  if(opts.computed) { initComputed(vm, opts.computed); }}function initComputed(vm, computed) {

    const watchers = vm._computedWatchers = {}
    for (let key in computed) {
        / / check
        const userDef = computed[key];
        // Revalue get when the dependent property changes
        let getter = typeof userDef == 'function' ? userDef : userDef.get;
        // Each attribute is essentially a watcher
        // Map the watcher to the property
        watchers[key] = new Watcher(vm, getter, () = > {}, { lazy: true }); // Not executed by default
        // Define the key on the VMdefineComputed(vm, key, userDef); }}Copy the code

Define the key on the VM so that computed values can be fetched directly on the page

  • this._computedWatchersContains all computed properties
  • throughkeyYou can get the correspondingwatcherthewatcherIncluded in thegetterIf thedirty δΈΊ tureThe callevaluate
function defineComputed(vm, key, userDef) {
    let sharedProperty = {};
    if (typeof userDef == 'function') {
       sharedProperty.get = createComputedGetter(key)
    } else {
        sharedProperty.get = createComputedGetter(key);
        sharedProperty.set = userDef.set ;
    }
    Object.defineProperty(vm, key, sharedProperty); // Computed is a defineProperty
}
Copy the code

Create a cache getter, get the value of the computed property, and go to this function.

function createComputedGetter(key) {
    
    return function computedGetter() { 
        // This._computedWatchers contains all computed attributes
        // The watcher contains the getter
        let watcher = this._computedWatchers[key]
        // Dirty means to call the user's getter

        if(watcher.dirty){ // Determine if you need to reapply for a job based on the dirty attribute
            watcher.evaluate();// this.get()
        }

        // If dep. target has a value that needs to be collected up after the current value is set
        if(Dep.target){
            watcher.depend(); // There are multiple dePs in watcher
        }
        return watcher.value
    }
}
Copy the code

2. The watcher

class Watcher {
    constructor(vm, exprOrFn, cb, options) {
       / /...
        this.lazy = !! options.lazy;this.dirty = options.lazy; // If the attribute is calculated, the default is lazy:true,
        this.getter = exprOrFn; // computed[key]/computed[key].get
        this.value = this.lazy ? undefined : this.get(); 
        
    }
    get() { 
        pushTarget(this);
        const value = this.getter.call(this.vm);
        popTarget();
        return value
    }
    update() { 
    if(this.lazy){
            this.dirty = true;
        }else{
            queueWatcher(this); }}evaluate(){
        this.dirty = false; // False indicates that the value is overevaluated
        this.value = this.get(); // User getter execution
    }
    depend(){
        let i = this.deps.length;
        while(i--){
            this.deps[i].depend(); //lastName,firstName collect render watcher}}}Copy the code

The implementation of computed tomography is rather convoluted 😭😭😭😭😭 Let’s clear the air with a few questions QS1: How to cache computed attributes? By defining a dirty attribute on Watcher. When dirty is true, evaluate is called.

QS2: How do I reevaluate the calculated attribute? Change dependency values –> trigger set –> trigger dep.notify –> watcher. Update –> calculate watcher –> this. Call evaluate to reevaluate.

QS3: How do I update a view to calculate the value change of a property dependency? Currently, there is only one calculated property wacher on the deP of the dependent value (if not used in the page). To update the view, put the render Wacher into the DEP of the dependent data, so that the view can update if the dependent property changes.

conclusion

The difference between watch and computed is that the watch implementation assigns a Watcher to each key of the watch object, and the value this.get() is used to collect the current user watcher and save the initial value. When the key changes, watcher.run() is triggered, the new value is saved, and the cb callback is executed. Implement computed by assigning a lazy Watcher to each key of a computed object, which is not executed by default and is executed only when the value is specified. Object.defineproperty defines each key for computed on the VM. The current render Watcher is collected by key dependency values to implement dependency changes and view updates. The cache effect is implemented with the dirty attribute.