重学Vue【计算属性和监听属性】

简介: 重学Vue源码,根据黄轶大佬的vue技术揭秘,逐个过一遍,巩固一下vue源码知识点,毕竟嚼碎了才是自己的,所有文章都同步在 公众号(道道里的前端栈) 和 github 上。

网络异常,图片无法展示
|

重学Vue源码,根据黄轶大佬的vue技术揭秘,逐个过一遍,巩固一下vue源码知识点,毕竟嚼碎了才是自己的,所有文章都同步在 公众号(道道里的前端栈)github 上。


正文


计算属性

计算属性的初始化是在Vue初始化的 initState 方法中:

// ...
initState(vm)
// ...

具体实现是在 src/core/instance/state.js 里面:

export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props)
  if (opts.methods) initMethods(vm, opts.methods)
  if (opts.data) {
    initData(vm)
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
  if (opts.computed) initComputed(vm, opts.computed)
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}

可以看到在里面初始化了 propsmethodsdata,另外也初始化了 computedwatch,这里就看看下 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)
      }
    }
  }
}

先定义了一个 watchers_computedWatchers 为空对象,然后判断是不是服务端渲染,这里肯定是false,后面遍历 computed,这个 computed 其实就是我们自定义写的 computed 属性,然后拿到每一个定义的值,它可以是函数也可以是对象,一般我们都会写一个函数,如果定义成对象的话,就必须定义一个 get 属性,相当于拿到 getter,接着判断 getter 有没有,如果没有就报错,接着实例化一个 Watcher,对应的值就是 watchers[key],这里就可以看出来 computed 里面的定义其实就是通过 watcher 来实现的,传入 watcher 的值分别是vm实例、getter(也就是定义的get)、noop回调函数和一个computedWatcherOptions(定义了一个对象: {computed: true})。接着判断 key 在不在 vm 中,如果不在就走一个 defineComputed 方法,如果在,那肯定在 data 或者 props 定义过,就报错,所以在 computed 里面定义的键值不可以在 data 或者 props 里出现。

来看下这个 defineComputed 方法:

const sharedPropertyDefinition = {
  enumerable: true,
  configurable: true,
  get: noop,
  set: noop
}
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)
}

如果定义的每个计算属性是一个函数,就把对象的 getset 赋值,也就是说当访问我们自定义的 computed 属性的时候,就会执行对应的重新赋值的 get 方法,这里 shouldCache 是true,所以会走 createComputedGetter 方法,如果我们写的是一个对象,就走下面的 else 逻辑,也会走到 createComputedGetter,因为这个 computed 属性一般是计算而来的,所以一般我们不会用对象的方式去写。


计算属性的依赖收集

接着看下这个 createComputedGetter 做了什么:

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

单从名字来看,就是定义了在我们访问 computed 属性的时候的 getter,可以看到每次访问的时候就会执行 computedGetter,拿到刚才存起来的 _computedWatchers,走一个 depend 方法,返回执行 watcher.evaluate() 方法。

到这里计算属性的初始化流程就结束了,回到上面说到的访问 computed 的时候会 new 一个 watcher,它和之前说过的创建渲染watcher有哪些不一样,我们来重新看下 watcher 的构造函数,在 src/core/observer/watcher.js 里,这里只看关键点:

if (this.computed) {
  this.value = undefined
  this.dep = new Dep()
} else {
  this.value = this.get()
}
复制代码

前面传进来的对象 {computed: true} 在这里用到了,作为 computed watcher,会在当前实例定义一个 value,并且定义一个 depnew Dep(),然后就结束了,并不会像以前一样去求值。举个例子:

computed: {
  sum(){
    return this.a + this.b
  }
}

render 函数执行访问到 this.sum 的时候,就触发了计算属性的 getter,它就会拿到计算属性的 watcher,执行上面提到的 watcher.depend()

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

这个时候的 Dep.target 不是当前的 computed watcher ,而是 渲染watcher,所以this.dep.depend() 就相当于 渲染 watcher 订阅了这个 computed watcher 的变化(有关的订阅可以看这篇),接着按照上面 computedGetter 的逻辑就会执行 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(dirty 就是传入的 computed 布尔值),如果是true,就通过 this.get() 求值,然后把 dirty 设置为false,而在执行 get 的时候,里面有一个 value = this.getter.call(vm, vm),也就是执行了计算属性定义的 getter 函数,在上面的例子中就是执行了我们自定义的 return this.a + this.b,由于 ab 都是响应式对象,所以在执行它们的 getter 的时候,会将自身的 dep 添加到当前正在计算的 watcher 中,此时的 Dep.target 就是 computed watcher 了,到此求值结束。


计算属性的派发更新

一旦对计算属性依赖的数据做了修改,就会触发 setter 过程,然后通知所有订阅它变化的 watcher 进行派发更新,最终执行 watcher.update() 方法(有关派发更新可以看这篇):

/**
   * Subscriber interface.
   * Will be called when a dependency changes.
   */
  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 有两种模式:lazyactivated。如果 this.dep.subs.length === 0 为true,就说明没有人订阅这个 computed watcher 的变化,就只把 this.dirty 改成true,然后在下次再访问这个计算属性的时候才会去重新求值;否则如果订阅了,就执行:

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 函数也比较简单,先通过 get 方法得到一个新值 value,然后让新值和之前的值做对比,如果值不相等,或者新值 value 是一个对象,或者它是一个 deep watcher 的话,就执行回调(user watcher 比 渲染watcher 多了一个报错提示而已),注意回调函数 cb 执行的时候会把第一个和第二个参数传入新值 value 和旧值 oldValue这就是当我们自定义 watcher 的时候可以拿到新值和旧值的原因。

getAndInvoke 会重新计算,对比新旧值,如果变化了就执行回调函数,这里的回调函数就是 this.dep.notify(),在求值 sum 的情况下,就是触发了 watcher 的重新渲染,对于这个 getAndInvoke 里面的 value !== this.value 其实算是做了一个优化,如果新值和旧值相等的话,就不做什么处理,也不会触发回调,这样就少走了一层渲染。


计算属性小总结

计算属性本质上就是一个 computed watcher,它在创建的过程中会实例一个 watcher,访问它的时候走定义的 computedGetter 进行依赖收集,在计算的时候触发依赖的更新,并且当计算属性最终计算的值更新发生变化才会触发 渲染 watcher 的重新渲染。


监听属性

最上面提到,在Vue实例初始化的时候会执行监听属性的初始化,它是在 computed 初始化之后的:

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

看下 initWatcher 的具体实现,它定义在 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)
    }
  }
}

可以看到对 watcher 做了遍历,拿到每一个 handler,因为Vue是支持 watcher 的同一个 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)
}

createWatcher 中,对 handler 的类型做了判断,然后拿到它的回调函数,也就是我们自定义的函数,最后调用 vm.$watch 函数,这个 vm.$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,首先判断了 cb 如果是一个对象,就直接调用 createWatcher,因为 $watch 是可以直接外部调用的,它可以传一个对象,也可以传一个函数。接着实例化一个 watcher,注意这个 watcher 是一个 user watcher,因为是用户直接调用的,而且 options.user = true。这样通过实例化一个 watcher 的方式,一旦我们自定义的 watcher 发生了变化,最终会执行 watcherrun 方法(watcher 里的 update 调用的 run),从而调用我们的回调函数,也就是 cb,如果 options 上的 immediate 也是true,就直接执行回调函数,最终返回了一个 unwatchFn 方法,调用一个 teardown 来移除这个 watcher

/**
   * Scheduler job interface.
   * Will be called by the scheduler.
   */
run () {
  if (this.active) {
    this.getAndInvoke(this.cb)
  }
}
/**
   * Remove self from all dependencies' subscriber list.
   */
teardown () {
  if (this.active) {
    // remove self from vm's watcher list
    // this is a somewhat expensive operation so we skip it
    // if the vm is being destroyed.
    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

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

deep watcher

这个一般在深度监听的时候会用到,我们一般会这样写:

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

这种情况下 handler 是不会监听到b的变化的,因为此时 watch 的是 a 对象,只触发了 agetter,并没有触发 a.bgetter,所以没有订阅它,所以在修改 a.b 的时候,虽然触发了 setter, 但是没有可通知的对象,所以也不会触发 watch 的回调函数。

此时如果想监听到 a.b,就得这样使用:

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

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

try {
  value = this.getter.call(vm, vm)
} catch (e) {
  //...
} finally {
  // "touch" every property so they are all tracked as
  // dependencies for deep watching
  if (this.deep) {
    traverse(value)
  }
  //...
}

在对 value 求值后,如果 this.deep 为true,就执行 traverse(value) 方法,它的定义在 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 的回调函数。

user watcher

这个在上面有分析到,通过 vm.$watch 创建的 watcher 是一个 user watcher,其实源码涉及到它的地方就是 get 求值方法和 getAndInvoke 调用回调函数了:

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

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

computed watcher

computed watcher 几乎就是为计算属性量身定制的,上面已经对它做了详细的分析,这里就不再赘述了。

sync watcher

这个在 watchupdate 方法里:

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

当响应式数据发生变化之后,就会触发 watch.update(),然后把这个 watcher 推到一个队列中,等下一个tick再进行 watcher 的回调函数,如果设置了 sync,就直接在当前tick中同步执行 watcher 回调函数了。

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

目录
相关文章
|
6天前
|
JavaScript 前端开发
如何在 Vue 项目中配置 Tree Shaking?
通过以上针对 Webpack 或 Rollup 的配置方法,就可以在 Vue 项目中有效地启用 Tree Shaking,从而优化项目的打包体积,提高项目的性能和加载速度。在实际配置过程中,需要根据项目的具体情况和需求,对配置进行适当的调整和优化。
|
6天前
|
JavaScript 前端开发 UED
vue学习第二章
欢迎来到我的博客!我是一名自学了2年半前端的大一学生,熟悉JavaScript与Vue,目前正在向全栈方向发展。如果你从我的博客中有所收获,欢迎关注我,我将持续更新更多优质文章。你的支持是我最大的动力!🎉🎉🎉
|
6天前
|
JavaScript 前端开发 开发者
vue学习第一章
欢迎来到我的博客!我是瑞雨溪,一名热爱JavaScript和Vue的大一学生。自学前端2年半,熟悉JavaScript与Vue,正向全栈方向发展。博客内容涵盖Vue基础、列表展示及计数器案例等,希望能对你有所帮助。关注我,持续更新中!🎉🎉🎉
|
20天前
|
数据采集 监控 JavaScript
在 Vue 项目中使用预渲染技术
【10月更文挑战第23天】在 Vue 项目中使用预渲染技术是提升 SEO 效果的有效途径之一。通过选择合适的预渲染工具,正确配置和运行预渲染操作,结合其他 SEO 策略,可以实现更好的搜索引擎优化效果。同时,需要不断地监控和优化预渲染效果,以适应不断变化的搜索引擎环境和用户需求。
|
7天前
|
存储 缓存 JavaScript
在 Vue 中使用 computed 和 watch 时,性能问题探讨
本文探讨了在 Vue.js 中使用 computed 计算属性和 watch 监听器时可能遇到的性能问题,并提供了优化建议,帮助开发者提高应用性能。
|
7天前
|
存储 缓存 JavaScript
如何在大型 Vue 应用中有效地管理计算属性和侦听器
在大型 Vue 应用中,合理管理计算属性和侦听器是优化性能和维护性的关键。本文介绍了如何通过模块化、状态管理和避免冗余计算等方法,有效提升应用的响应性和可维护性。
|
7天前
|
存储 缓存 JavaScript
Vue 中 computed 和 watch 的差异
Vue 中的 `computed` 和 `watch` 都用于处理数据变化,但使用场景不同。`computed` 用于计算属性,依赖于其他数据自动更新;`watch` 用于监听数据变化,执行异步或复杂操作。
|
8天前
|
存储 JavaScript 开发者
Vue 组件间通信的最佳实践
本文总结了 Vue.js 中组件间通信的多种方法,包括 props、事件、Vuex 状态管理等,帮助开发者选择最适合项目需求的通信方式,提高开发效率和代码可维护性。
|
8天前
|
存储 JavaScript
Vue 组件间如何通信
Vue组件间通信是指在Vue应用中,不同组件之间传递数据和事件的方法。常用的方式有:props、自定义事件、$emit、$attrs、$refs、provide/inject、Vuex等。掌握这些方法可以实现父子组件、兄弟组件及跨级组件间的高效通信。
|
13天前
|
JavaScript
Vue基础知识总结 4:vue组件化开发
Vue基础知识总结 4:vue组件化开发