Vue2 响应式原理解析

927 阅读4分钟

简介

Vue 中如何实现数据的响应式,很多人知道用的是 Object.defineProperty 实现,但是具体怎么实现的并不知道,当对象里面又嵌套对象是复杂对象的时候,当对象里面是数组的时候,响应式又是如何实现的呢?

Object.defineProperty MDN

首先我们要知道 Object.defineProperty 这个 api 的使用。代码如下:

var data = {
    a: 1
}
var _a = 0;
Object.defineProperty(data, 'a', {
    set: function(val) {
        console.log('给data的a属性赋值了')
        _a = val;
    },
    get: function() {
        console.log('获取data的a属性的值了')
        return _a;
    }
})
data.a = 2; // 给data的属性a赋值
data.a      // 获取data的属性a的值

执行结果

给data的a属性赋值了
获取data的a属性的值了

Object.defineProperty 可以监测被代理对象 data 的属性值的变化,当属性赋值或者获取的时候,能够在方法 set get 中做一些处理。有了这个基础,我们就应该知道 Vue 响应式原理用的就是 Object.defineProperty

复杂对象

如果 data 对象是一个复杂对象。如:

var data = {
    name: 'zhangsan',
    age: 32,
    hobby: {
        sing: true,
        swimming: false,
    }
}

我们用上面的 Object.defineProperty 里面的 a 属性换成 hobby 后,修改 sing 的值发现并没有起作用。那这种对象里面嵌套对象如何使用 Object.defineProperty 来监测 data 里面的 sing 的变化呢?

因为 Object.defineProperty 能监测的是对象里面基本数据类型的变化,如果属性值是一个对象的,是没法监测的。这个时候聪明的小伙伴肯定想到了递归来实现。

// 这里面是更新页面视图
function viewUpdate() {
    console.log('页面视图更新');
}
function reactiveBind(data, key, value) {
    // Object.defineProperty 监测传入的对象 data 的属性 key的变化,然后更新视图
    Object.defineProperty(data, key, {
        set(newVal) {
            if (value !== newVal) {
                value = newVal;
                viewUpdate();
            }
        },
        get() {
            return value;
        }
    })
}
function oberver(data) {
    // 如果传过来的data不是对象或者null就直接返回不做处理
    if (typeof data !== 'object' || data === null) {
        return data;
    }
    // 遍历对象,或者对象的属性
    for(let key in data) {
        reactiveBind(data, key, data[key]);
        // 递归绑定
        oberver(data[key]);
    }
}
var data = {
    name: 'zhangsan',
    age: 32,
    hobby: {
        sing: true,
        swimming: false,
    }
}
oberver(data);
data.hobby.sing = false;    // 控制台输出:页面视图更新
// viewUpdate 方法被执行了。监测到 sing 的变化了

有的小伙伴会对这个 reactiveBind 方法有点疑惑,这个方法 for 循环调用多次,为什么能取到各自对应的 value 值呢?

因为这个方法里面的 data, key, value 是形成了一个闭包。Object.defineProperty 监测到变化的时候,因为闭包环境。所以 data, key, value 都在各自的闭包环境中存在。

对象中包含数组

如果 data 对象里面包含了数组。如:

var data = {
    name: 'zhangsan',
    age: 32,
    hobby: {
        sing: true,
        swimming: false,
    },
    arr: [1, 2, 3, 4]
}

发现上面我们写的好好的代码又不能用了。Object.defineProperty 是没法监测数组变化的。但是 Vue 里面实现用了一个很巧的方法。

// 拿到数组的原型并定义变量保留
const oldArrayPro = Array.prototype;
// 使用 Object.create 创建一个新对象并赋值给 newArrayPro
const newArrayPro = Object.create(oldArrayPro);
// 定义了一个包含数组方法属性的数组(重写这7个常用的数组方法)
const methods = ['pop', 'push', 'shift', 'unshift', 'splice', 'sort', 'reverse'];
// 遍历数组给 newArrayPro 上添加数组对应的方法
methods.forEach(methodName => {
    newArrayPro[methodName] = function() {
        // newArrayPro 上添加的方法执行的时候会执行上面保留的数组原型上面的方法
        oldArrayPro[methodName].apply(this, Array.from(arguments));
        // 这个时候就可以更新视图了
    }
})

完整代码:

// 这里面是更新页面视图
function viewUpdate() {
    console.log('页面视图更新');
}
function reactiveBind(data, key, value) {
    // Object.defineProperty 监测传入的对象 data 的属性 key的变化,然后更新视图
    Object.defineProperty(data, key, {
        set(newVal) {
            if (value !== newVal) {
                value = newVal;
                viewUpdate();
            }
        },
        get() {
            return value;
        }
    })
}
// 拿到数组的原型并定义变量保留
const oldArrayPro = Array.prototype;
// 使用 Object.create 创建一个新对象并赋值给 newArrayPro
const newArrayPro = Object.create(oldArrayPro);
// 定义了一个包含数组方法属性的数组(重写这7个常用的数组方法)
const methods = ['pop', 'push', 'shift', 'unshift', 'splice', 'sort', 'reverse'];
// 遍历数组给 newArrayPro 上添加数组对应的方法
methods.forEach(methodName => {
    newArrayPro[methodName] = function() {
        // newArrayPro 上添加的方法执行的时候会执行上面保留的数组原型上面的方法
        oldArrayPro[methodName].apply(this, Array.from(arguments));
        // 这个时候就可以更新视图了
        viewUpdate();
    }
})
function oberver(data) {
    // 如果传过来的data不是对象或者null就直接返回不做处理
    if (typeof data !== 'object' || data === null) {
        return data;
    }
    // 如果是数组
    if (Array.isArray(data)) {
        // newArrayPro 赋值到 data 的隐士原型上 data 数组上面 pop, shift 等方法被调用的时候就会执行
        // newArrayPro 方法上添加的 pop, shifit 等方法了。
        data.__proto__ = newArrayPro;
    }
    // 遍历对象,或者对象的属性
    for(let key in data) {
        reactiveBind(data, key, data[key]);
        // 递归绑定
        oberver(data[key]);
    }
}
var data = {
    name: 'zhangsan',
    age: 32,
    hobby: {
        sing: true,
        swimming: false,
    },
    arr: [1, 2, 3, 4]
}
oberver(data);
data.arr.push(5);   // 控制台输出: 页面视图更新
data.arr.pop();     // 控制台输出: 页面视图更新

总结

Vue2 中响应式原理用的就是 Object.defineProperty ,深刻理解上面的代码,理解复杂对象,数组是如何监听的。这是面试中 Vue 的经典问题。在 Vue3 中不用 Object.defineProperty 而是用 Proxy 来监测属性变化。Proxy 可以深层监测,不需要再递归去调用 Object.defineProperty 了。因为当你对象很大。里面嵌套很多。递归调用是很耗性能的。