一道代码题看懂 Vue 核心原理

213 阅读3分钟

Vue 是一种基于 MVVM 模式的前端框架,其中的数据双向绑定是其核心特性之一。双向绑定可以拆分成两部分:

  1. 视图变动反映在数据层。这部分很简单,可以通过事件监听器实现,本文不讲。
  2. 数据层的变化自动同步到视图层。这部分就是本文要讨论的内容,在 Vue3 是通过 Proxy 实现的。

本文通过一道面试题介绍如何通过 Proxy 监控对象属性的变化,并执行期望的动作。

一、题目要求

编写一个函数,输入是一个对象,其属性可以是基本类型,也可以是数组、嵌套对象,甚至是函数。例如:

const obj1 = { 
    name: '🐔', 
    age: 30,
    like: ['唱', '跳', 'rap', '篮球'],
    greet: function() { console.log('大家好!'); },
    funs: {
        name: '小黑子',
        age: 18,
        parent: {
            name: 'parentName',
            age: 40
        }
    }
};

输出一个新对象,每当该对象的任何属性发生变化时,都会在控制台打印出来。

二、实现思路

要实现这一功能,需要实现两个函数:

  • 第一个是深拷贝函数,用来创建一个新对象。
  • 第二个代理函数,利用 JavaScript 的 Proxy 对象拦截并自定义基本操作(例如属性查找、赋值、枚举、函数调用等),从而实现对对象的全面监控。

三、代码实现

以下是实现上述功能的完整代码(可以在控制台运行):

function createReactiveObject(obj) {
    // 定义一个处理器 handler
    const handler = {
        get(target, property, receiver) {
            const result = Reflect.get(target, property, receiver);
            // 如果读取的属性值是对象,则返回该对象的代理
            if (typeof result === 'object' && result !== null) {
                return new Proxy(result, handler);
            }
            return result;
        },
        set(target, property, value, receiver) {
            const oldValue = target[property];
            const result = Reflect.set(target, property, value, receiver);
            // 仅处理 push 操作作为 demo,如需兼容更多数组操作需要重新设计
            if (Array.isArray(target) ) {
                //push 操作会触发两次 set 操作,一次是改 length,一次是改数据。
                if (property === 'length') {
                     console.log(`Array changed from ${JSON.stringify(target.slice(0, oldValue))} to ${JSON.stringify(target)}`);
                } else if (oldValue !== value) { 
                    //do nothing
                }
            } else if (oldValue !== value) {
                console.log(`Property "${property}" changed from ${oldValue && oldValue.toString()} to ${value && value.toString()}`);
            }
            return result;
        }
    };

    // 深度克隆对象,确保返回的新对象与原对象无直接引用关系
    function deepClone(obj) {
        if (obj === null || typeof obj !== 'object') {
            return obj;
        }
        if (obj instanceof Array) {
            return obj.map(item => deepClone(item));
        }
        if (obj instanceof Function) {
            return obj;
        }
        const newObj = {};
        for (const key in obj) {
            if (obj.hasOwnProperty(key)) {
                newObj[key] = deepClone(obj[key]);
            }
        }
        return newObj;
    }

    const clonedObj = deepClone(obj);
    // 返回代理后的新对象
    return new Proxy(clonedObj, handler);
}

// 测试代码
const obj1 = { 
    name: 'kunkun', 
    age: 18,
    like: ['chang', 'tiao', 'rap', 'lanqiu'],
    greet: function() { console.log('Hello!'); },
    funs: {
        name: 'funsName',
        age: 10,
        parent: {
            name: 'parentName',
            age: 40
        }
    }
};

const reactiveObj = createReactiveObject(obj1);

// 修改代理对象的属性,观察控制台输出变化信息
reactiveObj.name = 'kun';  // 控制台输出:Property "name" changed from "kunkun" to "kun"
reactiveObj.age = 20;      // 控制台输出:Property "age" changed from 18 to 20
reactiveObj.like.push('yinyue');  // 控制台输出:Array changed from ["chang","tiao","rap","lanqiu"] to ["chang","tiao","rap","lanqiu","yinyue"]
reactiveObj.funs.name = 'newFunsName';  // 控制台输出:Property "name" changed from "funsName" to "newFunsName"
reactiveObj.funs.parent.age = 50;  // 控制台输出:Property "age" changed from 40 to 50
reactiveObj.greet = function() { console.log('Hi!'); };  // 控制台输出:Property "greet" changed from function () { console.log('Hello!'); } to function () { console.log('Hi!'); }

四、结论

通过 Proxy 对象,可以方便地实现对 JavaScript 对象属性变化的监听。在 Vue3 中,我们只需要把上述代码里的 console.log 改为 Vue 的响应式更新逻辑,就可以实现数据层的变化自动同步到视图层了。