小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。
前期准备和资料
- 本文基于vue2.6.14版本进行解读
- demo地址传送门(为了方便查看源码执行过程,请在debugger下进行调试查看,在demo关键代码处已打debugger)
- demo相关代码
- 源码解读中会有封装的部分公共方法,类似于:
isPlainObject()请自行查看源码
解读
前端相关代码
<div id="app">
<input type="text" v-model="name">
<p>{{name}}</p>
<button v-on:click="changeName">改变名字</button>
</div>
<script>
let vue = new Vue({
el: '#app',
data: {
name: '张三'
},
watch: {
name: function (val, oldVal) {
console.log(`newVal:${val}; oldVal:${oldVal}`);
},
},
methods:{
changeName(){
this.name = this.name === '张三' ? '李四' : '张三'
}
},
});
</script>
在刚初始化页面的时候会调用vue中的initState()方法,在initState中分别调用初始化props、methods、data、computed和wantch相关的方法。代码如下所示:
function initState (vm) {
vm._watchers = [];
var opts = vm.$options;
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); }
// 此处只需要关注这里,在此处调用initWatch方法去初始化watch
debugger
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch);
}
}
接下来我们来看看initWatch()方法中都做了什么,initWatch接收vm和watch(初始化时watch中的内容)两个参数,在此处遍历watch中需要监听的数据,判断watch中的值是否是一个数组(可以查看watch的使用可以定义为一个函数或者一个数组传送门),并分别调用createWatcher方法去创建对应的观察者。代码如下:
function initWatch (vm, watch) {
debugger
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方法中判断watch中的hander(指的是案例watch对象中name对应的
function (val, oldVal) {}),isPlainObject方法判断是对象类型,如果是一个对象则设置options参数为handler,并对handler进行递归;如果hangdler只是一个字符串,则把vm实例中对应值赋值(可能是对应的method中的方法名)给handler。最后执行vm中的$watch方法。代码如下:
function createWatcher (vm,expOrFn,handler,options) {
debugger
if (isPlainObject(handler)) {
options = handler;
handler = handler.handler;
}
if (typeof handler === 'string') {
handler = vm[handler];
}
return vm.$watch(expOrFn, handler, options)
}
在vue的原型中定义$watch方法,解读放到代码注释中(new Watcher、pushTarget、invokeWithErrorHandling、popTarget、watcher.teardown后续紧跟)代码如下:
Vue.prototype.$watch = function (expOrFn,cb,options) {
debugger
var vm = this;
// 判断cb回调函数是否是对象类型,如果是的话继续回调createWatcher方法
if (isPlainObject(cb)) {
return createWatcher(vm, expOrFn, cb, options)
}
// options赋值
options = options || {};
options.user = true;
// 调用new Watcher创建对应的观察者,收集依赖,监听数据变化并作出对应的处理
var watcher = new Watcher(vm, expOrFn, cb, options);
// 判断是否有immediate立即执行属性,如果有的话,会立即执行一次cb
if (options.immediate) {
var info = "callback for immediate watcher \"" + (watcher.expression) + "\"";
pushTarget();
invokeWithErrorHandling(cb, vm, [watcher.value], vm, info);
popTarget();
}
// 返回unwatchFn去取消观察数据,把watcher实例从当前正在观察的状态的依赖列表中移除
return function unwatchFn () {
watcher.teardown();
}
};
创建Watcher实例实现对数据变换的监听和处理。除此以外,Watcher原型还还定义了get评估getter并重新收集依赖项,addDep将依赖项添加到此指令,cleanupDeps清理依赖项,update在依赖项更改时调用,depend依赖于此观察者收集的所有DEP,teardown从所有依赖项的列表中删除self等方法。代码如下:
var Watcher = function Watcher (vm,expOrFn,cb,options,isRenderWatcher) {
debugger
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是函数,则把它赋值给getter,如果不是则使用parsePath读取属性路径中的数据,例如a.b.c
if (typeof expOrFn === 'function') {
this.getter = expOrFn;
} else {
this.getter = parsePath(expOrFn);
if (!this.getter) {
this.getter = noop;
warn("观察者只接受简单的点分隔路径", vm);
}
}
// 调用Watcher原型上的get()方法进行依赖收集
this.value = this.lazy ? undefined : this.get();
};
Watcher原型中的get方法会进行依赖收集,代码如下:
Watcher.prototype.get = function get () {
// 调用pushTarget方法,评估当前Watcher是否订阅过该dep,如果没有则进行依赖收集
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 {
// 深度观察的依赖
if (this.deep) {
traverse(value);
}
popTarget();
this.cleanupDeps();
}
return value
};
当我们在依赖中收集到自己订阅哪些dep后,就可以在$watch方法代码中使用unwatchFn方法通过调用watcher原型中的teardown方法从所有依赖项的列表中删除self。下面来看看teardown的代码:
Watcher.prototype.teardown = function teardown () {
if (this.active) {
if (!this.vm._isBeingDestroyed) {
remove(this.vm._watchers, this);
}
var i = this.deps.length;
// 循环订阅列表,执行他的removeSub方法,把自己从依赖列表中删除
while (i--) {
this.deps[i].removeSub(this);
}
this.active = false;
}
};
更多细节方面(不同的回调方式,不同的使用方式)可自行下载代码调试