我的开源库:
- fly-barrage 前端弹幕库,项目官网:https://fly-barrage.netlify.app/,可实现类似于 B 站的弹幕效果,并提供了完整的 DEMO,Gitee 推荐项目;
- fly-gesture-unlock 手势解锁库,项目官网:https://fly-gesture-unlock.netlify.app/,在线体验:https://fly-gesture-unlock-online.netlify.app/,可高度自定义锚点的数量、样式以及尺寸;
1,响应式数据和副作用函数
首先说说响应式系统中最重要的两个概念,响应式数据和副作用函数。
响应式数据指的是在 Vue 中被劫持了读写操作的数据,并且如果在副作用函数中使用了某个响应式数据,如果响应式数据发生了变化的话,我们希望这个副作用函数能够自动重新执行。
副作用函数是指会产生副作用的函数,如果一个函数的执行影响了其他代码的执行,我们就可以说这个函数是副作用函数,一个函数产生副作用是很容易的,例如一个函数修改了一个全局的变量。
我们最终想要实现的效果是:副作用函数的执行读取了响应式数据,当响应式数据发生变化,副作用函数能够重新执行,简要代码如下所示:
// 响应式数据
let reactiveData = {
name: '小明',
age: 18
}
// 副作用函数,该副作用函数读取了 reactiveData 响应式数据中的值,并将一个文本设置到了 body 标签中
function effect() {
document.body.innerText = `大家好,我的名字是${reactiveData.name},我的年龄是${reactiveData.age}岁`
}
effect() // 手动执行
// 一秒钟后变更响应式数据,我们希望 effect 函数能够自动重新执行
setTimeout(() => {
reactiveData.age = 30
}, 1000)
2,响应式系统的核心思想
如果我们想实现上面代码中的效果,需要做好三件事,分别是:
- 对响应式数据的读写操作进行劫持。
- 当 effect 函数的执行读取了响应式数据的时候,进行依赖的收集。
- 当变更响应式数据的时候,触发依赖的执行。
2-1,如何对数据的读写操作进行劫持
Vue2 中对数据的读写操作是通过 Object.defineProperty() 实现的,这种实现方式有很多的缺陷,例如无法检测对象中新增属性和删除属性的操作。相关的 Vue2 源码解析点击这里。
在 Vue3 中,响应式数据的劫持操作是通过 Proxy 实现的,Proxy 相比 Object.defineProperty(),能够更加全面的监控对数据所进行的读写操作。接下来,我们实现一个最简的 reactive 函数,他能实现对初始数据的代理,代码如下所示:
function reactive(rawData) {
return new Proxy(rawData, {
get(target, key){
// 进行依赖的收集
console.log(`读取操作 - ${key}`)
return target[key]
},
set(target, key, newVal){
// 触发依赖副作用函数的执行
console.log(`设值操作 - ${key} - ${newVal}`)
target[key] = newVal
}
})
}
let reactiveData = reactive({
name: '小明',
age: 18
})
reactiveData.name // 读取操作 - name
reactiveData.name = '小红' // 设值操作 - name - 小红
2-2,当 effect 函数的执行读取了响应式数据的时候,进行依赖收集
Vue3 的依赖收集核心思路和 Vue2 是一样的,Vue2 的相关文章点击这里。
在真正讲解依赖收集前,首先讲解 2 个前置知识。
2-2-1,什么是依赖
依赖就是封装了 effect 函数的实例,这个实例能够被收集存储,并向外提供了能够重新执行 effect 函数的方法。最简实现如下所示:
class ReactiveEffect {
constructor(public fn) {
}
run(){
this.fn()
}
}
function effect(fn) {
// _effect 就是依赖
const _effect = new ReactiveEffect(fn)
_effect.run()
}
effect(() => {
// 副作用函数
console.log("哈哈")
})
2-2-2,依赖被收集到了哪里
依赖和响应式数据的每个属性之间的关系是多对多的关系,一个依赖中副作用函数的执行有可能读取了多个响应式属性,一个响应式属性也有可能被多个副作用函数读取。
在这里,我们先讨论一个响应式属性对应多个依赖的情况,此时的对应关系是:object + key ==> [依赖1、依赖2、依赖3......],可以发现,依赖对应的是某个对象中的某个属性,因此,我们需要设计一个数据结构来进行存储,数据结构如下所示:
type Dep = Set<ReactiveEffect>
type KeyToDepMap = Map<any, Dep>
const targetMap = new WeakMap<any, KeyToDepMap>()
依赖被收集到了 targetMap 中,它是一个 WeakMap 实例,targetMap 的 key 用来存储响应式对象,targetMap 的 value 是一个 Map 实例。
KeyToDepMap 的 key 用来存储响应式对象的属性,value 是一个 Set 实例。
Set 实例用来存储 ReactiveEffect 实例,也就是用来存储收集依赖。
2-2-3,讲解依赖收集
当我们执行下面的代码时。
effect(() => {
// 副作用函数
console.log("哈哈")
})
effect 函数的内部会创建一个 ReactiveEffect 类的实例,然后执行 _effect 实例的 run 方法,在 run 方法中会执行副作用函数。
依赖收集的思路是:在执行副作用函数之前,将当前的 ReactiveEffect 类的实例设置到全局中,然后执行副作用函数,在副作用函数中,我们会进行响应式数据的读取操作,这会触发响应式对象中指定属性的 get,在 get 中,我们可以获取到当前激活的 ReactiveEffect 实例(上面已经设置到了全局作用域中),将激活的 ReactiveEffect 实例存储到 targetMap 中,就完成了依赖收集的操作,简要代码如下所示:
let activeEffect: ReactiveEffect | undefined
class ReactiveEffect {
constructor(public fn) {
}
run(){
activeEffect = this
this.fn()
activeEffect = undefined
}
}
function reactive(rawData) {
return new Proxy(rawData, {
get(target, key){
// 进行依赖的收集
console.log(`读取操作 - ${key}`)
track(target, 'get', key)
return target[key]
},
set(target, key, newVal){
// 触发依赖副作用函数的执行
console.log(`设值操作 - ${key} - ${newVal}`)
target[key] = newVal
}
})
}
export function track(target: object, type: TrackOpTypes, key: unknown) {
let depsMap = targetMap.get(target)
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, (dep = new Set<ReactiveEffect>()))
}
dep.add(activeEffect)
}
2-3,当变更响应式数据的时候,触发依赖的执行。
触发依赖重新执行相比依赖收集就简单多了,在响应式数据的代理 set 中,我们能够获取到当前变更响应式数据的 target 和 key,通过 target 和 key,我们能从 targetMap 中获取到指定响应式属性对应的 dep(Set 实例),这个 dep 就保存着使用了当前这个响应式属性的 ReactiveEffect 实例,接下来遍历 dep,循环执行 ReactiveEffect 实例的 run 方法就可以了,简要代码如下所示:
function reactive(rawData) {
return new Proxy(rawData, {
get(target, key){
// 进行依赖的收集
console.log(`读取操作 - ${key}`)
track(target, 'get', key)
return target[key]
},
set(target, key, newVal){
// 触发依赖副作用函数的执行
console.log(`设值操作 - ${key} - ${newVal}`)
trigger(target, 'set', key)
target[key] = newVal
}
})
}
export function trigger(
target: object,
type: TriggerOpTypes,
key?: unknown
) {
const depsMap = targetMap.get(target)
const dep = depsMap.get(key)
triggerEffects(dep)
}
export function triggerEffects(
dep: Dep | ReactiveEffect[]
) {
// spread into array for stabilization
const effects = isArray(dep) ? dep : [...dep]
for (const effect of effects) {
triggerEffect(effect)
}
}
function triggerEffect(
effect: ReactiveEffect
) {
effect.run()
}
3,结语
Vue3 和 Vue2 的响应式系统核心思想并没有变,只不过 Vue3 的源码更加的简练、解耦和易于理解,对 Vue2 响应式感兴趣的朋友可以看我的这篇博客。