Vue:Object变化侦测

Vue:Object变化侦测

1. 什么是变化侦测

​ Vue.js会自动检测状态并生成DOM,然后将其输出到页面上,这个过程称为渲染,这个渲染过程是声明式的,我们通过模板来描述状态和DOM之间的映射关系。

​ 而通常情况下,我们的页面是不断在更新状态的,此时页面根据状态来重新渲染,想要检测这个过程,就涉及到了变化侦测

​ 变化侦测有两种类型:“推”和“拉”。国内三大主流框架中的Angular和React都采用“拉”,Vue采用的是“推”

​ “推”的优势是,当状态发生变化,Vue能够立刻知道,并将信息推送到各个相关依赖。因此,当知道信息越多,就可以进行更细粒度的更新。

​ 粒度越细,在依赖上追踪所消耗的内存开销就会越大。Vue引入了虚拟DOM,将粒度调整为中等粒度,则一个状态所绑定的依赖不再是具体的DOM节点,而是组件,组件再通知给DOM,大大降低了依赖数量,降低内存损耗。

2. 追踪变化和收集依赖的实现

2.1 JS中的对象追踪变化

​ 在JS中,检测对象变化的方法主要有两种:Object.definePropertyProxy

​ 下面我们来对Object.defineProperty来进行封装,使之成为响应式的:

function defineReactive(data,key,val){
    
    
    Object.defineProperty(data,key,{
    
    
        enumerable: true,
        configurable: true,
        get: function(){
    
    
            return val;
        },
        set: function(newVal){
    
    
            if(val === newVal){
    
    
                return ;
            }
            val = newVal;
        }
    })
}

2.2 收集依赖

​ 收集依赖,就是把模板中用到目标数据的地方先保存起来,等数据发生变化时,把之前收集的依赖循环触发一遍渲染即可。

​ 用一句话总结,就是getter收集依赖,setter触发依赖

​ 让我们将收集依赖的代码封装成类,并改进一下上面的封装内容,使之能够收集触发依赖。

class Dep{
    
    
    constructor(){
    
    
        this.subs = [];
    }
    
    addSub(sub){
    
    
        this.subs.push(sub);
    }
    
    removeSub(sub){
    
    
        let index = this.subs.indexOf(sub);
        if(index > -1){
    
    
            this.subs = this.subs.splice(index,1);
        }
    }
    depend(){
    
    
        if(window.target){
    
    
            this.addSub(window.target);
        }
    }
    notify(){
    
    
        let subs = this.subs.splice();
        for(let i=0;i<this.subs.length;i++){
    
    
            subs[i].update() 	// 这里的update方法在后续定义
        }
    }
}

function defineReactive(data,key,val){
    
    
    let dependencies = new Dep(); // 依赖
    Object.defineProperty(data,key,{
    
    
        enumerable: true,
        configurable: true,
        get:function(){
    
    
            dependencies.depend();
            return val;
        },
        set: function(newVal){
    
    
            if(newVal === val){
    
    
                return ;
            }
            val = newVal;
            dependencies.notify();
        }
    })
}
  • 在上面代码中,我们收集的依赖叫做window.target,实际上他有一个抽象的名字:Watcher

2.3 Watcher的角色

​ Watcher在Vue中扮演中介的角色,当有数据发生变化时通知他,然后就再通知给其他地方。

​ 在Vue中,Watcher的使用方式:

vm.$watch("name",function(newVal,oldVal){
	// do something
})

​ 想要实现watcher,只要把watcher实例添加到data.name属性中,将window.target指向它,当数据发生变化后,就可以通知到Watcher,Watcher再执行参数里的回调函数就行了。

class Watcher{
    
    
    // vm,属性或函数,callback
    constructor(vm,expOrFn,cb){
    
    
        this.vm = vm;
        this.getter = parsePath(expOrFn);	// 解析字符串路径
        this.cb = cb;
        this.value = this.get();
    }
    get(){
    
    
        window.target = this;
        let value = this.getter.call(this.vm,this.vm);
        window.target = undefined;
        return value;
    }
    update(){
    
    
        const oldVal = this.value;
        this.value = this.get();
        //触发回调函数
        this.cb.call(this.vm,this.value,oldVal);
    }
}
  • 在Watcher的get方法中,我们将window.target指向this,获取到当前依赖的值(如data.name)过程中,出发了getter函数;

  • getter触发后,就会将该依赖的this存入Dep实例中,之后每当data.name发生变化,就会触发Dep的notify方法,循环触发所有依赖的update方法,从而实现“推”的过程。

  • 代码中解析字符串路径的方法具体实现如下:

  • const exp = /[^\w.$]/;
    function parsePath(path){
          
          
        if(exp.test(path)){
          
          
            return ;
        }
        const segments = path.split('.');
        return function(obj){
          
          
            for(let i=0;i<segments.length;i++){
          
          
                if(!obj){
          
          
                    return;
                }
                obj = obj[segments[i]];
            }
            return obj;
        }
    }
    
  • 通过循环一层层去读取数据,最终得到的就是目标数据。

2.4 如何侦测所有的key

​ 在上面的代码中,我们一次只能够侦测到数据中的一个属性,但我们的目的时侦测所有的属性(包括子属性),所以我们可以封装一个类,将数据中的所有属性都变成可以侦测变化的key。

class Observer{
    
    
    constructor(value){
    
    
        this.value = value;
    	// 当值不为数组时,将其转化为可侦测的	
        if(!Array.isArray(value)){
    
    
            this.walk(value);
        }
    }
    
    walk(value){
    
    
        const keys = Object.keys(value);
        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)
    }
    let dep = new Dep();
    Object.defineProperty(data,key,{
    
    
        enumerable: true,
        configurable: true,
        get:function(){
    
    
            dep.depend();
            return val;
        },
        set:function(newVal){
    
    
            if(newVal === val){
    
    
                return ;
            }
            val = newVal;
            dep.notify();
        }
    })
}

2.5 整个变化侦测的流程

首先,data先通过Observer将所有数据转化成可侦测数据;
当外界读取数据时,Watcher会触发getter,将该数据的Watcher收集到Dep(收集依赖);
当数据发生变化时,触发setter,setter会通知Dep,Dep通知Watcher;
Watcher接到通知后,向外界发送通知,外界根据数据的变化进行相应操作。

3. 总结

  • 变化侦测就是侦测数据的变化。
  • Object可以通过Object.defineProperty来将属性转换成可侦测模式。获取数据时触发getter,修改数据时触发setter。
  • 通过getter来收集依赖,通过setter来通知依赖数据发生了变化。
  • 收集依赖我们需要一个储存依赖的方法,因此封装了一个Dep类,用于添加、删除、通知依赖。
  • 所谓依赖就是Watcher,只要Watcher触发了getter,就将其收集到Dep中。当数据发生变化后,会循环依赖列表,逐个通知。
  • Watcher的原理是将自己设置到全局唯一对象,然后读取数据,触发了getter,getter会获取全局对象,即当前的Watcher,将其添加到Dep中。Watcher就是通过这样的方式来主动订阅任意数据的变化。
  • 为了侦测object中的所有数据的变化,我们封装了Observer类,将object的所有数据(包括子数据)都通过defineReactive转化成可侦测数据。
  • 在ES6前,JS没有提供元编程的能力,因此无法追踪对象新增和删除属性的变化。

猜你喜欢

转载自blog.csdn.net/yivisir/article/details/114379919
今日推荐