面试官:Vue3.0里为什么要用 Proxy API 替代 defineProperty API ?

文摘   2024-11-10 10:09   四川  

今天来聊一个面试中常见的问题:为什么 Vue3.0 要用 Proxy API 来替代原来的 Object.defineProperty API 呢?作为一个前端开发工程师,这可是我们必须弄清楚的知识点!要理解这个问题,我们得先明白 Object.definePropertyProxy 的工作原理,接下来就来一起分析一下这背后的原因吧。

首先,Vue2.x 的响应式系统主要是基于 Object.defineProperty 实现的。简单来说,它通过“数据劫持”的方式来跟踪数据变化,每个数据属性都被设置了 gettersetter,一旦属性值被访问或修改时,这两个函数就会自动触发。例如,如果我们有一个对象 obj,希望它的属性 foo 是响应式的,我们可以这样写:

function defineReactive(obj, key, val{
    Object.defineProperty(obj, key, {
        get() {
            console.log(`获取 ${key}${val}`);
            return val;
        },
        set(newVal) {
            if (newVal !== val) {
                val = newVal;
                console.log(`设置 ${key}${newVal}`);
            }
        }
    });
}

const obj = {};
defineReactive(obj, 'foo''初始值');
console.log(obj.foo); // 获取 foo: 初始值
obj.foo = '新值';     // 设置 foo: 新值

通过上面的代码,我们实现了一个最简单的响应式系统。每当我们获取 obj.foo 或设置 obj.foo 时,都会触发 getset,从而可以执行相应的逻辑来更新视图。这听起来很棒对吧?但问题也随之而来。

问题1:无法监听属性的添加和删除

比如,当我们动态地给 obj 增加一个新的属性 bar 时,defineProperty 是无能为力的。它无法检测到这个新属性的变化。换句话说,Vue2.x 对新增或删除属性并不“敏感”。为了解决这个问题,Vue2.x 里不得不额外提供了 Vue.setVue.delete 方法,但这只是“治标不治本”。

const obj = { foo'foo' };
defineReactive(obj, 'foo', obj.foo);
delete obj.foo; // 无法检测
obj.bar = 'bar'// 无法检测

问题2:对数组的支持不够全面

defineProperty 对数组的支持更是一个麻烦。比如,我们无法劫持 pushpop 等数组操作,因为这些操作不会触发 set,这就导致数组的响应式更新需要大量额外的逻辑。为此,Vue2.x 特别重写了数组的操作方法(例如重写 pushpopshiftunshiftsplice 等方法)来实现响应式,但这种方式显然不是优雅的解决方案。

const arr = [123];
arr.push(4); // 无法检测到
arr[0] = 99// 可检测到

问题3:嵌套对象的深度监听性能问题

Vue2.x 使用 defineProperty 实现响应式时,必须对对象的每个属性递归地调用 defineProperty。也就是说,数据的每个嵌套层级都会遍历一遍,如果嵌套层级过深,性能开销就非常大。想象一下,如果一个对象嵌套了几十层属性,这种递归操作将极大地拖累页面性能。这就给 Vue2.x 的响应式系统带来了性能瓶颈。

为了解决这些问题,Vue3.0 引入了 Proxy 来替代 defineProperty

Proxy 的优势

Proxy 是 ES6 提供的一种新 API,它允许我们对对象的基本操作进行拦截和自定义。换句话说,Proxy 可以拦截我们对对象的所有操作,包括属性读取、设置、删除等等。对 Vue3.0 来说,Proxy 的引入不仅解决了上面提到的几个问题,还提供了更为简洁、高效的解决方案。

让我们来看一下一个简单的 Proxy 实现:

function reactive(obj{
    if (typeof obj !== 'object' || obj === nullreturn obj;
    
    return new Proxy(obj, {
        get(target, key, receiver) {
            const result = Reflect.get(target, key, receiver);
            console.log(`获取 ${key}${result}`);
            return typeof result === 'object' ? reactive(result) : result;
        },
        set(target, key, value, receiver) {
            const result = Reflect.set(target, key, value, receiver);
            console.log(`设置 ${key}${value}`);
            return result;
        },
        deleteProperty(target, key) {
            const result = Reflect.deleteProperty(target, key);
            console.log(`删除 ${key}`);
            return result;
        }
    });
}

const state = reactive({ foo'foo'nested: { bar'bar' } });
console.log(state.foo); // 获取 foo: foo
state.foo = '新值'// 设置 foo: 新值
state.newKey = '我是新加的键'// 设置 newKey: 我是新加的键
delete state.foo; // 删除 foo

从代码可以看出,我们在对象 state 上进行的每个操作都被 Proxy 监听并记录了下来。而且不管是新增、删除还是嵌套属性的修改,都能得到及时响应。

Proxy 的显著优点

  1. 对整个对象进行监听,支持动态属性的添加和删除
    Proxy 监听的是整个对象,而不是对象的某个属性。这意味着我们不需要再为了监听新增属性和删除属性而额外定义 Vue.setVue.delete 方法。

  2. 全面支持数组的各种操作
    Proxy 可以轻松地劫持数组的所有操作,例如 pushpopshiftunshift 等。也就是说,数组的任何操作都可以触发视图更新,无需额外重写数组方法。

  3. 性能更优
    由于 Proxy 不需要像 defineProperty 那样递归地对每个属性进行遍历劫持,因此在性能上有极大的提升。特别是对深层嵌套的对象和大型数据结构,Proxy 的性能表现尤为出色。

  4. 提供更丰富的拦截功能
    Proxy 支持多达 13 种拦截操作,例如 applyownKeysdeletePropertyhas 等,这让我们能够在不同场景下实现更复杂的逻辑。这是 defineProperty 所做不到的。

当然,Proxy 并非完美无缺,它最大的缺点就是兼容性问题——IE 浏览器不支持 Proxy。因此,Vue3.0 需要放弃对 IE 的兼容,专注于现代浏览器。

所以,如果面试官问我 Vue3.0 为什么要用 Proxy 取代 defineProperty,我会这样回答:

Vue3.0 使用 Proxy 替代 defineProperty 是因为 Proxy 可以监听整个对象的操作,而不仅仅是某个属性。它能够监控到属性的添加和删除,对数组的操作也能被有效监听,同时 Proxy 还支持深层嵌套对象的代理,减少了 Vue2.x 中由于 defineProperty 带来的性能瓶颈。虽然 Proxy 不支持 IE,但现代浏览器的支持让它成为更合适的选择。简而言之,Proxy 的出现让 Vue3.0 的响应式系统更简单、更强大,也更高效。

目前,对编程、职场感兴趣的同学,大家可以联系我微信:golang404,拉你进入“程序员交流群”。

虎哥私藏精品 热门推荐

虎哥作为一名老码农,整理了全网最前端资料合集

资料包含了《前端面试题PDF合集》、《前端学习视频》、《前端项目及源码》,总量高达108GB。

全部免费领取全面满足各个阶段程序员的学习需求!

web前端专栏
回复 javascript,获取前端面试题。分享前端教程,AI编程,AI工具,Tailwind CSS,Tailwind组件,javascript教程,webstorm教程,html教程,css教程,nodejs教程,vue教程。
 最新文章