深入浅出Vue.js----变化侦测相关的API实现原理----vm.$watch

一、vm.$watch

(1)用法

vm.$watch(expOrFn,callback,[options])

(2)参数

1、{string | Function} expOrFn

2、{Function | Object} callback

3、{Object} [options]

{boolean} deep

{boolean} immediate

(3)返回值

{Function} unwatch

(4)用法

1、用于观察一个表达式computed函数在Vue.js实例上的变化。 

2、回调函数调用时,会从参数得到新数据(new value)和旧数据(old value)。

3、表达式只接受以点分隔的路径,例如a.b.c。如果是一个比较复杂的表达式,可以用函数代替表达式。

vm.$watch('a.b.c',function(newVal,oldVal){
//做点什么
})

4、vm.$watch返回一个取消观察函数,用来停止触发回调

var unwatch = vm.$watch('a.b.c',function(newVal,oldVal){//做点什么})
//之后取消观察
unwatch();

(5)[options]选项deep

1、为了发现对象内部值的变化,可以在选项参数中指定deep:true

vm.$watch('someObject',callback,{
    deep:true
})
vm.someObject.nestedValue = 123;
//回调函数将被触发

2、注意:监听数组的变动不需要这么做。

(6)[options]选项immediate

1、在选项参数中指定immediate:true,将立即以表达式的当前值触发回调

vm.$watch('a',callback,{
    immediate:true
})
//立即以'a'的当前值触发回调

二、watch的内部原理

(1)vm.$watch其实是对Watcher的一种封装,通过Watcher完全可以实现vm.$watch的功能,但vm.$watch中的参数deep和immediate是Watcher中所没有的。

(2)实现

Vue.prototype.$watch = function(expOrFn,cb,options){
    const vm = this;
    options = options || {}
    const watcher = new Watcher(expOrFn,cb,options){
        if(options.immediate){
            cb.call(vm,watcher.value)
        }
        return function unwatchFn(){
            watcher.teardown()
        }
    }
}

1、执行new Watcher来实现vm.$watch的基本功能。

2、expOrFn是支持函数的,前面未做介绍。需修改Watcher

export default class Watcher{
    constructor(vm,expOrFn,cb){
        this.vm = vm;
        //执行this.getter()就可以读取data.a.b.c的内容
        if(typeof expOrFn === 'function'){
            this.getter = expOrFn;
        }else{
            this.getter = parsePath(expOrFn);
        }
        this.cb = cb;
        // 保存旧值
        this.value = this.get();
    }
    \\......
}

3、新增可判断expOrFn类型的逻辑。如果expOrFn是函数,则直接将它赋值给getter;如果不是函数,再使用parsePath函数来读取keypath中的数据。(keypath指的是属性路径,例如a.b.c.d就是一个keypath,说明从vm.a.b.c.d中读取数据)。

4、当expOrFn是函数时,它不只可以动态返回数据,其中读取的所有数据也会被Watcher观察。

5、当expOrFn是字符串类型的keypath时,Watcher会读取这个keypath所指向的数据并观察这个数据的变化。而当expOrFn是函数时,Watcher会同时观察expOrFn函数中读取的所有Vue.js实例上上的响应式数据。也就是说,如果函数从Vue.js实例上读取了两个数据,那么Watcher会同时观察这两个数据的变化,当其中任意一个发生变化时,Watcher都会得到通知。

6、事实上,Vue.js中计算属性(Computed)的实现原理与expOrFn支持函数有很大的关系。

7、执行 new Watcher后,代码会判断用户是否使用了immediate参数,如果使用了,则立即执行一次cb。

8、最后,返回一个函数unwatchFn。其作用是取消观察数据。当用户执行这个函数时,实例上是执行了watcher.teardown()来取消观察数据,其本质是把watcher实例从当前正在观察的状态的依赖列表中移除。

(3)Watcher的teardown方法

1、首先需要在Watcher中记录自己订阅了谁,也就是watcher实例被收集进了哪些Dep里

2、然后当Watcher不想继续订阅这些Dep时,循环自己记录的订阅列表来通知它们(Dep)将自己从它们(Dep)的依赖列表中移除掉。

3、先在Watcher中添加addDep方法,该方法的作用是在Watcher中记录自己都订阅过哪些Dep。

export default class Watcher{
    constructor(vm,expOrFn,cb){
        this.vm = vm;
        //新增
        this.deps = [];
         //新增
        this.depIds = new Set()
        if(typeof expOrFn === 'function'){
            this.getter = expOrFn;
        }else{
            this.getter = parsePath(expOrFn);
        }
        this.cb = cb;
        this.value = this.get();
    }
   //......
    addDep(dep){
        const id = dep.id;
        if(!this.depIds.has(id)){
            this.depIds.add(id);
            this.deps.push(dep);
            dep.addSub(this);
        }
    }
    //....
}

1)使用depIds来判断如果当前Watcher已经订阅了该Dep,则不会重复订阅。

2)Watcher读取value时,会触发收集依赖的逻辑。当依赖发生变化时,会通知Watcher重新读取最新的数据。如果没有这个判断,就会发现每次数据发生了变化,Watcher都会读取最新的数据。而读数据就会再次收集依赖,这就会导致Dep中依赖有重复。这样当数据发生变化时,会同时通知多个Watcher。为了避免这个问题,只有第一次触发getter的时候才会收集依赖。

3)执行this.depIds.add来记录当前Watcher已经订阅了这个Dep。

4)执行this.deps.push(dep)记录自己都订阅了哪些Dep。

5)触发dep.addSub(this),来将自己订阅到Dep中。

(4)Watcher中新增addDep方法后,Dep中收集依赖的逻辑也需要有所改变。

//新增
let uid = 0;
export default class Dep{
    constructor(){
        //新增
        this.id = uid++;
        this.subs = [];
    }
    //....
    depend(){
        if(window.target){
            //废弃
            // this.addSub(window.target);
            //新增
            window.target.addDep(this);
        }
    }
    //...
}

1)此时,Dep会记录数据发生变化时,需要通知哪些Watcher,而Watcher中也同样记录了自己会被哪些Dep通知。(多对多关系)

2)如果Watcher中的expOrFn参数是一个表达式,那么肯定只收集一个Dep,并且大部分都是这样。

3)但expOrFn可以是一个函数,此时如果该函数中使用了多个数据,那么这时Watcher就要收集多个Dep了

this.$watch(function(){
    return this.name + this.age
},function(newValue,oldValue){
    console.lognewValue,oldValue()
})

1、上面这个例子,表达式是一个函数,并且在函数中访问了name和age两个数据。

2、这种情况下,Watcher内部会收集两个Dep---name的Dep和age的Dep。

3、同时这两个Dep中也会收集Watcher。这导致age和name中任意一个数据发生变化时,Watcher都会收到通知。

4、已经在Watcher中记录自己订阅了哪些Dep之后,就可以在Watcher中新增teardown方法来通知订阅的Dep,让它们把自己从依赖列表中移除掉。

//从所有依赖项的Dep列表中将自己移除
teardown(){
    let i = this.deps.length;
    while(1--){
        this.deps[i].removeSub(this);
    }
}
export default class Dep{
    //....
    removeSub(sub){
        const index = this.subs.indexOf(sub);
        if(index>-1){
            return this.subs.splice(index,1)
        }
    }
    //...
}

1)Watcher从sub中删除掉,然后当数据发生变化时,将不再通知这个已经删除的Watcher,这就是unwatch的原理。

三、deep参数的实现原理

(1)Watcher想监听某个数据,就会触发某个数据收集依赖的逻辑,将自己收集进去,然后当它发生变化时,就会通知Watcher。

(2)要想实现deep的功能,其实就是除了要触发当前这个被监听数据的收集依赖的逻辑之外,还要把当前监听的这个值在内的所有子值都触发一遍收集依赖逻辑。这就可以实现当前这个依赖的所有子数据发生变化时,通知当前Watcher。

export default class Watcher{
    constructor(vm,expOrFn,cb,options){
        this.vm = vm;
        //新增
        if(options){
            this.deep = !!options.deep;
        }else{
            this.deep = false;
        }
        this.deps = [];
        this.depIds = new Set()
        if(typeof expOrFn === 'function'){
            this.getter = expOrFn;
        }else{
            this.getter = parsePath(expOrFn);
        }
        this.cb = cb;
        this.value = this.get();
    }
    get(){
        // 将this赋值给window.target,用于主动将自己添加到依赖中
        window.target = this;
        //触发了getter,触发收集依赖,将this主动添加到keypath的Dep中
        let value = this.getter.call(this.vm,this.vm);
        //新增
        if(this.deep){
            traverse(value);
        }
        window.target = undefined;
        return value;
    }
    //.....
}

1、如果用户使用了deep参数,则在window.target = undefined之前调用traverse来处理deep的逻辑,这样才能保证子集收集的依赖是当前这个Watcher。若在其之后,那么其实当前的Watcher并不会被收集到子值的依赖列表中,也就无法实现deep功能。

(3)递归value的所有子值来触发它们收集依赖的功能。

const seenObjects = new Set();
export function traverse(val){
    _traverse(val,seenObjects);
    seenObjects.clear();
}
function _traverse(val,seen){
    let i,keys;
    const isA = Array.isArray(val);
    if(!isA&&!isObejct(val)||Object.isFrozen(val)){
        return;
    }
    if(val._ob_){
        const 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])
    }
}

1、先判val类型,如果它不是Array和Object,或者已经被冻结,那么直接返回,什么也不干。

2、然后拿到val的dep.id,用这个id来保证不会重复收集依赖。

3、如果是数组,则循环数组,将数组中每一项递归调用_traverse。

4、如果是Object类型的数据,则循环Object中所有key,然后执行一次读取操作,再递归子值

_traverse(val[keys[i],seen])

1)其中val[keys[i]会触发getter,也就是说会触发收集依赖的操作,这时window.target还没有被清空,会将当前Watcher收集进去。

2)_traverse函数其实是一个递归操作,所以这个value的子值也会触发同样的逻辑,这样就可以实现通过deep参数来监听所有子值的变化。

你可能感兴趣的:(vue学习)