美文网首页
Vue源码分析—响应式原理(五)

Vue源码分析—响应式原理(五)

作者: oWSQo | 来源:发表于2019-07-24 11:40 被阅读0次

    Vue 的组件对象支持了计算属性 computed 和侦听属性 watch 2 个选项。我们接下来从源码实现的角度来分析它们两者有什么区别。

    computed

    计算属性的初始化是发生在Vue实例初始化阶段的initState函数中,执行了if (opts.computed) initComputed(vm, opts.computed)initComputed的定义在src/core/instance/state.js中:

    const computedWatcherOptions = { computed: true }
    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)
          }
        }
      }
    }
    
    

    函数首先创建vm._computedWatchers为一个空对象,接着对computed对象做遍历,拿到计算属性的每一个 userDef,然后尝试获取这个userDef 对应的getter函数,拿不到则在开发环境下报警告。接下来为每一个getter创建一个watcher,这个watcher和渲染watcher有一点很大的不同,它是一个computed watcher,因为 const computedWatcherOptions = { computed: true }。最后对判断如果key不是vm的属性,则调用 defineComputed(vm, key, userDef),否则判断计算属性对于的key是否已经被data或者prop所占用,如果是的话则在开发环境报相应的警告。

    那么接下来需要重点关注 defineComputed 的实现:

    export function defineComputed (
      target: any,
      key: string,
      userDef: Object | Function
    ) {
      const shouldCache = !isServerRendering()
      if (typeof userDef === 'function') {
        sharedPropertyDefinition.get = shouldCache
          ? createComputedGetter(key)
          : userDef
        sharedPropertyDefinition.set = noop
      } else {
        sharedPropertyDefinition.get = userDef.get
          ? shouldCache && userDef.cache !== false
            ? createComputedGetter(key)
            : userDef.get
          : noop
        sharedPropertyDefinition.set = userDef.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)
    }
    

    这段逻辑很简单,其实就是利用 Object.defineProperty 给计算属性对应的 key 值添加gettersettersetter通常是计算属性是一个对象,并且拥有 set 方法的时候才有,否则是一个空函数。在平时的开发场景中,计算属性有setter的情况比较少,我们重点关注一下getter部分,缓存的配置也先忽略,最终getter对应的是 createComputedGetter(key) 的返回值,来看一下它的定义:

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

    createComputedGetter返回一个函数computedGetter,它就是计算属性对应的getter

    整个计算属性的初始化过程到此结束,我们知道计算属性是一个computed watcher,它和普通的 watcher 有什么区别呢,为了更加直观,接下来来我们来通过一个例子来分析computed watcher的实现。

    var vm = new Vue({
      data: {
        firstName: 'Foo',
        lastName: 'Bar'
      },
      computed: {
        fullName: function () {
          return this.firstName + ' ' + this.lastName
        }
      }
    })
    

    当初始化这个computed watcher实例的时候,构造函数部分逻辑稍有不同:

    constructor (
      vm: Component,
      expOrFn: string | Function,
      cb: Function,
      options?: ?Object,
      isRenderWatcher?: boolean
    ) {
      // ...
      if (this.computed) {
        this.value = undefined
        this.dep = new Dep()
      } else {
        this.value = this.get()
      }
    }  
    

    可以发现 computed watcher 会并不会立刻求值,同时持有一个 dep 实例。

    然后当我们的 render 函数执行访问到 this.fullName 的时候,就触发了计算属性的 getter,它会拿到计算属性对应的 watcher,然后执行 watcher.depend(),来看一下它的定义:

    /**
      * Depend on this watcher. Only for computed property watchers.
      */
    depend () {
      if (this.dep && Dep.target) {
        this.dep.depend()
      }
    }
    

    注意,这时候的 Dep.target 是渲染 watcher,所以 this.dep.depend() 相当于渲染 watcher 订阅了这个 computed watcher 的变化。

    然后再执行 watcher.evaluate() 去求值,来看一下它的定义:

    /**
      * Evaluate and return the value of the watcher.
      * This only gets called for computed property watchers.
      */
    evaluate () {
      if (this.dirty) {
        this.value = this.get()
        this.dirty = false
      }
      return this.value
    }
    

    evaluate 的逻辑非常简单,判断 this.dirty,如果为 true 则通过 this.get() 求值,然后把 this.dirty 设置为 false。在求值过程中,会执行 value = this.getter.call(vm, vm),这实际上就是执行了计算属性定义的 getter 函数,在我们这个例子就是执行了 return this.firstName + ' ' + this.lastName

    这里需要特别注意的是,由于 this.firstNamethis.lastName 都是响应式对象,这里会触发它们的 getter,根据我们之前的分析,它们会把自身持有的 dep 添加到当前正在计算的 watcher 中,这个时候 Dep.target 就是这个 computed watcher

    最后通过 return this.value 拿到计算属性对应的值。我们知道了计算属性的求值过程,那么接下来看一下它依赖的数据变化后的逻辑。

    一旦我们对计算属性依赖的数据做修改,则会触发 setter 过程,通知所有订阅它变化的 watcher 更新,执行 watcher.update() 方法:

    /* istanbul ignore else */
    if (this.computed) {
      // A computed property watcher has two modes: lazy and activated.
      // It initializes as lazy by default, and only becomes activated when
      // it is depended on by at least one subscriber, which is typically
      // another computed property or a component's render function.
      if (this.dep.subs.length === 0) {
        // In lazy mode, we don't want to perform computations until necessary,
        // so we simply mark the watcher as dirty. The actual computation is
        // performed just-in-time in this.evaluate() when the computed property
        // is accessed.
        this.dirty = true
      } else {
        // In activated mode, we want to proactively perform the computation
        // but only notify our subscribers when the value has indeed changed.
        this.getAndInvoke(() => {
          this.dep.notify()
        })
      }
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)
    }
    

    那么对于计算属性这样的 computed watcher,它实际上是有 2 种模式,lazyactive。如果 this.dep.subs.length === 0 成立,则说明没有人去订阅这个 computed watcher 的变化,仅仅把 this.dirty = true,只有当下次再访问这个计算属性的时候才会重新求值。在我们的场景下,渲染 watcher 订阅了这个 computed watcher 的变化,那么它会执行:

    this.getAndInvoke(() => {
      this.dep.notify()
    })
    
    getAndInvoke (cb: Function) {
      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
        this.dirty = false
        if (this.user) {
          try {
            cb.call(this.vm, value, oldValue)
          } catch (e) {
            handleError(e, this.vm, `callback for watcher "${this.expression}"`)
          }
        } else {
          cb.call(this.vm, value, oldValue)
        }
      }
    }
    

    getAndInvoke 函数会重新计算,然后对比新旧值,如果变化了则执行回调函数,那么这里这个回调函数是 this.dep.notify(),在我们这个场景下就是触发了渲染 watcher 重新渲染。

    通过以上的分析,我们知道计算属性本质上就是一个 computed watcher,也了解了它的创建过程和被访问触发 getter 以及依赖更新的过程,其实这是最新的计算属性的实现,之所以这么设计是因为 Vue 想确保不仅仅是计算属性依赖的值发生变化,而是当计算属性最终计算的值发生变化才会触发渲染 watcher 重新渲染,本质上是一种优化。

    接下来我们来分析一下侦听属性 watch 是怎么实现的。

    watch

    侦听属性的初始化也是发生在Vue的实例初始化阶段的 initState 函数中,在 computed 初始化之后,执行了:

    if (opts.watch && opts.watch !== nativeWatch) {
      initWatch(vm, opts.watch)
    }
    

    来看一下 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)
        }
      }
    }
    

    这里就是对 watch 对象做遍历,拿到每一个 handler,因为Vue是支持 watch 的同一个 key 对应多个 handler,所以如果 handler 是一个数组,则遍历这个数组,调用 createWatcher 方法,否则直接调用 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 = vm[handler]
      }
      return vm.$watch(expOrFn, handler, options)
    }
    

    这里的逻辑也很简单,首先对 hanlder 的类型做判断,拿到它最终的回调函数,最后调用 vm.$watch(keyOrFn, handler, options) 函数,$watch 是 Vue 原型上的方法,它是在执行 stateMixin 的时候定义的:

    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
      const watcher = new Watcher(vm, expOrFn, cb, options)
      if (options.immediate) {
        cb.call(vm, watcher.value)
      }
      return function unwatchFn () {
        watcher.teardown()
      }
    }
    

    也就是说,侦听属性 watch 最终会调用 $watch 方法,这个方法首先判断 cb 如果是一个对象,则调用 createWatcher 方法,这是因为 $watch 方法是用户可以直接调用的,它可以传递一个对象,也可以传递函数。接着执行 const watcher = new Watcher(vm, expOrFn, cb, options) 实例化了一个 watcher,这里需要注意一点这是一个 user watcher,因为 options.user = true。通过实例化 watcher 的方式,一旦我们 watch 的数据发送变化,它最终会执行 watcherrun 方法,执行回调函数 cb,并且如果我们设置了 immediate 为 true,则直接会执行回调函数 cb。最后返回了一个 unwatchFn 方法,它会调用 teardown 方法去移除这个 watcher

    所以本质上侦听属性也是基于 Watcher 实现的,它是一个 user watcher。其实 Watcher 支持了不同的类型,下面我们梳理一下它有哪些类型以及它们的作用。

    Watcher options

    Watcher 的构造函数对 options 做的了处理,代码如下:

    if (options) {
      this.deep = !!options.deep
      this.user = !!options.user
      this.computed = !!options.computed
      this.sync = !!options.sync
      // ...
    } else {
      this.deep = this.user = this.computed = this.sync = false
    }
    

    所以 watcher 总共有 4 种类型,我们来一一分析它们,看看不同的类型执行的逻辑有哪些差别。

    deep watcher

    通常,如果我们想对一下对象做深度观测的时候,需要设置这个属性为 true,考虑到这种情况:

    var vm = new Vue({
      data() {
        a: {
          b: 1
        }
      },
      watch: {
        a: {
          handler(newVal) {
            console.log(newVal)
          }
        }
      }
    })
    vm.a.b = 2
    

    这个时候是不会log任何数据的,因为我们是watcha 对象,只触发了 a 的 getter,并没有触发a.bgetter,所以并没有订阅它的变化,导致我们对vm.a.b = 2赋值的时候,虽然触发了setter,但没有可通知的对象,所以也并不会触发watch的回调函数了。

    而我们只需要对代码做稍稍修改,就可以观测到这个变化了。

    watch: {
      a: {
        deep: true,
        handler(newVal) {
          console.log(newVal)
        }
      }
    }
    

    这样就创建了一个deep watcher了,在watcher执行get求值的过程中有一段逻辑:

    get() {
      let value = this.getter.call(vm, vm)
      // ...
      if (this.deep) {
        traverse(value)
      }
    }
    

    在对watch的表达式或者函数求值后,会调用traverse函数,它的定义在src/core/observer/traverse.js中:

    import { _Set as Set, isObject } from '../util/index'
    import type { SimpleSet } from '../util/index'
    import VNode from '../vdom/vnode'
    
    const seenObjects = new Set()
    
    /**
     * Recursively traverse an object to evoke all converted
     * getters, so that every nested property inside the object
     * is collected as a "deep" dependency.
     */
    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 的逻辑也很简单,它实际上就是对一个对象做深层递归遍历,因为遍历过程中就是对一个子对象的访问,会触发它们的getter过程,这样就可以收集到依赖,也就是订阅它们变化的watcher,这个函数实现还有一个小的优化,遍历过程中会把子响应式对象通过它们的dep id记录到seenObjects,避免以后重复访问。

    那么在执行了traverse后,我们再对watch的对象内部任何一个值做修改,也会调用 watcher 的回调函数了。

    deep watcher 的理解非常重要,今后工作中如果大家观测了一个复杂对象,并且会改变对象内部深层某个值的时候也希望触发回调,一定要设置 deep 为 true,但是因为设置了 deep 后会执行 traverse 函数,会有一定的性能开销,所以一定要根据应用场景权衡是否要开启这个配置。

    user watcher

    前面我们分析过,通过 vm.$watch 创建的 watcher 是一个 user watcher,其实它的功能很简单,在对 watcher 求值以及在执行回调函数的时候,会处理一下错误,如下:

    get() {
      if (this.user) {
        handleError(e, vm, `getter for watcher "${this.expression}"`)
      } else {
        throw e
      }
    },
    getAndInvoke() {
      // ...
      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)
      }
    }
    

    handleErrorVue中是一个错误捕获并且暴露给用户的一个利器。

    sync watcher

    在我们之前对 setter 的分析过程知道,当响应式数据发送变化后,触发了 watcher.update(),只是把这个 watcher 推送到一个队列中,在 nextTick 后才会真正执行 watcher 的回调函数。而一旦我们设置了 sync,就可以在当前 Tick 中同步执行 watcher 的回调函数。

    update () {
      if (this.computed) {
        // ...
      } else if (this.sync) {
        this.run()
      } else {
        queueWatcher(this)
      }
    }
    

    只有当我们需要watch的值的变化到执行watcher的回调函数是一个同步过程的时候才会去设置该属性为true

    总结

    计算属性本质上是computed watcher,而侦听属性本质上是user watcher。就应用场景而言,计算属性适合用在模板渲染中,某个值是依赖了其它的响应式对象甚至是计算属性计算而来;而侦听属性适用于观测某个值的变化去完成一段复杂的业务逻辑。

    watcher的4个options,通常我们会在创建 user watcher 的时候配置 deepsync,可以根据不同的场景做相应的配置。

    相关文章

      网友评论

          本文标题:Vue源码分析—响应式原理(五)

          本文链接:https://www.haomeiwen.com/subject/orsnhctx.html