细究 vue2 和 vue3 双向数据绑定原理

前言

相对于vue2,vue3 响应式重新配置,使用 proxy 替换 Object.defineProperty

  • Object.defineProperty:劫持整个对象,然后进行深度遍历所有属性 ,给每个属性添加gettersetter,实现响应式

  • proxy : 劫持整个对象,但不用深度遍历所有属性 ,同样需要添加 gettersetterdeleteProperty,实现响应式

但两者是有一定的区别和优缺点,下面使用 proxy 和 defineProperty 对比实现双向数据绑定,并总结其区别。

实现 vue2 的双向数据绑定

1、监听对象

js 复制代码
// 绑定对象监听
function defineReactive(target, key, val) {

    // 深度监听 
    observer(val);
    
    Object.defineProperty(target, key, {
        get() {
            return val;
        },
        set(newVal) {
            if (newVal != val) {
            
                // 关键点1
                observer(newVal); 

                val = newVal;

                // 更新视图
                updateView();
            }
        }
    })
}

注意:关键点1:深度监听(避免重新设置值为对象,导致新的值无法监听到)一次性全部递归监听

2、监听数组

js 复制代码
// 绑定数组监听
const arrProto = Object.create(Array.prototype);
// 将数组方法复写挂载到新对象中
['push', 'pop', 'shift', 'unshift', 'splice'].forEach(methodName => {
    arrProto[methodName] = function() {
        // 更新视图
        updateView();
        // 执行原型链的方法
        oldArrayProperty[methodName].call(this, ...arguments);
    }
})

arrProto = Object.create(oldArrayProperty)

创建新对象,并且该对象的原型指向数组原型 Array.prototype

目的:在新对象上拓展方法,不会影响原型对象

其他继承方法可以查看:# 图解 JavaScript 几种对象继承方法

3、完整代码及测试用例

js 复制代码
function updateView() {
    console.log("更新视图");
}

// 绑定对象监听
function defineReactive(target, key, val) {
    // 深度监听 (值嵌套对象)
    observer(val);
    
    Object.defineProperty(target, key, {
        get() {
            return val;
        },
        set(newVal) {
            if (newVal != val) {
                // 深度监听(避免重新设置值为对象,导致新的值无法监听到)
                observer(newVal);

                val = newVal;

                // 更新视图
                updateView();
            }
        }
    })
}

// 绑定数组监听
const oldArrayProperty = Array.prototype;
// 创建新对象,并且该对象的原型指向数组原型Array.prototype (目的:在新对象上拓展方法,不会影响原型对象)
const arrProto = Object.create(oldArrayProperty);
// 将数组方法复写挂载到新对象中
['push', 'pop', 'shift', 'unshift', 'splice'].forEach(methodName => {
    arrProto[methodName] = function() {
        // 更新视图
        updateView();
        // 执行原型链的方法
        oldArrayProperty[methodName].call(this, ...arguments);
    }
})

function observer(target) {
    if (typeof target !== 'object' || target == null) {
        return target;
    }

    // 数组
    if (Array.isArray(target)) {
        // 修改数组原型指向
        target.__proto__ = arrProto;
    }

    for (let key in target) {
        defineReactive(target, key, target[key]);
    }
}


///////////////////////////////

const data = {
    name: "haohao",
    age: 20,
    info: {
        sex: "男",
    },
    test: "",
    nums: [1, 2, 3]
}

// 双向绑定
observer(data);

///////////////////////////////




// 监听基本属性
data.name = 'lisi';
console.log(data.name);

// 深度监听
data.info.sex = '女';
console.log(data.info.sex);

// 监听设置值(引用类型是否还能监听到)
data.test = {
     a: 1
}
data.test.a = 2;
console.log(data.test.a);

// 监听数组
data.nums.push(6);

// 新增属性(监听不到 -- Vue.$set)
data.x = 123;
console.log(data.x);

// 删除属性 (监听不到 -- Vue.$delete)
delete data.name;

实现 vue3 的双向数据绑定

1、使用 Proxy 进行代理

js 复制代码
function reactive(target = {}) {
    if (typeof target !== 'object' || target == null) {
        return target;
    }

    // 代理
    const proxyConf = {
        get(target, key, receiver) {
            // 只处理本身(非原型)属性 关键点1
            const ownKeys = Reflect.ownKeys(target);
            if (ownKeys.includes(key)) {
                console.log("获取 get:", key)
            }

            const result = Reflect.get(target, key, receiver);

            // 关键点2
            return reactive(result);
        },

        set(target, key, val, receiver) {
            // 重复数据(不处理)
            if (val == target[key]) return true;

            const ownKeys = Reflect.ownKeys(target);
            if (ownKeys.includes(key)) {
                console.log("设置 key:", key, "val:", val);
            } else {
                console.log("新增 key:", "key", "val:", val);
            }

            const result = Reflect.set(target, key, val, receiver);
            return result; // 是否设置成功
        },

        deleteProperty(target, key) {
            const result = Reflect.deleteProperty(target, key);
            console.log("删除 key:", key);
            return result; // 是否删除成功
        }
    }

    const proxyData = new Proxy(target, proxyConf);
    return proxyData;
}

注意

  • 关键点1:通过 Reflect.ownKeys(target)获取 target 本身具备的属性,监听属于 target 自身属性
  • 关键点2:返回的对象必须是Proxy对象,目的在于确保能够实时监听该对象下的属性

vue3 关键点1对比 vue2 关键点1

(1)vue2:是一次性全部递归监听

(2)vue3:是分多次进行监听(只要访问对象属性,则会监听属性下的所有属性,没有返回到的就不会进行监听)

2、完整代码及测试用例

js 复制代码
function reactive(target = {}) {
    if (typeof target !== 'object' || target == null) {
        return target;
    }

    // 代理
    const proxyConf = {
        get(target, key, receiver) {
            // 只处理本身(非原型)属性
            const ownKeys = Reflect.ownKeys(target);
            if (ownKeys.includes(key)) {
                console.log("获取 get:", key)
            }

            const result = Reflect.get(target, key, receiver);

            // 关键点
            return reactive(result);
        },

        set(target, key, val, receiver) {
            // 重复数据(不处理)
            if (val == target[key]) return true;

            const ownKeys = Reflect.ownKeys(target);
            if (ownKeys.includes(key)) {
                console.log("设置 key:", key, "val:", val);
            } else {
                console.log("新增 key:", "key", "val:", val);
            }

            const result = Reflect.set(target, key, val, receiver);
            return result; // 是否设置成功
        },

        deleteProperty(target, key) {
            const result = Reflect.deleteProperty(target, key);
            console.log("删除 key:", key);
            return result; // 是否删除成功
        }
    }

    const proxyData = new Proxy(target, proxyConf);
    return proxyData;
}


const data = {
    name: 'haohao',
    age: 20,
    info: {
        city: 'shangtou',
        a: {
            b: {
                c: {
                    d: {
                        e: 100
                    }
                }
            }
        }
    },
    test: "",
    nums: [1, 2, 3, 4]
}
const proxyData = reactive(data);

// 监听基本属性
proxyData.name = 'lisi'; 
// 设置 key: name val: lisi

// 深度监听
proxyData.info.city = '汕头'; 
// 获取 get: info
// 设置 key: city val: 汕头

// 监听设置值(引用类型是否还能监听到)
proxyData.test = { a: 1 } // 设置 key: test val: { a: 1 }

proxyData.test.a = 2;
// 获取 get: test
// 设置 key: a val: 2

// 监听数组
proxyData.nums.push(6);
// 获取 get: nums
// 获取 get: length
// 新增 key: key val: 6

// 新增属性
proxyData.x = 123; 
// 新增 key: key val: 123

// 删除属性
delete proxyData.name; // 删除 key: name

总结

vue3 优缺点

  • 深度监听性能更好
  • 可监听 新增 / 删除 属性
  • 可监听数组变化
  • Proxy 能规避 Object.defineProxy 的问题
  • Proxy 无法兼容所有浏览器,无法 polyfill

vue2 优缺点

  • 可以兼容其他浏览器
  • 深度监听需要一次性递归
  • 无法监听 新增 \ 删除 属性
  • 无法原生监听数组,需要特殊处理
相关推荐
anyup_前端梦工厂2 小时前
了解几个 HTML 标签属性,实现优化页面加载性能
前端·html
前端御书房2 小时前
前端PDF转图片技术调研实战指南:从踩坑到高可用方案的深度解析
前端·javascript
2301_789169542 小时前
angular中使用animation.css实现翻转展示卡片正反两面效果
前端·css·angular.js
风口上的猪20153 小时前
thingboard告警信息格式美化
java·服务器·前端
程序员黄同学3 小时前
请谈谈 Vue 中的响应式原理,如何实现?
前端·javascript·vue.js
爱编程的小庄4 小时前
web网络安全:SQL 注入攻击
前端·sql·web安全
宁波阿成4 小时前
vue3里组件的v-model:value与v-model的区别
前端·javascript·vue.js
柯腾啊5 小时前
VSCode 中使用 Snippets 设置常用代码块
开发语言·前端·javascript·ide·vscode·编辑器·代码片段
Jay丶萧邦5 小时前
el-select:有关多选,options选项值不包含绑定值的回显问题
javascript·vue.js·elementui
weixin_535854225 小时前
oppo,汤臣倍健,康冠科技,高途教育25届春招内推
c语言·前端·嵌入式硬件·硬件工程·求职招聘