高频问题,谈谈你对vue中computed,watch,methods的理解

432 阅读3分钟

相信对于大多数用过vue的人都知道computed,watch,methods这三者是干嘛的,但是其各自使用场景和底层实现自己真的说的清楚吗? 先看官方的例子:

var vm = new Vue({
  el: '#example',
  data: {
    message: 'Hello'
  },
  computed: {
    // 计算属性的 getter
    reversedMessage: function () {
      // `this` 指向 vm 实例
      return this.message.split('').reverse().join('')
    }
  }
})

如果用methods应该怎么实现?

methods: {
  reversedMessage: function () {
    return this.message.split('').reverse().join('')
  }
}

如果用watch应该怎么实现?

watch: {
    firstName: function (val) {
      this.fullName = val + ' ' + this.lastName
    },
    lastName: function (val) {
      this.fullName = this.firstName + ' ' + val
    }
  }

从代码层面上讲computed和methods看起来没什么区别,要看区别还得从源码实现上理解。而watch的使用就有些区别,它的属性名称不是你要计算后的属性名称,而是其依赖的属性,有点发布订阅的意思。

那具体底层代码如何实现的,区别在哪,我们逐行分析下。

Computed的实现

 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 */ );
    }
    if (opts.computed) {
      initComputed(vm, opts.computed);//注意这个地方
    }
    if (opts.watch && opts.watch !== nativeWatch) {
      initWatch(vm, opts.watch);
    }
  }

initState就是初始化组件的各种数据状态,包括props,data,computed,watch。具体我们再看initComputed()是如何实现的。

initComputed()

 var computedWatcherOptions = {
    lazy: true
 };

function initComputed(vm, computed) {
	//创建一个空对象赋值给watchers
    var watchers = vm._computedWatchers = Object.create(null);
    //是否服务器端渲染
    var isSSR = isServerRendering();
	//遍历computed属性
    for (var key in computed) {
     //获取属性值
      var userDef = computed[key];
      //判断属性值是函数还是对象,如果是对象会取对象的get属性
      var getter = typeof userDef === 'function' ? userDef : userDef.get;
      //getter方法为null抛出异常
      if (getter == null) {
        warn(
          ("Getter is missing for computed property \"" + key + "\"."),
          vm
        );
      }
	  //如果非服务器端渲染
      if (!isSSR) {
        // 为每个属性创建内部watcher
        watchers[key] = new Watcher(
          vm,
          getter || noop,
          noop,
          computedWatcherOptions
        );
      }

      //如果computed中的属性不在组件中,则定义该属性为响应式,如果已存在,比如在data中定义了,则会抛出异常
      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);
        }
      }
    }
  }

defineComputed()

如果看过源码的肯定对defineReactive方法有印象,这里定义了一个defineComputed方法:

 var sharedPropertyDefinition = {
    enumerable: true,
    configurable: true,
    get: noop,
    set: noop,
  };
  
  function defineComputed(
    target,
    key,
    userDef
  ) {
 	//非服务器端渲染才有缓存
    var shouldCache = !isServerRendering();
    //userDef就是initComputed方法中定义的每个属性值,如果是方法就执行方法,如果是对象就执行对象的get方法。
    if (typeof userDef === 'function') {
      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
        );
      };
    }
    //定义成响应式数据,sharedPropertyDefinition对象的lazy属性为true
    Object.defineProperty(target, key, sharedPropertyDefinition);
  }

里面的核心方法是createComputedGetter()createGetterInvoker()

createComputedGetter()

  function createComputedGetter(key) {
  //返回值是一个函数
    return function computedGetter() {
    //上面提到的为computed对象属性创建的watcher
      var watcher = this._computedWatchers && this._computedWatchers[key];
      if (watcher) {
      	//computedWatcherOptions默认lazy为true,dirty为true
        if (watcher.dirty) {
          watcher.evaluate();//调用watcher的get方法,将dirty设置为false
        }
        //添加依赖收集,只有当值发生变化的时候才会触发通知
        if (Dep.target) {
          watcher.depend();
        }
        //注意这里返回的是当前watcher实例的值,这也是为什么computed属性方法需要return
        return watcher.value
      }
    }
  }

我们看下这里的watcher.evaluate()做了什么,其实很简单,就是调用当前的get()方法,获取最新值:

watcher.evaluate()

  Watcher.prototype.evaluate = function evaluate() {
    this.value = this.get();
    this.dirty = false;
  };

createGetterInvoker()

createGetterInvoker就是调用computed属性对象设置的get方法。

  function createGetterInvoker(fn) {
    return function computedGetter() {
      return fn.call(this, this)
    }
  }

比如:

computed:{
	name:{
    	get(){
        	retutn this.firstName+this.sencondName;
        }
    }
}

computed小结:

通过分析computed源码,我们发现:

1、computed属性也是响应式的;

2、computed属性值可以是函数也可以是对象;

3、computed计算属性是通过watcher来实现的;

4、只有当computed watcher属性dirty为true,响应式依赖发生变化时才会重新计算,这也是为什么computed是计算属性缓存。

5、computed属性最后是一个返回值;

Watch的实现

上面已经提到了watch的入口也是initState()方法中:

if (opts.watch && opts.watch !== nativeWatch) {
      initWatch(vm, opts.watch);
}

initWatch()

  function initWatch(vm, watch) {
  	//变量watch对象属性
    for (var key in watch) {
      var handler = watch[key];
      //这里说明watch属性值也可以是数组
      if (Array.isArray(handler)) {
        for (var i = 0; i < handler.length; i++) {
          createWatcher(vm, key, handler[i]);
        }
      } else {
        createWatcher(vm, key, handler);
      }
    }
  }

createWatcher

  function createWatcher(vm, expOrFn, handler, options) {
    //如果是纯对象,拿到最终的handler回调函数
    if (isPlainObject(handler)) {
      options = handler;
      handler = handler.handler;
    }
    //如果是字符串,则handler是当前组件的某一个方法
    if (typeof handler === "string") {
      handler = vm[handler];
    }
    return vm.$watch(expOrFn, handler, options);
  }

高潮来了,看下vm.$watch()的实现:

Vue.prototype.$watch = function (expOrFn, cb, options) {
      var vm = this;
      //如果是对象,继续遍历执行上一步方法
      if (isPlainObject(cb)) {
        return createWatcher(vm, expOrFn, cb, options);
      }
      options = options || {};
      options.user = true;
      //创建watcher
      var watcher = new Watcher(vm, expOrFn, cb, options);
      //如果immediate属性为true,则立即执行回调函数
      if (options.immediate) {
        try {
          cb.call(vm, watcher.value);
        } catch (error) {
          handleError(error, vm, 'callback for immediate watcher "' + watcher.expression + '"');
        }
      }
      //返回一个函数,移除当前watcher,避免内存泄漏
      return function unwatchFn() {
        watcher.teardown();
      };
    };

所以watch的核心是vue.$watch方法,其本质也是watcher,当监听属性值发生变化时调用回调函数,由此可见,computed属性函数执行结果是计算依赖值,而watch执行的是一个回调方法,watch实际上比computed更加强大,你可以在回调函数中做很多其他事情。

methods的实现

最后我们再看下methods的实现,同样methods也是在initState中调用的:

if (opts.methods) {
    initMethods(vm, opts.methods);
}

initMethods()


  function initMethods(vm, methods) {
    var props = vm.$options.props;
    //遍历methods属性
    for (var key in methods) {
      {
         //methods的属性必须是一个函数,否则会抛出异常
        if (typeof methods[key] !== "function") {
          warn('Method "' + key + '" has type "' + typeof methods[key] + '" in the component definition. ' + "Did you reference the function correctly?", vm);
        }
        //methods属性不能在props中有定义
        if (props && hasOwn(props, key)) {
          warn('Method "' + key + '" has already been defined as a prop.', vm);
        }
        //methods属性也不能在data属性中定义
        if (key in vm && isReserved(key)) {
          warn('Method "' + key + '" conflicts with an existing Vue instance method. ' + "Avoid defining component methods that start with _ or $.");
        }
      }
      //最后把methods每个属性方法赋值给vm属性,并重新指向vm
      vm[key] = typeof methods[key] !== "function" ? noop : bind(methods[key], vm);
    }
  }

由此可见methods实际上就是一个普通对象,每个对象属性就是一个方法,每次调用都会执行其对应的方法。