# Computed 和 Watch
Computed本质是一个具备缓存的watcher,依赖的属性发生变化就会更新视图。
适用于计算比较消耗性能的计算场景。当表达式过于复杂时,在模板中放入过多逻辑会让模板难以维护,可以将复杂的逻辑放入计算属性中处理。
Watch没有缓存性,更多的是观察的作用,可以监听某些数据执行回调。当我们需要深度监听对象中的属性时,可以打开deep:true选项,这样便会对对象中的每一项进行监听。这样会带来性能问题,优化的话可以使用字符串形式监听,如果没有写到组件中,不要忘记使用unWatch手动注销哦。
- 功能上:
computed是计算属性,也就是依赖其它的属性计算所得出最后的值。watch是去监听一个值的变化,然后执行相对应的函数 - 使用上:
computed中的函数必须要用return返回;watch的回调里面会传入监听属性的新旧值,通过这两个值可以做一些特定的操作,不是必须要用return - 性能上:
computed中的函数所依赖的属性没有发生变化,那么调用当前的函数的时候会从缓存中读取,而watch在每次监听的值发生变化的时候都会执行回调 - 场景上
- computed:当一个属性受多个属性影响的时候,例子:购物车商品结算;
- watch:当一条数据影响多条数据的时候,例子:搜索框
# Watch
侦听属性的初始化也是发生在 Vue 的实例初始化阶段的 initState 函数中,在 computed 初始化之后,执行了:
function initState(vm) { // 初始化所有状态时
vm._watchers = [] // 当前实例watcher集合
const opts = vm.$options // 合并后的属性
... // 其他状态初始化
if(opts.watch) { // 如果有定义watch属性
initWatch(vm, opts.watch) // 执行初始化方法
}
}
---------------------------------------------------------
function initWatch (vm, watch) { // 初始化方法
for (const key in watch) { // 遍历watch内多个监听属性
const handler = watch[key] // 每一个监听属性的值
if (Array.isArray(handler)) { // 如果该项的值为数组
for (let i = 0; i < handler.length; i++) {
createWatcher(vm, key, handler[i]) // 将每一项使用watcher包装
}
} else {
createWatcher(vm, key, handler) // 不是数组直接使用watcher
}
}
}
---------------------------------------------------------
function createWatcher (vm, expOrFn, handler, options) {
if (isPlainObject(handler)) { // 如果是对象,参数移位
options = handler
handler = handler.handler
}
if (typeof handler === 'string') { // 如果是字符串,表示为方法名
handler = vm[handler] // 获取methods内的方法
}
return vm.$watch(expOrFn, handler, options) // 封装
}
这里的逻辑也很简单,首先对 hanlder 的类型做判断,拿到它最终的回调函数,最后调用 vm.$watch(keyOrFn, handler, options) 函数,$watch 是 Vue 原型上的方法,它是在执行 stateMixin 的时候定义的:
Vue.prototype.$watch = function(expOrFn, cb, options = {}) {
const vm = this
if (isPlainObject(cb)) { // 如果cb是对象,当手动创建监听属性时
return createWatcher(vm, expOrFn, cb, options)
}
options.user = true // user-watcher的标志位,传入Watcher类中
const watcher = new Watcher(vm, expOrFn, cb, options) // 实例化user-watcher
if (options.immediate) { // 立即执行
cb.call(vm, watcher.value) // 以当前值立即执行一次回调函数
} // watcher.value为实例化后返回的值
return function unwatchFn () { // 返回一个函数,执行取消监听
watcher.teardown()
}
}
虽然watch内部是使用this.$watch,但是我们也是可以手动调用this.$watch来创建监听属性的,所以第二个参数cb会出现是对象的情况。接下来设置一个标记位options.user为true,表明这是一个user-watcher。
再给watch设置了immediate属性后,会将实例化后得到的值传入回调,并立即执行一次回调函数,这也是immediate的实现原理。最后的返回值是一个方法,执行后可以取消对该监听属性的监听。接下来我们看看user-watcher是如何定义的:
class Watcher {
constructor(vm, expOrFn, cb, options) {
this.vm = vm
vm._watchers.push(this) // 添加到当前实例的watchers内
if(options) {
this.deep = !!options.deep // 是否深度监听
this.user = !!options.user // 是否是user-wathcer
this.sync = !!options.sync // 是否同步更新
}
this.active = true // // 派发更新的标志位
this.cb = cb // 回调函数
if (typeof expOrFn === 'function') { // 如果expOrFn是函数
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn) // 如果是字符串对象路径形式,返回闭包函数
}
...
}
}
当是user-watcher时,Watcher内部是以上方式实例化的,通常情况下我们是使用字符串的形式创建监听属性,所以首先来看下parsePath方法是干什么的:
const bailRE = /[^\w.$]/ // 得是对象路径形式,如info.name
function parsePath (path) {
if (bailRE.test(path)) return // 不匹配对象路径形式,再见
const segments = path.split('.') // 按照点分割为数组
return function (obj) { // 闭包返回一个函数
for (let i = 0; i < segments.length; i++) {
if (!obj) return
obj = obj[segments[i]] // 依次读取到实例下对象末端的值
}
return obj
}
}
parsePath方法最终返回一个闭包方法,此时Watcher类中的this.getter就是一个函数了,再执行this.get()方法时会将this.vm传入到闭包内,补全Watcher其他的逻辑: