Vue响应式之watch
# watch 干么用的
先上官方文档 (opens new window) ,值支持 string | Function | Object | Array
对于对象类型,支持 immediate/deep/handler 参数
immediate: 开始监听时立即调用 handler 此时 oldValue 一定为 undefined
deep: 是否深入监听对象,即监听对象的子孙属性
# 实现
老样子,先上示例,简单起见,我们传入一个对象类型的 watch
<script type="text/x-template" id="demo-template">
<div>
{{user.name}}
</div>
</script>
<script>
var vm = new Vue({
el: '#el',
template: '#demo-template',
data: {
user: {
name: "xxx"
}
},
watch: {
user:{
deep:true,
immediate:true,
handler:function(val,oldVal){
console.log(val,oldVal)
}
}
},
})
</script>
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
尝试自己实现 watch
我们先看下 Watcher 支持哪些参数
首先是 expOrFn ,用于取值的,支持 xx.xx
字符串,因此这个参数应该是用于传入 watch 的属性
其次是 cb ,在 Watcher 执行更新时会进行触发,参数为新旧两个值,那么这个就很适合我们的 handler 函数
最后是 options 的 deep 和 lazy 参数, deep 应该是一样的,lazy 含义貌似和 immediate 相反
因此,我们遍历了 watch 的每个属性,并各自内部创建一个 Watcher
let watch = vm.$watch[key]
watcher[key] = new Watcher(vm,key,watch.handler,{
lazy: !watch.immediate,
deep: watch.deep
})
2
3
4
5
实例化 Watcher 时, watch 的 key 会被 parsePath 解析成一个 getter 方法,在实例化的最后,调用该 getter 以至 watcher 添加上 watch 的 key 的依赖
当观察目标发生变更时,通知 watcher 更新最后调用了 handler 函数
但是注意一点,当 lazy 为 true ( immediate = false )的时候,依赖是还没有绑定的,怎样才能调用 Watcher.get
去绑定依赖呢?
computed 属性创建的 Watcher 也是 lazy 的,同时 computed 属性是可以被取值的,当在其他地方访问到该计算属性(被取值)时才会开始依赖收集.
但是 watch 不同, watch 是对某个值的监听,不会有被取值的说法,于是 get 也就调用不到,依赖也就绑定不了
我们换个思路,无论是否 immediate 都会调用 get 进行去添加依赖,即 lazy 都设置为 false
并且在实例化 Watcher 之后,手动触发一个 handler
这样就可以实现 immediate 的效果了(我真是个小精灵鬼
不过这里我们还有一个疑问, Watcher 是怎么实现 deep ,这个我们稍后分析源码的时候再看
# 源码分析
前言:和我上面的实现基本无异
对于 watch ,Vue 组件上没有特殊的处理,我们从 initState 开始分析
这里有个小知识点,对 watch 参数做了判断
// src\core\instance\state.js
// nativeWatch = ({}).watch
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
2
3
4
5
6
注释是说:Firefox has a "watch" function on Object.prototype
根据 MDN Object.prototype.watch (opens new window) 上的描述,在 Firefox 58 版本之前, Object 原型上有个 watch 方法用于监听某个属性值的变动的。
所以以后在 watch 方法的判断上注意避免踩坑.
前面合并参数的时候已经判断过 watch 参数是否为纯对象了,本以为这里如果还有值就只有有赋值的情况,没想到还有 Firefox 的深坑
继续分析,进入 initWatch
// src\core\instance\state.js
function initWatch (vm: Component, watch: Object) {
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)
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
当 watch[key]
值为数组时遍历每一项并执行 createWatcher ,否则直接执行 createWatcher
之所以支持数组是考虑方法职责明确,方便代码复用
进入 createWatcher
// src\core\instance\state.js
function createWatcher (
vm: Component,
expOrFn: string | Function,
handler: any,
options?: Object
) {
if (isPlainObject(handler)) {
options = handler
handler = handler.handler
}
if (typeof handler === 'string') {
handler = vm[handler]
}
return vm.$watch(expOrFn, handler, options)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
纯对象的解析就不说了,来看下这个字符串类型的 handler 是怎么回事
这时候能从 vm 上取到的方法,也就 methods 中的了
所以支持字符串属性是为了代码复用
最后调用 vm.$watch
,这个在 官方文档 (opens new window) 上有说明
vm.$watch( expOrFn, callback, [ options:{deep,immediate}] )
该方法最后会返回一个取消监听的方法 unwatch
我们在 1.Vue实例化过程 中有提过,Vue 初始化的时候会执行 stateMixin 方法,最后再原型上挂载 $watch
方法
// src/core/instance/state.js
export function stateMixin (Vue: Class<Component>) {
// ... 省略部分代码
Vue.prototype.$watch = function (
expOrFn: string | Function,
cb: any,
options?: Object
): Function {
const vm: Component = this
if (isPlainObject(cb)) {
return createWatcher(vm, expOrFn, cb, options)
}
options = options || {}
options.user = true
const watcher = new Watcher(vm, expOrFn, cb, options)
if (options.immediate) {
try {
cb.call(vm, watcher.value)
} catch (error) {
handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
}
}
return function unwatchFn () {
watcher.teardown()
}
}
}
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
先说 unwatchFn ,执行 unwatchFn 时会调用 watcher.teardown
作用就是删除依赖,依赖清除观察者等等
里面一些细节: active 控制等可以自行研究
if (isPlainObject(cb)) {
return createWatcher(vm, expOrFn, cb, options)
}
2
3
这个处理是外部使用 $watch 时进行的,我们初始化 watch 时已经处理过纯对象的情况了
接着根据传入值设置 options , Watcher 中有用到的只有 deep 和 user
user 表示是用户主动创建的 Watcher ,其作用就是出现异常时提示用户哪个监听器回调出错了
系统自己创建的一般是不会有问题
而后实例化 Watcher: watcher = new Watcher(vm, expOrFn, cb, options)
,和我们前文分析的一样,会在实例化过程中解析 expOrFn 顺序访问对象(如'a.b'则顺序访问 a,a.b)并将 watcher 添加这些对象(a,a.b)的依赖, 之后无论是 a
还是 a.b
发生变更都会触发 watcher 更新执行 handler
实例化后,此时 watcher.value
的值为所观察的属性的最新值,若设置了 immediate ,将该值传入 handler 并调用
cb.call(vm, watcher.value)
// 此时 oldValue 必为 undefined
2
最后再来分析下 deep
对于这个场景:
data: {
user: {
name: {
firstName: 'xxx'
}
}
},
watch: {
'user.name': {
deep: true,
handler: function (val, oldVal) {
console.log(val, oldVal)
}
}
},
2
3
4
5
6
7
8
9
10
11
12
13
14
15
user.age 变化会不会触发 handler?
user.name.firstName 变化会不会触发 handler ,怎么实现
前者肯定是不会的,如果 user 的值(对象引用)变化了会触发
后者肯定会,在 Watcher 中做了处理
get () {
pushTarget(this)
let value
const vm = this.vm
try {
value = this.getter.call(vm, vm)
} catch (e) {
if (this.user) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
} else {
throw e
}
} finally {
// "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {
traverse(value)
}
popTarget()
this.cleanupDeps()
}
return value
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
一开始执行 getter 后,watcher 添加了 user 和 name 的依赖
而后执行 traverse(value)
// src\core\observer\traverse.js
/**
* Recursively traverse an object to evoke all converted
* getters, so that every nested property inside the object
* is collected as a "deep" dependency.
*/
export function traverse (val: any) {
// seenObjects = new Set()
_traverse(val, seenObjects)
seenObjects.clear()
}
function _traverse (val: any, seen: SimpleSet) {
let i, keys
const isA = Array.isArray(val)
if ((!isA && !isObject(val)) || Object.isFrozen(val) || val instanceof VNode) {
return
}
if (val.__ob__) {
const depId = val.__ob__.dep.id
if (seen.has(depId)) {
return
}
seen.add(depId)
}
if (isA) {
i = val.length
while (i--) _traverse(val[i], seen)
} else {
keys = Object.keys(val)
i = keys.length
while (i--) _traverse(val[keys[i]], seen)
}
}
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
递归遍历对象,触发嵌套属性已转换的 getter ,以便 watcher 添加这些嵌套属性的依赖
比如 name 的嵌套属性有 firstName ,此时 watcher 会添加 firstName 的依赖
这里有个细节,进行 traverse 时 seenObjects 用的是一开始就生成的 Set ,用完将其清空,这样每次执行 traverse 时用的都是 seenObjects ,而不是每次都新创建 Set ,因此新建 Set 的性能以及 GC 方法的处理肯定都不如复用 seenObjects
# 附加题1
firstName 变更,返回的 oldValue.firstName 是原来的值么?
答案
不是,还是新值,因为 value 和 oldValue 是同一个引用,此时访问 oldValue.firstName
其实和访问 value.firstName
是一样的
# 附加题2
如果这时执行 vm.$set(vm.user.name,'lastName','ttt')
会触发 handler 么,为什么?
答案
会,因为一开始依赖收集,触发了 user.name
的 getter , Watcher 观察者会添加了 user.name
的值的依赖
if (childOb) {
childOb.dep.depend()
// ...
}
2
3
4
通过 set 方法对 user.name
的属性值进行变更时,user.name
值的依赖会通知观察者更新,于是会触发 handler
最后,返回的 value/oldValue 都是同一个引用
# 总结
从 watch 和 computed 的源码分析上来看,两者的区别一个是主动监听,一个是被动监听
在分析源码的时候,也明白了一些平时忽略的用法,受益良多