Vue.js 深入理解 computed 与 watch

前言

vue中computed和watch是vue.js开发者的利器,也是面试必问的题目之一,问题的答案也是可深可浅,可以反应回答者对个这个问题的认识程度(类似于输入url到页面渲染发生了哪些事情)


分析

1 用法上的区别:

我的理解是,用到computed往往是我们需要使用他的值(vm[computedKey]),这个值是多个值求值的结果,相当于是我们保存了计算过程,计算过程中使用过的值发生变化时,会触发重新执行computed[key]函数(或者computed[key].get),例如:

1
2
3
4
5
6
7
8
9
10
11
12
vm = new Vue({
  el: '#demo',
  data: {
    firstName: 'Foo',
    lastName: 'Bar'
  },
  computed: {
    fullName: function () {
      return this.firstName + ' ' + this.lastName
    }
  }
})

fullName就是我们需要的值,fullName依赖this.firstNamethis.lastName,这两个依赖值变化时会触发computed函数重新执行求值。如果该需求使用watch就是这样子的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var vm = new Vue({
  el: '#demo',
  data: {
    firstName: 'Foo',
    lastName: 'Bar',
    fullName: 'Foo Bar'
  },
  watch: {
    firstName: function (val) {
      this.fullName = val + ' ' + this.lastName
    },
    lastName: function (val) {
      this.fullName = this.firstName + ' ' + val
    }
  }
})

与computed相比,watch实现这种需求显得很繁琐。
watch的使用场景如他的名字一样: 观察。webpack中可以在config中或者命令行模式中使用watch字段:
webpack.config.js

1
2
3
4
module.exports = {
  //...
  watch: true
};

命令行

1
webpack --watch

达到的效果是:当前执行目录(process.cwd())里面文件发生改变时,webpack能检测到他变化了,重新打包和热更新。
vue.js中也是如此,我们观察某个值变化,这个值变化了,我们来做相应的事情。
例如:
组件的v-model语法糖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
{
    props: {
        value: {
            type: String,
            required: true
        }
    },
    data () {
        return {
            text: ''
        }
    },
    watch: {
        // 父组件中可能改变value绑定的值
        value (val) {
            this.text = val
        },
        text (val) {
            this.$emit('input', val)
        }
    }
}

一句话概括就是: computed[key]这个值我们需要用到它,依赖变化运算过程自动执行,watch[key]这个值我们不需要用到它,它变化了我们想做一些事情。
当然,理论上来说computed能实现上面的需求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
computed: {
   // 这里xxx我们还需要使用到,不然无法触发求值
    xxx () {
        this.value // 这里啥都不做就是想做个依赖收集
        this.text  // 同上
        // this.text和this.value旧值都需要缓存起来
        if (this.value !== this.value的旧值) {
            this.text = this.value
        }
        if (this.text !== this.text的旧值) {
            this.$emit('input', this.text)
        }
    }
}

这样实现太繁琐,所以合适的场景使用合适的api的,这样才符合设计的初衷。有些场景两者使用没有多大区别。

2 源码分析:

看过源码的同学都清楚,watch和computed的每一项最终都会执行new Watcher生成一个watcher实例,执行上面会有一些差异。下面开始从源码分析一下:

注意:vue.js每个版本可能会更改一些逻辑,当前分析版本: v2.6.11 web版,下文中提到的vm[key]相当于我们在vue中使用的this.xxx属性值

当执行 new Vue({})的时候或者生成组件实例,(组件会类似Copm extend Vue 派生出组件构造类在Vue上),最终都会执行_init()逻辑,如下(这里其他逻辑省略):

1
2
3
4
5
6
7
8
9
10
11
12
_init () {
    ...
    initState(vm)
    ...
}

initState () {
   ...
   initComputed(vm, opts.computed)
   initWatch(vm, opts.watch)
   ...
}

1. watch:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
function initWatch (vm, watch) {
    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)
    }
  }
}

function createWatcher (vm,expOrFn,handler,options) {
  if (isPlainObject(handler)) { // handler 是否为对象
    // watch[key]可以是函数或者对象
    options = handler
    handler = handler.handler
  }
  //
  if (typeof handler === 'string') {
    handler = vm[handler]
  }
  return vm.$watch(expOrFn, handler, options)
}

Vue.prototype.$watch = function (expOrFn, cb, options) {
    options = options || {}
    options.user = true
    const watcher = new Wacter(vm, expOrFn,cb, options)
    if (options.immediate) {
      cb.call(vm, watcher.value)
    }
}

 class Watcher {
  constructor (vm, expOrFn, cb, options) {
   // 保留关键代码
    if (options) {
      this.user = !!options.user
      this.lazy = !!options.lazy
    }
    this.cb = cb
    this.active = true
    this.id = ++uid // uid for batching
    // parse expression for getter
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      // 用户watch逻辑下 expOrFn 为watch[key]的key,类型为 string
      this.getter = parsePath(expOrFn)
    }
    this.value = this.lazy
      ? undefined
      : this.get()
    }
    get () {
      // 这里做的事情是 Dep.target = this
      pushTarget(this)
      let value
      const vm = this.vm
      // 防止用户(你)做傻事让js报错终止运行
      try {
        // 访问了 vm.obj.a.b
        value = this.getter.call(vm, vm)
      } catch (e) {
        if (this.user) {
          handleError(e, vm, `getter for watcher "${this.expression}"`)
        } else {
          throw e
        }
      } finally {
        if (this.deep) {
          // deep 对obj的每个obj[key]访问 触发依赖收集
          traverse(value)
        }
        // Dep.target = 上一个watcher 实例
        popTarget()
      }
      return value
    }
    addDep (dep) {
      // 一个dep 在一个watcher上只添加一次
      const id = dep.id
      if (!this.newDepIds.has(id)) {
        this.newDepIds.add(id)
        this.newDeps.push(dep)
        if (!this.depIds.has(id)) {
          dep.addSub(this)
        }
      }
    }
    update () {
      if (this.lazy) {
        this.dirty = true
      } else if (this.sync) {
        this.run()
      } else {
        queueWatcher(this)
      }
    }
   
    run () {
      if (this.active) {
        const value = this.get()
        if (
          value !== this.value ||
          isObject(value) ||
          this.deep
        ) {
          const oldValue = this.value
          this.value = value
          if (this.user) {
            try {
              this.cb.call(this.vm, value, oldValue)
            }
          }
        }
      }
    }
  }
 
  function parsePath (path) {
    // 这个函数的目的是返回我们需要观察的那个值的求值函数
    /*
    我们的定义watch可能是 watch: {
        'obj.a.b.c': {
            handler () {}
        }
    }
    */
    const segments = path.split('.')
    return function (obj) {
      for (let i = 0; i < segments.length; i++) {
      if (!obj) return
      obj = obj[segments[i]]
      }
      return obj
    }
}

// 响应式核心代码
// 一个值有一个dep实例
const dep = new Dep()
Object.defineProperty(obj, key, {
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      const value = getter ? getter.call(obj) : val
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb = !shallow && observe(newVal)
      dep.notify()
    }
  })
 
  class Dep {
  constructor () {
    this.id = uid++
    this.subs = []
  }

  addSub (sub) {
    this.subs.push(sub)
  }
  removeSub (sub) {
    remove(this.subs, sub)
  }

  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }

  notify () {
    const subs = this.subs.slice()
    if (process.env.NODE_ENV !== 'production' && !config.async) {
      // 先创建的先执行 用户watcher computed watcher 在渲染watcher之前
      subs.sort((a, b) => a.id - b.id)
    }
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}

流程大概是这样的:

  1. _init -> initWath -> createWatcher(vm, key, handler) -> vm.$watch -> new Watch() -> this.get

  2. this.get()的时候,会执行const value = getter ? getter.call(obj) : valpushTarget(this)(做的事情:Dep.target =this),getter就是对我们要观测到的值访问值(比如:'obj.a.b' => obj.a.b),会触发obj.a.b的get劫持。针对deep的情况会进一步的递归访问值,触发get劫持。

  3. 执行: dep.depend() -> Dep.target.addDep(dep实例) ->dep.addSub(当前watcher实例),依赖收集完成。


  1. 派发更新逻辑开始, 当obj.a.b的值发生改变时,会触发set函数,执行dep.notify -> subs[i].update() -> watcher实例.update() -> queueWatcher(push到watcher队列,排序watcher)->nextTick(flushSchedulerQueue)(下个tick执行watcher队列的)->watcher.run()
    (后面的代码分支有点多,就不一一贴上了)

  2. 执行this.get,相当于执行了第二步的,逻辑,比较新旧值是否相等(value基础类型,引用类型或者deep直接执行接下来的逻辑),执行this.cb.call(this.vm, value, oldValue),this.cb就是用户定义的watch[key]的函数。所以我们在定义watch函数的时候第一个参数是newValue 第二个参数是oldValue

总结:我们在Vue.js中使用的watch是userWatch,我们观测某个属性的变化,监测逻辑和渲染时的依赖收集一样:dep添加watch,这个值变化了通知所有的watch, 最后会执行我们的定义的watch[key]的handler函数。

提示:渲染watcher类似与上面的watcher,监测的是template中用的值,只要有一个值发生变化,watcher就会触发,重新渲染。

2. computed:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
const computedWatcherOptions = { lazy: true }
function initComputed (vm, computed) {
  for (const key in computed) {
    // 不考虑设置了computed get set
    const userDef = computed[key]
    const getter = typeof userDef === 'function' ? userDef : userDef.get
    if (!isSSR) {
      watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        computedWatcherOptions
      )
    }
    if (!(key in vm)) {
      defineComputed(vm, key, userDef)
    }
  }
}

class Watcher {
  constructor (vm,expOrFn,cb,options) {
    this.vm = vm
    vm._watchers.push(this)
    if (options) {
      this.lazy = !!options.lazy
    }
    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.getter = expOrFn // 用户computed[key]的值
    // computed 不执行this.get
    this.value = undefined
  }
  get () {
    // Dep.target = 当前watcher
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      value = this.getter.call(vm, vm)
    }
    // Dep.target设置为上一个watcher 渲染watcher
    popTarget()
    this.cleanupDeps()
    return value
  }
  evaluate () {
    this.value = this.get()
    this.dirty = false
  }

  /**
   * Depend on all deps collected by this watcher.
   */
  depend () {
    let i = this.deps.length
    while (i--) {
      this.deps[i].depend()
    }
  }
 
  notify () {
    const subs = this.subs.slice()
    if (process.env.NODE_ENV !== 'production' && !config.async) {
      // 先创建的先执行 用户watcher computed watcher 在渲染watcher之前
      subs.sort((a, b) => a.id - b.id)
    }
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
  update () {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)
    }
  }
}
 
function defineComputed (
  target: any,
  key: string,
  userDef: Object | Function
) {
  if (typeof userDef === 'function') {
    sharedPropertyDefinition.get =  createComputedGetter(key)
    sharedPropertyDefinition.set = noop // noop 为空函数
  }
  Object.defineProperty(target, key, sharedPropertyDefinition)
}

function createComputedGetter (key) {
  return function computedGetter () {
    const watcher = this._computedWatchers && this._computedWatchers[key]
    if (watcher) {
      if (watcher.dirty) {
        // 确保执行一次
        watcher.evaluate()
      }
      if (Dep.target) {
        watcher.depend()
      }
      return watcher.value
    }
  }
}

大概的流程是:

  1. initComputed遍历options.computed对象执行new Watcher和defineComputed
  • new Watcher: new Watcher -> 只会执行Watcher类的构造函数

注意:组件这个逻辑会在Vue.extend()(Vue派生的组件构造类)过程中执行,这里分析根节点的

  • defineComputed: sharedPropertyDefinition.get = createComputedGetter(key)(生成vm[key] computed[key] 的get劫持函数),通过object.defineProperty设置vm[key]的get和set,所以能通过this[key]的方式访问computed的值
  1. 当我们组件中使用到computed[key]即是vm[key]或this[key] (如:前面提到的fullName),触发createComputedGetter(key)生成的get函数:
  • watcher.evaluate() -> this.get() -> Dep.target = 当前的watcher实例(computed[key]生成的) -> 执行this.getter(用户定义的computed[key]) -> 触发函数里面使用this.xxx(如:fullName () {return this.firstName + this.lastName})的get劫持函数和上面的watcher一样: firstName和lastName都会收集该watcher -> this.dirty = false(项目中多个地方用到了该computed[key],watcher.evaluate()只需要执行一次)
  • watcher.depend() -> watcher.dep[i].depend() computed[key]使用到的一个值(firstName lastName)就拥有一个dep,(deps包含了firstName和lastName的dep)->
    Dep.target.addDep(this) 上一步的时候Dep.target已经设置为上一个watcher了,即是渲染watcher ->
    这些dep也会收集渲染watcher
  • return watcher.value computed[key] (vm[key]或this[key])就是watcher.value
  1. 当computed 依赖的这些值(fistName或者lastName发生变化)发生变化时,触发set逻辑 -> dep.notify 通过id排序 确保computed watcher先执行,dep订阅的watcher遍历执行update -> this.dirty = true -> 执行渲染watcher.update -> 组件template重新渲染 -> 再执行第2步保持值为最新值。

注意:这个版本好像computed没有之前所谓的缓存,newVal oldVal不会比较了,依赖的值发生改变,重新求值。

而且vue的官方文档中也提到:

不同的是计算属性是基于它们的响应式依赖进行缓存的。只在相关响应式依赖发生改变时它们才会重新求值。这就意味着只要 message 还没有发生改变,多次访问 reversedMessage 计算属性会立即返回之前的计算结果,而不必再次执行函数。

之前的版本我记得是这样的, c () {return this.a + this.b},a = 1, b =2 -> a =2, b=1,最终的值不变就会缓存,现在不再说computed会比较新旧值了,而是说明依赖发生改变,computed就重新求值。

总结:

  1. watcher和dep相互收集,我们定义的data中的一个属性(基础类型,引用类型递归创建dep, 确保一个基础类型一个dep)拥有一个dep,dep会收集所有的watcher(渲染watcher 、computed watcher 、用户定义的watcher), watcher也会记录被哪些dep收集了,当然这个过程中会有一个去重处理, data.xxx发生变化会通知所有的watcher, dep是obj.xxx和watcher的一个桥梁。
  2. watch(用户watcher)和computed的异同点:
  • 相同点:computed[key]和user watcher都会生成一个watcher实例。

  • 不同点

    1. dep不同:watch监听的是vm[key]的变化,vm[key]的dep, vm[key]变化触发watch.get求值,触发watcher回调函数。computed中的dep是computed[key]执行过程中访问的dep,即用到了哪些值。
    2. dep收集的对象不同: 执行用户watcher.get的时候,watch的[key] (vm[key])的dep只会收集当然watcher,computed watcher中的dep会收集渲染watcher和computed watcher
    3. 执行时机不同: watcher immediate除外,computed[key]在我们使用到时会触发getter,触发watcher.get()执行computed[key], watch则只会在vm[key]改变触发this.cb即watch中定义的handler。

最后:

  • 如果有错误欢迎指出
  • 如有帮助欢迎点赞_
  • vue源码分析附上我总结的思维导图


    Vue源码分析.png