🤯vue3核心源码剖析(一)
🚀reactive & effect 且利用Jest测试实现数据响应式(一)
前言
🎉很高兴在此分享给大家Vue3的数据响应原理,小老弟我表达能力有限,所以
🤣如有错漏,请多指教❤
实现思路
vue3 的数据响应式实现我们想理清楚它的特征,才好往下写。
以下是基本的reactive
+effect
用法
let count = reactive({ num: 11 })
let result = 0
effect(() => {
result = count.num + 1
})
// result得到的是12 看起来effect立即执行了呢~
expect(result).toBe(12)
// 相当于count.num = count.num + 1 这里有count.num的get操作和set操作
count.num++
// result得到的是13 看起来他又执行了一遍effect的回调函数了呢~🤯
expect(result).toBe(13)
两大点
- 依赖收集
- 触发依赖
下面讲一下大体思路:
reactive
为源数据创建proxy
对象,其中proxy
的getter
、setter
分别用于数据的依赖收集,数据的依赖触发effect
立即执行一次回调函数,当回调函数内的依赖数据发生变化的时候会再次触发该回调函数- 收集依赖我们可以定义一个
track
函数,当reactive的数据发生get操作时,track
用一个唯一标识
(下面会讲这个唯一标识
是什么)记录依赖到一个容器
里面 - 触发依赖我们可以定义一个
trigger
函数,当reactive的数据发生set操作时,trigger
将关于这个数据的所有依赖从容器
里面拿出来逐个执行一遍
简单实现(详细代码在最后)
reactive
给源数据创建一个proxy对象,就好像给源数据套上了一层盔甲,敌人只能攻击这层盔甲,无法之间攻击源数据,在这基础之上我们就可以有机会去做数据的拦截。
reactive通过传入一个对象作为参数,返回一个该对象的proxy对象,其中Reflect.get(target, key)
返回target的key对应的属性值res,Reflect.set(target, key, value)
设置target的key对应的属性值为value
export function reactive(target: Record<string, any>) {
return new Proxy(target, {
get(target, key) {
let res = Reflect.get(target, key)
return res
},
set(target, key, value) {
let success: boolean
success = Reflect.set(target, key, value)
return success
}
})
}
这时候我们可以编写一个🛠测试用例,跑一跑测试有没有问题
describe('reactive', () => {
it.skip('reactive test', () => {
let original = { num: 1 }
let count = reactive(original)
expect(original).not.toBe(count) ✅
expect(count.num).toEqual(1) ✅
})
})
🤮什么?你不是说getter和setter要分别做两件事情吗?😒
- getter进行依赖收集 👀
- setter进行触发依赖 🔌
别急!还不是时候!
effect
根据官方给出的介绍:effect
会立即触发回调函数,同时响应式追踪其依赖
effect的基本用法:
let result = 0
// 假设count.num == 1
effect(() => {
result = count.num + 1
})
// 那么输出的result就是2
console.log(result) // output: 2
其中count
是已经通过了reactive
处理的proxy实例对象
根据上述的用法我们可以简单的写出一个effect
函数
class ReactiveEffect {
private _fn: Function
constructor(fn: Function) {
this._fn = fn
}
run() {
this._fn()
}
}
export function effect(fn: Function) {
let _reactiveFunc = new ReactiveEffect(fn)
_reactiveFunc.run()
}
再写一个测试用例验证一下
describe('effect test', () => {
it('effect', () => {
// 创建proxy代理
let count = reactive({ num: 11 })
let result = 0
// 立即执行effect并跟踪依赖
effect(() => {
result = count.num + 1
})
expect(result).toBe(12) ✅
count.num++
expect(result).toBe(13) ❌
})
})
欸!我们发现了测试最后一项没有通过,哦原来我们还没实现依赖收集和触发依赖啊。。。
track
做依赖收集
我想想,我们应该怎么进行依赖收集?对,上面我们提到过有一个唯一标识
和一个容器
。我们该去哪找这个依赖啊?欸是容器
,那些依赖是我们需要被触发的呢?欸看唯一标识
唯一标识是什么?
假设数据target是一个对象{num: 11}
,对象的属性名可以绑定很多依赖,这个属性名num
+target
就可以找到与num
相关的所有依赖集合,所以这里的num
相关的所有依赖集合的唯一标识就是num
+target
容器是什么?
存储不同数据下的所有属性对应的所有依赖集合,我们可以用Map存储不同数据,命名为targetMap
,每个数据作为targetMap
的键名,再定义一个以属性名key
为键名的depsMap
作为targetMap
的键值,depsMap
的键值是一个Set集合,命名作deps
,最终deps
就是存放特定的key
的依赖集合
类型定义:
我相信你们看到targetMap
的类型定义的时候应该会理解我上面说的存储结构是怎么样的吧
type EffectKey = string
type IDep = ReactiveEffect
const targetMap = new Map<Record<EffectKey, any>, Map<EffectKey, Set<IDep>>>()
let activeEffect: ReactiveEffect
下面我们来写一下track
函数的实现,要注意的是我们需要处理一下第一次没有存储Map的情况
export function track(target: Record<EffectKey, any>, key: EffectKey) {
// 寻找dep依赖的执行顺序
// target -> key -> dep
let depsMap = targetMap.get(target)
// 解决初始化没有depsMap的情况
if (!depsMap) {
depsMap = new Map()
targetMap.set(target, depsMap)
}
// deps是一个Set对象,存放着这个key相对应的所有依赖
let deps = depsMap.get(key)
// 如果没有key相对应的Set 初始化Set
if (!deps) {
deps = new Set()
depsMap.set(key, deps)
}
// 将activeEffect实例对象add给deps
deps.add(activeEffect)
}
解释一下
deps.add(activeEffect)
这里的activeEffect就是我们的依赖,怎么获取到的呢?
其实当effect
执行的时候,内部 new 了一个ReactiveEffect
类,而ReactiveEffect
类里面可以通过this
获取到activeEffect
,因为activeEffect
本来就是ReactiveEffect
类的实例
我们改写一下ReactiveEffect
代码
class ReactiveEffect {
private _fn: Function
constructor(fn: Function) {
this._fn = fn
}
run() {
// 为什么要在这里把this赋值给activeEffect呢?因为这里是fn执行之前,就是track依赖收集执行之前,又是effect开始执行之后,
// this能捕捉到这个依赖,将这个依赖赋值给activeEffect是刚刚好的时机
activeEffect = this
this._fn()
}
}
trigger
做触发依赖
这个trigger的实现逻辑很简单:找出target的key对应的所有依赖,并依次执行
- 用target作为键名拿到在targetMap里面键值depsMap
- 用key作为键名拿到depsMap的键值deps
然后遍历deps这个Set实例对象,deps里面存的都是
ReactiveEffect`实例对象dep,我们依次执行dep.run()就相当于执行了effect的回调函数了。
export function trigger(target: Record<EffectKey, any>, key: EffectKey) {
const depsMap = targetMap.get(target)
const deps = depsMap?.get(key)
// 注意deps可能为undefined的情况
if (deps) {
for (let dep of deps) {
dep.run()
}
}
}
- 添加
track
和trigger
函数到proxy
的getter和setter上
export function reactive(target: Record<string, any>) {
return new Proxy(target, {
get(target, key) {
let res = Reflect.get(target, key)
// 依赖收集
track(target, key as string)
return res
},
set(target, key, value) {
let success: boolean
success = Reflect.set(target, key, value)
// 触发依赖
trigger(target, key as string)
return success
}
})
}
最后再用jest运行一下响应式数据的测试用例
describe('effect test', () => {
it('effect', () => {
// 创建proxy代理
let count = reactive({ num: 11 })
let result = 0
// 立即执行effect并跟踪依赖
effect(() => {
// count.num触发get 存储依赖
result = count.num + 1
})
expect(result).toBe(12) ✅
// 这里会先触发proxy的get操作再触发proxy的set操作,触发依赖trigger 更新result
count.num++
expect(result).toBe(13) ✅
})
})
总结
上述的测试用例count触发了一次setter操作,两次getter操作。
-
第一次getter操作是在effect的回调函数执行的时候发生,effect立即执行,在执行之前我们拿到了
activeEffect
,之后在proxy的getter中执行了track函数,以num
为key的depsMap被第一次初始化,并初始化了targetMap
,把activeEffect
添加到deps
这个Set对象中,这就完成了依赖收集。 -
当代码执行到
count.num++
的时候,我们先执行的是proxy的getter操作,执行 1 的流程,之后执行的是proxy的setter操作Reflect.set(target, key, value)
这段代码把
{num: 11}
加一变成了{num: 12}
,并且在targetMap中寻找{num: 12}
为键名的键值,之后进一步获取到了depsMap和deps。通过循环把deps里面的所有activeEffect执行run()
方法,这就完成了触发依赖。
最后@感谢阅读!
完整代码
// in effect.ts
class ReactiveEffect {
private _fn: Function
constructor(fn: Function) {
this._fn = fn
}
run() {
// 为什么要在这里把this赋值给activeEffect呢?因为这里是fn执行之前,就是track依赖收集执行之前,又是effect开始执行之后,
// this能捕捉到这个依赖,将这个依赖赋值给activeEffect是刚刚好的时机
activeEffect = this
this._fn()
}
}
const targetMap = new Map<Record<EffectKey, any>, Map<EffectKey, Set<IDep>>>()
// 当前正在执行的effect
let activeEffect: ReactiveEffect
type EffectKey = string
type IDep = ReactiveEffect
// 这个track的实现逻辑很简单:添加依赖
export function track(target: Record<EffectKey, any>, key: EffectKey) {
// 寻找dep依赖的执行顺序
// target -> key -> dep
let depsMap = targetMap.get(target)
/**
* 这里有个疑问:target为{ num: 11 } 的时候我们能获取到depsMap,之后我们count.num++,为什么target为{ num: 12 } 的时候我们还能获取得到相同的depsMap呢?
* 这里我的理解是 targetMap的key存的只是target的引用 存的字符串就不一样了
*/
// 解决初始化没有depsMap的情况
if (!depsMap) {
depsMap = new Map()
targetMap.set(target, depsMap)
}
// deps是一个Set对象,存放着这个key相对应的所有依赖
let deps = depsMap.get(key)
// 如果没有key相对应的Set 初始化Set
if (!deps) {
deps = new Set()
depsMap.set(key, deps)
}
// 将activeEffect实例对象add给deps
deps.add(activeEffect)
}
// 这个trigger的实现逻辑很简单:找出target的key对应的所有依赖,并依次执行
export function trigger(target: Record<EffectKey, any>, key: EffectKey) {
const depsMap = targetMap.get(target)
const deps = depsMap?.get(key)
if (deps) {
for (let dep of deps) {
dep.run()
}
}
}
// 根据官方给出的介绍:effect会立即触发这个函数,同时响应式追踪其依赖
export function effect(fn: Function, option = {}) {
let _reactiveFunc = new ReactiveEffect(fn)
_reactiveFunc.run()
}
// in reactive.ts
import { track, trigger } from './effect'
export function reactive(target: Record<string, any>) {
return new Proxy(target, {
get(target, key) {
let res = Reflect.get(target, key)
// 依赖收集
track(target, key as string)
return res
},
set(target, key, value) {
let success: boolean
success = Reflect.set(target, key, value)
// 触发依赖
trigger(target, key as string)
return success
}
})
}
// in effect.spec.ts
import { effect } from '../index'
import { reactive } from '../index'
describe('effect test', () => {
it('effect', () => {
// 创建proxy代理
let count = reactive({ num: 11 })
let result = 0
// 立即执行effect并跟踪依赖
effect(() => {
// count.num触发get 存储依赖
result = count.num + 1
})
expect(result).toBe(12)
// 这里会先触发proxy的get操作再触发proxy的set操作,触发依赖trigger 更新result
count.num++
expect(result).toBe(13)
})
})
转载自:https://juejin.cn/post/7089244580394041375