🤯vue3核心源码剖析(二)
🚀reactive & effect 且利用Jest测试实现数据响应式(二)
山不在高,有仙则名。水不在深,有龙则灵。 —— 刘禹锡《陋室铭》
前言
还没看过上一篇的看这里🎉
上一篇讲了reactive和effect数据响应的基本原理,这篇打算讲effect的其他用法,effect的第二个形参option的实现,内容包括 runner
,scheduler
,stop
(删除依赖)和onStop
(依赖删除后的回调)。
🤣如有错漏,请多指教❤
🛠实现runner手动触发依赖
runner其实就是ReactiveEffect实例对象的run()函数的函数体,effect会返回一个runner函数,手动调用runner函数也可以实现触发依赖,不必等待reactive对象的set操作执行。如果effect的回调函数有返回值,那么runner函数被执行之后的返回值也应该是该effect回调函数的返回值。
先看以下测试用例
it('should return runner when effect was called', () => {
let foo = 1
let runner = effect(() => {
foo++
return 'foo'
})
expect(foo).toBe(2)
let returnValue = runner() // 手动触发依赖
expect(foo).toBe(3)
expect(returnValue).toBe('foo') // 返回值是foo
})
不过这里runner()执行的时候就是执行的effect.run()方法,run方法里面的this会丢失对effect的指向,所以这里应该对run改变this指向,让他重新绑定为effect对象。
// 根据官方给出的介绍:effect会立即触发这个函数,同时响应式追踪其依赖
export function effect<T = any>(fn: () => T): EffectRunner {
let _effect = new ReactiveEffect(fn)
_effect.run()
// 注意这里的this指向,return 出去的run方法,方法体里需要用到this,且this必须指向ReactiveEffect的实例对象
// 不用bind重新绑定this,this会指向undefined
let runner = _effect.run.bind(_effect) as EffectRunner
return runner
}
还没完,还有一件事~~(成龙历险记老爹口吻),ReactiveEffect的run()方法必须返回effect回调函数的返回值,这样的话runner()执行的时候就能得到effect回调函数的返回值了。
class ReactiveEffect {
constructor(public fn: Function, public scheduler?: EffectScheduler) {}
run() {
activeEffect = this
let returnValue = this.fn()
return returnValue
}
}
🛠实现scheduler
scheduler
是effect第二个形参option
的可选属性,它是一个函数
该功能描述:
effect
首次执行的时候不执行scheduler
,直接执行effect的回调函数- 之后每次触发
trigger
函数的时候都会执行scheduler
函数,不执行effect回调函数 - 当调用
run()
的时候才会触发runner
,也就是说调用effect的回调函数
在这里我直接拿vue源码的一个测试用例了
it('scheduler', () => {
let dummy
let run: any
const scheduler = jest.fn(() => {
run = runner
})
const obj = reactive({ foo: 1 })
const runner = effect(
() => {
dummy = obj.foo
},
{ scheduler }
)
expect(scheduler).not.toHaveBeenCalled()
expect(dummy).toBe(1)
// should be called on first trigger set操作的时候,也就是说在trigger被调用的时候
obj.foo++
expect(scheduler).toHaveBeenCalledTimes(1)
// should not run yet
expect(dummy).toBe(1)
// manually run 会触发effect的回调函数
run()
// should have run
expect(dummy).toBe(2)
})
实现起来也相对简单,effect
函数新增第二个参数option对象并且里面有一个类型为function的scheduler
属性,作为可选值的存在。
结合条件1:首次执行effect不会调用scheduler和条件2:每次触发trigger
函数的时候都会执行scheduler
函数可以推敲出scheduler()
的调用应该在trigger
里面,并且用if判断scheduler是否有值,要么执行effect
的run()
方法要么执行effect的scheduler()
方法。
export function trigger(target: Record<EffectKey, any>, key: EffectKey) {
const depsMap = targetMap.get(target)
const deps = depsMap?.get(key)
if (deps) {
// deps是Set集合 里面的子元素是ReactiveEffect实例对象
for (let dep of deps) {
// if判断scheduler是否有值
if (dep.scheduler) {
dep.scheduler()
} else {
dep.run()
}
}
}
}
effect()
的改写也很简单,只需要作为可选配置传入一个option
对象,通过extend
把option
里面的属性挂载到ReactiveEffect
的实例对象即可,务必在ReactiveEffect上添加公有成员scheduler
,这里说一下extend
,其实它就是Object.assign
,作用是把对象挂载到另一个对象上
export function effect<T = any>(fn: () => T, option?: EffectOption): EffectRunner {
let _effect = new ReactiveEffect(fn)
if(option){
extend(_effect, option) // 等价于Object.assign(_effect, option)
}
_effect.run()
let runner = _effect.run.bind(_effect) as EffectRunner
return runner
}
class ReactiveEffect {
public deps: Set<ReactiveEffect>[] = []
constructor(public fn: Function, public scheduler?: EffectScheduler) {}
run() {
activeEffect = this
let returnValue = this.fn()
return returnValue
}
}
🛠实现stop & onStop
我们有监听依赖的功能当然也少不了停止监听依赖的功能。
停止监听依赖本质上就是把effect
从deps
(Set集合)里面delete掉,那么当trigger
被再次触发的时候就不会执行该effect
的run()
方法了。
如果我们想在停止监听依赖之后做点什么事情,那我们可以用onStop
回调函数,它和scheduler
一样也是在effect函数的第二个可选形参option
定义,触发时机也很简单,就是当依赖被删除后执行onStop()
就好了
以下为effect的option类型定义
export type EffectScheduler = (...args: any[]) => any
export interface EffectOption {
scheduler?: EffectScheduler
onStop?: () => void
}
🛠实现stop停止监听依赖
功能描述:
🧠通过stop可以停止监听依赖,怎么样停止监听依赖呢?
可以通过删除deps依赖,那么trigger被调用的时候就不会被循环调用这个依赖了
我们继续通过测试驱动我们的实现
it('stop', () => {
let dummy
const obj = reactive({ prop: 1 })
const runner = effect(() => {
dummy = obj.prop
})
obj.prop = 2
expect(dummy).toBe(2)
stop(runner)
// 执行proxy的set操作,触发trigger()
obj.prop = 3
expect(dummy).toBe(2)
// stopped effect should still be manually callable
runner()
expect(dummy).toBe(3)
})
定义stop()
函数,并传入一个runner
函数(effect.run()的函数体)
🧠这里有个问题,那就是我们要怎么拿到runner对应的effect
呢?
runner并没有任何线索指向哪个effect
,我们拿到effect再拿到effect所在的deps
(Set<ReactiveEffect>
)才好 delete 掉啊。这里有个反向的思维且用到了js中函数的特性:
函数也是对象,函数对象上可以直接定义属性
让我们改写一下effect()
函数:
// 里面存有一个匿名函数
export interface EffectRunner<T = any> {
(): T
effect: ReactiveEffect
}
export function effect<T = any>(fn: () => T, option?: EffectOption): EffectRunner {
let _effect = new ReactiveEffect(fn)
if(option){
extend(_effect, option)
}
_effect.run()
let runner = _effect.run.bind(_effect) as EffectRunner
// 这里的effect挂载在了函数runner上,作为属性,这是利用了js中函数可以挂在属性的特性
// 之后呢,实现stop的时候runner就能拿到ReactiveEffect实例对象了
runner.effect = _effect
return runner
}
🧠好了现在我们已经拿到了 runner 的 effect 了,上面我们提到一个反向的思维怎么解释呢?
即deps集合(Set<ReactiveEffect>
)存储着相关key的effect,不妨反过来,ReactiveEffect
类里面定义一个公有成员变量数组,用于存储deps集合,每个effect对象都有该数组存放deps集合的之后,就可以用循环的方式找出effect并从deps集合中删除就可以了。
因为停止监听依赖是属于依赖行为范畴的,所以我们要在ReactiveEffect
类定义一个stop()
方法,这样才符合面向对象的思想嘛。
class ReactiveEffect {
// 此处deps就是数组,数组每个元素存储的Set集合
public deps: Set<ReactiveEffect>[] = []
constructor(public fn: Function, public scheduler?: EffectScheduler) {}
...
stop() {
// deps是Set集合
for (let i = 0; i < this.deps.length; i++) {
this.deps[i].delete(this)
}
this.deps.length = 0
}
}
🧠好了新的问题出来了,那么什么时候effect应该push进effect.deps呢?
依赖被收集的时候就应该是最好的时机了,也就是说track被触发的时候应该activeEffect.deps.push(deps)
export function track(target: Record<EffectKey, any>, key: EffectKey) {
let depsMap = targetMap.get(target)
if (!depsMap) {
depsMap = new Map()
targetMap.set(target, depsMap)
}
let deps = depsMap.get(key)
if (!deps) {
deps = new Set()
depsMap.set(key, deps)
}
deps.add(activeEffect)
// activeEffect的deps 接收 Set<ReactiveEffect>类型的deps
// 供删除依赖的时候使用(停止监听依赖)
activeEffect.deps.push(deps)
}
因为这里runner已经挂载了一个effect
对象,所以我们可以直接调用effect.stop()
方法用于停止监听依赖。
export function stop(runner: EffectRunner) {
runner.effect.stop()
}
🛠实现onStop停止监听依赖后执行的回调函数
我们回顾一下:该onStop需要从effect()的option参数作为可选属性传递进来,因为我们实现了一个工具函数extend(),它可以将option挂载在ReactiveEffect类的实例对象effect上。
所以只需要在循环deps数组并在每个Set集合中找出要删除的effect然后delete
这个操作之后,执行onStop即可,同样要在ReactiveEffect类中定义一个个公有成员变量onStop并置之为可选属性。
class ReactiveEffect {
public deps: Set<ReactiveEffect>[] = []
public onStop?: () => void // 可选属性
constructor(public fn: Function, public scheduler?: EffectScheduler) {}
...
stop() {
for (let i = 0; i < this.deps.length; i++) {
this.deps[i].delete(this)
}
this.deps.length = 0
// 当onStop有值的时候,执行onStop()
this.onStop?.()
}
}
📈代码优化
优化点:
- 如果用户调用多次
stop()
并且传入的都是相同的runner来停止监听依赖,那么代码将会执行不必要的循环操作(stop里面有循环来找出哪个应该被delete掉),降低代码性能。应该添加一个active
作为成员变量到ReactiveEffect类,用于标识依赖是否已经被delete掉了,杀死掉了。
- 同时stop循环的代码段也应该被提取出来作为单独的函数,我们命名为
cleanupEffect
好了。
class ReactiveEffect {
public deps: Set<ReactiveEffect>[] = []
public active = true // 该effect是否存活
public onStop?: () => void
constructor(public fn: Function, public scheduler?: EffectScheduler) {}
...
stop() {
// 追加active 标识是为了性能优化,避免每次循环重复调用stop同一个依赖的时候
if (this.active) {
cleanupEffect(this)
this.onStop?.()
this.active = false
}
}
}
// 清除指定依赖
function cleanupEffect(effect: ReactiveEffect) {
// 对effect解构,解出deps,减少对象在词法环境寻找属性的次数
const {deps} = effect
if (deps.length !== 0) {
for (let i = 0; i < deps.length; i++) {
deps[i].delete(effect)
}
deps.length = 0
}
}
- vue对stop功能的官方测试用例并不严谨,只检查了set操作,并没有遇到get操作的情况
it('stop', () => {
let dummy
const obj = reactive({ prop: 1 })
const runner = effect(() => {
dummy = obj.prop
})
obj.prop = 2
expect(dummy).toBe(2)
stop(runner)
// 单单只是检查set操作是不行的,还必须检查代码通过get操作之后,是否还能执行依赖
// obj.prop = 3
// 很明显如果换成obj.prop++,expect(dummy).toBe(2)就飘红了
obj.prop++
expect(dummy).toBe(2) ❌
runner()
expect(dummy).toBe(3)
})
🧠我们思考一下为什么把obj.prop = 3
换成obj.prop++
测试用例就不通过呢?
obj.prop++
---拆解-->obj.prop = obj.prop + 1
很明显这里多了一个get操作。先get后set。
🤯解释:经过get操作之后,也就是说执行track函数之后原来被删除的effect又被add到deps上面去了,所以我们这里必须添加shouldTrack
全局变量来表示应不应该被track
详细见effect.ts的 track 函数,控制shouldTrack
开关在ReactiveEffect的run()
方法中。shouldTrack
的初始值为 false ,代表不可被track。当依赖正式可回收的时候(没有被删除),置shouldTrack
为 true ,表示可被track。
让我们改写一下ReactiveEffect
类
// 初始化为false
let shouldTrack = false
class ReactiveEffect {
public deps: Set<ReactiveEffect>[] = []
public active = true // 该effect是否存活
public onStop?: () => void
constructor(public fn: Function, public scheduler?: EffectScheduler) {}
run() {
// 如果effect已经被杀死了,被删除了(stop()函数相关)
if (!this.active) {
return this.fn()
}
activeEffect = this
shouldTrack = true // 把开关打开让他可以收集依赖
let returnValue = this.fn()
// 之后把shouldTrack关闭,这样就没办法在track函数里面收集依赖了
shouldTrack = false
return returnValue
}
...
}
当执行this.fn()
的时候,fn里面会执行get操作,之后就会执行track收集依赖,因为shouldTrack是true,所以依赖收集能顺利完成。
开头判断this.active
是否为空,如果为空,那么effect将会被执行而不会收集依赖,进入track就马上被return掉了,这也是为了如果用户单单调用runner()
的时候能够执行effect回调函数(属于手动触发依赖)留的后路。
export function track(target: Record<EffectKey, any>, key: EffectKey) {
// 这里为什么要多一层非空判断呢?
// 我们查看reactive.spec.ts里面的测试用例
// 测试用例里根本就没有调用effect(),所以没有执行ReactiveEffect的run()自然activeEffect也就是undefined了
if (!activeEffect) return
// 应不应该收集依赖,从而避免删了依赖又重新添加新的依赖
if (!shouldTrack) return
let depsMap = targetMap.get(target)
if (!depsMap) {
depsMap = new Map()
targetMap.set(target, depsMap)
}
let deps = depsMap.get(key)
if (!deps) {
deps = new Set()
depsMap.set(key, deps)
}
deps.add(activeEffect)
activeEffect.deps.push(deps)
}
-
避免无效执行
deps.add(activeEffect)
和track中跳出函数体的代码段抽离- 用Set实例的方法
has
来判断是否已经收集过依赖,如果没有收集过依赖,那么就收集依赖,如果已经收集过依赖,那么就不再收集依赖。 - 定义一个函数
isTracking
表示当前是否在track的状态
- 用Set实例的方法
function isTracking(){
return activeEffect !== undefined && shouldTrack
}
export function track(target: Record<EffectKey, any>, key: EffectKey) {
// 拦截不必要的track
if (!isTracking()) return
let depsMap = targetMap.get(target)
if (!depsMap) {
depsMap = new Map()
targetMap.set(target, depsMap)
}
let deps = depsMap.get(key)
if (!deps) {
deps = new Set()
depsMap.set(key, deps)
}
// 避免不必要的add操作
if (deps.has(activeEffect)) return
deps.add(activeEffect)
activeEffect.deps.push(deps)
}
总结
经过上面代码的一步步解析,我们已经实现了runner、scheduler、stop以及onStop功能,我们还处理了stop的一些边缘cases,优化了代码,让我们的代码更加优雅,更接近于vue源码的写法。相信大家已经了解了vue的reactive的数据响应式原理了。
最后@感谢阅读!
未来的路还很长,一起成长,一起进步!
完整代码
// In effect.ts
import {extend} from '../shared/index'
export type EffectScheduler = (...args: any[]) => any
class ReactiveEffect {
public deps: Set<ReactiveEffect>[] = []
public active = true // 该effect是否存活
public onStop?: () => void
constructor(public fn: Function, public scheduler?: EffectScheduler) {}
run() {
// 如果effect已经被杀死了,被删除了(stop()函数相关)
if (!this.active) {
return this.fn()
}
// 为什么要在这里把this赋值给activeEffect呢?因为这里是fn执行之前,就是track依赖收集执行之前,又是effect开始执行之后,
// this能捕捉到这个依赖,将这个依赖赋值给activeEffect是刚刚好的时机
activeEffect = this
shouldTrack = true // 把开关打开让他可以收集依赖
let returnValue = this.fn() // 执行fn的时候,fn里面会执行get操作,之后就会执行track收集依赖,因为shouldTrack是true,所以依赖收集完成
// 之后把shouldTrack关闭,这样就没办法在track函数里面收集依赖了
shouldTrack = false
return returnValue
}
stop() {
// 追加active 标识是为了性能优化,避免每次循环重复调用stop同一个依赖的时候
if (this.active) {
cleanupEffect(this)
this.onStop?.()
this.active = false
}
}
}
// 清除指定依赖
function cleanupEffect(effect: ReactiveEffect) {
// 对effect解构,解出deps,减少对象在词法环境寻找属性的次数
const {deps} = effect
if (deps.length !== 0) {
for (let i = 0; i < deps.length; i++) {
deps[i].delete(effect)
}
deps.length = 0
}
}
const targetMap = new Map<Record<EffectKey, any>, Map<EffectKey, Set<IDep>>>()
// 当前正在执行的effect
let activeEffect: ReactiveEffect
let shouldTrack = false
type EffectKey = string
type IDep = ReactiveEffect
// 这个track的实现逻辑很简单:添加依赖
export function track(target: Record<EffectKey, any>, key: EffectKey) {
// 这里为什么要多一层非空判断呢?
// 我们查看reactive.spec.ts里面的测试用例
// 测试用例里根本就没有调用effect(),所以没有执行ReactiveEffect的run()自然activeEffect也就是undefined了
// if (!activeEffect) return
// 应不应该收集依赖,从而避免删了依赖又重新添加新的依赖
// if (!shouldTrack) return
if (!isTracking()) return
// 寻找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)
}
// 避免不必要的add操作
if (deps.has(activeEffect)) return
// 将activeEffect实例对象add给deps
deps.add(activeEffect)
// activeEffect的deps 接收 Set<ReactiveEffect>类型的deps
// 供删除依赖的时候使用(停止监听依赖)
activeEffect.deps.push(deps)
}
function isTracking(){
return activeEffect !== undefined && shouldTrack
}
// 这个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) {
if (dep.scheduler) {
dep.scheduler()
} else {
dep.run()
}
}
}
}
export interface EffectOption {
scheduler?: EffectScheduler
onStop?: () => void
}
// 里面存有一个匿名函数
export interface EffectRunner<T = any> {
(): T
effect: ReactiveEffect
}
// 根据官方给出的介绍:effect会立即触发这个函数,同时响应式追踪其依赖
export function effect<T = any>(fn: () => T, option?: EffectOption): EffectRunner {
let _effect = new ReactiveEffect(fn)
if(option){
extend(_effect, option)
}
_effect.run()
// 注意这里的this指向,return 出去的run方法,方法体里需要用到this,且this必须指向ReactiveEffect的实例对象
// 不用bind重新绑定this,this会指向undefined
let runner = _effect.run.bind(_effect) as EffectRunner
// 这里的effect挂载在了函数runner上,作为属性,这是利用了js中函数可以挂在属性的特性
// 之后呢,stop的runner就能拿到ReactiveEffect实例对象了
runner.effect = _effect
return runner
}
export function stop(runner: EffectRunner) {
runner.effect.stop()
}
// in shared/index.ts
export const extend = Object.assign
// All in effect.spec.ts
// 实现effect返回runner函数 这个runner函数其实就是effect的回调函数
it('should return runner when effect was called', () => {
let foo = 1
let runner = effect(() => {
foo++
return 'foo'
})
expect(foo).toBe(2)
let returnValue = runner()
expect(foo).toBe(3)
expect(returnValue).toBe('foo')
})
// 实现effect的scheduler功能
// 该功能描述:
// 1. effect首次执行的时候不执行scheduler,直接执行回调函数
// 2. 之后每次触发trigger函数的时候都会执行scheduler函数,不执行effect回调函数
// 3. 当调用run的时候才会触发runner,也就是说调用effect的回调函数
it('scheduler', () => {
let dummy
let run: any
const scheduler = jest.fn(() => {
run = runner
})
const obj = reactive({ foo: 1 })
const runner = effect(
() => {
dummy = obj.foo
},
{ scheduler }
)
expect(scheduler).not.toHaveBeenCalled()
expect(dummy).toBe(1)
// should be called on first trigger set操作的时候,也就是说在trigger被调用的时候
obj.foo++
expect(scheduler).toHaveBeenCalledTimes(1)
// should not run yet
expect(dummy).toBe(1)
// manually run 会触发effect的回调函数
run()
// should have run
expect(dummy).toBe(2)
})
// 实现effect的stop功能
// 功能描述:
// 通过stop可以停止监听依赖,怎么样停止监听依赖呢?可以通过删除deps依赖,那么trigger被调用的时候就不会被循环调用这个依赖了
it('stop', () => {
let dummy
const obj = reactive({ prop: 1 })
const runner = effect(() => {
dummy = obj.prop
})
obj.prop = 2
expect(dummy).toBe(2)
stop(runner)
// 单单只是检查set操作是不行的,还必须检查代码通过get操作之后,是否还能执行依赖
// obj.prop = 3
// 很明显如果换成obj.prop++,expect(dummy).toBe(2)就飘红了
// 这是因为obj.prop还有一个get操作,经过get操作之后,经过track函数之后原来被删除的effect又被add到deps上面去了
// 所以我们这里必须添加shouldtrack变量来表示应不应该被track 详细见effect.ts的track函数,控制shouldTrack开关在ReactiveEffect的run方法
obj.prop++
expect(dummy).toBe(2)
// stopped effect should still be manually callable
runner()
expect(dummy).toBe(3)
})
// 实现onStop
// 功能描述:
// 1. 当stop对一个runner执行的时候,runner对应的依赖的onStop就会被执行,相当于事件触发
it('onStop', () => {
const obj = reactive({ foo: 1 })
const onStop = jest.fn()
let dummy
const runner = effect(
() => {
dummy = obj.foo
},
{
onStop,
}
)
stop(runner)
// 被调用1次
expect(onStop).toBeCalledTimes(1)
})
转载自:https://juejin.cn/post/7090165509735317534