MST

星途 面试题库

面试题:自定义Vue响应式系统的实现及性能调优

假设没有Vue提供的现成响应式系统,要求你基于ES6的Proxy或Object.defineProperty() 实现一个简单的响应式系统,能够追踪数据变化并更新视图。请描述实现思路,并说明在实现过程中如何进行性能调优,以应对大规模数据的场景。
26.7万 热度难度
前端开发Vue

知识考点

AI 面试

面试题答案

一键面试

实现思路

  1. 基于 Object.defineProperty()
    • 通过 Object.defineProperty() 方法,对对象的每个属性进行劫持,定义 gettersetter
    • getter 中收集依赖,也就是哪些视图(函数)依赖了这个属性。可以维护一个全局的依赖数组或使用一个 WeakMap 来存储属性与依赖函数的关系。
    • setter 中,当属性值发生变化时,遍历之前收集的依赖,调用这些依赖函数(视图更新函数),从而更新视图。

示例代码如下:

function defineReactive(data, key, val) {
    Object.defineProperty(data, key, {
        enumerable: true,
        configurable: true,
        get: function reactiveGetter() {
            // 收集依赖
            console.log('getter');
            return val;
        },
        set: function reactiveSetter(newVal) {
            if (newVal === val) return;
            val = newVal;
            // 通知依赖更新
            console.log('setter, 通知更新');
        }
    });
}

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

let data = { name: 'John' };
observe(data);
data.name = 'Jane';
  1. 基于 Proxy
    • 创建一个 Proxy 实例,对目标对象进行代理。
    • Proxyget 陷阱中收集依赖,同样可以使用全局依赖数组或 WeakMap 存储依赖关系。
    • Proxyset 陷阱中,当属性值变化时,遍历依赖并调用依赖函数更新视图。

示例代码如下:

const dep = new Set();

const reactive = target => {
    return new Proxy(target, {
        get(target, property) {
            // 收集依赖
            dep.add(() => console.log(`${property} 被访问`));
            return target[property];
        },
        set(target, property, value) {
            target[property] = value;
            // 通知依赖更新
            dep.forEach(fn => fn());
            return true;
        }
    });
};

let data = reactive({ name: 'John' });
data.name;
data.name = 'Jane';

性能调优(应对大规模数据场景)

  1. 批量更新
    • 不要在每次数据变化时都立即更新视图,而是将视图更新函数收集起来,在一个事件循环周期结束时(例如使用 Promise.thenrequestAnimationFrame),批量执行这些更新函数,减少更新频率,提高性能。
  2. 减少依赖收集的开销
    • 对于 Object.defineProperty(),由于是对每个属性分别劫持,当数据规模大时,初始化性能开销大。可以采用延迟劫持的策略,即只有当属性被访问时才进行劫持定义 gettersetter
    • 对于 Proxy,虽然整体性能较好,但在依赖收集时,尽量使用高效的数据结构,如 WeakMap,因为它不会阻止其键对象被垃圾回收,能有效减少内存占用。
  3. 使用虚拟 DOM
    • 在视图更新时,通过虚拟 DOM 来计算最小化的实际 DOM 操作。对比新老虚拟 DOM,只更新发生变化的部分,而不是整个视图,大大减少 DOM 操作带来的性能开销。
  4. 防抖和节流
    • 对于频繁触发的数据变化,如用户输入等场景,使用防抖(debounce)或节流(throttle)技术。防抖确保在一定时间内多次触发只执行一次更新,节流则是按固定时间间隔执行更新,避免过于频繁的更新操作。