从源码的角度解析Vue Computed原理

560 阅读6分钟

前言

对于computed属性,几乎每次面试的时候都要去问一遍,之前也就靠着自己的一点业务中使用的理解去说说,但这种还是太浅了,很多时候并不是面试官所需要的答案。所以想着干脆把computed源码阅读一遍,知道它的工作原理,这样才能做到真正的理解。

先看一下初始化吧

先跟着源代码了解Computed是在哪里进行初始化的


function Vue(options) {
    ...
    this._init(options)
}

function initMixin(Vue) {
    Vue.prototype._init = function(options) {
	...
	initState()
	...
    }
} 

function initState() {
    ...
    if (opts.computed) initComputed(vm, opts.computed)
    ...
}

可以看到,当我们去用new Vue(...)来进行调用时,会进行初始化。会调用到initState()函数,该函数中调用了initComputed进行computed的初始化。

initComputed做了什么?

翻开initComputed可以看到很长的一段代码,为了更容易理解该段代码,我们可以抛去一些输出警告信息以及服务端渲染这块内容,从而提取出一段简短的代码用作分析。后续的代码也都会做相应的处理。

const computedWatcherOptions = { lazy: true }

function initComputed (vm, computed) {
  const watchers = vm._computedWatchers = Object.create(null)

  for (const key in computed) {
    const userDef = computed[key]
    const getter = typeof userDef === 'function' ? userDef : userDef.get
  
    watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        computedWatcherOptions
    )

    if (!(key in vm)) {
      defineComputed(vm, key, userDef)
    } 
  }
}

为了让大家能够理解透彻,会拿出些代码做出解释。

1.这块代码是为给每一个computed分配一个WatcherWatcher在整个Vue中都是个非常重要的类,在后面会贴出部分Watcher的代码进行分析。

watchers[key] = new Watcher(
    vm,
    getter || noop,
    noop,
    computedWatcherOptions
)

2.使用函数defineComputed处理computed

if (!(key in vm)) {
    defineComputed(vm, key, userDef)
} 

总结起来initComputed为每一个computed做了下面两件事:

  • 分配Watcher
  • 使用definComputed处理

针对这两件事拿出来再做单独分析

分配Watcher做了哪些事?

我们刚才的initComputed代码中可以看到在创建Watcher的时候传递的参数应该是下面这样(注:noop = function(a, b, c){})

new Watcher(vm, function c() { return this.a }, noop, { lazy: true }, undefined)

根据此传参,抛去一些判断,再来看一下Watcher的精简代码

function Watcher(vm, expOrFn, cb, options, isRenderWatcher) {
    this.vm = vm;

    this.dirty = this.lazy = !!options.lazy; 

    if (typeof expOrFn === 'function') {
      this.getter = expOrFn;
    }

    this.value = this.lazy ? undefined : this.get();
}

根据代码可以看到watcher做了以下几件事

1.将dirtylazy都赋值为truelazy在这里标记着这个watcher需要缓存,而dirty是标记着取缓存的值还是重新计算。可以理解为lazy是个缓存的开关,而dirty标记着缓存还是否有效。

this.dirty = this.lazy = !!options.lazy; 

2.将computedgetter缓存下来

this.getter = expOrFn;

3.computed的值会由get获取到,这里根据lazy形成了条件判断,这样初始化的时候就不会去计算获取值,而是后面用到的时候再去从其它地方调用get()。关于get函数我们放到后面再看。

this.value = this.lazy? undefined : this.get();

definComputed做了哪些处理?

看一下definComputed代码:

var sharedPropertyDefinition = {
    enumerable: true,
    configurable: true,
    get: noop,
    set: noop
};

function defineComputed (
    target,
    key,
    userDef
  ) {

    if (typeof userDef === 'function') {
      sharedPropertyDefinition.get = createComputedGetter(key)
      sharedPropertyDefinition.set = noop;
    } else {
      sharedPropertyDefinition.get = userDef.get
        ?  createComputedGetter(key): noop;
      sharedPropertyDefinition.set = userDef.set || noop;
    }

    if (sharedPropertyDefinition.set === noop) {
      sharedPropertyDefinition.set = function () {};
    }

    Object.defineProperty(target, key, sharedPropertyDefinition);
  }

上述代码中主要有这两件事

  1. 使用createComputedGetter函数创建一个getter'
  2. 使用Object.defineProperty修改get为刚创建的getter
createComputedGetter

createComputedGetter里做的事比较多,也涉及到了computed的原理所在,所以要拿出来单独讲讲。

function createComputedGetter (key) {
    return function computedGetter () {
      var watcher = this._computedWatchers && this._computedWatchers[key];
      if (watcher) {
        if (watcher.dirty) {
          watcher.evaluate();
        }
        if (Dep.target) {
          watcher.depend();
        }
        return watcher.value
      }
    }
}

1.由于在initComputed里创建watcher时也把所有创建的watcher都缓存了下来,所以这里会直接根据key来取出响应的watcher

var watcher = this._computedWatchers && this._computedWatchers[key];

2.这里dirty默认是为true的所以会执行evaluate()进行计算。evaluate()只做了两件事: 1. 使用get()函数获取值, 2. 将dirty属性设为false

if (watcher.dirty) {
     watcher.evaluate();
}

evaluate代码

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

先来看一下get的代码

Watcher.prototype.get = function get () {
    pushTarget(this);
    var value;
    var vm = this.vm;
    try {
      value = this.getter.call(vm, vm);
    } catch (e) {
      ...
    } finally {
      ...
      popTarget();
    }
    return value
}

虽然代码不多,但get中关联的其他函数很多,为了方便理解我们举例来说明

例:

var app = new Vue({
  el: '#app',
  data: {
    a: 'a'
  },
  computed: {
    c () {
      return this.a
    }
  },
  methods: {
    changeA () {
      this.a = 'changed'
    }
  }
})
<div id="app">
    <div>{{c}}</div>
    <button @click="changeA">Change A</button>
</div>

页面: 页面 结合上面的例子我们来拆分get的操作 1.pushTarget(this)这个操作时将当前watcher推入栈中,并且让Dep.target变为当前的watcher,也就是cwatcher(放了方便后续使用,如果是某个值的watcher就用watcher(xx)来简称。)

function pushTarget (target) {
    targetStack.push(target);
    Dep.target = target;
}

2.调用当前watcher缓存的getter函数,从例子我们可以知道getter就是

function c() { return this.a }

所以会调用agetterainitData时候已经使用defineProperty做了处理,给设置了gettersetter.先来看看agetter做了什么,同样是精简后的代码:

var value = getter ? getter.call(obj) : val;
if (Dep.target) {
   dep.depend();
}
return value

当前Dep.targetwatcher(c)的,所以会走到dep.depend()

再来你看一下depend()的源码

Dep.prototype.depend = function depend () {
    if (Dep.target) {
      Dep.target.addDep(this);
    }
};

上述代码可以得知:

  • 因为是从agetter走进来的所以thisa
  • Dep.targetwatcher(c)

Dep.target.addDep(this)实际是调用的代码watcher.addDep,然后来看看watcher.addDep的代码:

Watcher.prototype.addDep = function addDep (dep) {
    var id = dep.id;
    if (!this.newDepIds.has(id)) {
      this.newDepIds.add(id);
      this.newDeps.push(dep);
      if (!this.depIds.has(id)) {
        dep.addSub(this);
      }
    }
}

这段代码做了两件事:

  • cnewDeps中加入了a,后面会出来加到deps
  • asubs加入了watcher(c)

所以此时Dep.target(watcher(c))数据结构应该是: 数据结构

3.最后进行了popTarget()操作,将watcher(c)推出栈,此时Dep.target为当前页面的watcher。然后返回了value

至此get()函数执行完毕。 将this.dirty设置为falseevaluate执行完毕

继续走createComputedGetter代码会到这里

if (Dep.target) {
    watcher.depend();
}

接下来看watcher.depend()做什么

watcher.depend()

看一下watcher.depend()源码:

Watcher.prototype.depend = function depend () {
    var i = this.deps.length;
    while (i--) {
      this.deps[i].depend();
    }
}

分步拆解:

  • 此时的thiswatcher(c)
  • 从上图可以看出来watcher(c)deps只有a,所以实际就是拿adepend()
  • 当执行depend()时,get时说过,执行完evaluate后会将Dep.target还原回原来的,所以此时的Dep.target是页面,所以执行完毕后,watcher(page)deps会加入aasubs会加入watcher(page)

最终页面的watcher结构为: 数据结构

那么页面又是怎么更新的呢?

刚才所说的initData只说到了a属性的getter方法,当我们点击changeA时,a的值改变,会调用asetter方法。asetter里调用了dep.notify()

看一下notify代码:

Dep.prototype.notify = function notify () {
 
    var subs = this.subs.slice();
    
    for (var i = 0, l = subs.length; i < l; i++) {
      subs[i].update();
    }
};

notify中取出当前的subssubs里有什么上面已经说过了,遍历调用subswatcherupdate

再看一下update函数

Watcher.prototype.update = function update () {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true;
    } else if (this.sync) {
      this.run();
    } else {
      queueWatcher(this);
    }
};
  • 当调用到cupdate时,lazy的值为true,所以会把dirty的值也设为true
  • 当调用到页面的update时,会触发页面的更新,此时页面引用了ccdirty值又变成了true,就会重新从a中取值计算。

从上面我们不难看出,c实际上与页面更新并无关系,实际只取到个计算值的作用,真正与页面更新有关的实际是所依赖的a

总结

由于没有怎么做过源码的解读,可能有些地方描述的并不是很恰当,或不是很详细,有不明白的可以留言告诉我,在我能力之内的我会一一解答