什么是响应式?
通俗来讲,响应式原理就是指当数据一旦变化,就会立刻触发视图的更新。它是实现数据驱动视图的第一步。
vue2是如何实现响应式的
(一)Object.defineProperty(数据劫持)
这个方法就是在一个对象上定义一个新的属性,或者改变一个对象现有的属性,并且返回这个对象。里面有两个字段 set(用来取属性的值), get(获取属性的值)。defineProperty 函数并不是直接对所需要被操作的对象进行值的获取与修改操作,而是重新 copy 出一个完全一样的对象,再对 copy 出来的对象进行操作从而达到操作原对象的目的(也就是说,原对象中的值自始至终都没有变,我们修改及打印出来的都是 copy 对象)。举个例子:
var obj = { // 定义出来的 obj 对象
b: 20
};
var obj2 = { // copy 出来和 obj 一模一样的对象
b: 20
};
Object.defineProperty(obj, "b", { // 第一个参数为要定义属性的对象,第二个参数为要定义或修改的属性,第三个参数是一个提供了 get 方法和 set 方法的对象
get: function () {
console.log('正在获取b')
return obj2.b; // 返回出 copy 对象的值
},
set: function (newValue) { //接收一个参数 newvalue
console.log('正在设置b')
obj2.b = newValue; // 对 copy 出来的对象进行修改
}
});
obj.b = 30; // 想要修改 obj 对象中 b 的数据值,set 函数被调用
console.log(obj.b) // get 函数被调用
最终的打印结果为:
正在设置b
正在获取b
30
由此我们可以得知,obj对象中 b 的值,并不是直接被打印出来的。我们可以理解为对象 obj 被Object.defineProperty()函数劫持了,当编译到代码 obj.b = 30 时,就会调用 set() 函数,对 b 的值进行修改(其实本质上是对 copy 对象的属性进行修改),当获取 b 的值时,调用 get() 函数。
(二)vue2 的响应式原理
我们直接通过一段代码来进行解释:
function type(data){
return Object.prototype.toString.call(data).slice(8,-1)
}
let oldArrayPrototype = Array.prototype // 先把数组原型上的方法保留下来做备份
let proto = Object.create(oldArrayPrototype) // 创建实例对象继承
// 改写数组上的方法
Array.from(['push', 'shift', 'unshift', 'pop']).forEach(method => {
proto[method] = function () {
oldArrayPrototype[method].call(this, ...arguments)
updateView() // 手动视图更新
}
})
// 观察者函数
function observer(target){ // 专门用于劫持数据的
if(type(typeof target !== 'object' || typeof target == null)){ // 判断劫持的对象类型,若不是对象,则不劫持,直接返回
return target
}
if (Array.isArray(target)) { // 判断劫持的对象类型,若是数组,则改变其 prototype 对象
// target.__proto__ = proto
Object.setPrototypeOf(target, proto) // 将target对象的prototype对象设置为proto,让调用的数组相关函数都是我们重写的函数
}
for(let key in target){ // 一定是对象
defineReactive(target,key,target[key])
}
}
// 响应式
function defineReactive(target,key,value){
observer(value)
Object.defineProperty(target,key,{ // 只能劫持对象
get(){
return value
},
set(newValue){
if(newValue !== value){
value = newValue
updateView()
}
}
})
}
function updateView(){
console.log('更新视图');
}
// Object.defineProperty () 可以重新定义属性 给属性安插 getter setter 方法
let data = {
name:'小李',
grades:{
math:90,
chinese:88
},
hobbies:['dance','sing']
}
observer(data)
data.name = '小李子' // 改变对象中为基本数据类型的属性时,视图更新
data.grades.math = 100 // 改变被劫持对象中的对象属性的值,进入递归,视图更新
data.grades={ // 改变对象的属性值,视图更新
math:70,
chinese:88
}
data.age = 18 // 视图不更新,因为 age 是新增属性,在 observer 函数中,遍历不到 age 这个key值,所以不会进入 defineReactive 函数,从而也就无法触发视图更新
data.hobbies.push('draw') // 视图更新,hobbies是个数组,被调用的 push 方法是被重写的方法
Vue2的缺点:
1.vue2 对象中不存在的属性不能被拦截,当给对象添加不存在的属性时不是响应式的。
2.数组改变 length 属性是无效的,不会触发视图更新。
3.vue2 中递归是自动执行的,浪费性能。
Vue3是如何实现响应式的
(一)Proxy
该对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。语法:
const p = new Proxy(target, handler)
// target 是要使用 Proxy 包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。
// handler 是一个通常以函数作为属性的对象,它包含有 Proxy 的各个捕获器(trap),用来定制拦截行为。
(二)vue3 的响应式原理
let toProxy = new WeakMap() // 弱引用映射表 存放 {原对象:代理过的对象}
let toRaw = new WeakMap() // {代理过的对象:原对象}
// 判断对象为对象引用类型,只要不是原始类型就返回true
function isObject(val) {
return typeof val === 'object' && val !== null
}
function hasOwn(target,key){
return target.hasOwnProperty(key)
}
// 响应式的核心方法
function reative(target) {
return createReactiveObject(target)
}
// 创建响应式对象
function createReactiveObject(target) {
if (!isObject(target)) { // 是原始类型,直接返回
return target
}
let proxy = toProxy.get(target) // 检查弱引用对象中是否存在值为 target 的 key
if(proxy){ // 已经代理过了 防止多次代理
return proxy
}
if(toRaw.has(target)){ // 防止重复代理
return target
}
let baseHandler = { // 定制拦截行为
get(target, key, receiver) { // receiver 是代理的对象,也就是下文的 observed
console.log('获取');
// Reflect 对象与 Proxy 对象一样,也是 ES6 为了操作对象而提供的新 API
let result = Reflect.get(target, key, receiver) // Reflect做反射,用 result 来读取值,尽可能不操作 target 优点:Reflect 如果出错,会返回 false,不会影响后续代码的执行
return isObject ? reative(result) : result // 递归,只有用到了数据源中深层次的对象时才递归,判断 result 是否是个对象,从而实现深层次的代理
},
set(target, key, value, receiver) {
// 判断是新增属性还是修改属性
let hadKey = hasOwn(target,key)
let oldValue = target[key]
let res = Reflect.set(target, key, value, receiver)
if(!hadKey){ // 新增属性
console.log('新增属性');
}
else if( oldValue !== value ){ // 修改属性
console.log('修改属性');
}
return res
},
deleteProperty(target, key) {
console.log('删除');
let res = Reflect.deleteProperty(target, key)
return res
}
}
let observed = new Proxy(target, baseHandler) // observed 是代理的对象
toProxy.set(target,observed) // 往 toProxy 中存值
toRaw.set(observed,target)
return observed
}
// 数组
let arr = [1,2,3]
let proxy1 = reative(arr)
proxy1.push(4) // 新增属性
proxy1.length = 5 // 修改属性,数组的 length 属性被修改
let proxy2 = reative({ name: '小李', grades: { math: 98 } }) // 多层代理 利用 get 读取时才代理
proxy.name = '小吴' // 改变对象中为基本数据类型的属性时,修改属性,
delete proxy.name // 删除基本数据类型的属性时,删除属性
proxy.age.n = 19 // 改变对象属性的属性值时,修改属性
// 对同一个对象进行重复代理————解决:被代理的对象需要记录一下,防止再次代理
// reative(proxy)
// reative(proxy)
// reative(proxy)
// 对已经被代理过的对象进行多层代理
// let proxy2 = reative(proxy)
// let proxy3 = reative(proxy2)
Vue2 和 Vue3 的响应式区别
1.在 Vue2 中对数组的操作需要通过重写方法来实现,而 Vue3 基于 Proxy 和 Reflect ,可以原生监听数组,可以监听对象属性的添加和删除。
2.vue2 中递归是自动执行的,vue3 只有用到了数据源中深层次的对象时才递归,性能相对较好。