阅读 117

Vue 源码(四)Computed 和 Watch 原理

Computed

初始化过程

Vue 的计算属性在两个地方都有初始化过程,一个是在initState中,一个是在Vue.extend

initState

export function initState (vm: Component) {
  // ...
  
  const opts = vm.$options
  // ...
  
  if (opts.computed) initComputed(vm, opts.computed)
  // ...
}
复制代码

如果vm.$options 中有computed,就会调用initComputed

// computed Watcher 的 lazy 为 true
const computedWatcherOptions = { lazy: true }

function initComputed (vm: Component, computed: Object) {
  // 创建 vm._computedWatchers 对象
  const watchers = vm._computedWatchers = Object.create(null)
  const isSSR = isServerRendering()
  // 遍历 computed 中的 属性
  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) {
      // 为 computed 属性创建 Watcher
      // 创建 computedWatcher
      watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        computedWatcherOptions
      )
    }

    // 通过 extend 方法创建组件函数(Sub)的时候,已经将 computed 属性挂载到了 Sub 的 prototype 上
    // 在这里仅仅是定义实例化时定义的计算属性。比如 根组件的计算属性
    // 并保证 computed 的属性名和 data、props 的属性名没有重复
    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)
      }
    }
  }
}
复制代码

initComputed会为每个计算属性创建一个 Computed Watcher,这里要注意的点是Computed Watcheroptions.lazytrue,并将计算属性赋值给Watcher实例的getter属性。而创建的Computed Watcher会添加到vm._computedWatchers里面;接下来会执行defineComputed添加响应

看下 Computed WatcherRender Watcher的区别

// Watcher类内部代码

this.lazy = !!options.lazy
// ...

this.dirty = this.lazy
// ...

this.value = this.lazy ? undefined : this.get()
复制代码

相对于 Render WatcherComputed Watcherlazytrue,并且dirty也为true;因为 lazytrue,所以在创建Computed Watcher过程中并不会执行this.get() 方法;也就不会获取计算属性的返回值。

而 在创建Render Watcher 过程中会执行this.get(),从而执行组件的render函数

Vue.extend

if (Sub.options.computed) {
  initComputed(Sub)
}
复制代码

Vue.extend 会执行 initComputed函数

function initComputed (Comp) {
  const computed = Comp.options.computed
  for (const key in computed) {
    // 将组件的计算属性挂载到 组件函数的原型上
    // 作用:当实例化时,就可以通过 this.key 的方式访问了
    defineComputed(Comp.prototype, key, computed[key])
  }
}
复制代码

initComputed函数会对所有计算属性执行defineComputed方法。

defineComputed定义在src/core/instance/state.js

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

export function defineComputed (
  target: any,
  key: string,
  userDef: Object | Function
) {
  // 如果不是 ssr 则为 true
  const shouldCache = !isServerRendering()
  /*
   * userDef 可能是一个函数,也可能是一个有 getter和 setter 属性的对象
   */
  // 设置 取描述符
  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
  }
  // 如果 计算属性没有设置 setter 方法,则对计算属性赋值时,报错(计算属性 key 没有分配 setter 方法)
  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
      )
    }
  }
  // 添加拦截,让开发者可以通过 this.key(vm.key) 的方式访问
  Object.defineProperty(target, key, sharedPropertyDefinition)
}
复制代码

defineComputed方法通过Object.defineProperty将所有计算属性添加到vm / Sub.prototype上,并将createComputedGetter函数的返回值设置成 取描述符; 将计算属性的set方法设置成 存描述符

接着来看 createComputedGetter

function createComputedGetter (key) {
  return function computedGetter () {}
}
复制代码

createComputedGetter 函数的内部逻辑一会再看,现在就知道它返回一个函数就行,并且这个函数的执行时机是 获取该计算属性时触发

小结

组件computed的初始化

对于组件computed的初始化,就是在创建组件构造函数时,通过Object.defineProperty方法将组件中所有计算属性添加到组件构造函数的原型对象上,并设置存取描述符。

当创建组件实例时,为每个计算属性创建一个Computed Watcher,并将计算属性复制给Watcher实例的getter属性;并且开发环境下会判断computed中的keydataprops中的key是否重复。

根实例computed的初始化

对于根实例computed的初始化,就比较简单了,就是获取计算属性,并给computed的每个key创建一个Computed Watcher,通过Object.defineProperty方法将所有计算属性挂载到组件实例上,并设置存取描述符。

响应原理

组件执行render函数时,如果使用了某个计算属性,会触发该计算属性的getter,这个方法就是上面createComputedGetter中的返回值

function createComputedGetter (key) {
  return function computedGetter () {
    const watcher = this._computedWatchers && this._computedWatchers[key]
    if (watcher) {
      // 只做一次依赖收集
      if (watcher.dirty) {
        // 执行 定义的计算属性函数
        watcher.evaluate()
      }
      if (Dep.target) {
        // 将render watcher 添加到 依赖属性的 dep 中,当依赖属性修改后,通过 render watcher 的get方法去触发组件更新
        watcher.depend()
      }
      return watcher.value
    }
  }
}
复制代码

首先会根据key获取对应计算属性的Computed Watcher,因为在初始化过程中,watcher.dirtytrue,所以会执行 watcher.evaluate()方法

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

evaluate方法会执行this.get()方法,获取计算属性的返回值,并将当前Watcherdirty置为 false,从而防止多次执行this.get()方法。

get方法在响应式原理一节中看过

  get () {
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      value = this.getter.call(vm, vm)
    } catch (e) {
    } finally {
      // ...
      popTarget()
      this.cleanupDeps()
    }
    return value
  }
复制代码

首先将Computed Watcher入栈,执行this.getter也就是计算属性的属性值并获取结果value,然后出栈,将依赖属性的dep添加到depIdsdeps中,并将结果返回。

在执行this.getter过程中,会获取计算属性中依赖属性的变量值,从而触发响应式变量的getter,将Computed Watcher添加到响应式变量的dep.subs中。

回到createComputedGetter,此时Dep.target指向的是组件的Render Watcher,因为在执行组件render函数时,会将组件的Render Watcher 入栈,当获取计算属性的属性值时会将Computed Watcher入栈,执行结束后Computed Watcher出栈,所以此时的Dep.target指向的是组件的Render Watcher;接下来执行Computed Watcherdepend方法

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

depend方法内,遍历depsdeps是一个存放Dep实例的数组,执行每个Dep实例的depend方法,将组件的Render Watcher添加到该计算属性所有依赖属性的dep.subs里面

watcher.depend执行完成之后,会返回计算属性的返回值,到此依赖收集结束

小结

计算属性的依赖收集过程其实是对使用到的响应式属性进行依赖收集

当组件的render函数中使用了某个计算属性时,会执行计算属性,在这期间会将Computed Watcher添加到响应式属性的dep.subs中;并将组件的Render Watcher也添加到响应式属性的dep.subs

更新

当计算属性依赖的响应式属性修改时,会触发依赖属性的setter方法

set: function reactiveSetter (newVal) {
  // ...
  
  dep.notify()
}
复制代码

setter方法中,会通知所有Watcher更新,其中就包括Computed WatcherRender Watcher;调用Watcherupdate方法

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

对于 Computed Watcher 就是将dirty设为true。而Render Watcher会执行Watcher实例的run方法,从而重新执行组件的render函数,从而更新计算属性的返回值

dirty的作用其实就是只在相关响应式属性发生改变时才会重新求值。如果重复获取计算属性的返回值,只要响应式属性没有发生变化,就不会重新求值。

也就是说当响应式属性改变时,触发响应式属性的setter,通知Computed Watcherdirty置为false;等再次获取时,会获取到最新值,并重新给响应式属性的dep.subs添加Watcher

Watch

watch 的初始化过程也是发生在initState

export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  //...
  
  
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}
复制代码

首先为vm添加一个_watchers数组,用来存放当前组件的watch;然后调用initWatch方法初始化watch所有属性

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)
    }
  }
}
复制代码

initWatch对所有watch调用createWatcher

function createWatcher (
  vm: Component,
  expOrFn: string | Function,
  handler: any,
  options?: Object
) {
  if (isPlainObject(handler)) {
    // 处理参数
    options = handler
    handler = handler.handler
  }
  if (typeof handler === 'string') {
    // handler 可以是方法名
    handler = vm[handler]
  }
  return vm.$watch(expOrFn, handler, options)
}
复制代码

createWatcher就是获取回调函数,并调用vm.$watch方法

  Vue.prototype.$watch = function (
    expOrFn: string | Function,
    cb: any,
    options?: Object
  ): Function {
    const vm: Component = this
    // 如果通过 this.$watch 设置的监听,则会执行 createWatcher 获取回调函数
    if (isPlainObject(cb)) {
      return createWatcher(vm, expOrFn, cb, options)
    }
    options = options || {}
    options.user = true
    // 此时 user 为 true,说明创建的 Watcher 是一个 User Watcher
    const watcher = new Watcher(vm, expOrFn, cb, options)
    if (options.immediate) {
      try {
        // 如果 options.immediate 为 true,则立刻执行一次回调函数
        cb.call(vm, watcher.value)
      } catch (error) {
        handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
      }
    }
    // 返回一个函数,作用是取消监听
    return function unwatchFn () {
      watcher.teardown()
    }
  }
复制代码

Vue.prototype.$watch就是创建一个User Watcher,并判断options.immediate是否为true,如果为true则立即执行一次回调函数。最后会返回一个取消监听的函数。

自此watch的初始化过程结束

小结

watch的初始化过程最终目的就是给每个watch创建一个 User Watcher,在创建过程中会对被监听的属性做依赖收集(下一小节介绍)

Watch 更新

在初始化过程中会为每个watch创建一个User Watcher,而创建过程中会对被监听属性做依赖收集

const watcher = new Watcher(vm, expOrFn, cb, options)

先看下参数

vm 组件实例
expOrFn 被监听的属性名(xxx、'xxx.yyy')
cb 回调函数
options { user: true, deep: [自定义配置项], async: [自定义配置项] }
复制代码

User Watcher的创建过程

  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    this.vm = vm
    if (isRenderWatcher) {
      // 如果不是渲染 Watcher 则不会将 _watcher 挂载到 vm 上
      vm._watcher = this
    }
    vm._watchers.push(this)
    // options
    if (options) {
      /**
       * computedWatcher 的 lazy 为 true
       * userWarcher 的 user 为 true
       * deep、sync 是 watch 的配置项
       */
      this.deep = !!options.deep
      this.user = !!options.user
      this.lazy = !!options.lazy
      this.sync = !!options.sync
      this.before = options.before
    } else {
      this.deep = this.user = this.lazy = this.sync = 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()
    this.expression = process.env.NODE_ENV !== 'production'
      ? expOrFn.toString()
      : ''
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      // User Watcher 的 expOrFn 是一个字符串,代表被监听属性的属性名
      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
        )
      }
    }
    // computed Watcher 的 lazy 属性为 true,即不会立刻执行 get 方法
    // render Watcher 的 lazy 属性为 false,会立刻执行 get 方法,返回值为 undefined
    // user Watcher 的 lazy 属性为 false,会立刻执行 get 方法,返回值为 被监听属性的属性值
    this.value = this.lazy
      ? undefined
      : this.get()
  }
复制代码

User Watcher除了user属性为true外,还有deepasync两个属性,这两个属性都是watch的配置项。

实例化一个Watcher时会判断expOrFn参数的数据类型,对于User Watcher而言,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
  }
}
复制代码

parsePath方法根据.将字符串切割成字符串数组,并返回一个函数,这个函数会赋值给User Watchergetter属性;函数内部会依次获取数组中所有元素对应的属性值并返回改属性值

假设被监听的属性名是a.b.c,则此函数会依次获取this.athis.a.bthis.a.b.c的属性值

回到User Watcher的创建过程,此时this.getter已经赋好值,接下来会去执行this.get方法;也就是说只有Computed Watcher在创建过程中不会执行this.get 方法

再看一遍get 方法

  get () {
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      value = this.getter.call(vm, vm)
    } catch (e) {} finally {
      if (this.deep) {
        // 如果 deep 为 true,并且被监听属性是一个对象,则对象内的所有属性都做一次依赖收集
        traverse(value)
      }
      popTarget()
      this.cleanupDeps()
    }
    return value
  }
复制代码

其实不管是计算属性还是datapropswatch他们的get方法整体逻辑都是一样的,

  1. 当前Watcher入栈
  2. 执行this.getter每类Watchergetter属性不同
  3. 执行traverse 方法 (只有 deeptrueUser Watcher才会执行
  4. 当前Watcher出栈
  5. 处理Watcherdeps属性
  6. 返回 value

对于 User Watcher,他的getterparsePath函数的返回值,在执行getter过程中,会获取被监听属性的属性值,从而触发被监听属性的getter方法,将User Watcher添加到此属性的dep.subs中。

上述执行完成后,会判断deep是否为true,如果为true,执行traverse方法

const seenObjects = new Set()

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

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)
  }
}
复制代码

traverse方法整体思路其实很简单,如果被监听的属性是一个对象,则把对象的所有属性都访问一遍,从而触发所有属性的依赖收集;将User Watcher添加到每个属性的dep.subs中,这样当某个属性修改时,会触发属性的setter,从而触发watch回调

触发回调

当修改被监听属性的属性值时,触发属性的setter,通知 dep.subs中所有 Watcher 更新,执行watcher.update方法

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

如果 User Watchersync属性为true,立刻执行run方法;如果sync属性为false,通过queueWatcher(this) 在下一次任务队列中执行User Watcherrun 方法

先看下run方法

  run () {
    if (this.active) {
      const value = this.get()
      if (
        value !== this.value ||
        isObject(value) ||
        this.deep
      ) {
        // 当添加自定义 watcher 的时候能在回调函数的参数中拿到新旧值的原因
        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)
        }
      }
    }
  }
复制代码

对于User Watcherrun方法,首先会调用this.get()重新让被监听属性做依赖收集,并获取最新值;如果最新值和老值不想等,调用回调函数,并将新老值传入

上面的判断逻辑中除了判断新老值是否想等还会判断isObject(value) || this.deep,这是因为如果被监听的属性是一个对象/数组的话,修改对象/数组的属性后,新老值是相同的,所以为了防止出现这种情况导致回调不执行,从而增加这段逻辑

接下来看下User WatcherqueueWatcher中是怎么被调用的;正常情况下,和data 相同就是将User Watcher添加到队列中,并保证同一队列中每个User Watcher都是唯一的

不同情况

watch回调中修改另一个被监听属性的值

他的执行逻辑如下:

export const MAX_UPDATE_COUNT = 100
let circular: { [key: number]: number } = {}

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]
    // 执行组件的 beforeUpdate 钩子, 先父后子
    if (watcher.before) {
      watcher.before()
    }
    id = watcher.id
    has[id] = null
    watcher.run()
    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
      }
    }
  }
复制代码

在下一个队列中执行flushSchedulerQueue方法

  • flushing置为true,说明正在更新队列中的Watcher
  • 队列排序,保证Watcher更新顺序;
  • 遍历队列,更新队列中的所有Watcher
  • has[id] = null,将正在更新的Watcherhas中去掉;
  • 执行 User Watcherrun方法;
  • 执行第一个watch的回调,回调内修改被监听属性的值,触发属性的setter,将监听这个属性的User Wather通过queueWatcher添加到队列中,此时和之前就有差别了
export function queueWatcher (watcher: Watcher) {
  const id = watcher.id
  if (has[id] == null) {
    has[id] = true
    if (!flushing) {
      queue.push(watcher)
    } else {
      let 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 (process.env.NODE_ENV !== 'production' && !config.async) {
        flushSchedulerQueue()
        return
      }
      nextTick(flushSchedulerQueue)
    }
  }
}
复制代码

因为在上面已经将flushing置为true了,所以会走 else逻辑;else逻辑就是遍历队列,并将Watcher添加到对应位置。位置逻辑如下

1. 组件更新是从父到子。(因为父组件总是在子组件之前创建)
2. User Watcher 在 Render Watcher 之前执行
3. 如果一个组件在父组件的 Watcher 执行期间被销毁,那么它对应 Watcher 执行都可以被跳过,所以父组件的 watcher 应该先执行
复制代码

添加到对应位置后,因为waiting已经是true了,所以不会再次执行nextTick(flushSchedulerQueue),而是回到flushSchedulerQueue方法继续循环

watch回调内修改当前被监听属性的值

其实整体逻辑和上面说的一样,但是会多一步,就是在flushSchedulerQueue方法的循环里面

因为新添加的User Watcher和刚执行完的User Watcher是同一个Watcher,所以接下来触发的if条件在开发环境下是成立的

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
  }
}
复制代码

此时circular[id]数量会加一,并按照上面的逻辑一直重复执行,直到总数量大于MAX_UPDATE_COUNT时会报错

总结

Computed 和 watch 的区别

computed

  • 依赖其它属性值,并且 computed 的值有缓存
  • 当它依赖的属性值发生改变时,在下一次获取 computed 值的时候才会重新计算 computed 的值

watch

  • 没有缓存性,更多的是观察的作用
  • 每当监听的数据变化时都会执行回调进行后续操作

使用场景

  • 当需要进行数值计算,并且依赖于其它数据时,应该使用computed,因为可以利用computed的缓存特性,避免每次获取值时,都要重新计算
  • 当需要在数据变化时执行异步或开销较大的操作时,可以使用watch,并在得到最终结果前,可以设置中间状态

computed 的更新过程

在初始化阶段,会为每个计算属性创建一个Computed Watcher,通过Object.defineProperty将所有计算属性添加到组件实例 / 组件构造函数的原型对象上,并为所有计算属性添加存取描述符。

当获取计算属性时,触发计算属性的getter,计算computed的值,并将dirty置为false,这样做的目的是再次获取计算属性时直接返回缓存值;在计算computed值的过程中会将Computed Watcher添加到依赖属性的Dep中。

当依赖属性发生变化会触发Computed Watcher的更新,将dirty置为true,在下次获取计算属性时,会重新计算computed的值

watch 是怎么触发回调的

在初始化阶段,会为每个watch创建一个User Watcher,如果watchimmediatetrue,会马上执行一次回调;创建User Watcher过程中会获取一次被监听属性的值,从而触发被监听属性的getter方法,将User Watcher添加到被监听属性的Dep实例中。

当被监听属性发生改变时,通知User Watcher更新,如果watchsynctrue,会马上执行watch的回调;否则会将User Watcherupdate方法通过nextTick放到缓存队列中,在下一个的事件循环中,会重新获取被监听属性的属性值,并判断新旧值是否想等、是否设置了deeptrue、被监听属性是否是对象类型,如果成立就执行回调。

文章分类
前端
文章标签