# Watch
Watch 的使用例子:
watch: {
c: {
handler: function (val, oldVal) { /* ... */ },
},
}
回顾一下响应式原理:Vue 中的响应式是指数据层的变化能自动触发视图的更新。其原理是每个 data
属性都有一个容器 dep
去收集一个 Watcher
, 这个 Watcher
是一个包含了渲染组件的方法的实例。每次数据的变化都能触发这些收集到的 Watcher
, 进而实现视图的更新
这里的 watch
也是同理,watch
监听的每个属性都有对应的一个 handler
方法,这个 handler
方法跟上面的响应式原理一样也会被一个 Watcher
包装并被监听的属性所收集。接下从源码角度分析一下实现过程
# 源码分析
源码中处理 watch
属性的地方:initState(vn) => initWatch(vm, opts.watch)
// initWatch
function initWatch (vm, watch) {
for (var key in watch) {
var handler = watch[key];
if (Array.isArray(handler)) {
for (var i = 0; i < handler.length; i++) {
createWatcher(vm, key, handler[i]);
}
} else {
createWatcher(vm, key, handler);
}
}
}
// createWatcher
function createWatcher (
vm,
expOrFn,
handler,
options
) {
if (isPlainObject(handler)) {
options = handler;
handler = handler.handler;
}
if (typeof handler === 'string') {
handler = vm[handler];
}
return vm.$watch(expOrFn, handler, options)
}
initWatch
会遍历所有的 watch
, 并执行 createWatcher(vm, key, handler)
, 然后再执行 vm.$watch(expOrFn, handler, options)
,这里参数表示如下:
expOrFn
: 监测的属性名handler
: 监测的属性名对应的handler
重点看下 vm.$watch
的定义:
Vue.prototype.$watch = function (
expOrFn,
cb,
options
) {
var vm = this;
if (isPlainObject(cb)) {
return createWatcher(vm, expOrFn, cb, options)
}
options = options || {};
options.user = true;
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) + "\""));
}
}
return function unwatchFn () {
watcher.teardown();
}
};
}
跟响应式原理一样,在 vm.$watch
中也实例化了一个 Watcher
,分析下此时的这个 Watcher
构造函数做了啥:
var Watcher = function Watcher (
vm,
expOrFn, // 这个参数是 watch 要监听的属性名
cb, // 对应 watch 的 handler
) {
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 = expOrFn.toString();
// 如果 expOrFn 是函数,render 时这里是函数
if (typeof expOrFn === 'function') {
this.getter = expOrFn;
} else {
// watch时 expOrFn 是属性名,所以会走到这里
// parsePath 方法就是获取属性名对应值的方法
this.getter = parsePath(expOrFn);
if (!this.getter) {
this.getter = noop;
warn(
"Failed watching path: \"" + expOrFn + "\" " +
'Watcher only accepts simple dot-delimited paths. ' +
'For full control, use a function instead.',
vm
);
}
}
// this.lazy 为false 所以会执行 this.get()
this.value = this.lazy
? undefined
: this.get();
};
要注意此时传进来的参数:
expOrFn
:watch
要监听的属性名cb
:watch
的属名性名的handler
在执行 Watcher
构造方法时重点代码是以下几行
this.expression = expOrFn.toString();
// parse expression for getter
if (typeof expOrFn === 'function') {
this.getter = expOrFn;
} else {
this.getter = parsePath(expOrFn);
}
this.value = this.lazy
? undefined
: this.get();
expOrFn
此时是字符串所以走的代码是 this.getter = parsePath(expOrFn)
, 这里先简单介绍下 parsePath
方法就是获取属性名对应值的方法
function parsePath (path) {
if (bailRE.test(path)) {
return
}
var segments = path.split('.'); // 处理监测属性是一个对象属性的情况,比如 `obj.a: { handler: function(){}}`
return function (obj) {
for (var i = 0; i < segments.length; i++) {
if (!obj) { return }
obj = obj[segments[i]];
}
return obj
}
}
在响应原理中实例的
Watcher
时,expOrFn
是一个渲染组件的updateComponents()
函数
之后执行 get()
方法:
/**
* Evaluate the getter, and re-collect dependencies.
*/
Watcher.prototype.get = function get () {
pushTarget(this); // 将当前的 Watcher 保存到 `Dep.target` 全局属性中
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
};
上面的 get
主要做的事情有:
执行
pushTarget(this)
: 将当前的Watcher
实例保存到Dep.target
全局属性中, 方便被属性的dep
容器收集执行
value = this.getter.call(vm, vm)
: 此时的this.getter
是上文中parsePath(expOrFn)
的返回值, 这里执行完之后就是得到属性对应的值
data(){
message: 007,
},
watch: {
message: {
handler: function(){
console.log('message变了!')
}
}
}
// value = this.getter.call(vm, vm) 执行之后,value就是 007
以上面这个例子为例, this.getter
是获取 message
对应的值,就是意味着要读取 data
上的 message
属性, 此时就是会被 Object.definedProperty()
的 get
方法劫持,然后就是把当前保存在全局属性 Dep.target
中的当前的 Watcher
实例保存到容器 dep
中, 至此就完成当前属性(message
)对这个 Watcher
的收集
上面就完成了对 watch
监听属性机制的分析
# 触发handle
当要监听属性更新时,这会派发收集到的 Watcher
,执行 Watcher.update => Watcher.run => this.cb
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);
}
}
}
};
this.cb
就是 watch handle
方法
# deep
使用 watch
的时候可以配置一个 deep
属性来配置是否深度监听属性,那么 Vue 中是如何处理的呢? 以下在例子为例:
data(){
return {
tree: {
child: {
name: 'lan'
}
}
}
},
watch: {
tree: {
handler: function(){},
deep: true
}
}
首先 Vue 也可以劫持到 object
类型的数据的, 因为在初始化 data
数据时,会遍历对象对子元素使用 Object.definedProperry()
处理,也就是意味着每他对象的子元素也会有自己的一个收集容器的 dep
。
回到 Vue 对 watch
的处理中,上文有提到在 new Watcher
的构造方法时,结尾会执行 Watcher.property.get()
方法:
Watcher.prototype.get = function get () {
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
};
可以看到当前 this.deep
为 true
时, 会执行 traverse(value)
,它的定义如下:
function traverse (val) {
_traverse(val, seenObjects);
seenObjects.clear();
}
// _traverse
function _traverse (val, seen) {
var i, keys;
var isA = Array.isArray(val);
if ((!isA && !isObject(val)) || Object.isFrozen(val) || val instanceof VNode) {
return
}
// 获取当前 val 所对应 Observer 实例
if (val.__ob__) {
var 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
方法,传进去的参数为:
val
: 以上面的例子为例,此时的值为:{ child: { name: 'lan' }}
seenObjects
: 一个Set
结构
进入到 _traverse
后, 逐条分析一下代码:
只有当前
val
的值是对象类型才继续往下处理通过
depId
和seen
配合判断当前的val
是否是已经处理过的, 这个val.__ob__
属性在初化data
数添加的, 具体位置在new Observer(value)
中有一行代码def(value, '__ob__', this)
:
function def (obj, key, val, enumerable) {
Object.defineProperty(obj, key, {
value: val,
enumerable: !!enumerable,
writable: true,
configurable: true
});
}
参数为:
value
: 当前监听属性对应的值__ob__
this
: 当前Observer
实例
initData (vm)
=>observe (value, asRootData)
=>new Observer(value)
- 无论是数组还是对象都通过遍历, 继续
_traverse(val[keys[i]], seen)
然后整个函数就完了~,怎么感觉什么都做?
真正发挥作用 _traverse(val[keys[i]], seen)
这行代码,调用 _traverse
方法时一方面是遍历子元素递归调用自己,还有一个关键的语句是 val[keys[i]]
, val[keys[i]]
的作用是获取当前子属性的值,上文有提到,对于 object
类型的数据, Vue 会通过遍历对子属性也添加 Object.defindProperty
劫持,也就意味着当这里访问 val[keys[i]]
时,就会触发劫持的 get
方法,触发 get
方法时,此时的 Dep.target
还是这个 watch
对应的 Watcher
!所以子元素的 dep
也收集了这个 Watcher
,以此完成了对所谓 deep
的深度监听
# immediate
immediate
是 watch
的另一个属性,表示立即执行一个 handler
的意思,在 Vue.prototype.$watch
定义
Vue.prototype.$watch = function (
expOrFn,
cb,
options
) {
var vm = this;
if (isPlainObject(cb)) {
return createWatcher(vm, expOrFn, cb, options)
}
options = options || {};
options.user = true;
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) + "\""));
}
}
return function unwatchFn () {
watcher.teardown();
}
};
如果存在 options.immediate
则执行 cb.call(vm, watcher.value)
, 这个 cb
就是对应的 handler
方法
# 总结
watch
的监听原理其实也是把当前的 watch handler
包装在 watcher
中,然后这个 watch Watcher
被监听的属性所收集,之后当属性被更新时,这会派发收集的 Watcher
,执行对应的 handler
方法
deep
的实现本质是因为 Vue 对于对象类型的数据会遍历其子元素,并对这些子元素进行监听劫持,然后当前如果有 deep
属性时, watch Watcher
中会去遍历监听元素的子元素,注意此时全局 Dep.target
仍为当前这个 watch Watcher
,通过访问这些子属性,让子属性收集 watch Watcher
← 组件更新机制(diff) Computed →