Vue源码-computed源码分析

578 阅读1分钟

就以一个最简单的Vue文件开始

new Vue({
  render: h => h(App),
}).$mount('#app')
<!--App.vue-->
<template>
  <div>
    <div>{{oriValue}}</div>
    <div>{{oriValueAfterComputed}}</div>
    <div>{{oriValueObjectAfterComputed}}</div>
    <button @click="oriplus">oriValue加1</button>
    <button @click="oriValueObjectAfterComputedSetting">
        设置oriValueObjectAfterComputed的值
    </button>
  </div>
</template>

<script>
    export default {
        data() {
            return {
                oriValue:1
            }
        },
        methods:{
            oriplus(){
                this.oriValue++
            },
            oriValueObjectAfterComputedSetting(){
                this.oriValueObjectAfterComputed = 10
            }
        },
        computed:{
            oriValueAfterComputed(){
                return this.oriValue+"++computed"
            },
            oriValueObjectAfterComputed:{
                get:function(){
                    return this.oriValue + "ObjComputed"
                },
                set:function(newValue){
                    this.oriValue = newValue
                }
            }
        }
    }
</script>

上面的App.vue文件中, export default {}里面包含的就是App这个组件的options,options的computed的属性也是个对象,这个对象设置了oriValueAfterComputed这个属性,因此在Vue的初始化中,会执行一下代码,computed对象的初始化

initComputed

if (opts.computed) { initComputed(vm, opts.computed); }

上面的initComputed方法下面进行下面进行详细介绍(无关代码会忽略,以免影响理解)

function initComputed (vm, computed) {
  var watchers = vm._computedWatchers = Object.create(null);
  // computed properties are just getters during SSR
  <!--这里判断是否是服务端渲染,是的话则computed对象的属性值只能取不能设置-->
  var isSSR = isServerRendering();

  for (var key in computed) {
    var userDef = computed[key];
    <!--这里是判断computed[key]是一个对象还是方法(例如本例computed对象中oriValueAfterComputed是一个方法;oriValueObjectAfterComputed则是一个对象),如果是对象的话,get方法就是这个对象的get属性值-->
    var getter = typeof userDef === 'function' ? userDef : userDef.get;
    
  if (!isSSR) {
    // create internal watcher for the computed property.
    <!--用上面获得的getter方法,创建一个watcher对象,这个watcher的值watcher.value就是用getter方法计算出来的-->
    
    <!--初始化计算watcher的时候lazy为true,在创建watcher的时候不会立马计算value的值,而是把dirty初始化为true,所以在初始化的时候value还是undefined,等到执行render函数读取这个属性的时候, 由于dirty是true,所以会进行一次evaluate计算value的值-->
    var computedWatcherOptions = { lazy: true };
    watchers[key] = new Watcher(
    vm,
    getter || noop,
    noop,
    computedWatcherOptions
    );
    }

    if (!(key in vm)) {
      //设置这个key的setter和getter,实现对这个key值的监听
      defineComputed(vm, key, userDef);
    } else  {
       //如果computed属性已经在props,data,inject,methods等对象中被定义,则报警告。不执行computed该属性的监听
    }
  }
}

defineComputed

function defineComputed (
    target,
    key,
    userDef
  ) {
  <!--非服务端渲染则缓存,缓存的意思是,不是每一次获取computed对象的属性都执行一次上面对应的userDef函数,而是把这个计算值存在对应的watcher对象的value里面,以后使用可以直接获取watcher里面的value值(除非userDef的执行结果更新了,才重新获取)-->
    var shouldCache = !isServerRendering();
    <!--var userDef =  computed[key];上面的initComputed函数有说到,userDef可能是函数可能是对象,如果它是对象的话,则getter函数是这个对象属性get的值-->
    if (typeof userDef === 'function') {
    <!--关于createComputedGetter和createGetterInvoker方法请看下一段的函数分析,简单来说,
    createGetterInvoker是直接执行getter,也就是userDef.get/userDef函数,获得返回值,
        每次获取这个key的值,都会执行一遍,返回结果;
    createComputedGetter是获取这个key对应的watcher的value值,
        如果value值计算结果不发生改变的话,watcher不会重新执行this.getter
    -->
    
      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;
    }
    
    <!--如果没有设置这个key的set方法,但是使用了this[key的名字]=value,则会报warning-->
    if (sharedPropertyDefinition.set === noop) {
      sharedPropertyDefinition.set = function () {
        warn(
          ("Computed property \"" + key + "\" was assigned to but it has no setter."),
          this
        );
      };
    }
    <!--设置sharedPropertyDefinition的gette/setter,就是设置这个key的setter/getter-->
    Object.defineProperty(target, key, sharedPropertyDefinition);
  }

createGetterInvoker 和 createComputedGetter

//这个函数就很直接粗暴了,返回一个函数,这个函数的执行就是执行一遍fn
function createGetterInvoker(fn) {
  return function computedGetter () {
    return fn.call(this, this)
  }
}
function createComputedGetter (key) {
    return function computedGetter () {
      //获取key对应的watcher
      var watcher = this._computedWatchers && this._computedWatchers[key];
      if (watcher) {
        //在初始化计算watcher的时候,watcher.dirty是true
        //watcher的dirty表示watcher的计算值有变化,需要重新计算,所以执行watcher.evaluate(),watcher.value的值会更新
        if (watcher.dirty) {
          watcher.evaluate();
        }
        if (Dep.target) {
          watcher.depend();
        }
        return watcher.value
      }
    }
  }

至于这里的dirty为什么是true的时候就代表该watcher需要更新了呢?这是因为当某个属性被重新赋值的时候,会触发watcher.update方法,而这个方法在该watcher.lazy为true的时候,将dirty属性设为true,以此告知这个watcher,需要重新计算value值了。watcher.evaluate执行后,watcher.dirty会设置为false,在下一次update之前,不会再次重新计 算watcher的值

Watcher.prototype.update = function update () {
  /* istanbul ignore else */
  if (this.lazy) {
    this.dirty = true;
  } else if (this.sync) {
    this.run();
  } else {
    queueWatcher(this);
  }
};
Watcher.prototype.evaluate = function evaluate () {
  this.value = this.get();
  this.dirty = false;
};

compouted初始化完成后,render函数执行,读取计算属性的值

vm.$mount的过程中,updateComponent方法里面有一步是

vnode = render.call(vm._renderProxy, vm.$createElement)

<!--这个是App.vue模板文件-->
 <div>
    <div>{{oriValue}}</div>
    <div>{{oriValueAfterComputed}}</div>
    <div>{{oriValueObjectAfterComputed}}</div>
    <button @click="oriplus">oriValue加1</button>
    <button @click="oriValueObjectAfterComputedSetting">
      设置oriValueObjectAfterComputed的值
    </button>
  </div>

上面App.vue文件的template转换为render函数如下,render函数执行会读取computed对象的属性值(本例子中就是oriValueAfterComputed和oriValueObjectAfterComputed),触发属性的get方法,也就是createComputedGetter返回的方法:computedGetter,返回对应的watcher.value的值,这样我们就可以拿到oriValueAfterComputed和oriValueObjectAfterComputed的值了。


<!--这个是App.vue模板文件编译后的render函数-->
var render = function() {
  var _vm = this
  var _h = _vm.$createElement
  var _c = _vm._self._c || _h
  return _c("div", [
    _c("div", [_vm._v(_vm._s(_vm.oriValue))]),
    _c("div", [_vm._v(_vm._s(_vm.oriValueAfterComputed))]), //这里会触发oriValueAfterComputed的getter方法
    _c("div", [_vm._v(_vm._s(_vm.oriValueObjectAfterComputed))]), //这里会触发oriValueObjectAfterComputed的getter方法
    _c("button", { on: { click: _vm.oriplus } }, [_vm._v("oriValue加1")]),
    _c("button", { on: { click: _vm.oriValueObjectAfterComputedSetting } }, [
      _vm._v(" 设置oriValueObjectAfterComputed的值 ")
    ])
  ])
}

(ps:因为我用的是runtimeOnly版本,所以编译的时候,template里面的内容已经预编译成render函数),如果对模板编译成render函数的过程有兴趣的话,可以看看 vue.esm.js这个vue文件里面的createCompiler函数

本文是我学习源码过程中的一个小记录,以此梳理清楚自己对computed属性的理解和认识,希望如果有朋友发现文章中有任何不够或不正确的地方,请麻烦指出,实在太感谢你们了。