菜鸡手写vue(九)-监听器

347 阅读2分钟

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方法,而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;