likes
comments
collection
share

深入浅出Vue源码 - Computed和Watch

作者站长头像
站长
· 阅读数 66

本篇文章将针对Watcher的三种形态(计算属性 watcher (computed watcher)侦听器 watcher(user watcher)渲染 watcher (render watcher))进行源码分析,从而理解ComputedWatch的本质区别。

1.计算属性 watcher (computed watcher)

当我们使用computed属性时,代码通常是这样书写的

computed: {
  isComputed() {
    return this.count + 1
  },
  isGetterComp: {
    get() {
      return this.count + 1
    }
  }
}

之后Vue进行初始化的时候就会通过initComputed去解析我们所写的computed里面属性

src/core/instance/state.js

function initComputed (vm: Component, computed: Object) {
  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

    if (!isSSR) {
      // 创建 Watcher 实例
      watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        { lazy: true }
      )
    }
    ...
    // 挂载到 vm 实例上
    defineComputed(vm, key, userDef)
    ...
  }
}
  1. 初始化时会根据输入方式的不同获取computed中的getter方法,如果是函数,则直接将该函数赋值给getter方法;否则则取出计算属性的get函数,然后赋值给getter方法
  2. 如果不是服务器渲染,则创建computed Watcher,直接通过new Watcher的形式创建一个监听器Watcher
  3. 最后执行definedComputed方法,将计算属性挂载在vm实例上

接下来继续进入Watcher方法中生成实例

src/core/observer/watcher.js

export default class Watcher {
  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    this.vm = vm
    ...
    vm._watchers.push(this)
    this.lazy = !!options.lazy
    this.value = this.lazy
      ? undefined
      : this.get()
  }

  update () {
    // 更新时,转换dirty
    if (this.lazy) {
      this.dirty = true
    }
    ...
  }
  ...

  evaluate () {
    // computed 执行更新后,将dirty重新赋值
    this.value = this.get()
    this.dirty = false
  }
  ...
}

// src/core/instance/state.js
function createComputedGetter (key) {
  return function computedGetter () {
    const watcher = this._computedWatchers && this._computedWatchers[key]
    if (watcher) {
      // 初始化时直接执行,依赖未变更时,不执行
      if (watcher.dirty) {
        watcher.evaluate()
      }
      ...
      return watcher.value
    }
  }
}
  1. Watcher初始化的时候,会对this.dirty进行赋值,这个值就是传过来的{ lazy: true }
  2. 页面初始化渲染时,获取计算属性的值,会调用属性中getter方法,从而执行Watcher.evaluate方法
  3. 执行evaluate方法后,会将dirty值更新为false
  4. 所以我们下次再次使用这个computed属性的时候,由于dirty的原因,便不会执行watcher.evaluate,而是直接返回值
  5. 当计算属性依赖的值发生变化时,Watcher.update方法会将this.dirty重新变为true,所以当依赖发生变化时,Watcher会再次执行evaluate方法,从而对数据进行更新

2.侦听器 watcher(user watcher)

user watcher是我们书写在watch对象上的监听器,用于监听某个值,当数据发生变化时,执行watch中的自定义回调, 处理业务相关逻辑。关于use watcher的初始化是在initWatch方法中进行的。

src/core/instance/state.js

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)
    }
  }
}
  1. 初始化解析时,initWatch会遍历对象,拿到属性值
  2. 因为属性值可能为数组,所以做了判断处理参考官网watch用法,最终会循环获取每一个监听的值
  3. 最终调用createWatcher生成watcher实例

src/core/instance/state.js

function createWatcher (
  vm: Component,
  expOrFn: string | Function,
  handler: any,
  options?: Object
) {
  if (isPlainObject(handler)) {
    options = handler
    handler = handler.handler
  }
  if (typeof handler === 'string') {
    handler = vm[handler]
  }
  return vm.$watch(expOrFn, handler, options)
}

Vue.prototype.$watch = function (
  expOrFn: string | Function,
  cb: any,
  options?: Object
): Function {
  ...
  options = options || {}
  options.user = true
  const watcher = new Watcher(vm, expOrFn, cb, options)
  ...
}
  1. createWatcher中则是通过执行vm.$watch方法进行创建watcher实例
  2. 创建user watcher时,会将options中的user置为true
  3. new Watcher的时候就会执行this.get,其内部会先访问监听的属性,从而触发getter进行收集依赖
  4. 当数据发生变化时,setter通知依赖更新,最终通过nextTick函数执行了flushSchedulerQueue,而在此函数中会调用watcher.run()完成依赖更新,最终执行this.cb函数

3.渲染 watcher (render watcher)

渲染watcher顾名思义,是用来渲染DOM的,所以渲染watcher是在组件初始化完成之后,执行$mount时生成的,接下来根据源码继续分析。

Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
}

$mount挂载的过程其实就是执行了mountComponent

src/core/instance/lifecycle.js


export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  ...
  callHook(vm, 'beforeMount')
  updateComponent = () => {
    vm._update(vm._render(), hydrating)
  }
  ...
  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
  ...
  if (vm.$vnode == null) {
    vm._isMounted = true
    callHook(vm, 'mounted')
  }
  return vm
}
  1. 这里通过new Watcher进行初始化,初始化时传入了updateComponent,此函数会在执行this.get()方法时执行。
  2. updateComponent函数执行中会调用vm._update(vm._render(), hydrating),此_update函数则是更新DOM的关键
  3. 从这里也可以看出,组组件初始化挂载阶段才会生成render watcher,并且组件内只有一个render watcher

至此,Watcher三种形态的创建以及更新过程全部梳理完成。文中有不正确的地方还请指正。

4. 总结

通过阅读源码,我们知道其实computedwatch的本质都是通过 new watcher实现的,主要区别在于使用场景,computed是懒执行;而user watcher则是观察Vue组件中的属性,当属性更新时作出相应的操作,执行传入的回调函数;至于render watcher则是专门用于组件渲染的监听器,并且每个组件只有一个render watcher

转载自:https://juejin.cn/post/7205141492265418813
评论
请登录