学透Vue源码~Computed和Watch区别是什么?

·  阅读 2365
学透Vue源码~Computed和Watch区别是什么?

前言

Computed和Watch区别是什么?

这可能是Vue技术面试最常问到的面试问题之一,我们都知道计算数据是监听数据变化并返回计算函数返回值的,watch就是单纯的数据监听处理。那么二者在源码中究竟又是如何实现的呢?

下面我们就从源码实现层面为同学们详细说一说二者的区别,加深各位同学对计算属性和watch的理解。

读完本篇文章各位同学将会掌握如下几点知识:

  • 从源码理解计算属性和watch的实现原理;
  • Watcher的调度逻辑;
  • Vue源码中的依赖收集过程;
  • 渲染Watcher的理解
  • Vue实例的初始化流程

阅读本文前需要对Vue的响应式原理有一定的了解,响应式原理的内容不在这篇进行讲解,感兴趣的可以看这篇学透Vue源码~nextTick原理的响应式原理部分。

注意:

  • 本文是基于Vue2.6的源码解读
  • 本文旨在解析computed和watch的源码实现,同时剧情需要也对Vue源码中watcher调度、依赖收集、vue实例初始化流程等都有介绍,篇幅较长,可能需要各位同学30分钟左右时间详细阅读并思考+理解,相信各位坚持看到最后一定会有所收获。

准备开始

首先我们从Vue的初始化流程看起,我们首先找到Vue2.6源码中的src/core/instance/index.js,去查看计算属性的初始化过程,来理解其原理。

function Vue (options) {
  if (process.env.NODE_ENV !== 'production' &&
      !(this instanceof Vue)
     ) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  this._init(options)
}
//为Vue的原型对象挂在一些全局方法,如_init()等方法
initMixin(Vue)
//为Vue的原型对象挂一些处理状态的方法
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)
export default Vue
复制代码

我们看到在index.js中找到这样一段代码,此处的Vue函数就是我们通过new Vue()创建Vue实例的构造函数,内部调用了this._init()。我们全局搜索发现_init()这个内部方法是在initMixin(Vue)中添加的,我们直接查看这个函数。

export function initMixin (Vue: Class<Component>) {
  Vue.prototype._init = function (options?: Object) {
    /* 省略无关代码... */
    //对生命周期做初始化操作
    initLifecycle(vm)
    //事件初始化
    initEvents(vm)
    initRender(vm)
    //调用beforeCreate生命周期函数
    callHook(vm, 'beforeCreate')
    initInjections(vm) // resolve injections before data/props
    //初始化处理data/props/computed/watch等
    initState(vm)
    initProvide(vm) // resolve provide after data/props
    //调用created生命周期函数
    callHook(vm, 'created')
    /* 省略无关代码... */
  }
}
复制代码

我们省略无关代码,发现在initMixin函数中做了很多的初始化操作,包括生命周期、事件、渲染、状态等。这次我们只关注initSate()函数中,这个函数中做了对datacomputedwatch初始化操作,我们看一下下面的代码。

export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  //处组件属性
  if (opts.props) initProps(vm, opts.props)
  //处理method
  if (opts.methods) initMethods(vm, opts.methods)
  if (opts.data) {
    //处理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)
  }
}
复制代码

因为这次主要是理解计算属性computedwatch,因此我们就只看上面代码中的initComputedinitWatch函数的实现。因为initWatch的实现比较简单容易理解,我们把它放到最后🐶,我们就来先看一下computed的初始化过程。

Computed

创建计算属性Watcher

不多说,直接进入正题,我们一起来看initComputed()函数都做了啥事。

const computedWatcherOptions = { lazy: true }
//计算属性的初始化函数
function initComputed (vm: Component, computed: Object) {
  //在vm实例上创建一个_compiutedWatchers的空对象
  const watchers = vm._computedWatchers = Object.create(null)
  //遍历computed选项
  for (const key in computed) {
    const userDef = computed[key]
    //获取计算属性对应的getter函数,
    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
      )
    }
    //为计算属性创建一个Watcher,并保存到组件实例上的计算属性集合watchers中
    watchers[key] = new Watcher(
      vm,
      getter || noop,
      noop,
      computedWatcherOptions
    );
    //判断实例上是不是已经又了与计算属性同名的key
    if (!(key in vm)) {
      //在vm实例上定义计算属性,即定义计算属性对应的key(date或者props中)
      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)
      }
    }
  }
}
复制代码

虽然在上面的代码中我们给出了一些注释,为了加深理解,我们下面详细的说一下initComputed都做了那些事。

首先此函数在传入的vm实例还是那个创建一个_computedWatchers用于保存每个计算属性的Watcher实例,之后遍历computed选项中的key检查我们的定义是否合法,即是否为计算属性的每个key定义了一个函数作为getter,或者一个带有get函数的对象;

然后就是比较关键的一步,也是我们要着重关注一下的,就是我们为每个计算属性创建了一个Watcher实例,这里我们关注一下已给创建Watcher实例时的传参。

new Watcher(
  vm,
  getter || noop,
  noop,
  computedWatcherOptions
 );
复制代码

这里我们忽略无关的细节,不去关心第三个参数,分别说一下第1,2,4个参数的含义,这里说明一下noopVue的一个内部表示什么都不做的一个函数,即不做操作的意思。首先第一个参数就是当前的vm实例,第二个参数是计算属性对应的getter函数,最后一个参数就是我们在代码的第一行定义的计算属性的配置,只有一个属性lazytrue

代码的最后我们对每个计算属性做了一个判断,是不是已经在vm上定义过同名的data或者props,定义过的话不做处理,直接执行warn提醒开发者错误信息,否则执行defineComputed(vm,key,userDef)函数,这个函数传入的3个参数分别的,vm实例,计算属性对应的key,以及开发者定义的计算属性上的值,可能是一个带有get的对象或者直接是一个函数。那么接下来我们来看defineComputed函数的实现。

计算属性定义到组件实例

闲话少说,我们先进入代码中去看一下。

const sharedPropertyDefinition = {
  enumerable: true,
  configurable: true,
  get: noop,
  set: noop
}
export function defineComputed (
target: any,
 key: string,
 userDef: Object | Function
) {
  if (typeof userDef === 'function') {//如果是计算属性对应的key定义为一个函数
    sharedPropertyDefinition.get = createComputedGetter(key)
    sharedPropertyDefinition.set = noop
  } else {//如果是一个计算属性对应的key定义为一个对象
    sharedPropertyDefinition.get = userDef.get
      ? createComputedGetter(key): 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
      )
    }
  }
  //在vm上定义一个同名的key,并且为它设置属性定义配置
  Object.defineProperty(target, key, sharedPropertyDefinition)
}
复制代码

上面这段代码的核心就是最后一行,即为计算属性在vm上定义一个同名的属性,这就是为什么我们能够通过this的点语法直接在vm上访问到计算属性的的原因。

这里的关键就是我们在之前定义的sharedPropertyDefinition,这个结构我们很熟悉,就是定义属性时的一些配置,这里我们关心的是它的getset配置,即获取属性和设置属性的代理方法。

同时我们注意到,我们在此函数中也对sharedPropertyDefinitiongetset分别进行了赋值,这里分了两种情况进行处理,即userDef是函数和对象这两种情况。

不管是那种情况我们注意到都是通过createComputedGetter封装返回一个闭包函数,那么我们就看一下这个函数的处理过程。

function createComputedGetter (key) {
  //返回了一个函数,作为计算属性在vm上的get
  return function computedGetter () {
    //获取到当前计算属性对应的Watcher实例
    const watcher = this._computedWatchers && this._computedWatchers[key]
    if (watcher) {
      //检查数据是否是脏数据,需要更新
      if (watcher.dirty) {
        watcher.evaluate()//触发函数内响应式数据的依赖收集
      }
      //渲染Watcher是否存在
      if (Dep.target) {
        watcher.depend()//会遍历渲染Watcher的this.deps并执行每个dep的depend方法,把渲染Watcher也加入到计算属性内部调用的响应式数据对应的key的依赖中,这样响应式数据更新,不仅会触发计算属性的更新,也会触发渲染函数的更新
      }
      //最后返回watcher的值
      return watcher.value
    }
  }
}
复制代码

在第5行获取之前为计算属性创建的Watcher对象,Watcher默认dirtytrue的,会执行watcher.evaluate()方法。

上面的代码主要是对watcher的操作,需要各位同学熟系Watcher的实现,下面给出精简版的Watcher实现:

class Watcher{
  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ){
      // options
    if (options) {
      this.lazy = !!options.lazy
      this.before = options.before
    } else {
      this.lazy = false
    }
    this.cb = cb
    this.id = ++uid // 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()
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    }
    this.value = this.lazy
      ? undefined
      : this.get()
  }
  addDep (dep: Dep) {
    const id = dep.id
    if (!this.newDepIds.has(id)) {
      this.newDepIds.add(id)
      this.newDeps.push(dep)
      if (!this.depIds.has(id)) {//排重,不重复添加
        //添加当前Watcher到依赖集合中
        dep.addSub(this)
      }
    }
  }
  cleanupDeps() {
    let i = this.deps.length
    while (i--) {
      const dep = this.deps[i]
      if (!this.newDepIds.has(dep.id)) {
        dep.removeSub(this)
      }
    }
    let tmp: any = this.depIds
    this.depIds = this.newDepIds
    this.newDepIds = tmp
    this.newDepIds.clear()
    tmp = this.deps
    this.deps = this.newDeps
    this.newDeps = tmp
    this.newDeps.length = 0
  }
  depend () {
    let i = this.deps.length
    while (i--) {
      this.deps[i].depend()
    }
  }
  evaluate () {
    this.value = this.get()
    this.dirty = false
  }
  get () {
    pushTarget(this)//是当前vue实例的Watcher
    let value
    const vm = this.vm
    value = this.getter.call(vm, vm)//这里getter是render函数
    popTarget()
    //这一步是设置当前计算属性Watcher的this.deps为内部调用的响应式数据的Watcher
    this.cleanupDeps()
    return value
  }
  update () {
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)
    }
  }
  run () {
     const value = this.get()
     //watch的情况变更之后直接
     this.cb.call(this.vm, value, oldValue)
  }
}
复制代码

下面我们看watcherevaluate都做了什么;

evaluate () {
  this.value = this.get()
  this.dirty = false
}
复制代码

我们发现evaluate方法实际上是调用了watcherget方法。

为了更好的理解上面的get方法的实现,我们接下来需要了解两方面的知识:

  1. watcher是如何调度的
  2. 响应式数据的依赖收集是如何进行的

watcher是如何调度的

watcherget方法中我们遇到了两个全局函数,pushTargetpopTarget,要了解这块儿内容我们就需要去了解vue源码中对watcher是如何调度的。我们来看一下下面的代码实现。

Dep.target = null
const targetStack = []
//放入栈
export function pushTarget (target: ?Watcher) {
  targetStack.push(target)
  Dep.target = target
}
//退出栈
export function popTarget () {
  targetStack.pop()
  Dep.target = targetStack[targetStack.length - 1]
}
复制代码

这部分代码Vue使用一个全局唯一的栈targetStack来做Watcher的调度,分别定义了出栈和入栈的函数pushTargetpopTarget,我们看到入栈会直接把Dep.target设置为入栈的Watcher,出栈函数会把栈顶元素出栈,然后设置新的栈顶元素(Watcher)为Dep.target

总结一句话就是,Dep.taget永远是targetStack栈顶的Watcher对象。

响应式数据的依赖收集是如何进行的

我们都知道vue的响应式数据处理是使用 Object.defineProperty实现的,源码中我们通过添加get代理方法来做依赖收集,代码实现如下。

 Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    //get代理将Dep.target即Watcher对象添加到依赖集合中
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {//Dep.target是栈顶Watcher
        dep.depend();//这一步会把栈顶Watcher添加到对应响应式数据key的deps中。
      }
      return value
    }
 }
复制代码

为了方便阅读和理解我们省略了无关代码,如果Dep.target存在我们就执行dep.depend(),我们去Dep.js中看一下Dep类的这个方法的实现。

  //Dep.js
  depend () {
    if (Dep.target) {
      //这一步把当前dep放入到对应的watcher对象中,进行记录,不重复添加;并且在调用dep.addSub添加新的watcher到dep中。
      Dep.target.addDep(this)
    }
  }
复制代码

我们看到这个方法直接调用了Dep.target也就是当前栈顶WatcheraddDep方法, 看一下WatcheraddDep的实现

addDep (dep: Dep) {
    const id = dep.id
    if (!this.newDepIds.has(id)) {
      this.newDepIds.add(id)
      this.newDeps.push(dep)
      dep.addSub(this)
    }
}
复制代码

watcheraddDep中,把depid保存到newDepIds集合中,它是一个Set集合,把dep对象保存到newDeps集合中,它是一个数组,最后调用传入的dep对象的addSub方法把当前watcher保存到dep中。并且我们会通过对集合中是否已存在对应的dep.id来避免重复添加当前watcherdep中。

这里可能有的同学会感觉有点绕,可以这样简单理解依赖收集的过程:当代码中调用响应式数据对应的key时,会触发依赖收集操作,这个操作会把targetStack栈顶的watcher作为依赖收集到当前keydep中,同时也会把depid和对象引用保存到watcher中,做排重和后续计算属性的用途。

理解watcher.get

下面我们来看get的实现

get () {
  //当前计算属性Watcher入栈
  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 {
    //完成计算属性Watcher的依赖收集过程后需要将计算属性Watcher出栈
    popTarget()
    //这一步是设置当前计算属性Watcher的this.deps为内部调用的响应式数据的Watcher
    this.cleanupDeps()
  }
  return value
}
复制代码

首先调用pusTaget入栈,然后会调用getter方法,这个getter方法就是我们传入的计算属性的计算函数,执行和这个方法会使用响应式数据,因此会触发依赖收集过程,会把当前计算数据watcher作为依赖收集到响应式数据的dep中,同时会把响应式数据的dep保存到内部集合newDepsIdnewDeps中。

最后在finally中将当前计算属性出栈,执行cleanupDeps方法,cleanupDeps方法是把newDepsnewDepsId赋值给watcherdepsIddeps集合属性中。

至此就完成了computedGetter函数中watcher.evaluate()的执行。

初始化流程和渲染Watcher

上面我们完成了对evaluate的理解,我们继续分析computedGetter的代码。

   if (watcher) {
      //检查数据是否是脏数据,需要更新
      if (watcher.dirty) {
        watcher.evaluate()//触发函数内响应式数据的依赖收集
      }
      //渲染Watcher是否存在
      if (Dep.target) {
        watcher.depend()//会遍历渲染Watcher的this.deps并执行每个dep的depend方法,把渲染Watcher也加入到计算属性内部调用的响应式数据对应的key的依赖中,这样响应式数据更新,不仅会触发计算属性的更新,也会触发渲染函数的更新
      }
      //最后返回watcher的值
      return watcher.value
    }
复制代码

我们发现这段代码接下来是回去判断Dep.target是否存在,上面我们说过Dep.target是什么,它是targetStack栈顶的watcher对象。那么什么时候Dep.target存在,什么时候他不存在呢?
两点知识:

  1. 渲染watcher
  2. 初始化流程

首先我们要了解一个新概念,即渲染watcher对象,它是vue实例mounted阶段创建的Watcher对象,用于监听模板视图中所有使用到的响应式数据的变,更新视图,调用代码如下:

  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
 
复制代码

这个操作在一个updateComponent函数中执行,他的实现是这样的。

updateComponent = () => {
   vm._update(vm._render(), hydrating)
}
复制代码

render函数时通过compile(vm.tempalte)或者开发者直接在组件实例上定义的render函数,我们把updateComponent函数作为watcher的第二个函数传入到watcher中,他会被赋值到渲染watchergetter,执行getter就是执行updateComponent,进而执行render函数,执行render函数就会调用到template模板中使用到的响应式数据,进而触发依赖收集过程,把当前Dep.target也就是渲染watcher收集到依赖集合中,之后我们每次修改响应属性就会出发渲染watcherrun方法去执行视图变更了。

以上就是对渲染Watcher的简单讲解,下面我们还需要知道在Vue的源代码中计算属性初始化和渲染函数创建的先后顺序,我们通过Vue源码分析Vue实例初始化流程来分析。

//src\core\instance\init.ts 重点关注initState和$mount的执行顺序
Vue.prototype._init = function (options?: Record<string, any>) {
    //省略无关代码...
    initLifecycle(vm)
    initEvents(vm)
    initRender(vm)
    callHook(vm, 'beforeCreate', undefined, false /* setContext */)
    initInjections(vm) // resolve injections before data/props
    //状态初始化,包括data、computed、watch
    initState(vm)
    initProvide(vm) // resolve provide after data/props
    callHook(vm, 'created')
    if (vm.$options.el) {
      //组件模板编译、渲染和挂载等操作
      vm.$mount(vm.$options.el)
    }
 }
//src\platforms\web\runtime\index.ts 这里是$mount的定义
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
}
//src\core\instance\init.ts 关注渲染函数的创建过程
export function mountComponent(
  vm: Component,
  el: Element | null | undefined,
  hydrating?: boolean
): Component {
  //省略无关代码...
  vm.$el = el
  callHook(vm, 'beforeMount')
  let updateComponent = () => {
      vm._update(vm._render(), hydrating)
  }
  const watcherOptions: WatcherOptions = {
    before() {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }
  new Watcher(
    vm,
    updateComponent,
    noop,
    watcherOptions,
    true /* isRenderWatcher */
  )
  if (vm.$vnode == null) {
    vm._isMounted = true
    callHook(vm, 'mounted')
  }
  return vm
}
复制代码

通过代码我们知道,源码中会先执行initState(),后执行vm.$mount(),前者是vue实例中datacomputedwatch等选项的初始化,后者是会做组件模板编译、渲染Watcher创建、挂载dom等操作,因此在创建渲染watcher时,已经完成了数据的响应式处理和计算属性的处理。
如果我们现在模板中使用了计算属性,即在渲染函数中调用了计算属性的key,就会触发计算属性的get方法即上面定义的computeGetter闭包函数,这时会先根据dirty情况判断会有两种情况:

  • 是脏数据,执行evaluate(),就会计算属性watcher会先入targetStack栈,执行getter来得到计算属性watchervalue,这时栈顶元素是计算属性watcher,计算属性函数中所有调用到的响应式数据都会将Dep.target也就是当前计算属性watcher作为依赖收集起来,最后会先将当前计算属性watcher出栈,此时栈顶watcherDep.target是渲染watcher
  • 不是脏数据,此时栈顶watcherDep.target是渲染watcher

然后代码会判断Dep.target的存在,当然它是存在的并且现在它就是渲染watcher对象,因此接下来会执行计算属性watcher.depend(),我们来看下depend方法实现:

 //Watcher.js
 depend () {
    let i = this.deps.length
    while (i--) {
      this.deps[i].depend()
    }
  }
复制代码

方法实现很简单,遍历watcher中的deps集合,然后调用每个depdepend方法。这里的deps集合我们在上面的依赖收集部分说过,它是当前收集了当前watcher的响应式数据对应的dep对象集合,而depdepend方法就是把Dep.targettargetStack栈顶元素收集到当前响应式数据的依赖中。

理解了depend的实现,在这里就是把渲染watcher作为依赖收集到计算属性watcherdeps集合中每个dep中,即计算属性所依赖的响应式数据的dep中,这样,我们更新了计算属性的计算函数中调用的响应式数据的时候,不仅会触发计算属性的变更,也会触发渲染watcherupdate进而更新视图。

优化

计算属性Watcher是借助lazydirty属性实现的缓存优化。

首先我们定义Watcher是默认传入的lazy参数是true,即懒加载,使用缓存;而dirt则是标识当前value是否过期,dirttrue则表示数据已过期,是脏数据,需要更新。

因此对于新创建的watcher,如果lazytrue标识是懒加载的,不会再创建时调用get获取value,同时会使得dirty继承lazy的值;

如果是懒加载的情况即lazytrue,这时dirty也是true,即数据不是最新的,需要更新,那么会在调用代理的get方法是执行watcher.evaluate方法重新获取数据并赋值给value,然后设置dirtyfalse

evaluate () {
  this.value = this.get()
  this.dirty = false
}
复制代码

dirty会在计算属性依赖的key更新时触发当前计算属性对应的watcherupdate方法把dirty设置true,表示数据不再是最新的了,再次获取的时候需要执行evaluate()方法更新。

  update () {
    //懒加载,只标记为脏数据,不立即执行run获取最新的value
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {//同步更新watcher直接执行run
      this.run()
    } else {//异步更新需要加入到异步更新队列
      queueWatcher(this)
    }
  }
复制代码

我们看update中的处理,如果是懒加载,设置this.dirtytrue,即把计算属性标记为脏数据, 不立即执行run方法做更新操作。这里还有对this.sync的处理,这是是否为异步更新的处理。关于异步更新的内容不熟悉的同学可以看一下学透Vue源码~nextTick原理中对Vue异步更新的讲解。

举个例子

上面根据vue的源码详细的介绍了计算属性的实现原理,但是可能有的同学可能还是觉得好像有点晦涩难懂,没关系,下面我们通过一个一个例子,分几种情况,详细说一下计算属性整个的工作流程,相信能帮助各位同学更好的理解。

<template>
  <div>{{myComputedProp}}</div>
</tempalte>
<script>
export default{
  data:{
      num1:1,
      num2:2
  },
  computed:{
    myComputedProp(){
      return this.num1+this.num2;
    }
  },
  mounted(){
    console.log(this.myComputedProp)
  }
}
</script>
复制代码

当我们执行上面的代码时,会发生什么呢?

  1. 计算属性初始化:创建计算属性watcher,将计算属性的key的同名属性添加到组件实例上,并定义get代理方法;
  2. $mount渲染,创建渲染watcher,执行渲染函数出触发依赖收集:我们注意到我们在模板中使用了计算属性myComputedProp,即我们在执行渲染函数时会调用此计算属性myComputedProp,因为此时($mounted执行时)计算属性已经完成了初始化处理,我们调用此计算属性会触发get代理方法,因为计算属性watcher默认dirtytrue,因此会执行evaluate方法,evaluate中会先执行get然后设置dirtyfalseget方法会计算得到计算函数返回的值赋值给watcher.value,这里watcher.value=3num1+num2),并且收集计算属性watchernum1num2dep中,同时把num1num2dep保存到计算属性Watcherdeps中;之后会判断Dep.target的是否存在,这时它是存在的并且是渲染函数,进而执行计算属性watcherdepend方法,这个方法会把渲染watcher同时添加到num1num2dep中;
  3. mounted中打印this.myComputedProp,此时因为myComputedProp对应的watcherdirtyfalse,并且渲染完成后渲染函数也出栈了,这时Dep.targetnull了,因此不会执行watcherevaluatedepend方法,直接返回watchervalue

接下来我们修改一下代码,修改mounted代码如下:

  mounted(){
    this.num1=3;
    console.log(this.myComputedProp)
  }
复制代码

代码会如何执行呢:

  1. 同上
  2. 同上
  3. mounted中我们修改了num1的值,他会触发所有依赖的update方法,这里就会触发计算属性watcher和渲染watcherupdate方法,对于计算属性watcher,会设置dirtytrue,我们在下一行代码中打印this.myComputedProp时,就会执行watcher.evalueate执行计算属性函数,得到最新的计算属性的值赋值给watcher.value=5num1+num2),并再次设置计算属性watcherdirtyfalse;渲染watcher执行渲染函数更新视图(注意:上面提到过,这里不一定是立即执行视图更新,因为vue组件视图更新是异步更新的,因此这里会涉及到$nextTick的实现,不是本章的探讨内容,感兴趣的同学可以擦参考学透Vue源码~nextTick原理)。

再次修改一下代码,修改mounted代码如下:

  created(){
    this.num1=3;
    console.log(this.myComputedProp)
  }
  mounted(){
    console.log(this.myComputedProp)
  }
复制代码

代码会如何执行呢:

  1. 同上
  2. 我们在前面数据vue初始化流程时了解到,created的调用是在initState$mount之间进行的,因此这是计算属性完成了初始化,我们修改num1会触发所有依赖的update方法,这里就只会触发计算属性watcherupdate方法,因为这是在$mount之前,还未创建渲染watcher。计算属性watcher会设置dirtytrue,我们在下一行代码中打印this.myComputedProp时,就会执行watcher.evalueate执行计算属性函数,得到最新的计算属性的值赋值给watcher.value=5num1+num2),并再次设置计算属性watcherdirtyfalse
  3. 同上2
  4. 同上3

Watch

watch的实现就简单一些,只需要为要监听的key新创建一个watcher,把处理函数作为cb参数传入即可。

初始化

function initWatch (vm: Component, watch: Object) {
  for (const key in watch) {
    const handler = watch[key]
    //handler是数组的处理
    if (Array.isArray(handler)) {
      for (let i = 0; i < handler.length; i++) {
        createWatcher(vm, key, handler[i])
      }
    } else {
      createWatcher(vm, key, handler)
    }
  }
}
复制代码

initWatch函数入参是vm和选项中的watch,因为一个watch监听的key下可以有多个handler,所以需要对handler做是否为数组的判断。
最后就是调用createWatcher函数处理,下面我们找到Watcher函数看一下。

function createWatcher (
  vm: Component,
  expOrFn: string | Function,//传入的监听的key
  handler: any,
  options?: Object
) {
  //handler是对象的处理
  if (isPlainObject(handler)) {
    options = handler
    handler = handler.handler
  }
  //handler是字符串的处理
  if (typeof handler === 'string') {
    handler = vm[handler]
  }
  //直接调用$watch监听数据
  return vm.$watch(expOrFn, handler, options)
}
复制代码

$watch

$watch是怎么定义的呢,我们来找一下。
/src/core/instance/index.js

function Vue (options) {
  if (process.env.NODE_ENV !== 'production' &&
    !(this instanceof Vue)
  ) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  this._init(options)
}
//为Vue的原型对象挂在一些全局方法,如_init()等方法
initMixin(Vue)
//$set,$selete,$watch等状态修改原型方法的定义
stateMixin(Vue)
//事件相关
eventsMixin(Vue)
//生命周期相关
lifecycleMixin(Vue)
//渲染
renderMixin(Vue)
export default Vue
复制代码

在下面的函数中我们找到了$watch的定义。
/src/core/instance/state.js

export function stateMixin (Vue: Class<Component>) {
  //忽略无关代码...
  Vue.prototype.$watch = function (
    expOrFn: string | Function,
    cb: any,
    options?: Object
  ): Function {
    const vm: Component = this
    if (isPlainObject(cb)) {
      return createWatcher(vm, expOrFn, cb, options)
    }
    options = options || {}
    options.user = true
    //最终是调用new Watcher()创建了一个vm的data中key的监听对象
    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()
    }
  }
}
复制代码

最终是调用new Watcher()创建了一个vmdatakey的监听对象,这里传入的参数,vmwatch监听属性所在的组件对象,expOrFn为要监听的属性。

最后

通过上面的👆源码分析我们可以知道,计算属性和watch实际上都有监听属性变化的能力。只不过计算属性可以当做Vue实例上的响应式数据使用(通过Object.defineProperty定义了同名属性),会自动监听计算属性函数调用到的响应式数据的变更,并且会返回计算属性函数的返回值;watcher是显式的为要监听的数据创建一个Watcher监听数据变更,不能作为vue实例上的响应式数据值使用。

以上就是本人对于计算属性ComputedWatch的源码分析,码字不易,如果各位同学觉得还不错,有所收获,还望不吝点赞+收藏+关注

本文章收录在本人的专栏中,我会在专栏Vue的学习乐园中持续输出Vue相关文章,包括但不限于使用技巧、源码解读、面试技巧等,感兴趣的同学可以关注一下。如果有同学有Vue相关的问题,也可以在评论区留言,我会尽快答复。

我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿

分类:
前端
收藏成功!
已添加到「」, 点击更改