一、前言:
写这篇文章是为了总结我在学习Vue3中的一些心得和需要注意的细节,以及为什么要这样做,这样做的原理是什么。本文会以尽量简洁的语言来概述Vue3响应式原理,所以一些边界情况会省略。
Vue版本:V3.2.47
Vue3与Vue2响应式的区别 :
- Vue2为了兼容大部分的浏览器,使用Object.defineProperty()来实现响应式,Vue3使用proxy
- Vue2无法直接监听新增属性,删除属性以及直接通过下标修改数组等操作
相关知识:
- 代理(proxy)与反射 (Reflect)
- Map与WeakMap的区别
- 强引用与弱引用
- 垃圾回收机制
- Set数据结构
- TypeScript
二、目标:
实现如下代码的响应式:
<!-- HTML -->
<body>
<div id="app">
</div>
<div id="star">
</div>
</body>
//script
const user = {
name:'林夕',
age:18
}
const star = {
name:'张三',
sex:'男',
age:42
}
document.querySelector('#app').innerText = `${user.name} - ${user.age}`
document.querySelector('#star').innerText = `${star.name} - ${star.sex} - ${star.age}`
setTimeout(() => {
user.name = '林夕很帅'
setTimeout(() => {
star.age = 20
}, 1000)
}, 2000)
三、分析:
Vue3中使用的是proxy对数据进行拦截处理的,使用reactive来包装引用类型数据实现响应式,watchEffect函数可以监听对象数据的改变,我们来模仿一下Vue3的响应式实现方式。
我们要使用如下方式实现:
文件目录:
js文件为ts文件编译后生成。注意这里的tsconfig.json中的module要改为'ES2015',同时关闭全局严格模式
index.html:
<body>
<div id="app">
</div>
<div id="star">
</div>
</body>
<script type="module">
import { reactive } from './reactive.js'
import { watchEffect } from './effect.js'
const user = reactive({
name: "林夕",
age: 18
})
const star = reactive({
name: '张三',
sex: '男',
age: 42
})
watchEffect(() => {
document.querySelector('#app').innerText = `${user.name} - ${user.age}`
})
watchEffect(() => {
document.querySelector('#star').innerText = `${star.name} - ${star.sex} - ${star.age}`
})
setTimeout(() => {
user.name = '林夕很帅'
setTimeout(() => {
star.age = 20
}, 1000)
}, 2000)
</script>
效果:
1.他有如下几个特点:
- 当响应式对象的属性改变时,副作用函数可以监听到,并执行内部的函数
- 当响应式对象的属性改变时,视图也发生了改变
2.这其中需要用到的数据有:
- 响应式对象的属性(key)
- 响应式对象(target)
- 副作用函数(effect)
3.他们之间的关系:
-
一个属性(key)可以对应多个副作用函数(effect)
-
一个相同的属性名(例如:name)可能对应多个不同的响应式对象(user、star)
这里的属性key可以理解为副作用函数的依赖(dependency),而这里的属性值被用来执行这个作用,因此这个副作用函数也可以说是一个它依赖的订阅者(subscriber)
4.那么我们可以使用下图所示的数据结构
WeakMap<target, Map<key, Set<effect>>>
菱形表示键,矩形表示值
- 紫色表示响应式对象target的映射,数据类型为WeakMap
- 蓝色表示响应式对象的属性key的映射,数据类型为Map
- 绿色表示副作用函数effect集合(副作用函数桶),数据类型为Set
- 黄色表示副作用函数effect
四、代码实现
1.watchEffect实现
let activeEffect
export const watchEffect= (update: Function) => {
const effect = function () {
activeEffect = effect
update()
activeEffect = null
}
effect()
}
2.reactive实现
export const reactive = <T extends object>(target: T) => {
return new Proxy(target, {
get(target, key, receiver) {
const res = Reflect.get(target, key, receiver)
track(target, key)
return res
},
set(target, key, value, receiver) {
const res = Reflect.set(target, key, value, receiver)
// 这里的trigger()一定要放到反射之后,因为反射完成后,原始对象才会改变,track则没有这种问题
trigger(target, key)
return res
},
})
}
这里的track()函数用来收集副作用函数,trigger()函数用来触发副作用函数
3.track实现
let targetMap = new WeakMap()
export const track = (target, key) => {
// 响应式对象target的映射
let depsMap = targetMap.get(target)
if (!depsMap) {
depsMap = new Map()
targetMap.set(target, depsMap)
}
// 响应式对象target的key的映射
let deps = depsMap.get(key)
if (!deps) {
deps = new Set()
depsMap.set(key, deps)
}
// 若当前运行的副作用函数不为空,则把副作用函数添加到集合内
if (activeEffect) {
deps.add(activeEffect)
}
}
4.trigger实现
export const trigger = (target, key) => {
const depsMap = targetMap.get(target)
const deps = depsMap.get(key)
deps.forEach((effect) => effect())
}
到这里,我们已经完成了一个简单的响应式。但是如果响应式对象的属性若为引用类型,目前是做不到的,这时我们可以考虑使用递归,当获取的值类型为引用类型时,我们可以再次调用reactive函数
5.递归
reactive.ts:
// null也是对象
const isObject = (target) => target != null && typeof target == 'object'
export const reactive = <T extends object>(target: T) => {
return new Proxy(target, {
get(target, key, receiver) {
const res = Reflect.get(target, key, receiver) as object //这里使用类型断言
track(target, key)
if (isObject(res)) {
return reactive(res)
}
return res
},
set(target, key, value, receiver) {
const res = Reflect.set(target, key, value, receiver)
// 这里的trigger()一定要放到反射之后,因为反射完成后,原始对象才会改变,track则没有这种问题
trigger(target, key)
return res
},
})
}
index.html
<body>
<div id="app">
</div>
<div id="star">
</div>
</body>
<script type="module">
import { reactive } from './reactive.js'
import { watchEffect } from './effect.js'
const user = reactive({
name: "林夕",
age: 18
})
const star = reactive({
name: '张三',
sex: '男',
info: {
age: 42,
}
})
watchEffect(() => {
document.querySelector('#app').innerText = `${user.name} - ${user.age}`
})
watchEffect(() => {
document.querySelector('#star').innerText = `${star.name} - ${star.sex} - ${star.info.age}`
})
setTimeout(() => {
user.name = '林夕很帅'
setTimeout(() => {
star.info.age = 20
}, 1000)
}, 2000)
</script>
至此,这个响应式已经可以处理大部分场景了
五、思考
1.targetMap为什么使用WeakMap数据结构?
答:
1.WeakMap的键只能是引用类型
2.WeakMap是弱引用,无需手动删除,即可被垃圾回收机制自动回收
2. depsMap的值为什么使用Set数据结构而不使用数组?
答:Set数据结构中的值具有唯一性,天然支持去重