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