/**
* Evaluate the getter, and re-collect dependencies.
*/
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
}
get 方法首先执行 pushTarget() 。我们知道 pushTarget 会修改 Dep.target 为传入的 this 也就是 user Watcher 。
// src/core/observer/traverse.js
const seenObjects = new Set();
/**
* 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) {
_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);
}
}
traverse 这里调用了_traverse 函数。_traverse 函数会对传入的 val 参数进行校验,即 val 满足下面三个条件的话直接return:
val 不是一个数组或者不是一个对象
val是被冻结的
val是VNode实例
接着有 if (val.__ob__) 和 if (isA) else 两大段逻辑,我们先分析 if (isA) else 。这段逻辑就是针对 val 是数组或者是对象分别做遍历,遍历它们的元素或者属性递归调用 _traverse 函数。
我们再回过头来看 if (val.__ob__) 这段逻辑,它的作用是把 val 的 __ob__.dep.id 保存到 seen 也就是在全局定义的 seenObjects 中,并且还通过 if (seen.has(depId)) 逻辑防止重复保存。那什么情况会重复保存呢,那就是循环引用,比如下面这个例子:
以上就是 watch 的初始化渲染过程,其原理就是访问被 watch 的数据触发其 getter ,使得 user Watcher 被收集,在被 watch 的数据改变时就能触发 setter 通知 user Watcher 执行回调。
当被 watch 的数据发生变化时会触发 setter 派发更新,我们知道派发更新就是通知订阅者 user Watcher 去执行 update 方法:
// src/core/observer/watcher.js
update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
这里如果sync为true,那么就会执行run函数:
// src/core/observer/watcher.js
/**
* Scheduler job interface.
* Will be called by the scheduler.
*/
run () {
if (this.active) {
const value = this.get()
if (
value !== this.value ||
// Deep watchers and watchers on Object/Arrays should fire even
// when the value is the same, because the value may
// have mutated.
isObject(value) ||
this.deep
) {
// set new value
const oldValue = this.value
this.value = value
if (this.user) {
try {
this.cb.call(this.vm, value, oldValue)
} catch (e) {
handleError(e, this.vm, `callback for watcher "${this.expression}"`)
}
} else {
this.cb.call(this.vm, value, oldValue)
}
}
}
}
引言
上一节我们分析了计算属性
computed
,这一节我们一起来看下侦听器watch
。大家平时在项目开发中,有时会对这两种特性感到模棱两可,不知道该选择那个。相信看完本篇文章,结合上一篇,你会有一个答案的。
让我们开始吧!
watch
首先组件实例化过程中,会调用
initState
函数:这里面调用了
initWatch
方法,来看它的定义:initWatch
函数会遍历watch
的属性然后调用createWatcher
函数。这里判断了watch[key]
是数组的情况。如果是数组需要遍历数组每一项再调用
createWatcher
函数。来看createWatcher
函数的定义:createWatcher
对handler
分别是对象和字符串的情况,进行了处理,统一将handler
转成函数并调用vm.$watch
函数。$watch
方法定义在Vue
原型上:这里
$watch
先判断传入的cb
是不是对象,如果是对象则继续调用createWatcher
进行参数规范化处理。这里传入的
options
参数是undefined
,所以会给options
创建一个空对象并且options.user = true
。接着将
options
传入Watcher
创建一个user Watcher
实例。我们来看user Watcher
的实例化过程:这里因为不是渲染
Watcher
,所以if (isRenderWatcher)
不会执行,接着在if (options)
逻辑中初始化属性值,其中this.user = true
表明了这是一个user Watcher
,而其他的像deep
、sync
对应watch
中的配置项。接着会判断传入的
expOrFn
是不是函数,如果是则赋值给this.getter
。不是的话走else
逻辑调用parsePath
函数。我们来看
parsePath
函数的定义:parsePath
函数首先会通过正则解析这个path
看是否合法。如果不合法,直接return
。如果合法会将path
拆成数组。最后parsePath
函数会返回一个函数赋值给user Watcher
的getter
属性。返回的函数的逻辑也很简单,就是根据解析出来的路径去访问传入的
obj
参数对应的属性值并返回出去。回到
user Watcher
的实例化过程,最后这里会执行this.get
方法:get
方法首先执行pushTarget()
。我们知道pushTarget
会修改Dep.target
为传入的this
也就是user Watcher
。接着会执行
this.getter.call(vm, vm)
。getter
就是前面parsePath
函数返回的函数:之后会有一个判断
this.deep
的逻辑,如果有配置deep
为true
,这里就会执行traverse(value)
。traverse
函数定义如下:traverse
这里调用了_traverse
函数。_traverse
函数会对传入的val
参数进行校验,即val
满足下面三个条件的话直接return
:val
不是一个数组或者不是一个对象val
是被冻结的val
是VNode
实例接着有
if (val.__ob__)
和if (isA) else
两大段逻辑,我们先分析if (isA) else
。这段逻辑就是针对val
是数组或者是对象分别做遍历,遍历它们的元素或者属性递归调用_traverse
函数。我们再回过头来看
if (val.__ob__)
这段逻辑,它的作用是把val
的__ob__.dep.id
保存到seen
也就是在全局定义的seenObjects
中,并且还通过if (seen.has(depId))
逻辑防止重复保存。那什么情况会重复保存呢,那就是循环引用,比如下面这个例子:如果没有
if (seen.has(depId))
的逻辑,那么上述例子会在if (isA) else
的else
的逻辑中一直不停地执行。这样
_traverse
函数的逻辑就分析完了。其实实现深度观测很简单,就是深度遍历数组或者对象,相当于深度访问了数组(对象)的所有元素(属性)。这样就会触发它们的getter
去收集依赖,把user Watcher
收集进来。回到
traverse
函数,在执行完_traverse
函数后就将seenObjects
清空。这样user Watcher
的实例化过程就分析完了。回到$watch
函数:在创建完
user Watcher
之后,$watch
会判断options.immediate
是否为true
。如果为true
,则会立即执行cb
也就是我们编写的回调函数。最后
$watch
函数返回一个函数,这个函数会执行watcher.teardown
来解除当前观察者对属性的观察。以上就是
watch
的初始化渲染过程,其原理就是访问被watch
的数据触发其getter
,使得user Watcher
被收集,在被watch
的数据改变时就能触发setter
通知user Watcher
执行回调。当被
watch
的数据发生变化时会触发setter
派发更新,我们知道派发更新就是通知订阅者user Watcher
去执行update
方法:这里如果
sync
为true
,那么就会执行run
函数:可以看到
run
函数会对比被watch
的数据有没有发生变化,如果有就立即执行回调函数。如果没有配置
sync
为true
,那么在update
函数中就会走else
逻辑执行queueWatcher
函数。而
queueWatcher
函数的具体执行过程以及其中的nextTick
函数我们在Vue源码探秘(派发更新)
和Vue源码探秘(nextTick)
已经分析过了,简单来讲,会异步执行watch
的回调函数。总结
结合上一节的
computed
,我们这里简单对比下watch
和computed
:computed Watcher
user Watcher
。watch
可以是一个配置对象,可以配置deep
、sync
、immediate