vue原理依赖收集--watcher

2,669 阅读6分钟

前文,有聊到vue中的数据侦测机制(observer),如果实现对监听对象object和数组的数据变化。但是,如果我们只知道数据的变化,也无法及时的把这些数据更新到视图。所以,我们需要收集依赖,等数据更新了,就把收集到的依赖循环触发一遍就好了,这样数据的变化就可以及时更新到视图了。

收集依赖Dep

对于对象来说,依赖是在getter中收集,在setter中触发执行。那么,依赖存储在哪呢?vue中用了一个Dep类来管理依赖,对于响应数据对象的每一个key值,都有一个数组来存储依赖。先来看看Dep类,它可以帮助我们收集依赖、删除依赖和触发依赖。

export default class Dep {
  constructor() {
    this.subs = []
  }
  addSub(sub) {
    this.subs.push(sub)
  }
  removeSub(sub) {
    remove(this.subs,sub)
  }

  depend() {
    if(Window.target) {
      this.addSub(window.target)
    }
  }
  notify() {
    const subs = this.subs.slice();
    for(let i=0,len = subs.slice.length;i<1;i++) {
      subs[i].update()
    }
  }
}

function remove(arr,item) {
  if(arr.length) {
    const index = arr.indexOf(item);
    if(index > -1) {
      return arr.splice(index,1)
    }
  }
}
}

有了Dep类,我们对defineReactive函数来改造下:

function defineReactive(obj,key,value) {
    // 递归对象的值,如果值为对象,也监测
    observer(value);
    let dep = new Dep()
    Object.defineProperty(obj,key,{
        enumerable:true,
        configurable: true,
        get() {
            // 对于对象 我们在这里 收集依赖 watcher
            dep.depend()
            return value
        },
        set(newValue) {
            if(value == newValue) {
                return;
            }
            //对象: 在这里触发收集的依赖
            value = newValue;
            dep.notify()
            //给某个key设置值的时候 可能也是一个对象 也需要监听
            observer(newValue);
            
             console.log('视图更新');
        }
    })
}

此时便已完成了对象的依赖收集,那么依赖究竟是什么呢?

依赖watcher

在上面的代码中,我们收集的是Dep.target,它就是watcher,它是一个能集中处理页面用的数据或者用户自己写的watch的一个抽象类,我们执行它里面的方法即可。

export default class Watcher{
  constructor(vm,expOrFn,cb) {
    // vue的实例对象
    this.vm = vm;
    // 执行getter ,就可以得到用户传进的 如 data.a.b.c的值 
    this.getter = parsePath(expOrFn);
    this.cb = cb;
    // 触发 getter 收集依赖
    this.value = this.get()
  }
  get() {
    window.target = this;
    // 获取新值
    let value = this.getter.call(this.vm,this.vm);
    window.target = null;
    return value;
  }

  update() {
    const oldVaule = this.value;
    this.value = this.get();
    this.cb.call(this.vm,this.value,oldVaule)
  }
}

我们现在get方法中把window.target设为this,即watcher的当前实例,然后读取传入的属性值得到初始值(老值),就会触发getter收集依赖watcher到监听对象相应key得dep中。以后当这个key 如data.a.b.c发生变化时,就会走setter,从而执行watcher的update方法得到数据的最新状态。我们平常用的vm.$watch('a.b.c',cb)和模板中通过指令和插值语法绑定的数据都是基于watcher。还有parsePath的原理:

const reg = /[^\w.$]/
export function parsePath(path) {
  if (reg.test(path)) {
    return;
  }
  const args = path.split('.');
  // obj 在watcher 中值 this.vm
  return function (obj) {
    for (let i = 0, len = args.length; i < len; i++) {
      if (!obj) {
        return
      }
      obj = obj[args[i]]
    }
  }
}

先将字符用‘.’分割,然后一层层从data上取值。

object问题

至此,object类型的数据变化侦测和依赖收集触发都清晰了。但是getter/setter这种追踪方式,无法监听到在对象上,新增属性和delete,也不会通知依赖。但是官方提拱了两个API --- vm.set和vm.delete来填坑。

数组

数组有许多的原型方法,我们在vue种用来改变数组的方法都是内部的拦截器提供的。为了配合数组的依赖收集,我们在observe函数修改下。

export class Observer {
  constructor(value) {
    this.value = value;

    if (!Array.isArray(value)) {
      //  处理object
      this.walk(value)
    }
  }
  /* 
    walk 将对象的每一个属性监听
  */
  walk(obj) {
    const keys = Object.keys(obj);
    for(let i=0; i<keys.length;i++) {
      defineReactive(obj,keys[i],obj[keys[i]])
    }
  }
}

function defineReactive(data,key,val) {
  if(typeof val == 'object') {
    new Observer(val)
  }
  Object.defineProperty(data,key,{
    enumerable: true,
    configurable: true,
    get() {
      dep.depend();
      return val
    },
    set(newVal){
      if(val == newVal) {
        return
      }
      val = newVal;
      dep.nofify()
    }
  })
}

在改造了Observer函数后,我们快速过下数据拦截器的实现方法,数组常用的修改方法有push,pop,shift,unshift,splice,sort,reverse:

const arrayProto = Array.prototype;
export const arrayMethods = Object.create(null);
['push','pop','shift','unshift','splice','sort','reverse'].forEach(function(method) {
  // 缓存 原型上的方法
  const original = arrayProto[method];
  Object.defineProperty(arrayMethods,method,{
    enumerable: false,
    writable: true,
    configurable: true,
    value:function mutator(...args) {
      return original.apply(this,args)
    }    
  })
})

先自定义操作数组的原有方法,后面我们就可以在mutator函数中,发送通知,触发依赖。后面用**拦截器覆盖原型**

export class Observer {
  constructor(value) {
    this.value = value;

    if (Array.isArray(value)) {
        //覆盖原型
      value.__proto__ = arrayMethods;
    }else{
      //  处理object
      this.walk(value)
    }
  }
    
}

数组收集依赖

其实,数组的依赖也是在getter中收集的,因为数组也是通过data的key来访问,如this.list,也会触发list这个属性的getter。所以,数组实在getter中收集依赖,在拦截器中触发。然后数组的依赖时存放在Observer中,然后拦截器中要访问这些依赖:

export class Observer {
  constructor(value) {
    this.value = value;
    // 这个dep 会把对象和数组的依赖 都收集好,后面的 set和delete api 也会用到
    this.dep = new Dep();
    if (Array.isArray(value)) {
        //覆盖原型
      value.__proto__ = arrayMethods;
    }else{
      //  处理object
      this.walk(value)
    }
  }
}

把dep保存在Observer的属性上之后,我们就可以在getter上面收集依赖了

function defineReactive(data,key,val) {
  let childOb = observe(val);
  if(typeof val == 'object') {
    new Observer(val)
  }
  Object.defineProperty(data,key,{
    enumerable: true,
    configurable: true,
    get() {
      if(childOb) {
        // 再次收集依赖 
          childOb.dep.depend()
      }
      dep.depend();
      return val
    },
    set(newVal){
      if(val == newVal) {
        return
      }
      val = newVal;
      dep.nofify()
    }
  })
}
/*
 会为value返回一个Observe 实例,我们在拦截器中 就可以访问 dep
 如果创建成功,直接返回Observer实例
 如果这个实例已经存在 直接返回
*/
export function observe(value,asRootData) {
    if(typeOf value != 'object') {
        return
    }
    let ob;
    if(value.hasOwnProperty('__ob__') && value.__ob__ instanceof Observer ) {
        ob = value.__ob__;
    }else{
        ob = new Observer(value)
    }
    return ob
}

通过上面的方式,我们可以在getter中将依赖不管时对象还是数组的都收集到Observer实例中的dep中,这样我们就可以在拦截器中通过value.ob.dep通知依赖了。在此我还有标记当前的value是否已经被Observer转换成了响应式数据,所以还要再Observer中加上一行代码:

class Observer{
    constructor(value) {
        ...
        def(value,'__ob__',this)
        ...
    }
}
function def(obj,key,val,enumerable) {
    Object.defineProperty(obj,key,{
        enumerable: !!enumerable,
        writable: true,
        configurable: true,
        val
    })
}

接下来我们还需要监听数组的每一项,并再拦截器中发送通知:

class Observer{
    constructor(value) {
        this.value = value
        def(value,'__ob__',this)
        ...
        
        if(Array.isArray(value)) {
            //监听数组的每一项
            this.observeArray(value)
        }else{
            this.walk(value)
        }
    }
    observeArray(val){
        for(let i=0;i<val.length;i++) {
            observe(val[i])
        }
    }
}

拦截器

const arraryProto = Array.prototype;
// 数组原型上的方法
let proto = Object.create(arrayProto);
['push', 'unshift', 'splice', 'reverse', 'sort', 'shift', 'pop'].forEach(method=>{
    proto[method] = function (...args) {
        const ob = this.__ob__
        // 和 Object一样,我们也要处理 数组的新增数据 ,push unshift 和 splice都可以新增数据
        let inserted; // 默认没有插入新的数据
        switch(method) {
            case 'push':
            case 'unshift':
                inserted = args
                break;
            // 数组的splice 只有传递三个参数 才是往数组增加数据
            case 'splice':
                inserted = args.slice(2)
                break;
            default:
                break;
        }
         console.log('视图更新');
        // 检测新增的数据
        if(inserted) {
            ob.observeArray(inserted)
        }
        // 发送依赖
        ob.dep.depend()
        // 还是调用数组的原型方法,但是我们可以在这里 发送数组的变化通知
        arrayProto[method].call(this, ...args)
    } 
})

至此,数组原型的方法都被拦截器代理了,也能正常的发送依赖,但是还是无法拦截数组特有的修改方法,比图this.list[0] = 1,this.list.length=0 ;就无法追踪了。