【vue高频面试题—基础篇】:vue2和vue3的双向数据绑定的原理和比较

292 阅读12分钟

vue2:

  1. Object.defineProperty () 方法基础

    • 在 JavaScript 中,Object.defineProperty()方法可以用来定义对象的属性。它允许精确地添加或修改对象的属性。这个方法接收三个参数:要定义属性的对象、属性的名称和一个描述符对象。描述符对象可以包含getset方法,get用于获取属性值,set用于设置属性值。

    • 例如,下面是一个简单的示例,定义了一个具有getset方法的属性:

let data = {};
let internalValue;
Object.defineProperty(data, 'value', {
    get: function () {
        return internalValue;
    },
    set: function (newValue) {
        internalValue = newValue;
    }
});
data.value = 5;
console.log(data.value); 
  • 在这个例子中,当访问data.value时,get方法被调用并返回internalValue。当设置data.value为一个新值时,set方法被调用,新的值被赋给internalValue

  1. Vue 中的数据劫持(响应式数据)

    • Vue.js 在初始化时,会对data选项中的数据进行遍历。对于每个数据属性,使用Object.defineProperty()来进行数据劫持。这样,当属性被访问或修改时,Vue 可以拦截这些操作。

    • 例如,假设有一个 Vue 组件的data选项如下:

data: function () {
    return {
        message: 'Hello Vue'
    };
}
  • Vue 会将message属性转换为具有getset方法的形式。当模板中使用{{message}}来显示数据时,实际上是调用了message属性的get方法获取值。当组件中的方法修改message(如this.message = 'New Message')时,set方法被触发。

  • set方法中,Vue 会进行依赖收集和更新通知。它会通知所有依赖于这个属性的视图部分(如模板中的插值表达式或者使用了这个属性的计算属性)进行更新。

  1. 依赖收集和观察者模式(Watcher)

    • 依赖收集:当编译模板时,Vue.js 会解析模板中的表达式。对于每个表达式中用到的数据属性,会建立一个依赖关系。这些依赖关系会被收集起来存储在一个依赖收集器中。例如,在模板中有{{message}},Vue 会识别出这个表达式依赖于message属性,然后将这个依赖记录下来。
    • 观察者模式(Watcher) :每个数据属性都有一个对应的Watcher对象。Watcher对象会在属性被读取时将自己添加到依赖收集器中,在属性被更新时(通过set方法),依赖收集器会通知所有相关的Watcher对象。Watcher对象收到通知后,会触发相应的更新操作,比如更新 DOM 中的文本内容或者属性。
    • 可以简单地理解为,Watcher就像是一个监听器,它监听数据属性的变化,并且在数据变化时执行相应的更新操作。
  2. 双向数据绑定的实现 - v - model 指令

    • 在 Vue 中,v - model指令是实现双向数据绑定的关键。v - model在表单元素(如<input><textarea><select>)上使用时,会根据表单元素的类型进行不同的处理。
    • <input>元素为例,v - model实际上是一个语法糖。它会同时绑定input事件和value属性。当用户在输入框中输入内容时,input事件被触发。在input事件的处理函数中,会将输入框的新值赋给绑定的数据属性(通过数据劫持的set方法)。
    • 例如,对于<input v - model="message">,它等价于:
<input :value="message" @input="message = $event.target.value">
  • 这样,当数据属性message发生变化时(可能是通过组件中的方法修改),输入框的value属性会被更新(通过get方法获取新值并赋值给value)。当用户在输入框中输入内容时,message属性也会被更新,从而实现了双向数据绑定。

vue3:

  1. 响应式系统的重构 - Proxy 对象的使用

    • Proxy 基础:在 Vue 3 中,响应式数据是基于 JavaScript 的Proxy对象实现的。Proxy是一种可以拦截和自定义对象操作(如属性访问、赋值、函数调用等)的机制。与 Vue 2 中使用Object.defineProperty不同,Proxy可以代理整个对象,而不是逐个属性进行定义。

    • 例如,创建一个简单的Proxy对象:

let target = {
    message: 'Hello'
};
let handler = {
    get: function (obj, prop) {
        console.log(`Getting property ${prop}`);
        return obj[prop];
    },
    set: function (obj, prop, value) {
        console.log(`Setting property ${prop} to ${value}`);
        obj[prop]=value;
        return true;
    }
};
let proxy = new Proxy(target, handler);
console.log(proxy.message); 
proxy.message = 'Hi';
  • 在这个例子中,proxy对象是对target对象的代理。当访问proxy.message时,get方法被触发,当修改proxy.message时,set方法被触发。

  • 在 Vue 3 中的应用:Vue 3 使用Proxy来创建响应式数据。当一个组件的data选项被定义后,Vue 会使用Proxy来包装这个数据对象。这样,对数据对象的任何操作(包括新增属性、删除属性等)都可以被拦截。

  • 例如,在 Vue 3 组件中:

import { reactive } from 'vue';
export default {
    setup() {
        let data = reactive({
            message: 'Vue 3'
        });
        return {
            data
        };
    }
};
  • 这里的reactive函数会返回一个被Proxy代理的响应式数据对象。当模板中访问data.message或者修改data.message时,Proxygetset方法会相应地被触发,并且 Vue 会进行依赖收集和更新通知,就像在 Vue 2 中一样,但是Proxy提供了更强大的功能。

  1. 性能优化 - 细粒度的更新和静态提升

    • 细粒度的更新:Vue 3 的响应式系统能够更精准地追踪数据的变化。由于Proxy可以更好地处理嵌套对象和数组,在数据更新时,能够更准确地找到需要更新的部分。例如,对于一个嵌套很深的对象结构,当内层的一个小属性发生变化时,Vue 3 可以更高效地只更新与这个属性相关的视图部分,而不是像 Vue 2 那样可能会进行一些不必要的更新。
    • 静态提升:Vue 3 在编译阶段会进行静态提升。对于模板中的静态节点(即不依赖于数据变化的节点),会在编译时进行提取和优化。例如,如果一个模板中有很多纯文本节点或者静态的 HTML 标签,这些部分会被单独提取出来,在后续的更新过程中,不需要重新渲染这些静态部分,从而提高了性能。

Vue 中 define.property 和 proxy 的比较:

  1. Object.defineProperty

    • 基本原理

      • Object.defineProperty是一个 JavaScript 原生方法,用于在一个对象上定义一个新属性,或者修改一个现有的属性。它允许精确地控制属性的可枚举性、可配置性、可写性以及定义属性的gettersetter函数。

      • 例如,定义一个具有gettersetter的属性:

let data = {};
let internalValue;
Object.defineProperty(data, 'value', {
    get: function () {
        return internalValue;
    },
    set: function (newValue) {
        internalValue = newValue;
    }
});
  • 在这个例子中,当访问data.value时,会调用get函数返回internalValue;当设置data.value时,会调用set函数更新internalValue

  • 在 Vue 中的应用及局限

    • 在 Vue 2 中,Object.defineProperty用于实现数据劫持,从而达到响应式数据的目的。Vue 会遍历data对象中的属性,使用Object.defineProperty为每个属性添加getset函数,以监听数据的变化。

    • 然而,它有一些局限性。例如,它无法检测对象属性的添加和删除。如果在初始化后给对象添加新属性,这些新属性不会自动变成响应式的。另外,对于数组的变化监听比较复杂,需要重写数组的一些方法(如pushpop等)来触发更新,而且通过索引直接修改数组元素不会被自动检测到。

  1. Proxy

    • 基本原理

      • Proxy是 JavaScript 中的一个代理对象,它可以拦截和自定义对目标对象的各种操作,包括属性访问、赋值、函数调用、delete操作等。它接收两个参数:目标对象和一个处理函数对象(handler),handler中定义了各种拦截操作的方法,如getsetdeleteProperty等。

      • 例如,创建一个简单的Proxy对象:

let target = {
    message: 'Hello'
};
let handler = {
    get: function (obj, prop) {
        console.log(`Getting property ${prop}`);
        return obj[prop];
    },
    set: function (obj, prop, value) {
        console.log(`Setting property ${prop} to ${value}`);
        obj[prop]=value;
        return true;
    }
};
let proxy = new Proxy(target, handler);
console.log(proxy.message); 
proxy.message = 'Hi';
  • 这里proxy是对target对象的代理,当访问proxy.message时,get方法被触发;当修改proxy.message时,set方法被触发。

  • 在 Vue 中的应用优势

    • 在 Vue 3 中,使用Proxy来实现响应式数据。它可以代理整个对象,而不是像Object.defineProperty那样逐个属性地定义。这使得它能够更好地处理对象属性的添加和删除。例如,新添加的属性也会自动被Proxy拦截,从而实现响应式。
    • 对于数组,Proxy也能更自然地处理其变化,不需要像Object.defineProperty那样采用特殊的重写数组方法的方式。它能够直接拦截数组元素的访问和修改操作,提供了更灵活和全面的响应式处理。同时,Proxy在处理复杂的嵌套对象时,性能和灵活性上也有一定的优势。

使用define.property 有什么缺点?

  1. 无法检测对象属性的添加和删除

    • 在使用Object.defineProperty实现响应式数据时,它只能对已经存在于对象中的属性进行数据劫持。例如,如果有一个初始对象{a: 1},并且使用Object.defineProperty对属性a进行了响应式处理。
    • 当尝试添加一个新属性b(如obj.b = 2),这个新属性不会被自动设置为响应式的。Vue.js 在初始化阶段通过Object.definePropertydata选项中的属性进行遍历和劫持,后续新增的属性不会被这个过程捕获。
    • 这对于一些需要动态添加属性的场景来说是一个限制。例如,在处理从后端接口获取的数据,可能会根据接口返回的数据动态地在对象上添加新的属性,但这些新属性不会自动触发视图更新。
  2. 无法原生地对数组进行监听

    • Object.defineProperty主要是针对对象的属性进行操作。对于数组,它不能直接用于监听数组元素的变化,如数组的pushpopshiftunshiftsplice等方法的调用。
    • 在 Vue.js 中,为了解决这个问题,对于数组的响应式处理采用了一些特殊的方法。Vue.js 重写了数组的这些变异方法(会改变原数组的方法),在这些方法内部添加了通知更新的逻辑。
    • 但是这种方式比较复杂,而且如果直接通过索引修改数组元素(如arr[0]= 'new value'),这种变化不会被自动检测到,因为Object.defineProperty没有对这种操作进行拦截。这可能会导致在开发过程中出现数据和视图不一致的情况。
  3. 性能问题 - 深度监听的开销

    • 当使用Object.defineProperty对一个深度嵌套的对象进行响应式处理时,需要对每个属性进行递归遍历和定义。例如,对于一个具有多层嵌套的对象,如{a: {b: {c: 1}}}
    • 要使整个对象都是响应式的,需要对abc这些属性逐个进行Object.defineProperty的操作。这种深度遍历和定义会带来一定的性能开销,尤其是在对象结构比较复杂、嵌套层次较多的情况下。
    • 而且在某些情况下,可能并不需要对所有的子属性都进行响应式处理,这种深度监听的方式可能会导致不必要的性能损耗。

vue3 中为什么要使用 Proxy 而不是 Object.defineProperty?

  1. 对象属性添加和删除的处理能力

    • Object.defineProperty 的局限:在 Vue 2 中使用Object.defineProperty实现响应式数据时,它只能对已经存在于对象中的属性进行数据劫持。如果在初始化后给对象添加新属性(例如obj.newProperty = 'new value'),这个新属性不会被自动设置为响应式的。因为Object.defineProperty是在初始化阶段对data对象中的已有属性进行遍历和劫持。
    • Proxy 的优势:Vue 3 中使用的Proxy可以代理整个对象。这意味着它能够自动拦截对象的任何操作,包括新增属性和删除属性。例如,当使用Proxy代理一个对象后,不管是访问已有的属性,还是添加新属性(如proxy.newProperty = 'new value')或者删除属性(如delete proxy.existingProperty),Proxy都能通过其handler中的getsetdeleteProperty等方法进行拦截,并且可以方便地实现响应式处理。这使得在处理动态属性添加和删除的场景时更加灵活。
  2. 对数组的响应式处理

    • Object.defineProperty 的复杂处理:对于数组,Object.defineProperty不能直接用于监听数组元素的变化。在 Vue 2 中,为了实现数组的响应式,需要重写数组的一些变异方法(如pushpopsplice等)来添加通知更新的逻辑。而且如果直接通过索引修改数组元素(如arr[0]='new value'),这种变化不会被自动检测到,因为Object.defineProperty没有对这种操作进行拦截。
    • Proxy 的自然支持Proxy能够更自然地处理数组的变化。它可以直接拦截数组元素的访问和修改操作。例如,当使用Proxy代理一个数组时,无论是修改数组元素(如proxyArray[0]='new value')还是调用数组的方法(如proxyArray.push('new element')),Proxy都可以通过handler中的相应方法(如setapply)进行拦截并实现响应式处理,不需要像Object.defineProperty那样采用复杂的重写方法来处理数组。
  3. 性能和灵活性方面的考虑

    • 深度嵌套对象的处理:当处理深度嵌套的对象时,Object.defineProperty需要对每个属性进行递归遍历和定义,这会带来一定的性能开销。尤其是在对象结构比较复杂、嵌套层次较多的情况下,这种深度遍历和定义可能会导致性能问题。而且在某些情况下,可能并不需要对所有的子属性都进行响应式处理,这种深度监听的方式可能会导致不必要的性能损耗。
    • Proxy 的性能优势Proxy在处理复杂的嵌套对象时具有更好的性能和灵活性。它可以更高效地拦截和处理对象的操作,不需要像Object.defineProperty那样进行深度递归的属性定义。例如,对于一个大型的嵌套对象,Proxy可以根据实际的操作来决定是否以及如何进行响应式处理,而不是对所有可能的属性都进行预先的定义。
  4. JavaScript 语言的发展趋势和标准支持

    • Proxy是较新的 JavaScript 特性,它提供了更强大的元编程能力。随着 JavaScript 语言的发展,使用符合语言发展趋势的特性可以使框架更好地适应未来的变化和扩展。Vue 3 选择Proxy也是为了与现代 JavaScript 的标准和特性保持一致,以便在后续的开发和维护中能够更好地利用语言的新功能来提升框架的性能和功能。