Vue2 & Vue3 响应式实现原理
前言
分别展示 Vue2
和 Vue3
的数据响应式原理的实现过程,相比Vue3 ,Vue2
响应式实现有什么缺点。
完整代码地址
一. Vue2响应式实现原理
首先用Object.defineProperty()
这个方法劫持对象里的每个属性,并在设置值的时候进行视图更新,其次考虑到对象嵌套
,就要进行递归
,最后要对数组方法进行重写
,实现数组响应式实现,接下来就是具体代码实现。
1. 准备工作
//判断是否是对象
function isObject(obj) {
return typeof obj === 'object' && obj != null
}
//检测到数据 改变视图更新
function updateView() {
console.log('视图更新了')
}
//监测数据是否改变
function observer(target) {
//如果传入的不是对象,直接return
if (!isObject(target)) return target
//遍历对象
for (let key in target) {
defineReactive(target, key, target[key])
}
}
let data = {
name: 'mantaman',
age: 20,
}
observer(data)
data.age = 22
第一个函数是判断是否是对象,第二个函数是模拟检测到数据改变后视图更新,第三个函数就是监测数据是否改变,先是判读是否是对象,如果是对象就遍历这个对象的 key
,将这个对象和遍历到的key
,value
都传给一个函数,这个函数就是实现响应式原理的关键,接下来重点就是这个函数。
2. defineReactive()
//定义响应式数据
function defineReactive(target, key, value) {
//数据劫持
Object.defineProperty(target, key, {
//取值时就会触发get()函数
get() {
console.log('取的值是:' + value)
return value
},
//设置值时就会触发get()函数
set(newVal) {
if (newVal !== value) {
console.log('将原来的值:' + value + ' 设置为:' + newVal)
value = newVal
//值发生改变,调用视图改变的函数
updateView()
}
},
})
}
defineProperty()
这个方法可以劫持对象里的属性,在修改和取值的时候都会触发对应的方法
MDN详情
首先要做的就是用defineProperty()
劫持对象中的每个key
,新值与旧值不相等,就将value
赋予新的值,并进行视图更新,取值时触发get()
函数,将value return
出去
运行这份代码,设置data.age=22
视图就会更新
3. 对象嵌套
现在问题就是如果data
对象是嵌套的,长下面这样
let data = {
name: 'mantaman',
age: {
num: 20,
},
}
那么data.age.num=22
并不会引起视图的更新,因为如果value
还是一个对象,那么这个对象并没有被监测到,为了解决这个问题,就需要用到递归,代码如下图
//定义响应式数据
function defineReactive(target, key, value) {
//如果对象的value还是一个对象,就再调用监测函数监测这个对象,实现递归
if (isObject(value)) {
observer(value)
}
for (let key in target) {
defineReactive(target, key, target[key]) // 每个属性都能劫持到
}
//数据劫持
Object.defineProperty(target, key, {
//取值时就会触发get()函数
get() {
console.log('取的值是:' + value)
return value
},
//设置值时就会触发get()函数
set(newVal) {
if (newVal !== value) {
console.log('将原来的值:' + value + ' 设置为:' + newVal)
value = newVal
//值发生改变,调用视图改变的函数
updateView()
}
},
})
}
就是在 defineProperty(
)开始判断value
是否是对象,如果还是对象,就再调用一次observer()
函数,实现递归,这样对象里的对象的值被修改也能监测到,并进行视图更新。
4. 数组响应式实现
首先,原本Array.xxx()
这类方法并不会触发数据监测,但是vue又需要用到这些方法,那么就需要对这些方法进行重写。重写后的方法需要有两个作用:一
,具有方法原来的功能;二
,能使操作的数组被监测到。下面就是具体的代码实现。
// 数组的响应式
let oldArrayPrototype = Array.prototype // 首先创建一个数组
let proto = Object.create(oldArrayPrototype) // 继承
//遍历数组的方法,并循环
Array.from(['push', 'pop', 'shift', 'unshift']).forEach((method) => {
// 重写这些方法
proto[method] = function () {
// this 指向调用这个方法的数组
oldArrayPrototype[method].call(this, ...arguments) //为了重写后的方法也具有原来的功能
updateView() //让重写后的方法能实现响应式
}
})
Object.create(oldArrayPrototype)
可以创建出一个继承另一个对象的对象MDN详解
先创建一个对象,再创建一个对象继承前一个对象,这样做的原因就是因为Object.create()
创建出的对象,对其原型上的方法重写不会影响 Array.xxx()
。接下来列举了几个数组的方法,循环重写。让其具有:一
,方法原来的功能;二
,能使操作的数组被监测到的功能。这样后,proto
这个对象上的方法就是被重写了的。
// 判断target是否是数组
if (Array.isArray(target)) {
// 重写数组的原型
// target.__proto__ = proto // Array.prototype
Object.setPrototypeOf(target, proto)
}
Object.setPrototypeOf(target, proto)
可以将一个对象的原型设置给另一个对象MDN详情
接下来就只要在observe()
函数内判断传来的对象是不是数组就行,如果是数组,就将proto
的原型给 目标(target)
数组就行了,这样目标数组就具有了重写的方法,也就能实现响应式了。
5. 缺点
(1)对象上原本不存在属性无法被劫持
如果往对象上添加属性无法做到数据响应,这时vue2
就提供了额外的方法实现这一逻辑,也就是用this.$set( this.data,'job','teacher' )
替代this.data.job='teacher'
,才能实现响应式
(2)默认递归会有性能问题 这样的代码会遍历出现在数据源里的所有的对象,即使没有使用到的数据也会被监测,如果这个对象嵌套很深,那么递归就会浪费性能。
(3)改变数组的length
属性无效
data.job.length=3
也是对数组进行了操作,但并不会被检测到。
二. Vue3响应式实现
Vue3
响应式实现是通过ES6
中的proxy
代理对象,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。这样在改写对象时就能触发响应式。同样通过递归解决对象嵌套问题。
1. 准备工作
function isObject(obj) {
return typeof obj === 'object' && obj !== null
}
function updateView() {
console.log('试图更行')
}
//将对象变成响应式
function reactive(target) {
if (!isObject(target)) return target
//代理对象target,handler函数对外界的访问进行过滤和改写
let proxy = new proxy(target, handler)
toProxy.set(target, proxy)
return proxy
}
let data = {
name: 'mantaman',
age: 20,
}
let state = reactive(data)
先是两个功能性函数,再是将对象代理成响应式的函数,这个函数的内容就是要通过handler
对象对目标对象改写。接下来重点就是handler
对象。
2. 代理中的handler对象
let handler = {
get(target, key, receiver) {
console.log('获取')
// 读取对象的值
return target[key]
},
set(target, key, newValue, receiver) {
let hasKey = target.hasOwnProperty(key)
let oldValue = target[key]
//判断对象是否有这个属性
if (!hasKey) {
console.log('新增')
} else if (oldValue !== newValue) {
//判断值是否相等
console.log('修改')
}
//值不相等就返回新的值
if (oldValue !== newValue) {
updateView()
return newValue
}
},
handler
是个对象,这个对象内有两个函数,分别是取值的get()
函数,和修改值的set()
函数。get()
函数只要返回值就行了,set()
函数首先需要判断这个属性是否存在,不存在需要新增;属性存在,但是值不等,也需赋予新的值.
3. 对象嵌套
如果对象有嵌套,还是需要进行递归
get(target, key, receiver) {
console.log('获取')
// 读取对象的值
return isObject(target[key]) ? reactive(target[key]) : target[key]
},
只要在get()
函数返回值的时候判断返回的是否是对象,如果不是对象,直接返回,如果是对象,则需要再调用reactive()
,进行递归。
4. 重复代理
如果已经代理过的对象再次代理,这显然是没有意义的,所以应该避免这一情况
let toProxy = new WeakMap() // 原对象:代理过的对象
// target是否已经被代理过了,如果已经被代理过了,就不要再次代理了
let isProxy = toProxy.get(target)
if (isProxy) {
return isProxy
}
let proxy = new Proxy(target, handler)
toProxy.set(target, proxy)
// toRaw.add(proxy, target)
return proxy
重复代理需要借助WeakMap对象,将代理前后的对象存入WeakMap对象,便于重复代理时验证是否已经代理过了
先在全局创建一个WeakMap
对象,再在reactive()
函数开始判断目标兑对象是否已经被代理过了,最后再在reactive()
函数返回proxy
前将target,proxy
以键值对的形式存入WeakMap
数组toProxy
,以防代理过得对象再次被代理,用于下次传入时判断目标对象是否已经被代理过的。
转载自:https://juejin.cn/post/7253148953600262203