vue双向绑定原理

前言

vue是一款mvvm前端框架,其vm的双向绑定特性很是让人津津乐道。

Vue.js中的双向数据绑定指的是将视图(View)与模型(Model)之间建立起自动同步关系。
当用户在界面上修改了输入值时,对应的数据会被更新;
反过来,如果后台数据发生变化,也能及时地反映到前端页面上。

我们也都知道其原理是 数据劫持+发布订阅模式,那具体怎么做呢?---下边一一道来。

数据劫持

首先程序需要知道数据被改变了,然后逞能根据改变做出同步的处理。
那怎么知道数据被改变了呢?
vue2和vue3分别利用了不同的手段!

vue2-defineProperty

在vue2时代 是利用 对象属性的 属性描述符(defineProperty)之访问器属性set函数做劫持监听。

defineProperty基本使用方式

const dog = {
    name: '小花',
    age:20
};

// 利用属性描述符里的set方法,对修改对象属性做监听或劫持
let dogAgeTemp = dog.name;
Object.defineProperty(dog, 'name', {
    set(newVal){
        dogAgeTemp = newVal;
        // 这里可以监听和劫持对象属性的改变
        console.log('dog的name有变化,新值为'+newVal);
    },
    get(){
        return dogAgeTemp;
    }
})


dog.name = '王五'
console.log(dog);

显然如果要监听一个对象所有的属性,这样一个个手写出来实在太麻烦,我们可以结合遍历

const dog = {
    name: '小花',
    age: 20
};

// 利用属性描述符里的set方法,对修改对象属性做监听或劫持
for (const key in dog) {
    let dogAttrTemp = dog[key];
    Object.defineProperty(dog, key, {
        set(newVal) {
            dogAttrTemp = newVal;
            // 这里可以监听和劫持对象属性的改变
            console.log('dog的' + key + '有变化,新值为' + newVal);
        },
        get() {
            return dogAttrTemp;
        }
    })
}

dog.name = '王五'
dog.age = 22
console.log(dog);

显然,这还是不够完美,无法处理对象属性仍然可能是对象的问题,我们可以结合递归

const dog = {
    name: '小花',
    age: 20,
    child: {
        cname: '小花花'
    }
};

// 利用属性描述符里的set方法,对修改对象属性做监听或劫持
(() => {
    const handel = (o) => {
        for (const key in o) {
            let oAttrTemp = o[key];
            const isObject = Object.prototype.toString.call(oAttrTemp)==='[object Object]';
            if (isObject) {
                console.log('是对象,',oAttrTemp);
                handel(oAttrTemp);
            } else {
                Object.defineProperty(o, key, {
                    set(newVal) {
                        oAttrTemp = newVal;
                        // 这里可以监听和劫持对象属性的改变
                        console.log('dog的' + key + '有变化,新值为' + newVal);
                    },
                    get() {
                        return oAttrTemp;
                    }
                })
            }

        }
    }
    handel(dog);
})();

dog.name = '王五'
dog.age = 22
dog.child.cname = '小王五'
console.log(dog);

至此,算是完成了。
但是我们也能发现两个问题:
后续添加的属性,因为再遍历处理之后加入的,无法被监听。
数组或Set、Map内部的修改无法被监听。

针对一个问题,vue放任没管,而是让开发者注意或者使用$forceUpdate强制脏值检测,直至es6 proxy的出现。
后者 vue重写了数组的部分常用方法,来达到监听更新的目的。

vue3-proxy

proxy是es6出的一个api,它可以很好地去拦截和代理处理js对象数据。
代理对象和原始对象的改动都会相互影响,不过只有代理对象被改动的时候才会触发内部的set等函数钩子。

const dog = {
    name: '小花',
    age: 20
};

// 利用代理 里的set对修改对象属性做监听或劫持
const dogProxy = new Proxy(dog, {
    set(target, key, val) {
        // 可以使用Reflect.set(...arguments) 或  Reflect.set(target, key, val);       
        target[key] = val;  
        // 这里可以监听和劫持对象属性的改变
        console.log('dog的' + key + '有变化,新值为' + val);
    }
});

dogProxy.name = '王五'
dogProxy.age = 20
console.log(dog, dogProxy);

可以看到,代理对象在监听set的时候,多个属性不需要自己手动循环处理,这比es5的属性描述法好用多了。
但Proxy和defineProperty的一个共同特性,不支持对象嵌套,需要递归去实现。

const dog = {
    name: '小花',
    age: 20,
    child: {
        cname: '小花花'
    }
};


const getProxy = (obj) => {

    // 代理处理器
    const proxyHandler = {
        set(target, key, val) {
            // 可以使用Reflect.set(...arguments) 或  Reflect.set(target, key, val);       
            target[key] = val;
            // 这里可以监听和劫持对象属性的改变
            console.log( key + '有变化,新值为' + val);
        }
    }


    // 递归处理器
    const temp = new Proxy(obj, {});
    const reHandler = (o) => {
        for (const key in o) {
            const isObject = Object.prototype.toString.call(o[key]) === '[object Object]';
            if (isObject) {
                o[key] = new Proxy(o[key], proxyHandler);
                reHandler(o[key])
            }
        }
    }
    reHandler(temp);

    // 返回结果
    return temp

}
const dogProxy = getProxy(dog);

dogProxy.child.cname = '小王五'
console.log(dogProxy);
posted @ 2024-02-27 16:22  丁少华  阅读(13)  评论(0编辑  收藏  举报