前言
vue中computed和watch是vue.js开发者的利器,也是面试必问的题目之一,问题的答案也是可深可浅,可以反应回答者对个这个问题的认识程度(类似于输入url到页面渲染发生了哪些事情)
分析
1 用法上的区别:
我的理解是,用到computed往往是我们需要使用他的值(
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 } } }) |
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 |
达到的效果是:当前执行目录(
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 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() } } } |
流程大概是这样的:
-
_init -> initWath -> createWatcher(vm, key, handler) -> vm.$watch -> new Watch() -> this.get -
this.get() 的时候,会执行const value = getter ? getter.call(obj) : val 和pushTarget(this) (做的事情:Dep.target =this),getter就是对我们要观测到的值访问值(比如:'obj.a.b' => obj.a.b),会触发obj.a.b的get劫持。针对deep的情况会进一步的递归访问值,触发get劫持。 -
执行: dep.depend() -> Dep.target.addDep(dep实例) ->dep.addSub(当前watcher实例),依赖收集完成。
-
派发更新逻辑开始, 当obj.a.b的值发生改变时,会触发set函数,执行dep.notify -> subs[i].update() -> watcher实例.update() -> queueWatcher(push到watcher队列,排序watcher)->nextTick(flushSchedulerQueue)(下个tick执行watcher队列的)->watcher.run()
(后面的代码分支有点多,就不一一贴上了) -
执行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 } } } |
大概的流程是:
- 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的值
- 当我们组件中使用到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
- 当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就重新求值。
总结:
- watcher和dep相互收集,我们定义的data中的一个属性(基础类型,引用类型递归创建dep, 确保一个基础类型一个dep)拥有一个dep,dep会收集所有的watcher(渲染watcher 、computed watcher 、用户定义的watcher), watcher也会记录被哪些dep收集了,当然这个过程中会有一个去重处理, data.xxx发生变化会通知所有的watcher, dep是obj.xxx和watcher的一个桥梁。
- watch(用户watcher)和computed的异同点:
-
相同点:computed[key]和user watcher都会生成一个watcher实例。
-
不同点:
- dep不同:watch监听的是vm[key]的变化,vm[key]的dep, vm[key]变化触发watch.get求值,触发watcher回调函数。computed中的dep是computed[key]执行过程中访问的dep,即用到了哪些值。
- dep收集的对象不同: 执行用户watcher.get的时候,watch的[key] (vm[key])的dep只会收集当然watcher,computed watcher中的dep会收集渲染watcher和computed watcher
- 执行时机不同: watcher immediate除外,computed[key]在我们使用到时会触发getter,触发watcher.get()执行computed[key], watch则只会在vm[key]改变触发this.cb即watch中定义的handler。
最后:
- 如果有错误欢迎指出
- 如有帮助欢迎点赞_。
-
vue源码分析附上我总结的思维导图
Vue源码分析.png