Vue响应式原理03-实现观察者Watcher和依赖收集器Dep

815 阅读3分钟

1.依赖收集器Dep:

  • constructor中有个subs的数组,用于收集观察者
  • addSub方法:用于将所有的观察者添加到subs数组中
  • 看上图的Dep → Watcher这一步,还需要通知变化,所以需要一个notify方法,内部遍历每一个watcher观察者,去更新发生变化的部分
//依赖收集器
class Dep {
    constructor() {
        this.subs = []
    }
    //收集观察者
    addSub(watcher){
        this.subs.push(watcher)
    }
    //通知观察者去更新
    notify(){
        // 每个观察者有个自己对应的update方法,然后去更新视图
        this.subs.forEach(watcher=>watcher.update())
    }
}

2.观察者Watcher:

  • 观察者主要是通过比较新值旧值的变化,然后决定是否调用回调函数callback更新视图
  • getOldVal方法中通过之前文章中写过的compileUtil对象的getValue方法来获取值
//观察者
class Watcher {
    constructor(vm,attrValue,callback) {
        //获取旧值
        this.oldVal = this.getOldVal()
        this.vm = vm;
        this.attrValue = attrValue;
        this.callback = callback;
    }
    //更新视图的方法,判断新值和旧值是否一样,从而判断是否更新视图
    update(){
        const newVal = compileUtil.getValue(this.attrValue,this.vm)
        if(newVal !==this.oldVal){
            this.callback(newVal)
        }
    }
    // 通过compileUtil对象的getValue方法获取到旧值
    getOldVal(){
        return  compileUtil.getValue(this.attrValue,this.vm);
    }
}

3.DepObserver关联:

首先是在初始化一开始Observer类中截取数据的defineReactiveget方法,添加观察者wathcer

  • 通过const dep = new Dep();生成了收集器Dep的一个实例

  • 然后通过dep.addSub方法去添加观察者watcher,但是问题来了,如果获得观察者watcher并添加进去呢?或者说,如何在实例化Dep的时候,能后拿到watcher观察者呢?解决办法如下:

    • 为了让WatcherDep建立联系,在初始化调用this.getOldeVal()方法的时候,让Dep类target属性等于this,也就是当前wathcer实例,获取完了oldVal之后将Dep类的target清空()

    • //Watcher类(观察者)
      class Watcher {
          constructor(vm,attrValue,callback) {
              //获取旧值
              this.vm = vm;
              this.attrValue = attrValue;
              this.callback = callback;
              this.oldVal = this.getOldVal()
          }
          //更新视图的方法,判断新值和旧值是否一样,从而判断是否更新视图
          update(){
              const newVal = compileUtil.getValue(this.attrValue,this.vm)
              if(newVal !==this.oldVal){
                  this.callback(newVal)
              }
          }
          // 通过compileUtil对象的getValue方法获取到旧值
          getOldVal(){
              Dep.target = this;
              const oldVal = compileUtil.getValue(this.attrValue,this.vm);
              Dep.target = null;
              return oldVal;
          }
      }
      
  • 然后在Observer中,初始化劫持对象属性的时候--Dep.target && dep.addSub(Dep.target),就说如果Dep的target有属性,那就通过dep实例方法addSub添加进收集器中。

  • set方法中设置新值的时候,需要通过dep实例的notify方法通知变化。

//数据劫持类Observer
class Observer {
    constructor(data) {
        this.observe(data);
    }
    observe(data){
        if(data && typeof data ==="object"){
            console.log("我是data数据中的所有key键",Object.keys(data))
            Object.keys(data).forEach(key=>{
                this.defineReactive(data,key,data[key])
            })
        }
    }
    defineReactive(obj,key,value){
        //递归遍历
        this.observe(value);
        //在初始化劫持数据的时候就创建dep实例,对每个属性进行关联
        const dep = new Dep();
        Object.defineProperty(obj,key,{
            enumerable:true, //可遍历枚举的
            configurable:false,
            get(){
                //订阅数据变化,往dep中添加观察者
                Dep.target && dep.addSub(Dep.target)
                return value
            },
            set:(newVal)=>{
                //这里调用observe让新设置的值newVal也支持监听
                this.observe(newVal)
                if(newVal !== value){
                    value =  newVal;
                }
                //通知变化
                dep.notify()
            }
        })
    }
}

4.Watcher观察者添加绑定时机

Watcher是在解析指令渲染页面的时候进行的,也就是compileUtil对象中,先来看下html方法中的处理

  • html方法中直接通过new Watcher实例化,回调函数中通过this.updater.htmlUpdater(node,newVal);来更新值。

        new Watcher(vm,attrValue,(newVal)=>{
            this.updater.htmlUpdater(node,newVal);
        })
    
//compileUtil方法
const compileUtil = {
    text(node,attrValue,vm){
        let value ;
        if(attrValue.indexOf('{{') !== -1)
        {
            value = attrValue.replace(/\{\{(.+?)\}\}/g,(...args)=>{
                console.log("我是args",args)
                return this.getValue(args[1],vm)
            })
        }else
        {
            value = this.getValue(attrValue,vm);
        }
        this.updater.textUpdater(node,value)
    },
    html(node,attrValue,vm){
        const value = this.getValue(attrValue,vm);
        new Watcher(vm,attrValue,(newVal)=>{
            this.updater.htmlUpdater(node,newVal);
        })
        this.updater.htmlUpdater(node,value);
    },
    model(node,attrValue,vm){
        const value = this.getValue(attrValue,vm);
        this.updater.modelUpdater(node,value);
    },
    on(node,attrValue,vm,eventName){
        //获取到options中data的方法
        const handler = vm.$options.methods && vm.$options.methods[attrValue];
        node.addEventListener(eventName,handler.bind(vm),false);
    },
    // 更新函数对象
    updater:{
        textUpdater(node, value) {
            node.textContent = value;
        },
        htmlUpdater(node, value) {
            node.innerHTML = value;
        },
        modelUpdater(node, value) {
            node.value = value;
        }
    },
    // 获取<div v-text='person.name'></div>中person.name以及其他形式的值
     // 这里将vm.$data传入,第一次的随后prevValue的值就是vm.$data
     //然后return 回去的prevValue将作为新的prevValue,之道获取到person.name的值为止。
    getValue(attrValue,vm){
        return attrValue.split(".").reduce((prevValue,currValue)=>{
                console.log("我是currentValue",currValue)
                return prevValue[currValue]
        },vm.$data)
    }
}

测试:

  • 刷新inex.html页面,打开控制台,输入vm.$data.htmlStr="<h1>我是测试compileUtile中html方法的</h1>"

    ✅正确结果如下图:

  • 再来看下如果Watcher类中,如果不将Dep.target = null;会出现如下图结果:

 getOldVal(){
        Dep.target = this;
        const oldVal = compileUtil.getValue(this.attrValue,this.vm);
        //Dep.target = null;
        return oldVal;
    }

❎打印了多个Watcher,就是说,多次设置vm.$data.htmlStr时,上一次的Wathcer没有被清空

流程梳理:

修改数据后Observer的set方法中调用dep.notify方法通知观察者更新this.subs.forEach(watcher=>watcher.update())调用对应的watcher方法wathcer的update中有自己的回调函数,更新页面渲染。

5.compileUtil中其他方法的处理:

1.text方法:

  • 主要是更新调用this.updater.textUpdater时,需要通过this.getContent(attrValue,vm)重新获取值。
const compileUtil = {
    text(node,attrValue,vm){
        let value ;
        //这里需要对{{这种形式进行处理 <h2>{{person.name}} -- {{person.age}}</h2>
        if(attrValue.indexOf('{{') !== -1)
        {
            value = attrValue.replace(/\{\{(.+?)\}\}/g,(...args)=>{
                //可能会是 {{person.name}} -- {{person.age}} 这种形式
                new Watcher(vm,args[1],()=>{
                    this.updater.textUpdater(node,this.getContent(attrValue,vm));
                })
                return this.getValue(args[1],vm)
            })
        }else
        {
            value = this.getValue(attrValue,vm);
        }
        this.updater.textUpdater(node,value)
    },

    // 更新函数对象
    updater:{
        textUpdater(node, value) {
            node.textContent = value;
        },
        htmlUpdater(node, value) {
            node.innerHTML = value;
        },
        modelUpdater(node, value) {
            node.value = value;
        }
    },
    // 获取<div v-text='person.name'></div>中person.name以及其他形式的值
     // 这里将vm.$data传入,第一次的随后prevValue的值就是vm.$data
     //然后return 回去的prevValue将作为新的prevValue,之道获取到person.name的值为止。
    getValue(attrValue,vm){
        return attrValue.split(".").reduce((prevValue,currValue)=>{
                console.log("我是currentValue",currValue)
                return prevValue[currValue]
        },vm.$data)
    },
  // 重新处理getContent方法
    getContent(attrValue,vm){
        return attrValue.replace(/\{\{(.+?)\}\}/g,(...args)=>{
            return this.getValue(args[1],vm)
        })
    }
}

测试:

控制台输入vm.$data.msg="测试compileUtil的text方法"会得到