Vue计算/监听属性

897 阅读13分钟

计算属性

对于计算属性,我们首先来熟悉一下它的使用方法。

var vm = new Vue({
  el: '#example',
  data: {
    message: 'Hello'
  },
  computed: {
    reversedMessage: function () {
      return this.message.split('').reverse().join('')
    }
  }
})

计算属性的本质是向我们要访问的变量添加一个getter函数,只要我们访问该变量就会触发该函数。计算属性有一个特点:具有缓存操作,当我们计算出结果后,只要我们的依赖变量的值没有改变,那么就不会执行getter函数,只会返回以前计算好的值。 当我们第一次去访问我们规定计算属性的值的时候,它会执行一次我们的函数,然后将我们计算好的结果返回并保存,我们再去访问这个计算属性的时候并不会去执行函,而是将以前缓存的值返回给我们。那么该计算属性在什么情况还会执行函数呢?当我们依赖的变量的值发生改变的时候,我们再去访问计算属性,那么这次就不会返回缓存的值了,而是执行函数,然后将计算得到的值返回并缓存。

在这个函数里我们可能依赖其他的变量,当我们依赖的变量的值发生改变,那么Vue会自动触发我们的getter函数,让我们的视图更新为最新的

接下来我们将会站在源码的角度上去看待计算属性。

对于computed属性,它是在处理数据阶段被处理的,具体执行的是initState函数,在该函数中执行了下面的这些代码:

  if (opts.computed) initComputed(vm, opts.computed)

首先是判断我们是否定义了computed这个属性如果定义了就执行initComputed这个函数,而传入的参数就是该组件实例和该组件实例的配置项中的computed计算属性。接下来我们来看initComputed函数内部的代码逻辑:

function initComputed (vm: Component, computed: Object) {
  // $flow-disable-line
  const watchers = vm._computedWatchers = Object.create(null)
  // computed properties are just getters during SSR
  const isSSR = isServerRendering()
​
  for (const key in computed) {
    const userDef = computed[key]
    const getter = typeof userDef === 'function' ? userDef : userDef.get
    if (process.env.NODE_ENV !== 'production' && getter == null) {
      warn(
        `Getter is missing for computed property "${key}".`,
        vm
      )
    }
​
    if (!isSSR) {
      // create internal watcher for the computed property.
      watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        computedWatcherOptions
      )
    }
​
    // component-defined computed properties are already defined on the
    // component prototype. We only need to define computed properties defined
    // at instantiation here.
    if (!(key in vm)) {
      defineComputed(vm, key, userDef)
    } else if (process.env.NODE_ENV !== 'production') {
      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)
      }
    }
  }
}

首先就是为组件实例添加属性:

  const watchers = vm._computedWatchers = Object.create(null)

然后判断是否是服务段渲染:

  const isSSR = isServerRendering()

然后就开始循环操作了:

  for (const key in computed)

在循环体中首先就是获取到每一个计算属性的值:

 const userDef = computed[key]

然后对这个值的类型进行判断:

   const getter = typeof userDef === 'function' ? userDef : userDef.get

首先判断计算属性的值是否是一个函数,如果是一个函数,那么就将getter赋值为这个函数,如果不是,那么就获取到这个计算属性对应的get函数。假如说两者都不是,那么就报错,说你忘记了getter的定义。然后就是判断关于服务端渲染的问题,因为我们不是服务端渲染,所以会执行:

   if (!isSSR) {
      // create internal watcher for the computed property.
      watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        computedWatcherOptions
      )
    }

watchersinitConputed函数内部创建的一个变量。上面的代码相当于是为每一个计算属性都创建一个对应的watcher实例对象。现在我们来看Watcher构造函数。这个构造函数就是我们为组件创建watcher时的构造函数。首先我们来看传入的参数,在这里我们只传入的四个参数,最后一个参数没传,那么就是undefined。而最后一个参数恰好是isRenderWatcher。也就是说我们现在创建的watcher并不是渲染watcher。而其他的参数大致一样。

当我们进入到Watcher构造函数中后,接着会执行:

  this.vm = vm
    if (isRenderWatcher) {
      vm._watcher = this
    }
    vm._watchers.push(this)

首先将vm挂载到watcher.vm上,然后判断是否是渲染watcher。因为我们传入的是undefined。所以不是,然后就为vm实例挂载一个_watchers的属性。然后将我们创建的计算属性的watcher放到里面。这里需要注意以下,我们可以回想一下,我们一般为谁创建wathcer。目前我们接触到的就只有vm组件实例对象了,创建的目的是为每一个组件做一个标记,当该组件在渲染的时候访问到了某一个属性的时候,那么就将组件对应的watcher放到属性的deps当中,也就是说watcher作为一种标记,这种标记被称为渲染watcher。但是其实这里隐含了一个问题,那么就是当一个组件在进行创建的时候,引用属性的不只是在组件节点中,也有可能是在配置项的方法中或者计算属性中,这里就是在计算属性中,那么在计算属性中访问的属性值又该怎么处理呢?其实处理计算属性中的属性访问和在组件节点中的访问时大同小异的,为什么这么说呢,我们后面会有具体的解析。

回到上面的代码,我们为组件的vm._watchers的数组中添加了计算属性对应的watcher。然后就是对计算属性的watcher进行属性的初始化。同样的,和组件一样,依旧会执行:

   this.getter = expOrFn//this指向的是计算属性的watcher

接着就是执行:

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

因为此时的this.lazytrue,所以返回的是undefined注意,这里没有执行this.get()函数

接着就回退到initComputed函数当中,也就是说我们执行new Watcher构造函数,只是为了创建一个计算属性的watcher实例,并没有进行依赖收集。好了我们继续看,然后执行:

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

一般情况下,vm中是没有和我们的计算属性同名的属性。所以通常这个分支条件是成立的。然后执行:

     defineComputed(vm, key, userDef)

这个函数就很奇妙了,我们来看defineComputed函数:

export function defineComputed (
  target: any,
  key: string,
  userDef: Object | Function
) {
  const shouldCache = !isServerRendering()
  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 (process.env.NODE_ENV !== 'production' &&
      sharedPropertyDefinition.set === noop) {
    sharedPropertyDefinition.set = function () {
      warn(
        `Computed property "${key}" was assigned to but it has no setter.`,
        this
      )
    }
  }
  Object.defineProperty(target, key, sharedPropertyDefinition)
}

首先判断我们传入的userDef(也就是计算属性对应的函数)是否是一个函数,一般情况下是,然后就会走:

 if (typeof userDef === 'function') {
    sharedPropertyDefinition.get = shouldCache
      ? createComputedGetter(key)
      : createGetterInvoker(userDef)
    sharedPropertyDefinition.set = noop
  }

首先我们要知道sharedPropertyDefinition是什么,代码如下:

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

其实它就是一个对象属性描述的通用模板。然后我们向该模板中添加了get属性,那么问题就来了,我们为谁添加属性,有人说,不就是sharedPropertyDefinition吗,不,我们不能从这个角度出发,而是从另一个角度。那就是我们给computed中的属性添加get函数,也就是说当我们在模板中任意一个地方引用这个计算属性,就会触发这个函数。那么这个get函数具体是什么函数呢?我们来看代码:

createComputedGetter(key)

我们进入到该函数中,看它内部具体是怎么做的:

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

该函数返回一个函数。我们暂时不去讲解这个函数,因为这个函数直到我们在模板中访问的时候才会触发。所以我们暂时忽略。然后我们回到defineComputed函数中来:

 Object.defineProperty(target, key, sharedPropertyDefinition)

此时我们将计算属性添加到对应的组件实例上。从vm.options.computed移到vm上,这是一个巨大的转变。

我们现在回想一下第一阶段Vue对计算属性做了哪些事情。第一件事就是为组件创建一个watchers,用来装计算属性的watcher。第二件事就是为组件中的计算属性创建与其对应的watcher实例。第三件事就是执行defineComputed函数,为组件的计算属性添加改造后的getter函数。

然后我们进行下一个阶段的计算属性的分析,那就是当我们在页面中引入一个计算属性的时候。

当我们的组件在进行渲染的时候,在生成VNode的这一阶段,就会访问到计算属性。当我们访问到计算属性的时候就一定会触发这个计算属性的getter函数,然后就会执行:

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

这个函数,首先执行:

const watcher = this._computedWatchers && this._computedWatchers[key]

这里的this指向的是该组件实例对象vm。首先判断这个组件是否有_computedWatchers_computedWatchers[key]。这两个条件都是成立的。然后开始进行分支判断,首先是判断:

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

watcher.dirtytrue。这个属性应该是内定,我们过滤源码的时候没有发现这个属性的值是受到我们的传值的影响的。然后执行:

watcher.evaluate()

这个函数内部代码如下:

  evaluate () {
    this.value = this.get()
    this.dirty = false
  }

首先就是执行this.get()。这个函数的内部执行如下:

 get () {
    pushTarget(this)
    let value
    const 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
  }

首先是执行pushTarget函数,传入到该函数的thiswatcher,这里的watcher就是我们计算属性所对应的watcher。然后执行:

value = this.getter.call(vm, vm)

这里的this.getter其实就是计算属性对应的函数,在执行计算属性函数的时候,就会执行内部的代码,一旦执行内部的代码就一定会做一件事,那就是去访问属性,一旦访问的属性的话,就会触发属性的getter函数,然后就会进行依赖收集,因为我们已经将计算属性对应的watcher挂载到了Dep.target函数上,用来表明现在如果有属性被访问,那一定是我计算属性干的,所以当属性去获取Dep.target上获取的时候,其实就是获取的是计算属性的watcher。然后将其放入到自己的deps中。当执行完计算属性的函数时,该计算属性所对应的函数依赖也已经完成了,然后返回。返回到get()函数当中,然后执行:

popTarget()
this.cleanupDeps()

popTarget是用来撤销watcher的,而cleanupDeps函数是用来清除依赖的,具体原理这里就不在赘述。然后就返回value。此时这里的value就是执行计算属性的计算结果,也就是我们计算属性的初始值。然后就将计算的值赋值给watcher.value。然后将watcher.dirty置为false。这个操作是很有用的。然后我们回到createComputedGetter函数当中。其实我们发现watcher.evaluate()函数干了两件事,那就是进行依赖收集和获取计算属性的值。然后接着向下执行:

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

此时的Dep.target已经是组件的watcher实例对象了。接着执行depend()函数。这里的watcher还是我们的计算属性的watcher。可能有同学会产生疑惑,watcher.depend()函数的调用是起到什么作用呢?要想回答这个问题,首先我们因该去看一下它内部的源码。

 depend () {
    let i = this.deps.length
    while (i--) {
      this.deps[i].depend()
    }
  }

depend函数的内部,this指向的是watcher,也就是我们的计算属性的watcher。首先获取到this.deps.length。执行这个操作是进行获取我们的计算属性的依赖。也就是计算属性内部引用的属性的个数。然后逐一的调用属性自身depend()函数,调用属性自身的depend函数,这个操作的目的是为了将当前的Dep.target加入到属性自己的deps中,而当前的Dep.target是谁?是组件的watcher,也就是说将组件的watcher添加到了属性的deps中。而这样做会产生什么后果,那就是将我们的计算属性中引入的属性和我们的组件之间建立了关系。也就是说watcher.depend()函数的调用就是为了让组件模板中引入到的计算属性中的属性与我们的组件实例发生关系。

当执行完之后执行:

return watcher.value

你是否还记得我们执行的computedGetter是什么函数吗,其实就是我们属性值的getter函数,也就是说现在我们返回的值,就是我们访问计算属性所需要的值。然后将获取到的值传给_c函数,用于创建VNode

我们现在想一个场景,那就是假如说我们在组件的模板中多次引用了这个计算属性或发生什么呢?首先当我么你去解析第一处引用的时候,理所当然的会执行getter函数,原因是因为watcher.dirty === true。然后就会执行:

  watcher.evaluate()

这就使得属性进行依赖收集,但是当我们在第二处地方引用的时候,就不会执行evaluate函数,原因是因为当我们第一次执行这个函数的时候,执行这段代码:

  evaluate () {
    this.value = this.get()
    this.dirty = false
  }

因为我们创建的计算属性的watcher创建完之后就挂载到了组件的实例对象上,也就是说它一直会保持着我们修改后的状态,不会因为我们访问而再一次的创建对象。

也就是说当我们第一执行的时候就已经将我们计算属性的dirty修改了,此时就不能继续执行get函数了,而是直接返回原有的计算属性值,这样就完成了缓存计算结果的功能,其实这样做可以避免属性再一次的收集依赖。

虽然它避免了直接的收集依赖,但是它没有避免组件和计算属性内部的属性再一次产生关系。

上面就是我们关于计算属性的初始化过程,没有包含其数据变化和数据更改的过程。数据的更改和数据的变化过程我们后续会讲到。

接下来我们讲解计算属性的最后一个阶段,数据改变阶段。

其实对于计算属性内部的值发生变化,我们可以假设两种情况:第一种,我们定义的计算属性并没有被引用到组件的模板当中。第二种,我们定义的计算属性被引入到组件的模板当中。

我们接下来将以两种不同的角度来讲解我们的计算属性值变化而导致组件变化的过程。

因为我们知道,我们在计算属性的第一个阶段的一个重要任务是用来为每一个计算属性创建对应的watcher的,当创建完之后并没有执行this.get()函数,这样就导致计算属性的内部引用的属性并不能收集到计算属性这个依赖。当我们去改变计算属性内部的值的时候,因为计算属性没有去收集依赖,所以当内部引用的属性变化的时候,不会通知我们的计算属性重新计算,进而也不会去让我们计算属性对应的函数执行。着就是为什么我们的计算属性要去收集依赖,目的是让计算属性和属性之间产生依赖联系,当属性的值发生变化的时候,会通知计算属性去进行函数的调用进而重新计算。

当我们在组件模板中去引入我们的计算属性的时候,那么就一定会执行计算属性的getter函数,这个函数我们在前面讲过,它的内部主要是执行两个函数:

watcher.evaluate()
watcher.depend()

第一个函数的代码如下:

  evaluate () {
    this.value = this.get()
    this.dirty = false
  }

它的主要任务是通过调用计算属性的函数,在内部访问属性,进而触发属性的getter函数,让其把计算属性的watcher放到自己的deps中,当属性的值发生改变的时候,方便通知。这样就建立了我们属性与计算属性之间的依赖关系。

但是有一点还需要注意,那就是在组件的眼里,我们的计算属性是什么?当在组件中引入计算属性的时候,组件就把它当作属性一样来对待,此时组件要做的一件事就是将它和计算属性之间建立联系。那么怎么建立联系呢,那就是把我们的计算属性当作普通的属性,然后执行watcher.depend()函数,因为我们的计算属性的watcher已将从Dep.target上移除了,所以此时占山为王的是组件的watcher。当调用depend()函数的时候,因为会执行:

 depend () {
    let i = this.deps.length
    while (i--) {
      this.deps[i].depend()
    }
  }

首先我们要知道this.deps是谁,它其实就是我们计算属性所访问的属性的dep标记。然后通过while循环将计算属性内部涉及到的属性逐一调用它们的depend()函数,此时调用函数就会执行:

  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }

而这个this就是我们计算属性内部访问到的属性,此时的Dep.target其实指的是组件的watcher。此时可能会疑惑,这段代码的作用是什么。其实这段代码的作用是就是将我们的组件的watcher给放到计算属性所访问到的属性的deps中,当完成这段代码,这样就完成了属性和组件watcher之间的联系。注意,是组件和属性之间的联系,而不是组件和计算属性之间的联系

当我们尝试去改变计算属性内部的属性的属性值的时候,此时便会触发属性的set函数。但是此时它会做这样的一个对不:

  if (newVal === value || (newVal !== newVal && value !== value)) {
          return
        }

加入我们设置的新值和旧值一样,那么就会直接返回,不会进行后面的代码执行,因为没有必要。如果我们设置的和以前不一样的值,那么就会在函数中会调用这个函数:

dep.notify()

首先我们来了解一下dep是什么,这是属性的deps,专门来放依赖的,这里一共放了两个依赖:

0: Watcher {vm: Vue, deep: false, user: false, lazy: true, sync: false, …}
1: Watcher {vm: Vue, deep: false, user: false, lazy: false, sync: false, …}
length: 2
__proto__: Array(0)

第一个依赖是计算属性的watcher,第二个依赖是组件的watcher。然后进入到notify函数中,在该函数中注意调用watcher.update函数:

  Dep.prototype.notify = function notify () {
    var subs = this.subs.slice();
    if (!config.async) {
      subs.sort(function (a, b) { return a.id - b.id; });
    }
    for (var i = 0, l = subs.length; i < l; i++) {
      subs[i].update();
    }
  };

我们首先来看计算属性的update函数做了什么:

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

首先是判断this.lazy。因为第一个调用的watcher是计算属性的,所以这里的this指向的就是我们的计算属性的watcher。这里this.lazytrue。所以设置watcher.dirtytrue。然后返回到notify函数中,然后进行对组件watcherupdate的调用。当组件调用update函数的时候,其实走的是queueWatcher(this)这个函数,然后我们来看这个函数内部做了什么。

 function queueWatcher (watcher) {
    var id = watcher.id;
    if (has[id] == null) {
      has[id] = true;
      if (!flushing) {
        queue.push(watcher);
      } else {
        // if already flushing, splice the watcher based on its id
        // if already past its id, it will be run next immediately.
        var i = queue.length - 1;
        while (i > index && queue[i].id > watcher.id) {
          i--;
        }
        queue.splice(i + 1, 0, watcher);
      }
      // queue the flush
      if (!waiting) {
        waiting = true;
​
        if (!config.async) {
          flushSchedulerQueue();
          return
        }
        nextTick(flushSchedulerQueue);
      }
    }
  }

这个函数做了两件事:第一就是将组件的watcher放到queue渲染队列中,然后就是调用nextTick函数并将flushSchedulerQueue传入。其实nextTick就是延迟执行flushSchedulerQueue函数,该函数的具体内容如下:

function flushSchedulerQueue () {
  currentFlushTimestamp = getNow()
  flushing = true
  let watcher, id
​
  queue.sort((a, b) => a.id - b.id)
​
  for (index = 0; index < queue.length; index++) {
    watcher = queue[index]
    if (watcher.before) {
      watcher.before()
    }
    id = watcher.id
    has[id] = null
    watcher.run()
    // in dev build, check and stop circular updates.
    if (process.env.NODE_ENV !== 'production' && has[id] != null) {
      circular[id] = (circular[id] || 0) + 1
      if (circular[id] > MAX_UPDATE_COUNT) {
        warn(
          'You may have an infinite update loop ' + (
            watcher.user
              ? `in watcher with expression "${watcher.expression}"`
              : `in a component render function.`
          ),
          watcher.vm
        )
        break
      }
    }
  }
​
  const activatedQueue = activatedChildren.slice()
  const updatedQueue = queue.slice()
​
  resetSchedulerState()
​
  callActivatedHooks(activatedQueue)
  callUpdatedHooks(updatedQueue)
​
  if (devtools && config.devtools) {
    devtools.emit('flush')
  }
}

在这个函数中做了一件很重要的事前,那就是执行:

watcher.run()

函数,我们来看这个函数:

  run () {
    if (this.active) {
      const value = this.get()
      if (
        value !== this.value ||
        isObject(value) ||
        this.deep
      ) {
        // set new value
        const oldValue = this.value
        this.value = value
        if (this.user) {
          try {
            this.cb.call(this.vm, value, oldValue)
          } catch (e) {
            handleError(e, this.vm, `callback for watcher "${this.expression}"`)
          }
        } else {
          this.cb.call(this.vm, value, oldValue)
        }
      }
    }
  }

在这个函数中会调用get()。而这个函数内部会执行:

value = this.getter.call(vm, vm)

而这个函数,正是组件的updateComponent函数,当执行这个函数的时候就会再一次的引发一次组件的VNode创建,而在创建的过程中,这一定会访问到我们的计算属性的getter函数,当触发计算属性的getter函数的时候就会和我们上面讲的一样,会执行:

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

首先判断watcher.dirty。因为属性在调用notify进行通知的时候,将我们的计算属性的dirty改为了真,所以才会通过这个条件分支,这就是我们建立计算属性和属性之间的联系的好处。因为这个成立了,所以就会执行evaluate函数,这个函数的作用前面讲了,主要是执行:

  evaluate () {
    this.value = this.get()
    this.dirty = false
  }

通过调用watcher.get()函数来获取最新的属性改变后的计算结果。然后赋值给watcher.value。在这个过程中会伴随着依赖的重新收集,而对于依赖的重新收集的处理,我们在组件渲染的时候已将进行讲解了,这里就不在过多的介绍了。然后将this.dirty置为false,这一步是很重要的。当this.dirty === true时候才会调用watcher.get()重新渲染求值。但是当我们调用一次该函数的时候就会将dirty变为false。当我们在组件的其他地方引用的时候,因为是false所以直接返回以前的计算结果,即watcher.value。现在有一个问题,那就是什么时候才会让我们的计算属性的函数重新执行,也就是说什么时候再把我们的watcher.dirty置为true。那就是当计算属性引入的属性改变的时候,就将其置为true。我们在前面讲过,当属性的值发生变化的时候,会执行:

 for (var i = 0, l = subs.length; i < l; i++) {
      subs[i].update();
    }

如果这个属性是我们计算属性引入的,那么该属性就会有计算属性的watcher。当在循环执行watcher.update的时候,就会执行到计算属性的update。而这个计算属性的update的主要功能就是将wathcer.dirty置为true。表示下一次在访问到计算属性的时候需要重新执行计算属性的函数,因为内部引入的属性值发生了变化。这就是Vue对于计算属性缓存的原理。其实很简单就是实用了watcher.dirty这个开关。

回到我们的代码中。我们上面说到了通过执行watcher.get()获取到了最新的值,但是这个函数执行的时候会伴随依赖的重新收集。然后将获取到的最新的数据赋值给wathcer.value。然后回到computedGetter也就是计算属性的getter函数当中,然后将watcher.value返回,此时我们的组件的VNode就获取到了最新的计算属性的值了。

我们来总结一下计算属性的更新过程,当我们去视图改变一个计算属性引入的属性的值的时候,首先判断你设置的值和以前的是否一致,如果一致,那么就直接返回,因为没有去更新的必要。如果你设置的值是一个新值,首先就会去调用这个属性的dep.notify函数,这个函数就是通知与这个属性建立联系的watcher。首先是计算属性的watcher。调用watcher.update()函数,计算属性的upate()函数其实就只做了一件事,那就是将this.dirty = true。然后就开始执行组件的watcher.update()函数,这个函数的作用其实是用来重新渲染这个组件的,因为是重新渲染组件,那么就一定会做一件事,那就是构建VNode。在构建的过程中,一定会重新的访问计算属性,此时就会调用计算属性的getter函数,这个函数有两个作用,第一是重新计算属性值,第二件事就是重新进行依赖收集。当这两件事都做完的时候,那么此时组件就获取到了最新的计算属性的值了,然后就是进行组件和计算属性中属性的联系建立。

其实我们发现对于计算属性的更新是一个曲线救国的过程,为什么这么说呢,因为当属性更待,那么属性就会将我们的计算属性的函数可执行的开关打开。执不执行和它就没有一点关系了,然后就是通知组件更新,在更新的时候一定会访问计算属性而导致函数的执行(因为此时将计算属性的函数置为可执行)。

以上就是Vue对计算属性操作的整体性原理。

监听属性

对于监听属性,我们首先来看它的使用:

var vm = new Vue({
          el:"#app",
        data:{
            message:2
        }
          watch: {
            message:function(val){
                console.log("changed")
            }
          }
})

当我们试图去改变message这个值的时候,那么此时就会去调用监听属性所对应的函数。

因为监听属性还有很多的使用场景,所以我们暂时先以我们的实例为主线,然后再进行其他情况的讲解。我们来看源码:

if (opts.watch && opts.watch !== nativeWatch) {//火狐中也有watch这个属性
    initWatch(vm, opts.watch)
  }

这段代码是在initState函数中,首先执行的是initWatch()函数,我们来看这个函数:

function initWatch (vm: Component, watch: Object) {
  for (const key in watch) {
    const 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)
    }
  }
}

通过循环,获取到监听属性对应的函数值,因为我们的回调函数不是一个数组,所以会调用createWatcher()函数,该函数的具体代码如下:

{
  if (isPlainObject(handler)) {
    options = handler
    handler = handler.handler
  }
  if (typeof handler === 'string') {
    handler = vm[handler]
  }
  return vm.$watch(expOrFn, handler, options)
}

上面的两个分支是对我们回调函数的规范化处理,然后调用vm.$watch()函数。这里的expOrFn其实就是key。我们来看$watch函数的内部实现:

 {
    const vm: Component = this
    if (isPlainObject(cb)) {
      return createWatcher(vm, expOrFn, cb, options)
    }
    options = options || {}
    options.user = true
    const watcher = new Watcher(vm, expOrFn, cb, options)
    if (options.immediate) {
      try {
        cb.call(vm, watcher.value)
      } catch (error) {
        handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
      }
    }
    return function unwatchFn () {
      watcher.teardown()
    }
  }

因为我们的回调函数不是一个对象,所以不会执行第一个分支,然后创建一个options。并且options.user = true。然后就是创建watcher对象了。我们进入到这个构造函数的内部:

if (isRenderWatcher) {
      vm._watcher = this
    }
    vm._watchers.push(this)
    // options
    if (options) {
      this.deep = !!options.deep
      this.user = !!options.user
    //....
    } else {
      this.deep = this.user = this.lazy = this.sync = false
    }
  //........
    this.expression = process.env.NODE_ENV !== 'production'
      ? expOrFn.toString()
      : ''
    // parse expression for getter
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
      if (!this.getter) {
        this.getter = noop
        process.env.NODE_ENV !== 'production' && 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()
  }

因为代码很多,我删掉了一些无关紧要的代码。首先,我们创建的watcher并不是一个渲染watcher,所以会将其添加到vm._watchers中。然后就是一些挂载,然后它会去判断我们的key是否是一个函数,不是所以走else分支,然后执行:

this.getter = parsePath(expOrFn)

parsePath函数的具体代码如下:

export function parsePath (path: string): any {
  if (bailRE.test(path)) {
    return
  }
  const segments = path.split('.')
  return function (obj) {
    for (let i = 0; i < segments.length; i++) {
      if (!obj) return
      obj = obj[segments[i]]
    }
    return obj
  }
}
​

这个函数返回一个函数,我们待会会讲解到它,然后继续执行:

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

因为this.lazyfalse。所以会调用this.get函数,这个函数的内部代码如下:

  get () {
    pushTarget(this)
    let value
    const 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 {
      if (this.deep) {
        traverse(value)
      }
      popTarget()
      this.cleanupDeps()
    }
    return value
  }

首先就是调用pushTatget,将我们当前监听属性的watcher挂载到全局的Dep.target上。然后执行:

this.getter.call(vm,vm)

this.getter.call函数如下:

function (obj) {//obj === vm
    for (let i = 0; i < segments.length; i++) {
      if (!obj) return
      obj = obj[segments[i]]
    }
    return obj
  }

segments是装有key即属性名的元素。然后执行obj[segments[i]]。当执行这个代码的时候一定会触发一件事,那就是我们对segments[i]属性的访问,这里是message属性的访问。也就是说这段代码其实就是用来访问我们的属性的,然后触发属性的getter函数,将当前Dep.target上的watcher放到当前访问的属性的deps中。这段代码就是干这件事的。当执行完之后,开始回退,然后就回退到get()函数当中。接着执行popTarget函数将我们的当前计算属性的watcher移除。

接着继续进行回退,然后回退到$watch函数当中。到这里,其实关于watcher的初始化工作就已经完成了。

接着我们来看,当我们试图去改变message会发生什么。首先就是触发属性的setter函数:

 set: function reactiveSetter (newVal) {
     //....
      dep.notify()
    }

在这个函数中,最重要的一段代码就是调用dep.notify函数:

  notify () {
//.......
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }

这个函数会触发监听属性的watcher中的update函数,我们来看这个函数:

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

在这个函数中会最终执行queueWatcher(watcher)函数,而在queueWatcher(watcher)函数中,最后会执行:

 nextTick(flushSchedulerQueue);

也就是执行flushSchedulerQueue。而这个函数的作用是用来更新的,它的内部代码为:

 function flushSchedulerQueue () {
        //......
      watcher.run();
    //....
  }

因为代码很长,所以我省略调用了大部分暂时无用的代码。从这个函数的内部代码可以看到,该函数最中执行了watcher.run()函数,我们来看这个函数的内部实现:

  run () {
    if (this.active) {
      const 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
        const oldValue = this.value
        this.value = value
        if (this.user) {
          try {
            this.cb.call(this.vm, value, oldValue)
          } catch (e) {
            handleError(e, this.vm, `callback for watcher "${this.expression}"`)
          }
        } else {
          this.cb.call(this.vm, value, oldValue)
        }
      }
    }
  }

进入该函数中,首先就是再一次调用watcher.get()函数,在这个函数中又会访问一次属性,然后导致属性对我们该监听属性的依赖收集。然后就执行:

  this.cb.call(this.vm, value, oldValue)

可能后有同学会问,这个cb是什么,其实就是我们的监听属性对应的监听函数。然后将新旧值分别传入。接着就进行函数回退。因为我们这个跟新属于异步更新,所以会一直回退到整个项目代码的最后。当会退完之后,我们的监听工作就执行完毕了。

我们现在来总结一下关于监听属性的流程。首先关于监听属性,一共分为两个阶段:第一是初始化监听阶段。第二是修改属性触发监听函数阶段。接下来我们分段来说。

【用中文括号括起来的部分是为了方便自己理解,发文的时候不要放到网上,这种理解是迫不得已的(最后还是发到网上了,hahaha)】

【在初始化监听阶段,首先Vue回去watch配置项中去循环遍历,为每一个监听属性去创建一个watcher实例对象,在创建watcher时有一个操作是很重要的,那就是:

 this.cb = cb//这里的cb就是监听属性的函数值

当这段代码一执行,那么此时的watcher就和我们的监听属性产生了关系,我们暂且不去关心这个监听属性叫什么名字,我们只需要知道这个watcher已经和当前的监听属性产生了关系。为什么我这里说暂时不去关心这个监听属性的名字。有人会说,如果我们的watchmessage,那么这个watcher就一定和message有联系,这个watcher就一定会被装到messagedeps中,其实你有这种想法是很正常的,但是这里需要明白的是,我们的watcher被那个属性收集,其实不是监听属性的属性名来决定的,而是由后面当一个属性被访问的时候,Dep.target上是谁来决定的。假如我们的监听属性的属性名是一个message,但是当message被访问的时候,Dep.target上面不是这个watcher

其实监听属性的属性名只是一个媒介,什么意思呢?假如我们这样写:

function fnc(){}
watch:{
    message:fnc
}

其实是想表达,希望vm.$data中的message能和我们的fnc函数关联起来,当vm.$data中的message发生改变的时候,希望调用fnc这个函数。那么就不得不将我们创建的监听属性的watcher和我们的vm.$data.message联系起来了,因为在watcher.cb中存的就是fnc。现在问题就来了,我们如何让两者产生关系呢。Vue的内部是这样做的。

首先是创建watcher。然后将watcher添加到Dep.target。然后通过监听属性的属性名来进行对vm.$data中同名属性的访问,这就导致了会触发vm.$data.messagegetter函数,然后这个函数就会去收集Dep.target变量中的watcher。正是含有fnc函数的watcher。当vm.$data.message属性

也就是说,属性名只不过是 将watcher添加到vm.$data.messagedeps中的一个关键标识。而真正让watcher和属性建立关系的还是正确的依赖收集,而谁能够保证正确的依赖收集,那就是监听属性的属性名。请记住我们的目的,是让vm.$data.messagefnc产生关系,而不是让watch.messagefnc产生关系。

其实属性名的存在就是为了让watcher能正确到对应属性的deps中。】

首先是创建一个唯一的watcher,该实例的cb属性挂载的就是我们的监听函数。然后调用watcher.get()函数,让当前监听属性的watcher被挂载到Dep.target属性上。然后就开始通过字符串形式的监听属性的属性名去驱动Vue访问我们想要监听的属性的值,当访问到这个属性值的时候,就一定会发生一件事情,那就是触发该属性的依赖收集,然后该属性就会将Dep.target上的watcher也就是当前的watcher实例,添加到属性的deps中,也就是依赖收集完成。

接下来我们来看第二个阶段,那就是当我们去改变属性的值的时候。当我们去改变属性的值的时候,就一定会发生一件事,那就是触发属性的setter函数。当我们去触发属性的setter函数,那么就一定会执行这段代码:

dep.notify()

这个函数的内部的主要任务就是循环执行deps中的watcherupdate函数。而update函数的内部是这样的:

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

也就是说update这个函数最终会执行:queueWatcher。而在queueWatcher函数的内部会执行nextTick(flushSchedulerQueue);这个函数,记住这个函数是异步的。也就是当Vue把任务完成之后才能够去执行flushSchedulerQueue这个函数。我们假设当前运行栈内部没有其他的任务,那么就会执行:flushSchedulerQueue函数,这个函数的内部会执行:watcher.run()函数。而这个函数的内部会再一次的执行:watcher.get()函数,然后导致让对应属性进行依赖的重新收集。然后就会执行:

this.cb.call(this.vm, value, oldValue)

这一步很重要,这一步是我们整个流程的核心部分。那就是调用我们的监听函数。当调用完这个函数之后,我们的监听任务就算是基本完成了。

其他情况的补充

首先是我们的initWatch函数:

function initWatch (vm: Component, watch: Object) {
  for (const key in watch) {
    const 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)
    }
  }
}

通过上面的代码,我们可以发现它对handler进行了数组判断,也就是说它可以允许我们监听属性的属性值是一个数组,例如:

watch:{
    name:[()=>{},()=>{}]
}

那么当name发生改变的时候,就会依次调用数组中的监听函数。而能够依次调用的原理是,它为每一个监听函数都创建了一个watcher。从这个角度来看,Vue对于watcher的创建并不是按照计算属性的属性名来做的,而是根据监听函数来创建的,这就再一次印证了我们前面说的计算属性的属性名执行让watcher能够正确的放入到目标属性的deps中的一个可靠媒介。毕竟我们想让监听函数和我们的属性发生关系,而不是监听属性。按照我们上面的实例,那么name这个属性就会由两个watcher实例。

接着就是createWatcher函数:

{
  if (isPlainObject(handler)) {
    options = handler
    handler = handler.handler
  }
  if (typeof handler === 'string') {
    handler = vm[handler]
  }
  return vm.$watch(expOrFn, handler, options)
}

其实通过上卖弄的第一个if判断,我们就可以知道,Vue允许我们这样去定义监听函数:

watch:{
    name:{
        handler(){
            //....
        }
    }
}

然后就是$.watch函数了,这个函数是Vue对外暴露的API,所以我们可以直接的去使用它,而具体的使用如下:

// 键路径
vm.$watch('a.b.c', function (newVal, oldVal) {
  // 做点什么
})
​
// 函数
vm.$watch(
  function () {
    // 表达式 `this.a + this.b` 每次得出一个不同的结果时
    // 处理函数都会被调用。
    // 这就像监听一个未被定义的计算属性
    return this.a + this.b
  },
  function (newVal, oldVal) {
    // 做点什么
  }
)

vm.$watch 返回一个取消观察函数,用来停止触发回调:

var unwatch = vm.$watch('a', cb)
// 之后取消观察
unwatch()
  • 选项:deep

    为了发现对象内部值的变化,可以在选项参数中指定 deep: true。注意监听数组的变更不需要这么做。

    vm.$watch('someObject', callback, {
      deep: true
    })
    vm.someObject.nestedValue = 123
    // callback is fired
    
  • 选项:immediate

    在选项参数中指定 immediate: true 将立即以表达式的当前值触发回调:

    vm.$watch('a', callback, {
      immediate: true
    })
    // 立即以 `a` 的当前值触发回调
    

    注意在带有 immediate 选项时,你不能在第一次回调时取消侦听给定的 property。

    // 这会导致报错
    var unwatch = vm.$watch(
      'value',
      function () {
        doSomething()
        unwatch()
      },
      { immediate: true }
    )
    

    如果你仍然希望在回调内部调用一个取消侦听的函数,你应该先检查其函数的可用性:

    var unwatch = vm.$watch(
      'value',
      function () {
        doSomething()
        if (unwatch) {
          unwatch()
        }
      },
      { immediate: true }
    )
    

其实我们可以发现,Vue给了我们很大的空间去让我们监听一个值的变化,灵活,那么就代表内部的代码要处理多种情况,我们一同来看:

{
    const vm: Component = this
    if (isPlainObject(cb)) {
      return createWatcher(vm, expOrFn, cb, options)
    }
    options = options || {}
    options.user = true
    const watcher = new Watcher(vm, expOrFn, cb, options)
    if (options.immediate) {
      try {
        cb.call(vm, watcher.value)
      } catch (error) {
        handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
      }
    }
    return function unwatchFn () {
      watcher.teardown()
    }
  }

假如我们传入的cb是一个对象,那么就通过调用createWatcher函数来解析,一般情况下是不会的。然后就去调用new Watcher构造函数,其实这个构造函数内部还有很多我们需要知道的事情,但是因为还没有讲到哪些情况,所以我们暂时忽略它。但是我们要知道new Watcher期间发生了什么。首先创建一个watcher实例对象,然后通过调用get()函数进行依赖收集等一系列的工作。如果我们传递了immediate == true,那么就会立即调用监听函数。然后返回一个unwatchFn函数。这个函数的作用是用来停止触发回调函数的。我们来看看这个函数的内部实现:

  teardown () {
    if (this.active) {
      if (!this.vm._isBeingDestroyed) {
        remove(this.vm._watchers, this)
      }
      let i = this.deps.length
      while (i--) {
        this.deps[i].removeSub(this)
      }
      this.active = false
    }
  }

首先判断watcher.active这个属性,一般情况下都为true。然后判断是否这个组件被销毁了,如果销毁了,那么直接就remove掉这个组件和其他组件的关系等一系列操做。如果没有被销毁,那么就执行:

  let i = this.deps.length
      while (i--) {
        this.deps[i].removeSub(this)
      }

其实这段代码的操做就是将我们的watcher从属性的deps中删除掉。这样,当我们再去改变属性的时候,就通知不到watcher,进而也不会调用监听函数了。

上面还有一个事项,那就是:

注意在带有 immediate 选项时,你不能在第一次回调时取消侦听给定的 property。

// 这会导致报错
var unwatch = vm.$watch(
  'value',
  function () {
    doSomething()
    unwatch()
  },
  { immediate: true }
)

如果你仍然希望在回调内部调用一个取消侦听的函数,你应该先检查其函数的可用性:

var unwatch = vm.$watch(
  'value',
  function () {
    doSomething()
    if (unwatch) {
      unwatch()
    }
  },
  { immediate: true }
)

为什么会发生这样的事情呢,原因是因为,当我们去立即执行cb的时候,$watch函数并没有将我们的unWatchFn函数赋值给unwatch函数,此时便会导致unwatch变量为undefined,这个值是不能通过函数来调用的。

监听对象属性

当我们去监听一个对象属性的时候,其实是有点麻烦的,为什么这么说呢,监听对象属性的格式和我们普通属性的监听格式不一样。例如:

普通格式

watch:{
    name:()=>{}
}

我们可以直接写属性名,但是如果我们去监听对象属性:

watch:{
    person.name:()=>{}
}

我们这样写一定会报错,因为不能通过语法检查,那么我们就需要这样写了:

watch:{
    "person.name":()=>{}
}

此时问题就来了,vm.$data中没有一个名为"person.name"的属性,其实我们想监听的是vm.$data.person.name这个属性,所以我们需要对我们的监听属性的属性名进行处理。解析的具体过程在parsePath函数当中:

export function parsePath (path: string): any {
  if (bailRE.test(path)) {
    return
  }
  const segments = path.split('.')
  return function (obj) {
    for (let i = 0; i < segments.length; i++) {
      if (!obj) return
      obj = obj[segments[i]]
    }
    return obj
  }
}

这个函数就是用来进行监听属性名分析的,它首先会执行:

  const segments = path.split('.')

假如我们监听属性的属性名是person.name,那么最后生成的segments将会是:

segmengts = ['person','name']

有意思的事情要来了。当我们在watcher.get()函数执行上面的函数的时候,我们会执行:

  for (let i = 0; i < segments.length; i++) {
      if (!obj) return
      obj = obj[segments[i]]
    }

首先是执行:

obj = vm[segments[0]]//即 obj = vm['person']

首先是访问vm.$data.person。当访问这个属性的时候,该属性就会把监听函数对应的watcher添加到自己的deps中,然后继续执行。此时的obj就变成了vm.$data.person对象然后再次进入循环:

  for (let i = 0; i < segments.length; i++) {
      if (!obj) return
      obj = obj[segments[i]]
    }

此时上面的代码就等价于:

obj = vm.$data.person['name']

当执行这段代码的时候,就会访问到name属性,然后会将我们的监听函数的watcher放到该属性的deps中。

那么这就产生了一个问题,那就是我们原本只想监听一个属性,但是我们却为两个属性添加了watcher。也就是当我们去改变vm.$data.person.namevm.$data.person都会去触发监听函数的执行。

有时候我们会遇到这样的一个情况:

vm.$watch('person',function(){
    console.log("这个对象内部的属性值发生了变化")
})

我们想去监听一个对象内部的属性值的变化。但是这样写有什么缺点呢,如果我们这样写,那么什么时候才能够触发这个函数的,答案是vm.$data.person这个属性被重新赋值。但是我们并不只是想改变这个对象时触发函数,而是还有当我们去改变对象中的属性的时候去触发函数,那么我们就可以为vm.$watch传入第三个参数:

vm.$watch('person', callback, {
  deep: true
})

第三个参数是一个对象,当这个对象有一个名为deep:true的参数的时候,那么当person对象中的任意层级的属性值发生了变化,那么这个函数都会被执行。那么它是怎么做到的呢?我们来看源码:

this.deep = !!options.deep

new Watcher的时候内部会有这样一行代码,就是判断你否传入了deep这个参数。那么这个属性在什么时候会用到呢。当我们执行watcher.get()函数的时候,会进行依赖收集,此时会将watcher放到persondeps中。而且它还回去执行:

    if (this.deep) {
        traverse(value)
      }

它会去判断我们是否传入了deep这个值,因为我们设置了这个值,所以会执行:traverse(value)。而value就是我们person的值,即一个对象。那么我们来看这个函数的内部做了什么:

export function traverse (val: any) {
  _traverse(val, seenObjects)
  seenObjects.clear()
}
​

这个函数的内部调用了_traverse()函数,我们来看这个函数:

function _traverse (val: any, seen: SimpleSet) {
  let i, keys
  const isA = Array.isArray(val)
  if ((!isA && !isObject(val)) || Object.isFrozen(val) || val instanceof VNode) {
    return
  }
  if (val.__ob__) {
    const 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)
  }
}

这里的seenObject是一个new Set()实例。首先判断我们的value是否是一个arr,因为我们传入的是对象,所以不是,然后进行第一个if判断。这个判断走不到。然后判断val.__ob__,看我们传入的对象是否是一个已经被观察的对象。很显然,是的。因为_traverse这个函数其实是一个递归函数,第二个if分支是一个出口,所以我们暂时不讲,我们先来看后面的代码:

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)
  }

首先看我们的value是否是一个数组,很显然不是,所以我们走else分支,然后回去到当前value对象的key。这个key是直属属性的,不包括子对象的属性名。然后获取直接属性名的个数,通过循环调用_traverse。注意:重点来了,我们传给_traverse函数的第一个参数是什么:val[keys[i]]。那么我问你,这是什么,是不是属性的属性值,那么它哪来的属性值,是不是我们访问该属性得到的,也就是说,当我们传入这个参数的时候,我们同时也访问了这个属性,那么就会触发这个属性的getter函数,此时这个属性就会进行依赖收集,此时挂载到Dep.target属性上的是谁,是我们的监听函数。也就是说通过调用_traverse函数并传入参数这种形式来将我们的value对象中的每一个层级属性都访问一遍,这样就使得这些属性都会收集该监听函数的watcher。这样就完成了一个操作,那就是当value中的任何一个属性发生变化的时候,都会执行这个监听函数。这样就完成了我们对一个对象的整个监听操作。