watch
watch是vue里面的监听属性,它能监听挂载在模版上的属性(data里面的数据都能),包括监听计算属性。当监听属性发生变化时就会触发他的回调函数,因此适合一个数据影响多个数据的场景。
实现过程
初始化watch
export function initState(vm){
const opts = vm.$options;
// 初始化状态的顺序应该是 props > methods > data > computed > watch
if(opts.data){
initData(vm);
}
if(opts.watch){
initWatch(vm);
}
}
watch的写法有多样,例如数组、对象、字符串形式,因此要兼容这些写法。
// watch的声明方式
watch: {
a: function (val, oldVal) {
console.log('new: %s, old: %s', val, oldVal)
},
// 方法名
b: 'someMethod',
// 深度 watcher
c: {
handler: function (val, oldVal) { /* ... */ },
deep: true
},
// 该回调将会在侦听开始之后被立即调用
d: {
handler: function (val, oldVal) { /* ... */ },
immediate: true
},
e: [
function handle1 (val, oldVal) { /* ... */ },
function handle2 (val, oldVal) { /* ... */ }
],
// watch vm.e.f's value: {g: 5}
'e.f': function (val, oldVal) { /* ... */ }
}
function initWatch(vm){
let watch = vm.$options.watch;
for(let k in watch){
const handler = watch[k];
if(Array.isArray(handler)){
handler.forEach(handle => {
createWatcher(vm, k, handle);
})
}else{
createWatcher(vm, k, handler);
}
}
}
// 创建watcher的核心
function createWatcher(vm, exprOrFn, handler, options = {}){
if(typeof handler === 'object'){
options = handler; // 保存用户传入的对象
handler = handler.handler; // 这个是用户真正传入的对象
}
if(typeof handler === 'string'){
handler = vm[handler]; // 获取用户定义的methods方法
}
// 调用vm.$watch创建用户watcher
return vm.$watch(exprOrFn, handler, options);
}
$watch
监听属性的核心是watch是定义在Vue原型链上的,因此我们也可以在js里使用Vue.$watch(exprOrFn, cb, options)来实现监听某个数据。需要给其创建一个监听watcher,并且标明是一个用户watcher,与其他watcher(渲染watcher等)区分开。
Vue.prototype.$watch = function(exprOrFn, cb, options){
const vm = this;
// user: true 表示这是一个用户watcher
let watcher = new Watcher(vm, exprOrFn, cb, { ...options, user: true });
// 如果immediate为true则立即执行
if(options.immediate){
cb();
}
}
修改watcher
修改watcher的一些逻辑以便兼容监听watcher。如果是监听watcher,那么exprOrfn将是一个字符串,为了统一,需要把exprOrfn改造成一个函数,并且返回exprOrfn在data中的值,触发数据劫持中的setter方法,建立dep和watcher之间的关系,实现依赖收集。当数据发生改变时,就会通知watcher执行run方法,如果前后两次数据发生变化就执行watch的回调函数。
class Watcher{
constructor(vm, exprOrfn, cb, options){
this.vm = vm;
// this.getter = exprOrfn;
this.cb = cb;
this.options = options;
this.id = id++;
this.deps = []; // 记录dep
this.depsId = new Set();
this.user = this.options.user;
// 处理exprOrfn
if(typeof exprOrfn == 'function'){
this.getter = exprOrfn;
}else{
this.getter = function(){
// 用户watcher传过来的可能是字符串 类似a.b.b.b
let path = exprOrfn.split('.');
let obj = vm;
for(let i=0; i<path.length; i++){
obj = obj[path[i]];
}
return obj;
}
}
// 实例化时进行一次取值操作,进行依赖收集
this.value = this.get();
}
get(){
// 在对属性取值之前先把watcher记录一下,只有在对属性取值时才有watcher
pushTarget(this);
// 这个方法中会对属性进行取值操作
//如果watcher是渲染watcher 那么就相当于执行 vm._update(vm._render()) 这个方法在render函数执行的时候会取值 从而实现依赖收集
const res = this.getter.call(this.vm);
popTarget();
return res;
}
addDep(dep){
let id = dep.id;
if(!this.depsId.has(id)){
this.depsId.add(id);
this.deps.push(dep);
dep.addSub(this);
}
}
run(){
const newVal = this.get(); // 新值
const oldVal = this.value; // 老值
this.value = newVal;
if(this.user){
// 如果两次的值不相同 或者值是引用类型 因为引用类型新老值是相等的 他们是指向同一引用地址
if(newVal !== oldVal || utils.isObject(newVal)){
this.cb.call(this.vm, newVal, oldVal);
}
}else{
// 渲染watcher
this.cb.call(this.vm);
}
}
update(){
// 批处理更新
queueWatcher(this);
}
}
// new Watcher(渲染、创建监听)的时候会产生一个watcher,同时每个属性会增加一个dep,watcher和dep互相记住,当属性值发生变化时执行自身对应的watcher
export default Watcher;