vue中的watcher的分类
在文章vue异步更新流程梳理中提到了vue中的watcher的分类,分别是:
- 渲染watcher, 负责更新视图变化的,即一个vue实例对应一个渲染watcher
- 用户自定义watcher,用户通过watch:{value(val, oldVal){}}选项定义的,或者this.$watch()方法生成的。
- computed选项里面的计算属性也是watcher, 和第2点中的watcher的区别是它的watcher实例有dirty属性控制着watcher.value值的变化
先看一下 Watcher 是怎么定义的:
export default class Watcher {
vm: Component;
expression: string;
cb: Function;
id: number;
deep: boolean;
user: boolean;
lazy: boolean;
sync: boolean;
dirty: boolean;
active: boolean;
deps: Array;
newDeps: Array;
depIds: SimpleSet;
newDepIds: SimpleSet;
before: ?Function;
getter: Function;
value: any;
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
this.vm = vm
if (isRenderWatcher) {
vm._watcher = this
}
vm._watchers.push(this)
// options
if (options) {
this.deep = !!options.deep
this.user = !!options.user
this.lazy = !!options.lazy
this.sync = !!options.sync
this.before = options.before
} else {
this.deep = this.user = this.lazy = this.sync = false
}
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.expression = process.env.NODE_ENV !== 'production'
? expOrFn.toString()
: ''
// parse expression for getter
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
if (!this.getter) {
this.getter = noop
process.env.NODE_ENV !== 'production' && warn(
`Failed watching path: "${expOrFn}" ` +
'Watcher only accepts simple dot-delimited paths. ' +
'For full control, use a function instead.',
vm
)
}
}
this.value = this.lazy
? undefined
: this.get()
}
/**
* 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
}
/**
* Add a dependency to this directive.
*/
addDep (dep: Dep) {
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)
}
}
}
/**
* Clean up for dependency collection.
*/
cleanupDeps () {
let i = this.deps.length
while (i--) {
const dep = this.deps[i]
if (!this.newDepIds.has(dep.id)) {
dep.removeSub(this)
}
}
let tmp = this.depIds
this.depIds = this.newDepIds
this.newDepIds = tmp
this.newDepIds.clear()
tmp = this.deps
this.deps = this.newDeps
this.newDeps = tmp
this.newDeps.length = 0
}
/**
* Subscriber interface.
* Will be called when a dependency changes.
*/
update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
/**
* 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) {
const info = `callback for watcher "${this.expression}"`
invokeWithErrorHandling(this.cb, this.vm, [value, oldValue], this.vm, info)
} else {
this.cb.call(this.vm, value, oldValue)
}
}
}
}
/**
* Evaluate the value of the watcher.
* This only gets called for lazy watchers.
*/
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()
}
}
/**
* Remove self from all dependencies' subscriber list.
*/
teardown () {
if (this.active) {
// remove self from vm's watcher list
// this is a somewhat expensive operation so we skip it
// if the vm is being destroyed.
if (!this.vm._isBeingDestroyed) {
remove(this.vm._watchers, this)
}
let i = this.deps.length
while (i--) {
this.deps[i].removeSub(this)
}
this.active = false
}
}
}
看不懂没关系,先分析,用到了再看。
渲染watcher
在vue实例初始化的时候会走的流程中,会生成一个渲染watcher
function mountComponent(){
// 省略其他代码
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
}
// 省略其他代码
可以看到第5个参数 为 true /* isRenderWatcher */; 说明是渲染watcher;它的作用其实也非常明确,就是组件内的响应式数据变更了,就会触发setter, setter里面调用了dep.nofity(), dep.subs循环拿到每一个watcher, 执行watcher.update();当这个watcher是渲染watcher时,其实就是走update 方法
update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
的 queueWatcher(this)方法,这里最终会走到patch流程里面去。具体可以看我另外一篇文章彻底理解vue的patch流程和diff算法
computed watcher
其实就是计算属性,内部也是采用wacther实现,我们经常听说computed 计算属性求值可以缓存,有利于性能提升,现在看一下怎么提升性能的吧
首先是初始化 if (opts.computed) initComputed(vm, opts.computed)
看initComputed做了啥
其实分三步:
- 创建一个vm._computedWatchers对象
- 遍历computed 中的key记录所有computed属性的watcher
-
definComputed(vm, key, userDef) 这里就是vm实例代理了计算属性
我们仔细看这个createComputedGetter 方法
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
}
}
}
这里就是计算属性的关键位置: return function computedGetter () {}
作为计算属性的getter方法
例子来说明
data() {
return {a: 1}
},
computed: {
double(){ return this.a * 2}
}
经过上面的初始化流程后会有这么一个样子:
vm: {
_computedWatchers: {
double: {
value: undefined, // 一开始并没有求值
dirty: true // 初始化会设置为true,代表脏了,脏了的意思是要重新求值
} as computedWatcher // 这里是一个watcher实例
},
double: { // double是通过Object.defineProperty(target, key, sharedPropertyDefinition)代理挂上去的
getter: computedGetter (){
const watcher = this._computedWatchers &&this._computedWatchers['double']
if (watcher) {
if (watcher.dirty) {
watcher.evaluate()
}
if (Dep.target) {
watcher.depend()
}
return watcher.value
}
}
}
}
重点来了,一开始initComputed这套流程下来只是设置好上面这种关系。其中double这里并没有求值,所以vm._computedWatchers.double.value是undefined, 因为computed在new Watcher时是传的lazy: true进去的。
什么时候开始求值呢?答案是在执行组件render函数的时候,因为走渲染流程会生成组件vnode, 里面会对所有用到的变量进行读取,就会触发变量的getter! 当模板中有这么一句话:
{{double}}
时,在render函数时会读取double的值,触发double.getter方法:
此时看getter方法里面dirty: true 会走evaluate方法,看看evaluate做了啥?
其实就是this.getter去读取值,此时更新了watcher.value为最新值了,然后就可以把dirty设置为false了。
this.getter是干啥呢?返回到文章上面初始化时的new Watcher看看 ,就是userDef 或者是 userDef.get; 当我们以对象形式配置计算属性的get(){}, set(){}时,就是取userDef.get 如果我们只是配置了function直接当作get函数了,此处例子的userDef就是computed定义的
function () { return this.a * 2}
绕了一大圈,最后求值时还是执行了我们自定义的computed get函数!
那么问题来了:具体是如何体现double的值是缓存呢?如何确定计算属性中的对应依赖this.a更新了,double值才会跟着更新呢?
其实还是在我们的定义get函数这里做了衔接才能形成闭环。
当执行
function () { return this.a * 2}
的时候,this.a会触发a的getter函数, 那么此时因为computed watcher在执行this.get()的时候把有这么一句:pushTarget(this)
get () {
pushTarget(this)
// 省略其他代码
}
这里就是把Dep.target = 该computed watcher了,this.a的getter函数里面的dep的subs会把(Dep.target = 该computed watcher) push进去。这样子,this.a 重新赋值就会触发setter ,就会走watcher.update方法:
此时方法内部会走
if (this.lazy) { this.dirty = true}
逻辑,因为computed初始化传参进来时lazy就是true; 现在好了,this.dirty又变为true, watcher.value又脏了!下次再读取double的时候,还会重复上面的流程
至此,computed的计算属性值是缓存的解释就走通了。当this.a不变的时候,this.dirty不会变,每次读取double都直接return watcher.value;不用走evaluate方法重新求值,this.a变化时也是只改变watcher.dirty = true, 并没有重新求double值, 只在读double的时候才会去重新evaluate求值,这样子真正做到了按需更新计算属性的值
用户定义的watch
用户可以通过this.$watch() 或者配置watch:{}选项来观察变量变化做出处理;统称user watcher
通过源码可以知道,watche:{}选项最后也是调用了$watch()来实现的; 而且可以配置成数组形式:
watch: {
a: [
function(val, oldVal){},
function(val, oldVal){}
]
}
看看$watch()方法的定义
所以用户最终定义的watch是通过这一行代码生成watcher实例的
const watcher = new Watcher(vm, expOrFn, cb, options)
vm: 当前vue组件实例
expOrFn: 其实就是表达式, 就是watch选项的key字符串,如下例子
data(){
return {
a: 1,
b: {
c: 2
}
}
}
watch: {
a: function(val, oldVal){},
'b.c': function(val, oldVal){}
}
expOrFn就是 'a' 或者 'b.c';
cb: 用户定义的回调处理函数,watcher执行get()方法之后会执行
options: 一些配置,一般可以是 {deep: true, immedate: true, user: true}, 其中deep 和 immedate看用户是否需要配置,user: true是vue代码主动注入的,代表是user watcher
至此,watch选项的每个key都要生成一个watcher, 追踪new Watcher过程,发现
this.getter = parsePath(expOrFn)
// ...
this.value = this.lazy
? undefined
: this.get()
同样,在读取vm.b.c的过程,触发vm.b的getter,vm.b.c的getter,这两个key的内部的dep.subs会对这个watcher进行收集,到时候,vm.b, vm.b.c重新setter的时候,会走watcher.update()流程。
说了这么多,那么整个流程是怎么样的呢?假如b.c 重新赋值是怎么触发watch回调的呢?
this.b.c = 3 触发c这个key的setter,然后 dep.notify() , wacher.update() , queueWatcher(this),这里是把watcher放到一个queue数组里面去,在nextTick之后会把数组中的watcher拿出来,执行watcher.run();如下图1 2 3解释了流程
watcher.run() 干啥了?如下图
总结
渲染watcher: 数据变动,重新patch
computed watcher: 计算属性的依赖变动,watcher.dirty会置为true
user watcher:观察的key变动,会随着 渲染watcher一起在nextTick里面走watcher.run(), 然后重新取值,再执行cb回调函数;
他们之间有什么区别呢?
其实从流程分析,它们的流程都是一样的
都是要走这么一个流程
try{
watcher.value = watcher.get()
} finally{
watcher.cb()
}
不同点:
- 渲染watcher的作用是watcher.get()里面的getter执行的就是patch流程,它的cb是一个noop函数,就是一个空函数 function(){}
- computed watcher的watcher.get()里面的getter执行的就是用户定义的计算函数; 它的cb也是一个noop函数
- user watcher的watcher.get()里面的getter只是拿被观察的key的值而已,用户定义的回调cb在拿到值之后再执行