likes
comments
collection
share

Vue2 & Vue3 响应式实现原理

作者站长头像
站长
· 阅读数 15

前言

分别展示 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 ,将这个对象和遍历到的keyvalue都传给一个函数,这个函数就是实现响应式原理的关键,接下来重点就是这个函数。

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视图就会更新

Vue2 & Vue3 响应式实现原理

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,以防代理过得对象再次被代理,用于下次传入时判断目标对象是否已经被代理过的。