title: VUE源码之计算属性和监听属性,到底该用谁?
date: 2020-11-07 20:45:15
tags:

  • vue
  • 源码
    categories: 学习
    copyright: false
    top_img: /img/bg5.png

前言

上一篇我们分析了Vue的响应式原理,今天我们来搞一下,Vue的计算属性和监听属性的实现原理,以让我们更清楚什么时候该使用computed,什么时候该使用watch,以及为什么官方不建议使用watch?

正文

还记得我们在data渲染视图中讲的,New Vue()会发生什么么?这其中有一段源代码:

  1. /* 初始化状态 */
  2. export function initState (vm: Component) {
  3. // ...
  4. /*初始化computed*/
  5. if (opts.computed) initComputed(vm, opts.computed)
  6. /*初始化watchers*/
  7. if (opts.watch && opts.watch !== nativeWatch) {
  8. initWatch(vm, opts.watch)
  9. }
  10. }

可以看出我们在new Vue()之后,会执行initState方法,该方法去初始化initComputed(计算)和initWatch(监听),我们首先看计算属性;

computed

先看initComputed
源码:src/core/instance/state.js

  1. /* 为了在属性值不变的情况下get()只执行一次而设置的标志位,下边会讲的 */
  2. const computedWatcherOptions = { lazy: true }
  3. /* 初始化computed */
  4. function initComputed (vm: Component, computed: Object) {
  5. const watchers = vm._computedWatchers = Object.create(null)
  6. /* 循环计算属性,给每个属性添加Watcher监听,不知道Watcher干什么的可以去看https://juejin.im/post/6844904045694418951#heading-13 */
  7. for (const key in computed) {
  8. const userDef = computed[key]
  9. /* 拿get方法 */
  10. const getter = typeof userDef === 'function' ? userDef : userDef.get
  11. /* 添加watcher监听 */
  12. watchers[key] = new Watcher(
  13. vm,
  14. getter || noop,
  15. noop,
  16. computedWatcherOptions
  17. )
  18. if (!(key in vm)) {
  19. defineComputed(vm, key, userDef)
  20. } else if (process.env.NODE_ENV !== 'production') {
  21. /* 如果定义的计算属性在data或者props中已经被定义过了,会报警告 */
  22. if (key in vm.$data) {
  23. warn(`The computed property "${key}" is already defined in data.`, vm)
  24. } else if (vm.$options.props && key in vm.$options.props) {
  25. warn(`The computed property "${key}" is already defined as a prop.`, vm)
  26. }
  27. }
  28. }
  29. }

那么我们重点看一下defineComputed的实现

  1. /**
  2. * 定义计算属性
  3. * @param {Object | Function} userDef 计算属性的值
  4. */
  5. export function defineComputed (
  6. target: any,
  7. key: string,
  8. userDef: Object | Function
  9. ) {
  10. /* 非服务端渲染,执行createComputedGetter */
  11. const shouldCache = !isServerRendering()
  12. /* 计算属性是函数时 */
  13. if (typeof userDef === 'function') {
  14. sharedPropertyDefinition.get = shouldCache
  15. ? createComputedGetter(key)
  16. : createGetterInvoker(userDef)
  17. /* noop是一个空函数,Vue中定义的工具函数 */
  18. sharedPropertyDefinition.set = noop
  19. } else {
  20. /* 计算属性是对象时 */
  21. sharedPropertyDefinition.get = userDef.get
  22. ? shouldCache && userDef.cache !== false
  23. ? createComputedGetter(key)
  24. : createGetterInvoker(userDef.get)
  25. : noop
  26. sharedPropertyDefinition.set = userDef.set || noop
  27. }
  28. // ...
  29. Object.defineProperty(target, key, sharedPropertyDefinition)
  30. }

可以看出,本质上就是利用Object.defineProperty去给属性添加setter和getter,并且无论计算属性是函数还是对象,都会去执行createComputedGetter方法,并传入属性键。

  1. function createComputedGetter (key) {
  2. /* 返回一个函数,即对应的getter */
  3. return function computedGetter () {
  4. /* this._computedWatchers是在initComputed方法中定义的一个空对象 */
  5. const watcher = this._computedWatchers && this._computedWatchers[key]
  6. if (watcher) {
  7. /*****
  8. Watcher中有evaluate这么一个方法,当取到get()值以后,将dirty置为false,那么下次再去取这个计算属性值的时候因为dirty已经变为false了,就不会再去执行get()方法了,而是用的之前的取的值,这就是computed的缓存机制
  9. evaluate () {
  10. this.value = this.get()
  11. this.dirty = false
  12. }
  13. ******/
  14. if (watcher.dirty) {
  15. watcher.evaluate()
  16. }
  17. /* 为了避免重新渲染的时候,计算属性渲染的部分不被重新渲染,因此进行依赖收集 */
  18. if (Dep.target) {
  19. watcher.depend()
  20. }
  21. /* 返回属性值 */
  22. return watcher.value
  23. }
  24. }
  25. }

createComputedGetter方法返回一个函数,即对应的是getter方法,该方法主要是返回watcher的值,也就是getter的值,看Watcher的源码我们可以发现dirty的值就是lazy, 而上边说的const computedWatcherOptions = { lazy: true },lazy初始值为true,并在上边initComputed方法中合并给Watcher了,因此计算属性在属性值不变的情况下,只会去执行一次get()方法取值,这也就是为什么Vue的计算属性有缓存作用。


我们举个例子看一下computed和watch的不同,我们知道computed也会对数据尽心监听,下边我们把计算属性的监听暂且叫做computed watcher

  1. var vm = new Vue({
  2. data: {
  3. firstName: 'yang',
  4. lastName: 'bo'
  5. },
  6. computed: {
  7. fullName: function () {
  8. return this.firstName + ' ' + this.lastName
  9. }
  10. }
  11. })

当初始化fullName时,我们会执行到Watcher
源码:/src/core/observer/watcher.js

  1. constructor () {
  2. /* 这一步是给computed watcher设置的,计算属性并不会去立刻求值 */
  3. this.value = this.lazy
  4. ? undefined
  5. : this.get()
  6. }

然后当render函数访问到this.fullName的时候,就会触发计算属性的getter,它会拿到计算属性对应的watcher,然后执行watcher.depend()进行依赖收集。

  1. depend () {
  2. let i = this.deps.length
  3. while (i--) {
  4. this.deps[i].depend()
  5. }
  6. }

然后还执行了watcher.evaluate()

  1. evaluate () {
  2. this.value = this.get()
  3. this.dirty = false
  4. }

这个方法我们上边已经讲了,就不啰嗦了。我们在看Watcher中的get方法

  1. get () {
  2. /* 收集Watcher实例,也就是Dep.target */
  3. pushTarget(this)
  4. let value
  5. const vm = this.vm
  6. try {
  7. value = this.getter.call(vm, vm)
  8. }
  9. return value
  10. }

get执行了getter方法,也就是我们例子中的

  1. this.firstName + ' ' + this.lastName

然后拿到计算属性最后的value值。

watch

watch初始化也是在initState方法中,上边已经讲到了。

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

来看一下 initWatch 的实现
源码:src/core/instance/state.js

  1. /**
  2. * 初始化侦听
  3. */
  4. function initWatch (vm: Component, watch: Object) {
  5. for (const key in watch) {
  6. const handler = watch[key]
  7. if (Array.isArray(handler)) {
  8. for (let i = 0; i < handler.length; i++) {
  9. createWatcher(vm, key, handler[i])
  10. }
  11. } else {
  12. createWatcher(vm, key, handler)
  13. }
  14. }
  15. }

这里就是对 watch 对象做遍历,拿到每一个 handler,因为 Vue 是支持 watch 的同一个 key 对应多个 handler,所以如果 handler 是一个数组,则遍历这个数组,调用 createWatcher 方法,否则直接调用 createWatcher

  1. function createWatcher (
  2. vm: Component,
  3. expOrFn: string | Function,
  4. handler: any,
  5. options?: Object
  6. ) {
  7. if (isPlainObject(handler)) {
  8. options = handler
  9. handler = handler.handler
  10. }
  11. if (typeof handler === 'string') {
  12. handler = vm[handler]
  13. }
  14. return vm.$watch(expOrFn, handler, options)
  15. }

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

  1. export function stateMixin (Vue: Class<Component>) {
  2. // ...
  3. Vue.prototype.$watch = function (
  4. expOrFn: string | Function,
  5. cb: any,
  6. options?: Object
  7. ): Function {
  8. const vm: Component = this
  9. if (isPlainObject(cb)) {
  10. return createWatcher(vm, expOrFn, cb, options)
  11. }
  12. options = options || {}
  13. /* 用户自定义watch */
  14. options.user = true
  15. const watcher = new Watcher(vm, expOrFn, cb, options)
  16. if (options.immediate) {
  17. try {
  18. cb.call(vm, watcher.value)
  19. }
  20. }
  21. /* 返回卸载watcher的方法 */
  22. return function unwatchFn () {
  23. watcher.teardown()
  24. }
  25. }
  26. }

侦听属性 watch 最终会调用 $watch 方法,这个方法首先判断 cb 如果是一个对象,则调用 createWatcher 方法,这是因为 $watch 方法是用户可以直接调用的,它可以传递一个对象,也可以传递函数。接着执行 const watcher = new Watcher(vm, expOrFn, cb, options) 实例化了一个 watcher,这里需要注意一点这是一个 user watcher,因为 options.user = true。通过实例化 watcher 的方式,一旦我们 watch 的数据发生变化,它最终会执行 watcher 的 run 方法,执行回调函数 cb,并且如果我们设置了 immediate 为 true,则直接会执行回调函数 cb。最后返回了一个 unwatchFn 方法,它会调用 teardown 方法去移除这个 watcher
所以本质上侦听属性也是基于 Watcher 实现的,它是一个 user watcher。其实 Watcher 支持了不同的类型,下面我们梳理一下它有哪些类型以及它们的作用。

Watcher Options

  1. if (options) {
  2. this.deep = !!options.deep // 深度监听
  3. this.user = !!options.user // 在对 watcher 求值以及在执行回调函数的时候,会处理一下错误
  4. this.lazy = !!options.lazy // 惰性求值,赋值给this.dirty,计算属性的时候用到的
  5. this.sync = !!options.sync // 在当前 Tick 中同步执行 watcher 的回调函数,否则响应式数据发生变化之后,watcher回调会在nextTick后执行;
  6. }

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

deep watcher

也就是我们通常说的深度监听,看一下我们如果将一个对象进行深度监听会发生什么:

  1. get () {
  2. if (this.deep) {
  3. traverse(value)
  4. }
  5. return value
  6. }

看一下traverse源码:src/core/observer/traverse.js

  1. export function traverse (val: any) {
  2. _traverse(val, seenObjects)
  3. seenObjects.clear()
  4. }
  5. function _traverse (val: any, seen: SimpleSet) {
  6. let i, keys
  7. const isA = Array.isArray(val)
  8. if ((!isA && !isObject(val)) || Object.isFrozen(val) || val instanceof VNode) {
  9. return
  10. }
  11. if (isA) {
  12. i = val.length
  13. while (i--) _traverse(val[i], seen)
  14. } else {
  15. keys = Object.keys(val)
  16. i = keys.length
  17. while (i--) _traverse(val[keys[i]], seen)
  18. }
  19. }

很清晰,对传入对watch对象进行递归遍历,因为递归有一定对性能开销,因此,我们一定要在合适的场景去设置deep。

user watcher

就是用户手写的watch监听,前面讲过了,略过。

computed watcher

为计算属性量身定制的监听,具有“缓存”功效,前面讲过了,略过。

sync watcher

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

  1. update () {
  2. if (this.lazy) {
  3. this.dirty = true
  4. } else if (this.sync) {
  5. /* 执行Watcher回调,触发视图更新 */
  6. this.run()
  7. } else {
  8. queueWatcher(this)
  9. }
  10. }

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

总结

计算属性和监听属性都是通过Watcher这个类去实现当,本身都具有监听数据的能力。
计算属性:计算属性本质上是 computed watcher,计算属性适合用在模板渲染中,某个值是依赖了其它的响应式对象甚至是计算属性计算而来,它具有缓存能力,当依赖的值没有变化甚至是计算结果没有发生变化,触发更新的回调则不会执行;
监听属性:侦听属性本质上是 user watcher,适用于观测某个值的变化去完成一段复杂的业务逻辑,当新老值相同,也不会去触发更新回调。