Vue源码学习之Computed与Watcher原理

前言

 computed与watch是我们在vue中常用的操作,computed是一个惰性求值观察者,具有缓存性,只有当依赖发生变化,第一次访问computed属性,才会计算新的值。而watch则是当数据发生变化便会调用回调函数。我们虽然知道vue是这么操作的,但是原理我们又知多少呢?接下来我们来分析一下两者的原理。

Computed

 我们知道new Vue()的时候会调用_init方法,此时会调用initState方法,源码如下:

	    function initState(vm) {
    
    
        vm._watchers = [];
        var opts = vm.$options;
        if (opts.props) {
    
    
            initProps(vm, opts.props);
        }
        if (opts.methods) {
    
    
            initMethods(vm, opts.methods);
        }
        if (opts.data) {
    
    
            initData(vm);
        } else {
    
    
            observe(vm._data = {
    
    }, true /* asRootData */ );
        }
        // 初始化computed
        if (opts.computed) {
    
    
            initComputed(vm, opts.computed);
        }
        // 初始化watch
        if (opts.watch && opts.watch !== nativeWatch) {
    
    
            initWatch(vm, opts.watch);
        }
    }

 在initState方法中调用了initComputed,初始化了计算属性,那么我们看一下initComputed方法:

   		// 声明一个watchers且同时将_computedWatchers挂载到vm实例上
        var watchers = vm._computedWatchers = Object.create(null);
        // 在SSR模式下computed属性只能触发getter方法
        var isSSR = isServerRendering();

        // 遍历传入的computed方法
        for (var key in computed) {
    
    
            // 取出computed对象中key值的每个方法并赋值给userDef
            var userDef = computed[key];
            var getter = typeof userDef === 'function' ? userDef : userDef.get;
            if (getter == null) {
    
    
                warn(
                    ("Getter is missing for computed property \"" + key + "\"."),
                    vm
                );
            }   
            
            // 如果不是SSR服务端渲染,则创建一个watcher实例
            if (!isSSR) {
    
    
                // create internal watcher for the computed property.
                watchers[key] = new Watcher(
                    vm,
                    getter || noop,
                    noop,
                    // {lazy: true}
                    computedWatcherOptions
                );
            }

            // component-defined computed properties are already defined on the
            // component prototype. We only need to define computed properties defined
            // at instantiation here.
            // 如果computed所对应的key值在vue实例中没有重复,则执行defineComputed方法
            if (!(key in vm)) {
    
    
                defineComputed(vm, key, userDef);
            } else {
    
    
                if (key in vm.$data) {
    
    
                    warn(("The computed property \"" + key + "\" is already defined in data."), vm);
                } else if (vm.$options.props && key in vm.$options.props) {
    
    
                    warn(("The computed property \"" + key + "\" is already defined as a prop."), vm);
                } else if (vm.$options.methods && key in vm.$options.methods) {
    
    
                    warn(("The computed property \"" + key + "\" is already defined as a method."), vm);
                }
            }
        }

 在initComputed遍历computed,通过userDef去获取当前computed,然后不断的向vm._computedWatchers中添加{lazy: true}配置的watcher,添加的配置lazy:true代表是computed,接着判断如果在vue实例上没有这个对应的key,我们就执行defineComputed,接下来我们看一下defineComputed源码部分:

    function defineComputed(
        target,
        key,
        userDef
    ) {
    
    
        var shouldCache = !isServerRendering();
        // 一般都是function
        if (typeof userDef === 'function') {
    
    
            // 判断是否是服务端渲染,不是的话则走createComputedGetter
            sharedPropertyDefinition.get = shouldCache ?
                createComputedGetter(key) :
                createGetterInvoker(userDef);
            sharedPropertyDefinition.set = noop;
        } else {
    
    
            sharedPropertyDefinition.get = userDef.get ?
                shouldCache && userDef.cache !== false ?
                createComputedGetter(key) :
                createGetterInvoker(userDef.get) :
                noop;
            sharedPropertyDefinition.set = userDef.set || noop;
        }
        if (sharedPropertyDefinition.set === noop) {
    
    
            sharedPropertyDefinition.set = function () {
    
    
                warn(
                    ("Computed property \"" + key + "\" was assigned to but it has no setter."),
                    this
                );
            };
        }
        // 数据劫持,当进行获取computed的话,则执行sharedPropertyDefinition的get方法
        Object.defineProperty(target, key, sharedPropertyDefinition);
    }
    // 默认会走这个函数去创建
    function createComputedGetter(key) {
    
    
        return function computedGetter() {
    
    
            var watcher = this._computedWatchers && this._computedWatchers[key];
            if (watcher) {
    
    
            	// 如果订阅者的值发生了改变,则dirty会变为true,此时会执行evaluate
                if (watcher.dirty) {
    
    
                    watcher.evaluate();
                }
                if (Dep.target) {
    
    
                    watcher.depend();
                }
                return watcher.value
            }
        }
    }
    /**
     * Evaluate the value of the watcher.
     * This only gets called for lazy watchers.
     */
    Watcher.prototype.evaluate = function evaluate() {
    
    
        // 调用this.get此时会触发数据劫持的getter方法,进行重新计算computed的值
        this.value = this.get();
        // 将dirty变为false,代表此时是新值,下次获取的时候不需要重新计算
        this.dirty = false;
    };
    /**
     * Evaluate the getter, and re-collect dependencies.
     */
    Watcher.prototype.get = function get() {
    
    
        // 将全局的Dep.target指向当前watcher
        pushTarget(this);
        var value;
        var vm = this.vm;
        try {
    
    
            value = this.getter.call(vm, vm);
        } catch (e) {
    
    
            if (this.user) {
    
    
                handleError(e, vm, ("getter for watcher \"" + (this.expression) + "\""));
            } else {
    
    
                throw e
            }
        } finally {
    
    
            // "touch" every property so they are all tracked as
            // dependencies for deep watching
            if (this.deep) {
    
    
                traverse(value);
            }
            popTarget();
            this.cleanupDeps();
        }
        return value
    };

 在defineComputed去设置对应的getter与setter,最终通过Object.defineProperty进行绑定,对于getter如果不是服务端渲染的话则会走createComputedGetter来进行设置其getter。在createComputedGetter函数中,获取对应的watcher,如果watcher.dirty为true的话则去触发evaluate方法,在evaluate方法中调用this.get(),去触发this.getter.call,其实是执行计算属性的方法,比如:

computed:{
    
    
  fullName() {
    
    
    return this.firstName + this.lastName;
  }
}

 执行fullName回调,执行过程中,又会对firstName和lastName取值,触发了它俩的getter,所以firstName和lastName的dep会搜集当前Dep.target所指向的computed watcher。同样firstName和lastName的dep也会被添加到computed watcher中,此时fullName计算属性watcher保留着两个dep。
 在createComputedGetter中watcher.value拿到返回的value;接着将watcher.dirty值改为false,表示当前已为计算属性取过值了,只要firstName和lastName值不变,以后就不需要重新计算取值,达到一个缓存的效果。
 在createComputedGetter中Dep.target指向渲染watcher,执行watcher.depend(),目的:给firstName和lastName的dep添加当前的渲染watcher,这样firstName和lastName值改变就会更新组件了,此时firstName和lastName的dep有两个watcher。
 当执行更新时,此时也就触发了setter对应的方法,此时会触发render watcher与computed watcher对应的update方法,源码如下:

  update () {
    
    
    /* istanbul ignore else */
    if (this.lazy) {
    
    
      this.dirty = true
    } else if (this.sync) {
    
    
      this.run()
    } else {
    
    
      queueWatcher(this)
    }
  }

 如果为computed watcher则会将this.dirty变为true,如果是render watcher则会调用queueWatcher方法。
 dirty的作用其实就是只在相关响应式属性发生改变时才会重新求值。如果重复获取计算属性的返回值,只要响应式属性没有发生变化,就不会重新求值。也就是说当响应式属性改变时,触发响应式属性的setter,通知Computed Watcher将dirty置为false;等再次获取时,会获取到最新值,并重新给响应式属性的dep.subs添加Watcher

总结

 在初始化阶段调用对所有的计算属性执行defineComputed方法,对挂载到vue实例上的计算属性进行拦截,并设置对应的存取描述符
get(createComputedGetter函数)和set。当首次触发拦截器中对应的
get时会进行依赖收集,将computed watcher 与 render watcher添加到计算属性中依赖属性dep实例的订阅者列表中。当计算属性所依赖的属性发生变化时,会通知dep实例中所有的订阅者进行更新,其中包含computed watcher与render watcher,当computed watcher执行update方法时,会执行dirty变为true,使得下次访问计算属性时,需要进行重新计算。当render watcher执行update方法时,会调用对应的run方法。

Watcher

初始化

 首先初始化阶段是在initState阶段,去调用initWatch,那么我们分析下initWatch这个方法。

    function initWatch(vm, watch) {
    
    
        for (var key in watch) {
    
    
            var handler = watch[key];
            // 如果是数组的话则挨个调用createWatcher方法
            if (Array.isArray(handler)) {
    
    
                for (var i = 0; i < handler.length; i++) {
    
    
                    createWatcher(vm, key, handler[i]);
                }
            } else {
    
    
                // 如果不是数组的话,则单个调用createWatcher
                createWatcher(vm, key, handler);
            }
        }
    }

 在initWatcher中主要是对所有watcher调用createWatcher方法,那么我们接着分析。

    function createWatcher(
        vm,
        expOrFn,
        handler,
        options
    ) {
    
    
        if (isPlainObject(handler)) {
    
    
            // 处理参数
            options = handler;
            handler = handler.handler;
        }
        // handler为方法名
        if (typeof handler === 'string') {
    
    
            handler = vm[handler];
        }
        return vm.$watch(expOrFn, handler, options)
    }

 那么接下来我们分析一下vue实例的$watch方法:

扫描二维码关注公众号,回复: 15488220 查看本文章
  // $watch
  Vue.prototype.$watch = function (
      expOrFn,
      cb,
      options
  ) {
    
    
      var vm = this;
      // 如果通过 this.$watch 设置的监听,则会执行 createWatcher 获取回调函数
      if (isPlainObject(cb)) {
    
    
          return createWatcher(vm, expOrFn, cb, options)
      }
      options = options || {
    
    };
      // 此时 user 为 true,说明创建的 Watcher 是一个 User Watcher,将user Watcher进行标记
      options.user = true;
      var watcher = new Watcher(vm, expOrFn, cb, options);
      // 如果immediate为true则立即执行回调函数
      if (options.immediate) {
    
    
          var info = "callback for immediate watcher \"" + (watcher.expression) + "\"";
          pushTarget();
          invokeWithErrorHandling(cb, vm, [watcher.value], vm, info);
          popTarget();
      }
      return function unwatchFn() {
    
    
          watcher.teardown();
      }
  };

依赖收集

 vue实例的$watch方法是创建一个User Watcher,并判断immediate是否为true,如果为true则立即执行一次回调函数。watch的初始化结束。
 那么接下来是依赖收集阶段。在初始化过程中会为每个watch创建一个User Watcher,而创建过程中会对被监听属性做依赖收集。在User Watcher的创建过程中:

    var Watcher = function Watcher(
        vm,
        expOrFn,
        cb,
        options,
        isRenderWatcher
    ) {
    
    
        this.vm = vm;
        if (isRenderWatcher) {
    
    
            vm._watcher = this;
        }
        vm._watchers.push(this);
        // options
        if (options) {
    
    
            this.deep = !!options.deep;
            this.user = !!options.user;
            this.lazy = !!options.lazy;
            this.sync = !!options.sync;
            this.before = options.before;
        } else {
    
    
            this.deep = this.user = this.lazy = this.sync = false;
        }
        this.cb = cb;
        this.id = ++uid$2; // uid for batching
        this.active = true;
        this.dirty = this.lazy; // for lazy watchers
        this.deps = [];
        this.newDeps = [];
        this.depIds = new _Set();
        this.newDepIds = new _Set();
        this.expression = expOrFn.toString();
        // parse expression for getter
        if (typeof expOrFn === 'function') {
    
    
            this.getter = expOrFn;
        } else {
    
    
            this.getter = parsePath(expOrFn);
            if (!this.getter) {
    
    
                this.getter = noop;
                warn(
                    "Failed watching path: \"" + expOrFn + "\" " +
                    'Watcher only accepts simple dot-delimited paths. ' +
                    'For full control, use a function instead.',
                    vm
                );
            }
        }
        this.value = this.lazy ?
            undefined :
            this.get();
    };

 User Watcher除了user属性为true外,还有deep、async两个属性,这两个属性都是watch的配置项。对于deep属性,是我们在设置watch时可以有选择的去配置,在user watcher回调用getter获取对应的属性进行赋值,因为lazy为false所以最终调用this.get()方法,也就是说只要不是computed watcher就都会调用this.get方法,接下来我们看一下this.get方法

 /**
     * Evaluate the getter, and re-collect dependencies.
     */
    Watcher.prototype.get = function get() {
    
    
        // 将全局的Dep.target指向当前watcher
        pushTarget(this);
        var value;
        var vm = this.vm;
        try {
    
    
            value = this.getter.call(vm, vm);
        } catch (e) {
    
    
            if (this.user) {
    
    
                handleError(e, vm, ("getter for watcher \"" + (this.expression) + "\""));
            } else {
    
    
                throw e
            }
        } finally {
    
    
            // "touch" every property so they are all tracked as
            // dependencies for deep watching
            if (this.deep) {
    
    
                traverse(value);
            }
            popTarget();
            this.cleanupDeps();
        }
        return value
    };

 当前Watcher入栈,执行this.getter(每类Watcher的getter属性不同),执行traverse 方法(只有 deep为true的User Watcher才会执行)
当前Watcher出栈,处理Watcher的deps属性,返回 value。当入栈时,此时会将Dep.target设置为当前的user watcher,执行this.getter时会去触发监听属性的getter,将user watcher添加到dep的订阅者列表中,如果deep为true的话,就执行traverse,traverse的逻辑如下:

    function traverse(val) {
    
    
        _traverse(val, seenObjects);
        seenObjects.clear();
    }

    function _traverse(val, seen) {
    
    
        var i, keys;
        var isA = Array.isArray(val);
        if ((!isA && !isObject(val)) || Object.isFrozen(val) || val instanceof VNode) {
    
    
            return
        }
        if (val.__ob__) {
    
    
            var depId = val.__ob__.dep.id;
            if (seen.has(depId)) {
    
    
                return
            }
            seen.add(depId);
        }
        if (isA) {
    
    
            i = val.length;
            while (i--) {
    
    
                _traverse(val[i], seen);
            }
        } else {
    
    
            keys = Object.keys(val);
            i = keys.length;
            while (i--) {
    
    
                _traverse(val[keys[i]], seen);
            }
        }
    }

 traverse逻辑为如果被监听的属性是一个对象,则把对象的所有属性都访问一遍,从而触发所有属性的依赖收集;将User Watcher添加到每个属性的dep.subs中,这样当某个属性修改时,会触发属性的setter,从而触发watch回调。

触发回调

 当对应的值发生变化时,此时会触发setter,通知所有订阅者调用update方法。

  update () {
    
    
    if (this.lazy) {
    
    
      this.dirty = true
    } else if (this.sync) {
    
    
      this.run()
    } else {
    
    
      queueWatcher(this)
    }
  }

 如果 User Watcher的sync属性为true,立刻执行run方法;如果sync属性为false,通过queueWatcher(this) 在下一次任务队列中执行User Watcher的 run 方法,run方法如下:

    Watcher.prototype.run = function run() {
    
    
        if (this.active) {
    
    
            var value = this.get();
            if (
                value !== this.value ||
                // Deep watchers and watchers on Object/Arrays should fire even
                // when the value is the same, because the value may
                // have mutated.
                isObject(value) ||
                this.deep
            ) {
    
    
                // set new value
                var oldValue = this.value;
                this.value = value;
                if (this.user) {
    
    
                    var info = "callback for watcher \"" + (this.expression) + "\"";
                    invokeWithErrorHandling(this.cb, this.vm, [value, oldValue], this.vm, info);
                } else {
    
    
                    this.cb.call(this.vm, value, oldValue);
                }
            }
        }
    };

 对于User Watcher的run方法,首先会调用this.get()重新让被监听属性做依赖收集,并获取最新值;如果最新值和老值不想等,调用回调函数,并将新老值传入,执行对应的回调函数。

总结

 在初始化阶段,会为每个watch创建一个User Watcher,如果watch的immediate为true,会马上执行一次回调;创建User Watcher过程中会获取一次被监听属性的值,从而触发被监听属性的getter方法,将User Watcher添加到被监听属性的Dep实例中。
当被监听属性发生改变时,通知User Watcher更新,如果watch的sync为true,会马上执行watch的回调;否则会将User Watcher的update方法通过nextTick放到缓存队列中,在下一个的事件循环中,会重新获取被监听属性的属性值,并判断新旧值是否想等、是否设置了deep为true、被监听属性是否是对象类型,如果成立就执行回调。

Vue源码系列文章:

猜你喜欢

转载自blog.csdn.net/liu19721018/article/details/125574441