Vue响应式原理

269 阅读4分钟

首先建议了解 Object.defineproperty ,它是有get与set方法的,分别在获取与设置时触发。

我们先来进行对象的响应式,它是经历了一下的流程:

未命名文件.png

从上可以看出,它是经历循环去把所有的属性都响应式的,我们现在来把这几个基数方法写下来:

对象的响应式

observe判断类型

import Observer from './Observer.js'
export default function observe(value){
    if(typeof(value) != 'object' ) return;
    var ob;
    if(typeof(value.__ob__) != 'undefined'){
        ob = value.__ob__;
    }else{
        ob = new Observer(value);
    };
    return ob;            
}

Observer

import defineReactive from "./defineReactive.js"
import def from './utils.js'
export default class Observer {
    constructor(value) {
        //给对象加上__ob__属性,不可枚举,这里的this是Observer的实例
        def(value, '__ob__', this, false);
        //遍历对象,将每个对象中的属性都去defineReactive,响应式一样
        this.walk(value)
    }
    walk(value) {
        for (let key in value) {
            defineReactive(value, key, value[key]);
        }
    }
}

def添加__ob__

export default function def(data, key, value, enumerable) {
    Object.defineProperty(data, key, {
        configurable: true,
        writable: true,
        enumerable: enumerable,
        value
    })
}

/**
 * 
 * 
 * let obj = {
        a: 10,
        b: 11
    }
    def(obj, 'a', 10, false);

    for (let key in obj) {
        console.log(key) //b  a将不可枚举
    }
 * 
 */


defineReactive将数据响应式

import observe from "./observe.js";
export default function defineReactive(data, key, value) {
    //继续将value响应式,如果value是基础类型,直接返回,如果是对象,继续Observer也会走到这里,直到所有的属性都是响应式
    observe(value);
    Object.defineProperty(data, key, {
        //可枚举
        enumerable: true,
        //可删除
        configurable: true,
        get(){
            console.log(`${key}被读取了,${value},get`);
            return value;
        },
        set(newVal){
            console.log(`${key}被更改了,${newVal},set `);
            value = newVal;
            //设置新值,也需要observe变为响应式的
            observe(value);
        }
    })
}


测试对象响应式

import observe from './observe.js'

let  obj = {
    a:888,
    b:{
        c:{
            d:999
        }
    }
}

observe(obj);
obj.b.c.d

image.png

数组的响应式

首先object.defineProperty是可以get与set数组但无法监听数组的操作, 源码中是对7个数组的方法进行了重写,分别是 'pop', 'push', 'unshift', 'shift', 'splice', 'sort', 'reverse' 我们只是想对数组的方法进行一部分的操作,添加的数据响应式,所以我们应该保留数组原有的方法。 所以需要将原有的方法设为新方法的原型。

未命名文件 (1).png

当我们在一个对象(数组)上查询属性或方法时,本身没有会去原型链上去查找,原型链上也没有,会去原型链的原型链上去查找,这里当数组 [1,2,3]上想使用push方法时,本身会去 Array.prototype上去查找,但是我们加了一层 arrayMethods,就会去使用 arrayMethods上的 push等 7个方法。 这样我们就相当于加了一层拦截。

Observer判断对象还是数组

import defineReactive from "./defineReactive.js"
import def from './utils.js'
import { arrayMethods } from './array.js'
import observe from "./observe.js";
export default class Observer {
    constructor(value) {
        //给对象加上__ob__属性,不可枚举,这里的this是Observer的实例
        def(value, '__ob__', this, false);
        //判读是数组还是对象
        if (Array.isArray(value)) {
            //arrayMethods是重写的7个数组方法,arrayMethods.__proto__是Array.prototype, Array.prototype上有原始的数组方法
            Object.setPrototypeOf(value, arrayMethods)
            this.observeArray(value)
            //value.__proto__ = arrayMethods
            // console.log(value)

        } else {
            this.walk(value);
        }
    }
    walk(value) {
        //遍历对象,将每个对象中的属性都去defineReactive,响应式一样
        for (let key in value) {
            defineReactive(value, key, value[key]);
        }
    }
    observeArray(value) {
        for (let i = 0; i < value.length; i++) {
            observe(value[i]);
        }
    }
}

array.js


import def from './utils.js';

//arrayMethods可以使用数组方法  等于arrayMethods.__proto__ = Array.prototype
export let arrayMethods = Object.create(Array.prototype);

let methodNeedChange = [
    'pop', 'push', 'unshift', 'shift', 'splice', 'sort', 'reverse'
]

methodNeedChange.forEach(methodName => {
    //备份初始的方法
    const original = Array.prototype[methodName]
    def(arrayMethods, methodName, function () {
        const result = original.apply(this, arguments)

        //参数
        const args = [...arguments];

        //vue中data为什么要是对象,因为第一次必须是对象,才能监听data 里的属性
        const ob = this.__ob__;
        //要插入的数据
        let insertList = [];
        
        //当时这3个方法时,是需要添加数据的
        switch(methodName){
            case 'push':
            case 'unshift':
                //push(55,66)
                insertList = args;
                break;
            case 'splice':
                //splice( index ,howmany , 55,66 )  下标为2以后的数据是添加的
                insertList = args.slice(2);
                break;     
        }
        //将添加的数据响应式  observeArray是ob上的属性
        ob.observeArray(insertList)
        console.log(ob)
        console.log('我是被重写的,此时会调用!')
        return result;
    }, false)
})

测试数组响应式

import observe from './observe.js'

let  obj = {
    a:888,
    b:{
        c:{
            d:999
        }
    },
    e:[11,22,33,44]
}

observe(obj);
obj.e.push(55)



会触发我们def设置的新方法, ob就是Observer的实例,上面有observeArray,walk等方法。 image.png

Vue的watch,相信大家一定都用过,watch可以监听数据,返回新旧值,下面为vue.js官网上的一样图:

深入响应式原理

image.png

可以看出,Data中有getter 与setter,getter 中 collect as Dependency (收集依赖) , setter 中有 Notify(通知 )

Dep 收集依赖与通知


export default class Dep{
    constructor(){
       //存放依赖  subs是所有的依赖,什么是依赖,就是我们去监听的数据,可能会很多个,都存放在subs中
       this.subs = [];
    }
    //订阅添加
    addSubs(sub){
        this.subs.push(sub) 
    }
    //依赖添加
    depend(){
        //这里的Dep.target很难理解,到底是什么呢,Dep就是class Dep{} ,  Dep.target是Watcher{} 
        this.addSubs(Dep.target)
    }
    //通知Watch更新
    notice(){
        let subs = this.subs;
        //这个地方不定义l , i<subs.length 会死循环。
        let l = subs.length;

        for(let i=0;i<l;i++){
            //subs[i]就是Watcher{} 
            //循环通知Watch去更新,只有变了才会更新,这个是在Watch判断的
            subs[i].update();
        }   
    }
}

Watch

import Dep from "./Dep.js";

export default class Watch{
    constructor(target,exp,callBack){
       //对象 
       this.target = target;
       //getter 这个是根据key获取val的function
       this.getter = this.parseExp(exp);
       //回调函数
       this.cb = callBack;
       //值
       this.value = this.get();
    }
    parseExp(exp){
        var segments = exp.split('.');
        return (obj) => {
            for (let i = 0; i < segments.length; i++) {
                if (!obj) return;
                obj = obj[segments[i]]
            }
            return obj;
        };
    }
    get(){
        //Dep.target是全局的属性  是Watch{}
        Dep.target = this;
        const obj = this.target;
        let value;
        try{
            value = this.getter(obj);  
        }finally{
            Dep.target  = null;  
        }
     
        return value;
    }
    //set时触发
    update(){
       this.getAndInvoke(); 
    }
    //只有改变了  才会触发cb,需要判断一下新旧值
    getAndInvoke(){
        //获取Object.defineProperty set之后的值
        const value = this.get();
        if( value != this.value || typeof value == 'object' ){
            //在vue中 $watch() 会返回新旧值
            const oldVal = this.value;
            //将新增更新
            this.value = value;
            this.cb.call(this.target,oldVal,value);
        } 
    }

}

Observer中给实例添加上dep

class Observer {
   constructor(){
     //给对象加上__ob__属性,不可枚举,这里的this是Observer的实例
    def(value, '__ob__', this, false);
    //每个实例上都有dep
    this.dep = new Dep()
     ...
   }
}

get中 depend 和 set 中 notify

import observe from "./observe.js";
import Dep from "./Dep.js";
export default function defineReactive(data, key, value) {
    //继续将value响应式,如果value是基础类型,直接返回,如果是对象,继续Observer也会走到这里,直到所有的属性都是响应式
    let child =  observe(value);
    
    let dep = new Dep()
    
    Object.defineProperty(data, key, {
        //可枚举
        enumerable: true,
        //可删除
        configurable: true,
        get(){
            console.log(`${key}被读取了,${value},get`);
            if(Dep.target){
                //收集依赖
                dep.depend();
                if(child){
                    //实例上都有dep属相在observer上添加的
                    child.dep.depend();
                } 
            }
            return value;
        },
        set(newVal){
            console.log(`${key}被更改了,${newVal},set `);
            value = newVal;
            //设置新值,也需要observe变为响应式的
            child = observe(value);
            //发布订阅
            dep.notice();
        }
    })
}

到这就完整的写完了数据响应式原理了,我们来测试一下

import observe from './observe.js'
import Watch from './Watch.js';

let  obj = {
    a:888,
    b:{
        c:{
            d:999
        }
    },
    e:[11,22,33,44]
}

observe(obj);

new Watch(obj,'e',(oldVal,newVal)=>{
    console.log(`${oldVal}改变为${newVal}`)
})

obj.e = [55,66];

image.png

END。