Vue响应式数据原理

752 阅读2分钟

Vue2.0 Object.defineProperty

数据模型仅仅是普通的 JavaScript 对象,但是对这些对象进行操作时,却能影响对应视图,简而言之,就是你动我也动。 它的核心实现就是「响应式系统」,核心内容为Object.defineProperty 使用方法如下:

/*
    obj: 目标对象
    prop: 目标对象的属性名
    descriptor: 描述符
    
    return value 传入对象
*/
Object.defineProperty(obj, prop, descriptor)

descriptor的一些属性

  • enumerable,属性是否可枚举,默认 false。
  • configurable,属性是否可以被修改或者删除,默认 false。
  • get,获取属性的方法。
  • set,设置属性的方法。

实现 observer(可观察到你动了)

首先定义一个假的函数来模拟更新

function updateView(val) {
    /* 假装是视图 */
    console.log("我动了");
}

然后我们定义一个 defineReactive ,这个方法通过 Object.defineProperty 来实现对对象的「响应式」化,经过 defineReactive 处理以后,我们的 target 的 key 属性在「」的时候会触发 get 方法,而在该属性被「」的时候则会触发 set 方法。

function defineReactive (target, key, val) {
    Object.defineProperty(obj, key, {
        get() {
            return val;
        },
        set(newVal) {
            if (newVal === val) return;
            updateView(newVal);
        }
    });
}

这样貌似ok了,但是没人让他动起来,我们再封装一层observer,这个函数传入一个 value(需要「响应式」化的对象),通过遍历所有属性的方式对该对象的每一个属性都通过 defineReactive 处理。

function observer (target) {
    if (!target || (typeof target !== 'object')) {
        return;
    }
    
    Object.keys(target).forEach((key) => {
        defineReactive(target, key, target[key]);
    });
}

最后为了好看点,封装一个Vue

class Vue {
    constructor(options) {
        this._data = options.data;
        observer(this._data);
    }
}
// 测试
let o = new Vue({
    data: {
        test: "I am test."
    }
});
o._data.test = "hello,world.";  /* 我动了 */

问题来了

1,这只测试了字符串,要是测试嵌套对象就很容易发现问题,不动啦,原理也很简单,指向同一内存,可以类比深浅拷贝

那么我们要进行递归调用

function defineReactive(target, key, value){
    observer(value); // 递归 我就将这个对象 继续拦截
    Object.defineProperty(target,key,{
        get(){
            return value 
        },
        set(newValue){
            if(newValue !== value){ // 不同值才更新
                observer(newValue)  // o_data.age = {n:200};o _data.age.n = 300;这种情况就需要重新观察
                updateView();
                value = newValue
            }
        }
    });
}

2,新加数组项不会触发更新

改写observer, 为了让不改变原数组,巧妙运用切片编程

let oldArrayPrototype = Array.prototype;
let proto = Object.create(oldArrayPrototype); // 继承
['push','shift','unshift'].forEach(method=>{
    proto[method] = function(){ //函数劫持 把函数进行重写 内部 继续调用老的方法
        updateView(); // 切片编程
        oldArrayPrototype[method].call(this, ...arguments)
        // oldArrayPrototype[method].apply(this, arguments)
    }
});
function observer(target){
    if(typeof target !== 'object' || target == null){
        return target;
    }
    if(Array.isArray(target)){ // 拦截数组 给数组的方法进行了重写 
        Object.setPrototypeOf(target,proto); // 写个循环 赋予给target
        // target.__proto__ = proto;
        for(let i = 0; i< target.length ;i++){
            observer(target[i]);
        }
    }else{
        Object.keys(target).forEach((key) => {
            defineReactive(target, key, target[key]);
        });
    }
   
}

3,新增的属性不更新,使用$set啦

整合代码如下

let oldArrayPrototype = Array.prototype;
let proto = Object.create(oldArrayPrototype); // 继承
['push','shift','unshift'].forEach(method=>{
    proto[method] = function(){ //函数劫持 把函数进行重写 内部 继续调用老的方法
        updateView(); // 切片编程
        oldArrayPrototype[method].call(this,...arguments)
    }
});
function observer(target){
    if(typeof target !== 'object' || target == null){
        return target;
    }
    if(Array.isArray(target)){ // 拦截数组 给数组的方法进行了重写 
        Object.setPrototypeOf(target,proto); // 写个循环 赋予给target
        // target.__proto__ = proto;
        for(let i = 0; i< target.length ;i++){
            observer(target[i]);
        }
    }else{
        Object.keys(target).forEach((key) => {
            defineReactive(target, key, target[key]);
        });
    }
   
}
function defineReactive(target,key,value){
    observer(value); // 递归 我就将这个对象 继续拦截
    Object.defineProperty(target,key,{
        get(){ // get 中会进行依赖收集
            return value 
        },
        set(newValue){
            if(newValue !== value){
                // data.age = {n:200}; data.age.n = 300;这种情况就需要重新观察
                observer(newValue) 
                updateView()
                value = newValue
            }
        }
    });
}
function updateView(){
    console.log('我动啦')
}

class Vue {
    constructor(options) {
        this._data = options.data;
        observer(this._data);
    }
}
// 测试
let o = new Vue({
    data: {
        test: "I am test."
    }
});
o._data.test = "hello,world.";  /* 我动了 */

Proxy 面向3.0的数据响应式

Proxy比较简单,可直接监听对象,文档可参照此处,详细可参考阮大师的es6 proxy

具体简单实现如下:

let toProxy = new WeakMap(); // 弱引用映射表 放置的是 原对象:代理过的对象
let toRaw  = new WeakMap(); // 被代理过的对象:原对象
function isObject(val){
    return typeof val === 'object' && val !== null;
}
function hasOwn(target,key){
    return target.hasOwnProperty(key);
}
// 1.响应式的核心方法
function reactive(target){
    // 创建响应式对象 
    return createReativeObject(target);
}
// 创建响应式对象的
function createReativeObject(target){
    if(!isObject(target)){ // 如果当前不是对象 直接返回即可
        return target;
    }
    let proxy = toProxy.get(target); // 如果已经代理过了 就将代理过的结果返回即可
    if(proxy){
        return proxy;
    }
    if(toRaw.has(target)){ // 放置代理的过的对象再次被代理
        return target;
    }
    let baseHandler = { 
        // reflect 优点 不回报错 而且 会有返回值 会替代掉Object 上的方法
        get(target,key,receiver){
            // proxy + reflect 反射,获取值
            let result = Reflect.get(target,key,receiver);

            return isObject(result)?reactive(result):result; // 是个递归
        },
        set(target,key,value,receiver){ // [1,2,3,4]
            // 怎么去 识别是改属性 还是 新增属性
            let hadKey = hasOwn(target,key); // 判断这个属性 以前有没有
            let oldValue = target[key];
            let res = Reflect.set(target,key,value,receiver);
            if(!hadKey){
                console.log('添加新对象');
            }else if(oldValue !== value){ // 这里表述属性 更改过了
                console.log('设置新对象');
            }
            return res;
        },
        deleteProperty(target,key){
            let res = Reflect.deleteProperty(target,key)
            console.log('删除')
            return res;
        }
    }
    let observed = new Proxy(target,baseHandler); //es6
    toProxy.set(target,observed);
    toRaw.set(observed,target);
    return observed;
}

很明显,对于新添加的值proxy支持的很好,不用$set也不用重写数组方法,很好的体现了优势,唯一缺点就是兼容性差,不支持ie11,未来可期,预计3.0会兼容,两个各写一套。