如何创建computed和watch?
在项目中computed和watch非常实用,它们是如何实现的呢?
我们的编码目标是下面的demo能够成功渲染,最终渲染结果<h1>未读消息:2</h1>
。
let v = new Vue({
el: '#app',
data () {
return {
news: [1]
}
},
computed: {
newsCount() {
return this.news.length
},
},
render (h) {
return h('h1', '未读消息:' + this.newsCount)
}
})
setTimeout(() => {
v.news.push(2)
}, 1000)
Vue响应式原理
根据上图可以知道,Vue将数据构造为响应式的,如果需要监听数据则要新建Watch
的实例,建立Dep和Watch之间联系。
实现watch
watch的常见用法如下:
watch: {
news () {
console.log('watch news!')
}
}
watch功能依托Watch
类实现,在Vue初始化时,为所有watch属性创建Watch
实例。
function initWatch(vm: Vue) {
const watch = vm.$options.watch
for (let key in watch) {
new Watch(vm._proxyThis, key, watch[key], { user: true })
}
}
new Watch
实例化过程中,会将key
转为函数,执行该函数可以获取被监听的属性值,另外会将watch[key]
函数保存在this.cb
变量中。
this.getter = isFunction(key) ? key : parsePath(key) || noop
this.cb = cb
function parsePath(key: string): any {
return function(vm: any) {
return vm[key]
}
}
接着直接执行上一步的函数this.getter
,收集所有依赖。
private get(): any {
let vm = this.vm
pushTarget(this)
let value = this.getter.call(vm, vm)
popTarget()
return value
}
当被监听属性发生变化时,会通知Watch
实例进行更新,从而执行this.cb.call(vm, value, this.value)
函数。
实现computed
computed的调用形式主要有以下两种:
computed: {
newsCount() {
return this.news.length
},
newsStr: {
get () {
return this.news.join(',')
},
set (val) {
this.news = val.split(',')
}
}
}
computed属性可以定义get
和set
函数,因此比较特殊:1.它的值依赖于其他数据属性;2.修改它也会驱动视图进行更新。
computed功能同样依赖Watch
类实现,在Vue初始化时,为所有的computed属性创建watch实例:new Watch(vm._proxyThis, getter, noop, {lazy: true})
。
function initComputed(vm: Vue) {
let proxyComputed: any
const computed = vm.$options.computed
if (!isPlainObject(computed)) return
for (let key in computed) {
let userDef = computed[key]
let getter = isFunction(userDef) ? userDef : userDef.get
vm._computedWatched[key] = new Watch(vm._proxyThis, getter, noop, {
lazy: true
})
}
vm.$options.computed = proxyComputed = observeComputed(
computed,
vm._computedWatched,
vm._proxyThis
)
for (let key in computed) {
proxyForVm(vm._proxyThis, proxyComputed, key)
}
}
接着将computed属性本身设置为响应式,同时调用createComputedGetter
对属性进行封装。
当修改computed属性时,computed触发闭包变量dep.notify
通知渲染更新。
当修改news
属性时,会触发Vue进行渲染更新,在重新获取computed属性值的时候,会执行createComputedGetter
封装后的函数,其本质是执行上一步的getter
函数,并将计算结果返回。
function observeComputed(obj: VueComputed, _computedWatched: any, proxyThis: any): Object {
if (!isPlainObject(obj) || isProxy(obj)) return obj
let proxyObj = createProxy(obj)
for (let key in obj) {
defineComputed(proxyObj, key, obj[key], _computedWatched[key], proxyThis)
}
return proxyObj
}
function defineComputed(
obj: any,
key: string,
userDef: VueComputedMethod,
watcher: any,
proxyThis: any
): void {
if (!isProxy(obj)) return
let dep: Dep = new Dep()
const handler: any = {}
if (isFunction(userDef)) {
handler.get = createComputedGetter(watcher)
handler.set = noop
} else if (isObject(userDef)) {
handler.get = createComputedGetter(watcher)
handler.set = userDef.set || noop
}
defineProxyObject(obj, key, {
get(target, key) {
Dep.Target && dep.depend()
return handler.get.call(proxyThis)
},
set(target, key, newVal) {
handler.set.call(proxyThis, newVal)
dep.notify()
return true
}
})
}
function createComputedGetter(watcher: Watch): Function {
return function computedGetter() {
if (watcher) {
// 计算值
watcher.evaluate()
// 将computed-dep添加watch对象
Dep.Target && watcher.depend()
return watcher.value
}
}
}
分析computed肯定要提到其缓存特性,这又是如何实现的?
我们知道获取computed的属性值时,会执行createComputedGetter
封装后的函数,通过给Watch
类添加dirty
属性控制是否重新计算computed的属性值。Watch
类的其他函数中肯定需要配合修改,如evaluate
和update
方法。
function createComputedGetter(watcher: Watch): Function {
return function computedGetter() {
if (watcher) {
// 计算值
if (watcher.dirty) {
watcher.evaluate()
}
// 将computed-dep添加watch对象
Dep.Target && watcher.depend()
return watcher.value
}
}
}
Vue的数据处理流程
Vue在实例化过程中,会对传入的数据进行初始化处理。
首先肯定是为prop、data创建闭包变量dep,接着才是初始化computed和watch的属性,在后者中创建Watch
实例监听属性的变化。
initProps(this)
initMethods(this)
initData(this)
initComputed(this)
initWatch(this)
总结
Vue的响应式渲染依赖Dep和Watch,computed和watch功能也依赖它们,另外,Vue还封装了方法$watch
对属性进行监听。为了支持上述功能,Watch和Dep添加了一些配置项,在理解源码时,可以进行一定忽略。
Dep和Watch设计的相当巧妙,我们自己编程能不能想到这样的方式?推荐学习下设计模式,或许能有所帮助。