一、疑问
项目过程中经常遇到使用@watch的场景:当监听的属性发生变化时,执行相应的回调。那么watch是如何工作的呢?于是带着以下问题去看源码:
-
什么时候初始化watch?
-
怎么对设置的key进行监听?
-
监听的数据改变时,watch如何工作?
-
设置immediate时,watch 如何工作?
-
设置了deep时,watch 如何工作?
二、watch
- watch对象:
- 类型:
{ [key: string]: string | Function | Object | Array }
- 详细:一个对象,键是需要观察的表达式,值是对应回调函数。值也可以是方法名,或者包含选项的对象。Vue 实例将会在实例化时调用
$watch()
,遍历 watch 对象的每一个属性。(详见:cn.vuejs.org/v2/api/#wat…)
var vm = new Vue({
data: {
a: 1,
b: 2,
c: 3,
d: 4,
e: {
f: {
g: 5
}
}
},
watch: {
a: function (val, oldVal) {
console.log('new: %s, old: %s', val, oldVal)
},
// 方法名
b: 'someMethod',
// 该回调会在任何被侦听的对象的 property 改变时被调用,不论其被嵌套多深
c: {
handler: function (val, oldVal) { /* ... */ },
deep: true
},
// 该回调将会在侦听开始之后被立即调用
d: {
handler: 'someMethod',
immediate: true
},
// 你可以传入回调数组,它们会被逐一调用
e: [
'handle1',
function handle2 (val, oldVal) { /* ... */ },
{
handler: function handle3 (val, oldVal) { /* ... */ },
/* ... */
}
],
// watch vm.e.f's value: {g: 5}
'e.f': function (val, oldVal) { /* ... */ }
}
})
vm.a = 2 // => new: 2, old: 1
2,vm.$watch(expOrFn, callback, [options])
参数
-
exOrFn
:要监视的$data
中的属性,类型string | Function
,观察 Vue 实例变化的一个表达式或计算属性函数,表达式只接受监督的键路径。回调函数得到的参数为新值和旧值。对于更复杂的表达式,用一个函数取代。 -
callback
:数据变化后执行的函数,类型Function | Object
。 -
options
:可选的选项,类型Object
。常用监听选项如下:
-
deep: 布尔类型,深度监听
-
immediate: 布尔类型,是否立即执行一次回调函数
举例
// 键路径
vm.$watch('a.b.c', function (newVal, oldVal) {
// do something
})
// 函数
vm.$watch(
function () {
// 表达式 `this.a + this.b` 每次得出一个不同的结果时
// 处理函数都会被调用。
// 这就像监听一个未被定义的计算属性
return this.a + this.b
},
function (newVal, oldVal) {
// do something
}
)
三、源码解析
(注:红色标注的是data数据更新会触发的流程)
3.1,初始化
调用 Vue 创建实例过程中,会去处理各种选项。在beforeCreate之后,created之前,会进行监听初始化。
export function initMixin (Vue: Class<Component>) {
// ... 其他处理
initState(this)
// ...解析模板,生成DOM 插入页面
}
export function initState (vm: Component) {
vm._watchers = []
/** options对象的可选属性包括五大类:数据、DOM、生命周期钩子、资源、组合,详情可参考:https://cn.vuejs.org/v2/api/#watch */
const opts = vm.$options
/** 处理 data,props,computed 等数据 */
if (opts.props) initProps(vm, opts.props)
if (opts.methods) initMethods(vm, opts.methods)
if (opts.data) {
initData(vm)
} else {
observe(vm._data = {}, true /* asRootData */)
}
if (opts.computed) initComputed(vm, opts.computed)
/** 初始化watch */
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
3.2,initWatch
初始化监听:遍历watch对象,根据key和handler创建watcher。
function initWatch (vm: Component, watch: Object) {
/** key: 监听属性,类型string */
/** handler: 监听回调,类型有string | Function | Object | Array四种情况 */
for (const key in watch) {
/** watch的定义详见:https://cn.vuejs.org/v2/api/#watch */
const handler = watch[key]
/** 处理handler为数组的情况 */
if (Array.isArray(handler)) {
for (let i = 0; i < handler.length; i++) {
createWatcher(vm, key, handler[i])
}
} else {
createWatcher(vm, key, handler)
}
}
}
3.3,createWatcher
获取到监听回调(如果handler是个对象,就取handler.handler字段;如果handler是字符串,就从实例上获取函数),然后调用watch。
function createWatcher (
/** vm: vue实例 */
/** expOrFn: 监听属性 */
/** handler: 监听回调,可能是对象(包含handler,deep,immediate),也可能是回调函数(string | Function) */
/** options: 监听相关的选项,包括deep,immediate等 */
vm: Component,
expOrFn: string | Function,
handler: any,
options?: Object
) {
/** 监听回调是一个对象,包含handler,deep,immediate。(就是把监听回调再剥一层) */
if (isPlainObject(handler)) {
options = handler
handler = handler.handler
}
/** 监听回调是一个方法名,则从 vm 获取回调函数 */
if (typeof handler === 'string') {
handler = vm[handler]
}
return vm.$watch(expOrFn, handler, options)
}
3.4,watch
每个watch配发watcher,判断是否立刻执行监听回调。
Vue.prototype.$watch = function (
/** expOrFn 是监听的key,cb是监听回调,opts是监听选项 */
expOrFn,
cb,
options
) {
var vm = this;
/** 如果监听回调还是对象,就调用createWatcher函数。取到cb.handler后再调用watch函数(相当于把监听回调再剥一层) */
if (isPlainObject(cb)) {
return createWatcher(vm, expOrFn, cb, options)
}
options = options || {};
options.user = true;
/** 为每个监听配发watcher实例 */
var 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) + "\""));
}
}
/** Remove self from all dependencies' subscriber list. */
return function unwatchFn () {
watcher.teardown();
}
};
3.5,watcher
Watcher是Vue中的观察者类,主要任务是:观察Vue组件中的属性,当属性更新时作相应的操作,即实例化时传入的回调函数。在Vue对属性做响应式处理时,会收集每个属性的依赖,即每个属性所依赖的watcher,当属性更新时,通知watcher执行更新dom操作。
Watcher有三种类型:计算属性computed创建的computedWatcher
、侦听器watch创建的userWatcher
、用于渲染更新dom的renderWatcher
。一个组件只有一个renderWatcher,有多个computedWatcher和userWatcher。
通过Vue初始化过程可以知道,组件执行initState处理数据时,处理顺序是props => methods => data => computed => watch,然后最后在$mount阶段才实例化了渲染watcher,所以组件内watcher创建顺序是:computed Watcher => user Watcher(即监听器watcher) => renderWatcher
。
watcher可获取监听的key、监听回调 (Watch 中的cb)、监听配置。这里面也是依赖收集和更新的触发点。
var Watcher = function Watcher (
vm,
expOrFn,
cb,
options,
isRenderWatcher
) {
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$2; // 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()
: '';
if (typeof expOrFn === 'function') {
this.getter = expOrFn;
} else {
/** 传进来的expOrFn是watch对象的键值,如果键值是'a.b.c.d',那么需要parsePath方法将d从实例上一层一层的取出来,包装成一个函数function(){return d}。如果不进行这层包装,下面方法会报错 */
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
);
}
}
/** 当前watcher的值,为执行getter函数的结果值 */
this.value = this.lazy
? undefined
: this.get();
};
/** 参数 obj 是 vm 实例,segments 是解析后的键值数组,循环去获取每项键值的值,触发它们的“数据劫持get”。接着触发 dep.depend 收集依赖(依赖就是挂在 Dep.target 的 Watcher) */
export function parsePath (path: string): any {
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
}
}
3.6,get
watcher.get
函数作用:获取对应属性的值、与观察的属性建立关系的过程。
Watcher.prototype.get = function get () {
/** pushTarget将当前的“Watcher”(即当前实例this)挂到Dep.target上。在收集依赖时,找的就是Dep.target。 */
pushTarget(this);
var value;
var 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
};
function pushTarget (target) {
targetStack.push(target);
/**
* A dep is an observable that can have multiple
* directives subscribing to it.
*/
Dep.target = target;
}
Watch 在结尾会立即执行一次watcher.get
,在get()中调用this.getter
,根据监听的key,去vm实例上读取属性并返回,存放在watcher.value
上。
3.7,深度监听
depId
是每一个被观察属性都会有的唯一标识;- 去重,防止相同属性重复执行逻辑;
- 根据数组和对象使用不同的策略,最终目的是递归获取每一项属性,触发它们的“数据劫持get”收集依赖,和
parsePath
的效果是异曲同工。
从这里能得出,深度监听利用递归进行监听,肯定会有性能损耗。因为每一项属性都要走一遍依赖收集流程,所以在业务中尽量避免这类操作。
function traverse (val) {
_traverse(val, seenObjects);
seenObjects.clear();
}
function _traverse (val, seen) {
var i, keys;
var isA = Array.isArray(val);
if ((!isA && !isObject(val)) || Object.isFrozen(val) || val instanceof VNode) {
return
}
if (val.__ob__) {
var depId = val.__ob__.dep.id;
if (seen.has(depId)) {
return
}
seen.add(depId);
}
/** 如果val是数组,循环遍历 */
if (isA) {
i = val.length;
// val[i] 就是读取值了,然后值的对象就能收集到 watch-watcher
while (i--) { _traverse(val[i], seen); }
} else {
/** 递归遍历val的每一个属性 */
keys = Object.keys(val);
i = keys.length;
// val[keys[i]] 就是读取值了,然后值的对象就能收集到 watch-watcher
while (i--) { _traverse(val[keys[i]], seen); }
}
}
3.8,更新
在更新时首先触发的是“数据劫持set”,调用 dep.notify
通知每一个 watcher
的 update
方法。接着就走 queueWatcher
进行异步更新,这里先不讲异步更新。只需要知道它最后会调用的是 run
方法。
Watcher.prototype.update = function update () {
/*
update函数触发时机:watcher所观察的属性 触发更新
update函数执行流程:
1、如果this.lazy为true,即当前watcher属于computedWatcher,只是设置dirty属性
2、如果this.sync, 执行run函数
3、否则将当前watcher入队,后面在异步更新时,会遍历执行watcher的run方法
*/
if (this.lazy) {
this.dirty = true;
} else if (this.sync) {
/** 依赖收集的时候当数据发生改变时会触发这个方法 */
this.run();
} else {
queueWatcher(this);
}
};
Watcher.prototype.run = function run () {
if (this.active) {
var 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
var 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);
}
}
}
};
Dep.prototype.notify = function notify () {
// stabilize the subscriber list first
var subs = this.subs.slice();
for (var i = 0, l = subs.length; i < l; i++) {
subs[i].update();
}
};
三、解答困惑
- watch什么时候初始化?
在beforeCreate之后,created之前,会进行监听初始化。
- 怎么对设置的key进行监听?
每个监听的key创建watcher实例时,会执行watcher.get,其中便会执行 getter,便会根据你监听的key,去实例上读取并返回,存放在 watcher.value 上。详见以上第2.5及2.6节部分(watcher、get函数源码)。
- 监听的数据改变的时,watch 如何工作?
-
watch 在一开始初始化的时候,会读取一遍监听的数据的值,于是,此时那个数据的dep就收集到 watch-watcher 了;
-
watcher中存放了监听的数据的值、监听回调、监听选项,具有update()方法;
-
当数据改变时,会通过Dep.notify通知watcher进行更新(执行watcher.update()函数),于是,设置的 handler 就被调用了。
- 设置 immediate 时,watch 如何工作?
当设置了 immediate 时,就不需要在数据改变的时候才触发监听回调。而是在初始化watch时,在读取了监听的数据的值之后,便立即调用一遍你设置的监听回调,然后传入刚读取的值。
- 设置了 deep 时,watch 如何工作?
-
初始化watcher时,会读取监听的data的属性,watch-watcher 被收集在这个属性的依赖收集器中
-
在读取data 属性的时候,发现设置了 deep 而且值是一个对象,会递归遍历这个值,把内部所有属性逐个读取一遍,于是属性和它的对象值内每一个属性都会收集到 watch 的 watcher
-
无论对象嵌套多深的属性,只要改变了,就会通知相应的 watch-watcher 去更新,从而触发监听回调