前言の前言:写得极其不通顺,只是快速做个零碎总结,后续会不断润色
vue2与vue3在实现上的区别
vue2实现数据劫持使用的是Object.defineProperty, Vue3使用的是Proxy
共同需要解决的问题
- 响应式的对象
- 响应式的数组
看看vue响应式的表现
- 定义在data中的数据才是响应式的
- 【对象】使用vm.a =1视图不会更新,需要用Vue.set 或 vm.$set
- 给对象批量添加属性Object.assign(this.obj, { a:1, b:2})不行;得用this.obj=Object.assign({}, this.obj, {a:1, b:2}),即加上原对象一起混入
- 【数组】arr[1] = 2不行,得用Vue.set或者vm.$set
- arr.length=2改长度不行,需要用splice
- 动态在vm.data中加入属性是无效的,必须在初始化前声明
- Vue更新dom是异步的;只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更
在了解这些vue2的表现后,我们往原理层面看看,到底为什么会是这样
使用Object.defineProperty
表面上,这个Object的API Object.defineProperty 和直接使用字面量给对象赋值的效果貌似是一样的,那么实际上他们到底有什么区别呢
(1)定义方式
// 使用Object.defineProperty给对象定义属性
let obj = {};
// 参数: 对象、属性名/Symbol、描述符(里面的叫做键、值)
Object.defineProperty( obj, "name", {
configurable: false, // 1.是否能删除 2.描述符是否能修改
enumerable: false, // 是否可枚举
value: undefined, // 值
writable: false, // 能否被赋值运算符=赋值
get: undefined, // 访问该属性时调用,传入this,返回结果用作属性值
set: undefined // 修改属性值时调用,传入修改值和this
} )
// 使用字面量的方式
obj.name = undefined;
很显然,Object.defineProperty可以使用描述来控制该属性的配置、枚举、修改,拦截get和set的过程,而这正是我们实现数据劫持所需要的特性。
此外,如果想要批量添加属性的话,可以使用Object.defineProperties,示例
const obj = {};
Object.defineProperties(obj, {
name: {
value: "123",
writable: true
},
name2: {
value: "456"
}
});
基本的响应式实现
// 数据
const data = {
name: "123",
name2: "456"
};
// 将data变成响应式
observer(data);
function observer(target) {
// 非数组、对象 直接返回
if(typeof target !== 'object' || typeof target !== null) {
return target;
}
// 数组、对象实现数据劫持
for(let key in target) {
defineReactive(target, key, target[key]);
}
}
// 使用defineProperty实现简单的数据劫持
function defineReactive(target, key, value) {
Object.defineProperty(target, key, {
value,
get: function() {
return value;
},
set: function(newVal) {
value = newVal;
// todo: 更新视图
}
})
}
这里补一张图:
vue实际上是通过发布订阅者模式进行数据驱动视图更新的。
在我理解,发布订阅者模式可以高度概括为一句话:控制并发布数据的Subject会在数据发生变动时通知所有注册&订阅了的Observer进行更新。
细化.复杂对象
const data = {
name: "123",
name2: "456",
name3: {
firstName: "c",
lastName: "xk"
}
};
// 将data变成响应式的数据
observer(data);
// observer的实现:递归侦听
function observer(target) {
if(typeof target !== 'object' || typeof target !== null) {
return target;
}
// s数组、对象使用defineProperty实现简单的数据劫持
for(let key in target) {
observer(newVal);
Object.defineProperty(target, key, {
value,
get: function() {
return value;
},
set: function(newVal) {
observer(newVal);
value = newVal;
// todo: 更新视图
}
})
}
}
当数据是多层级对象时,之前的实现就无法侦听到深层次属性的变化了,所以需要使用递归进行优化;另外,如果set的时候,给定的也是个对象 如 obj.name = { name }, 那么也无法监听,所以这里也需要递归优化下,递归时间复杂度很高,所以在vue2在遇到复杂对象时性能不会很好,vue3使用proxy解决了这个问题。
细化.数组
const {
arguments
} = require("file-loader");
const {
method
} = require("lodash");
const data = {
name: "123",
name2: "456",
name3: {
firstName: "c",
lastName: "xk"
},
names: ['1', '2', '3'];
};
// 利用Array原型创建新原型
const oldArrayProto = Array.prototype;
const newArrayProto = Object.create(oldArrayProto);
['push', 'pop', 'shift', 'unshift', 'splice'].forEach(methodName => {
newArrayProto[methodName] = function () {
// todo: 更新视图
oldArrayProto[methodName].call(this, ...arguments);
}
});
// 将data变成响应式的数据
observer(data);
// observer的实现:递归侦听
function observer(target) {
if (typeof target !== 'object' || typeof target !== null) {
return target;
}
// 将_proto_指向新原型
if (Array.isArray(target)) {
target._proto_ = newArrayProto;
}
// s数组、对象使用defineProperty实现简单的数据劫持
for (let key in target) {
observer(newVal);
Object.defineProperty(target, key, {
value,
get: function () {
return value;
},
set: function (newVal) {
observer(newVal);
value = newVal;
// todo: 更新视图
}
})
}
}
Object.create方法的作用是Creates an object that has the specified prototype or that has null prototype,即创建有原型指向旧对象的新对象。
后续将继续补充完善