首先得强调一点
原理 !== 源码
源码可能很复杂,但是原理却容易理解的多
响应式原理说白无非就是在更新变量的时候顺便更新视图
Vue2
使用的API是defineProperty
Vue3
使用的API是Proxy
Vue2响应式原理之defineProperty
要点提及
- 对于对象而言,只需要循环遍历所有的
property
即可 - 对于数组而言,同样可以使用
defineProperty
来实现响应式,但是性能成本太大,每次数组的修改都需要重新监听数组 - 因此数组采用创建具有响应式功能的原型
代码实现
// 基于数组原型创建新的原型newArrProto,目的是不污染全局Array
let oldArrayProto = Array.prototype;
// Object.create()方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__
let newArrayProto = Object.create(oldArrayProto);
// 使用 Array["push"] = function() {} 为原型添加方法
["push"].forEach((methodName) => {
newArrayProto[methodName] = function () {
oldArrayProto[methodName].call(this, ...arguments);
console.log("视图更新");
};
});
function defineReactive(target, key, value) {
// 再次调用 observer(),来检查当前属性是否嵌套对象
observer(value);
Object.defineProperty(target, key, {
get() {
return value;
},
set(newVal) {
// 监听 user.name = { num: 1 }; 方式创建的新对象
observer(newVal);
value = newVal;
console.log("视图更新");
},
});
}
function observer(target) {
// 判断是否为数组或者对象,不是则直接返回
if (typeof target !== "object" || target === null) {
return target;
}
// 使用 Array.isArray() 判断是否为数组
if (Array.isArray(target)) {
// 将隐式原型改变为具体更新视图能力的 newArrayProto
target.__proto__ = newArrayProto;
return;
}
// 将对象的属性都设置为响应式
for (let key in target) {
defineReactive(target, key, target[key]);
}
}
user = {
name: "zhangsan",
age: 18,
skills: {
java: true,
javascript: true,
},
nums: [1, 2, 3, 4],
};
observer(user);
// user.name = "lisi";
// user.skills.java = "false";
// user.name = { num: 1 };
// user.name.num = 2;
user.nums.push(1);
console.log(user);
/* 输出
{
name: [Getter/Setter],
age: [Getter/Setter],
skills: [Getter/Setter],
nums: [Getter/Setter]
}
*/
// 使用了 ES5 Getter/Setter 特性就会这样。因为 getter 是你自定义的函数,其中有可能会带有副作用,而 console.log 通常不应当干涉业务逻辑,所以浏览器默认不会直接调用 getter,需要你手动点一下明确让它触发 getter 才行
console.log(Object.assign({}, user.nums));
缺点
- 创建或者删除对象的属性,无法被监听到
- 使用下标的方法修改数组的值和修改数组的长度,无法被监听到
如上问题可以使用Vue.set
更新值来解决
Vue3响应式原理之Proxy
待更新