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

vm.$watch

1. 用法

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

参数:

  • {string | Function} expOrFn
  • {Function | Object} callback
  • {Object} [options]
    • {boolean} deep
    • {boolean} immediate
      返回值:{Function} unwatch
      用法:用于观察一个表达式或computed函数在Vue.js实例上的变化.回调函数调用时,会从参数得到新数据(new value)和旧数据(old value)。表达式只接受以点分割的路径,例如:a.b.c。如果是一个比较复杂的表达式,可以使用函数代替表达式:
vm.$watch("a.b.c",function(newVal,oldVal){

})

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

var unwatch = vm.$watch("a",(newVal,oldVal){

})

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

vm.$watch('someObject',callback,{
    deep:true
})
vm.someObject,nestedValue = 123;

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

vm.$watch('a',callback,{
    immediate:true
})

2. watch的内部原理

  通过Watcher完全可以实现vm.$watch的功能,但vm.$watch中的参数deepimmediateWatcher中没有的。

// watch内部实现原理
Vue.prototype.$watch = function(expOrFn,cb,options){
    const vm = this;
    options = this.options;
    const watcher = new  Watcher(vm,expOrFn,cb,options);
    if(options.immediate){
        cb.call(vm,watcher.value);
    }
    retrun function unwatchFn(){ 
        watcher.teardown;
    }
}

  先执行new Watcher来实现vm.$watch的基本功能,但是expOrFn是支持函数的,对Watcher改造一下:

export default class Watcher{
    constructor(vm,expOrFn,cb){
        this.vm = vm;
        
        // expOrFn参数支持函数
        if(tyoeof expOrFn === 'function'){
            this.getter = expOrFn;
        }else{
            this.getter = parsePath(expOrFn)
        }   

        this.cb = cb;
        this.value = this.get()
    }
}

新增判断expOrFn类型的逻辑。如果是expOrFn是函数,则直接将它赋值给getter;如果不是函数,再使用parsePath函数来读取keypath中的数据。
  expOrFn是函数时,不只可以动态返回数据,其中读取的也都会被Watcher观察。

  1. expOrFn是字符串类型的keypath:
    Watcher会读取这个keypath所指向的这个数据并观察这个数据的变化。
  2. expOrFn是函数时:
    Watcher会同时观察expOrFn函数中读取的所有Vue.js实例上的响应式数据。
      执行new Watcher后,代码会判断用户是否使用了immediate参数,如果使用了,则例级执行一次cb。 最后返回一个函数unwatchFn,用来取消观察数据。
      当用户执行这个函数时,实际上执行了watcher.reardown()来取消观察函数,本质是把watcher实例从当前正在观察的状态的依赖列表中移除。
      在Watcher中添加方法来实现unwatch的功能:
      首先需要在Watcher中记录自己都订阅了谁,也就是watcher实例被收集进了哪些Dep,然后当Watcher不想继续订阅这些Dep时,循环自己记录的订阅列表来通知它们(Dep),将自己从它们(Dep)的依赖列表中移除掉。先在Watcher中添加addDep方法,该方法的作用是在Watcher中记录自己都订阅过哪些Dep
export default class Watcher{
    constructor(vm,expOrFn,cb){
        this.vm = vm;
        this.deps = {} //新增
        this.depIds = new  Set(); //新增
        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);
        }
    }
}

  使用depIds来判断如果当前Watcher已经订阅了Dep,则不会重复订阅。
  当依赖发生变化时,会通知Watcher重新读取最新的数据。如果没有这个判断,就会发现每当数据发生了变化,Watcher都会读取最新的数据。而读数据就会再次收集依赖,这就会导致Dep中的依赖有重复。这样数据发生变化时,会同时同时多个Watcher。为了避免这个问题,只有第一次触发getter的时候才会收集依赖。
  接着执行this.depId.add来记录当前Watcher已经订阅了这个Dep
  然后执行this.deps.push(dep)记录自己都订阅了哪些Dep
  最后,触发dep.addSub(this)来将自己订阅到Dep中。
  在Watcher中新增addDep方法后,Dep中收集依赖的逻辑也需要有所改变:

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

  此时,Dep会记录数据发生变化时,需要通知哪些Watcher,而Watcher中也同样记录了自己会被哪些Dep通知。
  为什么是多对多的关系,Watcher每次只读一个数据,怎么会有多个Dep

  1. 如果Watcher中的expOrFn参数是一个表达式,那么肯定只收集一个Dep
  2. expOrFn是一个函数,如果该函数中使用了多个数据,那么此时Watcher就要收集多个Dep
this.$watch(
    function(){
        return this.name +this.age;
    },
    function(newVal,oldVal){
        console.log(newVal,oldVal);
    }
)

  在上述例子中,函数访问了agename两个数据,Watcher内部会收集两个Dep,同时这两个Dep中也会收集Watcher,这导致nameage中的任意一个数据发生变化,Watcher都会收到通知。
  已经在Watcher中记录了自己已经记录了哪些Dep之后,就可以在Watcher中新增teardown方法来通知这些订阅的Dep,让它们把自己从依赖列表中移除掉。

/* 
从所有依赖项的Dep中将自己移除
*/
export default class Watcher{
    constructor(vm,expOrFn,cb){
        this.vm = vm;
        this.deps = {} //新增
        this.depIds = new  Set(); //新增
        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);
        }
    }

    /* 
    从所有依赖项的Dep中将自己移除
    */
    teatdown(){
        let i = this.deps.length;
        while(i--){
            this.deps[i].removeSub(this);
        }
    }
}

  循环订阅列表,分别执行它们的removeSub方法,把自己从它们的依赖列表中移除掉。

export default class Dep{
    // ...

    removeSub(sub){
        const index = this.subs.indexOf(sub);
        if(index>-1){
            return this.subs.splice(index,1);
        }
    }

    // ...
}

  把Watchersub中删除掉,然后当数据发生变化时,将不再通知这个已经删除的Watcher

3. deep参数的实现原理

  Watcher想监听某个数据,就会触发某个数据收集依赖的逻辑,将自己收集进去,当它发生变化时,就会通知Watcher
  要想实现deep的功能,要做到两点:

  1. 触发当前被监听数据的收集依赖逻辑
  2. 把当前监听的这个值在内的所有子值都触发一遍收集依赖逻辑。
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();
        this.getter = parsePath(expOrFn);
        this.cb = cb;
        this.value = this.get()
    }

    get(){
        window.target = this;
        let value = this.getter.call(vm,vm);

        // 新增
        if(this.deep){
            traverse(value);
        }
        window.target = undefined;
        return value;
    }
}

   一定要在window.target=undefined之前去触发子值的收集依赖逻辑,这样才能保证子集收集的这个依赖是当前这个Watcher。如果在window,target=undefined之后去触发收集依赖的逻辑,那么其实当前Watcher并不会被收集到子值的依赖列表当中,也就无法实现deep功能。
  递归value的所有子值,来触发它们收集依赖的功能:

/* 递归value的所有子值来触发他们收集依赖的功能 */
const seenObjects = new Set();
export function traverse(val){
    _traverse(val,seenObjects);
    seenObjects.clear();
}
function _traverse(val,seen){
    let i,key;
    const isA = Array.isArray(val);
    if(!isA && !isObject(val) || Object.isFrozen(val)){
        return;
    }
    if(val.__ob__){
        cost 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 = leys.length;
            while(i--){
                _traverse(val[key[i]],seen);
            }
        }
    }
}

  先判断val的类型,如果它不是ArrayObject,或者已被冻结,直接返回。
  拿到valdep.id,用这个id来保证不会重复收集依赖。
  如果是数组,则循环数组,将数组的每一项递归调用_traverse
  如果是Object类型的数据,则循环Object中所有的key,然后执行一次读取操作,再递归子值。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值