简介
在 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 了。因为当你对象很大。里面嵌套很多。递归调用是很耗性能的。