Vue3源码阅读(八)effect
effect
- effect 作为 reactive 的核心,主要负责收集依赖,更新依赖,其会在 mountComponent、doWatch、reactive、computed 时被调用。
- 实质:其实就是一个改良版的发布订阅模式。get 时通过 track 收集依赖,而 set 时通过 trigger 触发了依赖,而 effect 收集了这些依赖并进行追踪,在响应后去触发相应的依赖。effect 也正是 Vue3 响应式的核心。
- 参数
- fn 回调函数
- options 参数
- 执行
- 在调用 effect 时会触发 track 开启响应式追踪,将追踪数据放入 targetMap
- 执行 reactive 时,通过 Proxy 类劫持对象
- 劫持 getter 执行 track
- 劫持 setter 执行 trigger
- 劫持的对象放在一个叫 targetMap 的 WeakMap
export interface ReactiveEffectOptions {lazy?: boolean // 是否延迟触发 effectcomputed?: boolean // 是否为计算属性scheduler?: (job: ReactiveEffect) => void // 调度函数onTrack?: (event: DebuggerEvent) => void // 追踪时触发onTrigger?: (event: DebuggerEvent) => void // 触发回调时触发onStop?: () => void // 停止监听时触发
}export function effect<T = any>(fn: () => T,options?: ReactiveEffectOptions
): ReactiveEffectRunner {// 如果已经是effect,先重置为原始对象if ((fn as ReactiveEffectRunner).effect) {fn = (fn as ReactiveEffectRunner).effect.fn}// 创建`effect`const _effect = new ReactiveEffect(fn)if (options) {extend(_effect, options)if (options.scope) recordEffectScope(_effect, options.scope)}// 如果没有传入 lazy 则不延迟触发,直接执行一次 `effect`if (!options || !options.lazy) {_effect.run()}const runner = _effect.run.bind(_effect) as ReactiveEffectRunnerrunner.effect = _effectreturn runner
}
- effect的创建
- 首先对 effect 做了一些初始化,然后初次创建 effect 的时候,如果当前的 effect 栈(effectStack)不包含当前 effect,仅将activeEffect设为当前effect,将activeEffect压入effectStack再开始依赖收集,根据依赖数目判断是否需要清空依赖数组,这样可以避免依赖的重复收集。依赖收集后重置activeEffect。这里的effect实际上是vue中垃圾回收
export class ReactiveEffect<T = any> {active = truedeps: Dep[] = []// can be attached after creationcomputed?: booleanallowRecurse?: booleanonStop?: () => void// dev onlyonTrack?: (event: DebuggerEvent) => void// dev onlyonTrigger?: (event: DebuggerEvent) => voidconstructor(public fn: () => T,public scheduler: EffectScheduler | null = null,scope?: EffectScope | null) {recordEffectScope(this, scope)}run() {// 如果没有调度者,直接执行fnif (!this.active) {return this.fn()}// 判断effectStack中有没有effect, 如果在则不处理if (!effectStack.includes(this)) {try {// 如果不在则将activeEffect设为当前effect,将activeEffect压入effectStackeffectStack.push((activeEffect = this))// 开始重新依赖收集enableTracking()// 依赖数目+1trackOpBit = 1 << ++effectTrackDepthif (effectTrackDepth <= maxMarkerBits) {// 依赖数目小于最大调用数,清空依赖数组initDepMarkers(this)} else {// 清除effect依赖,定义在下方cleanupEffect(this)}// 返回回调return this.fn()} finally {if (effectTrackDepth <= maxMarkerBits) {// 依赖数目小于最大调用数,清空依赖数组后继续重新进行依赖收集finalizeDepMarkers(this)}// 依赖数目-1trackOpBit = 1 << --effectTrackDepth// 重置依赖resetTracking()// 完成后将effect弹出effectStack.pop()const n = effectStack.length// 重置activeEffect activeEffect = n > 0 ? effectStack[n - 1] : undefined}}}// 仅在依赖数组中清除传入的effect依赖function cleanupEffect(effect: ReactiveEffect) {const { deps } = effectif (deps.length) {for (let i = 0; i < deps.length; i++) {deps[i].delete(effect)}deps.length = 0}
}
// dep.ts
// 每次 effect 运行都会重新收集依赖, deps 是 effect 的依赖数组, 需要全部清空
export const initDepMarkers = ({ deps }: ReactiveEffect) => {if (deps.length) {for (let i = 0; i < deps.length; i++) {deps[i].w |= trackOpBit // set was tracked}}
}
export const finalizeDepMarkers = (effect: ReactiveEffect) => {const { deps } = effectif (deps.length) {let ptr = 0for (let i = 0; i < deps.length; i++) {const dep = deps[i]if (wasTracked(dep) && !newTracked(dep)) {dep.delete(effect)} else {deps[ptr++] = dep}// clear bitsdep.w &= ~trackOpBitdep.n &= ~trackOpBit}deps.length = ptr}
}
- track 收集依赖(get操作)
- 当对一个对象进行get、has、iterate的时候,会触发该对象的track,收集依赖到targetMap。
// track// get、 has、 iterate 三种类型的读取对象会触发 track
export const enum TrackOpTypes {GET = 'get',HAS = 'has',ITERATE = 'iterate'
}
let shouldTrack = true// target:目标对象;type:收集的类型;key: 触发 track 的 object 的 key
export function track(target: object, type: TrackOpTypes, key: unknown) {// activeEffect为空没有依赖或者不应当触发track时,直接returnif (!isTracking()) {return}// targetMap是依赖管理中心,用于收集依赖和触发依赖 // 检查targetMap中有没有当前target let depsMap = targetMap.get(target)if (!depsMap) {// 如果目标对象不存在于targetMap,即没有被追踪,则新建一个放入targetMaptargetMap.set(target, (depsMap = new Map()))}// 检车depsMap中是否存在触发track的keylet dep = depsMap.get(key)if (!dep) {// 如果目标 key 没有被追踪,添加一个depsMap.set(key, (dep = createDep()))}const eventInfo = __DEV__? { effect: activeEffect, target, type, key }: undefinedtrackEffects(dep, eventInfo)
}
// activeEffect不为空且应当Track
export function isTracking() {return shouldTrack && activeEffect !== undefined
}
// deps来收集依赖函数,当监听的 key 值发生变化时,触发dep中的依赖函数
export function trackEffects(dep: Dep,debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {let shouldTrack = falseif (effectTrackDepth <= maxMarkerBits) {if (!newTracked(dep)) {dep.n |= trackOpBit // set newly trackedshouldTrack = !wasTracked(dep)}} else {// Full cleanup mode.shouldTrack = !dep.has(activeEffect!)}if (shouldTrack) {dep.add(activeEffect!)activeEffect!.deps.push(dep)// 开发环境会触发onTrack, 仅用于调试if (__DEV__ && activeEffect!.onTrack) {activeEffect!.onTrack(Object.assign({effect: activeEffect!},debuggerEventExtraInfo))}}
}
- trigger 触发依赖(触发更新后执行监听函数之前触发)
- 对对象进行set、add、delete、clear时会触发trigger,使用target中的deps触发依赖追踪。
- trigger的运行过程。其实是消费targetMap的依赖。在 trigger 方法中,拿到了之前收集到的依赖(也就是之前添加好的 effect)并添加到了任务队列中。然后遍历找到依赖后,开始触发依赖,执行任务
// trigger
// 会触发依赖的几种操作类型
export const enum TriggerOpTypes {SET = 'set',ADD = 'add',DELETE = 'delete',CLEAR = 'clear'
}export function trigger(target: object, // 目标对象type: TriggerOpTypes, // 操作类型key?: unknown, newValue?: unknown,oldValue?: unknown,oldTarget?: Map<unknown, unknown> | Set<unknown>
) {// 依赖管理中没有目标对象, 代表没有收集过依赖,直接返回const depsMap = targetMap.get(target)if (!depsMap) {// never been trackedreturn}// 对依赖进行分类 let deps: (Dep | undefined)[] = []if (type === TriggerOpTypes.CLEAR) {// 正在进行清除操作// 触发目标的所有效果deps = [...depsMap.values()]} else if (key === 'length' && isArray(target)) {//如果是数组的length修改且不是清除操作, 这里就能监听到数组的 length 变化了depsMap.forEach((dep, key) => {if (key === 'length' || key >= (newValue as number)) {// 如果是数组的length修改的新增,则push进depsdeps.push(dep)}})} else {// 如果是新增/删除/编辑操作且目标 key 没有被追踪,添加一个if (key !== void 0) {deps.push(depsMap.get(key))}// 在 新增/删除/编辑 的方法中,判断了 target 的类型然后添加 depsMap 中的不同依赖到 effect 中// effects 代表普通依赖,// computedRunners 为计算属性依赖 // 都是 Set 结构,避免重复收集switch (type) {case TriggerOpTypes.ADD:if (!isArray(target)) {deps.push(depsMap.get(ITERATE_KEY))if (isMap(target)) {deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))}} else if (isIntegerKey(key)) {// new index added to array -> length changesdeps.push(depsMap.get('length'))}breakcase TriggerOpTypes.DELETE:if (!isArray(target)) {deps.push(depsMap.get(ITERATE_KEY))if (isMap(target)) {deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))}}breakcase TriggerOpTypes.SET:if (isMap(target)) {deps.push(depsMap.get(ITERATE_KEY))}break}}const eventInfo = __DEV__? { target, type, key, newValue, oldValue, oldTarget }: undefined// 触发操作函数triggerEffectsif (deps.length === 1) {if (deps[0]) {if (__DEV__) {triggerEffects(deps[0], eventInfo)} else {triggerEffects(deps[0])}}} else {const effects: ReactiveEffect[] = []for (const dep of deps) {if (dep) {effects.push(...dep)}}if (__DEV__) {triggerEffects(createDep(effects), eventInfo)} else {triggerEffects(createDep(effects))}}
}export function triggerEffects(dep: Dep | ReactiveEffect[],debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {// spread into array for stabilizationfor (const effect of isArray(dep) ? dep : [...dep]) {if (effect !== activeEffect || effect.allowRecurse) {if (__DEV__ && effect.onTrigger) {effect.onTrigger(extend({ effect }, debuggerEventExtraInfo))}// 如果 scheduler 存在则调用 scheduler,计算属性拥有 schedulerif (effect.scheduler) {effect.scheduler()} else {effect.run()}}}
}
- 总结
- 调用方调用effect函数,参数为函数fn,options(默认为{});
- 判断是否已经是effect过的函数,如果是的话,则直接把原函数返回。
- 调用createReactiveEffect生成当前fn对应的effect函数,把上面的参数fn和options直接传进去;
- 为effect函数赋值
- 然后为effect函数添加属性:id, _isEffect, active, raw, deps, options,把effect返回。
- 判断options里面的lazy是否是false
- 如果不是懒处理,就直接调用下对应的effect函数,返回生成的effect函数。
- 如果是懒处理
- 首先判断了是否是active状态,如果不是,说明当前effect函数已经处于失效状态,直接返回return options.scheduler ? undefined : fn()
- 查看调用栈effectStack里面是否有当前effect,如果无当前effect,接着执行下面的代码。
- 先调用cleanup,把当前所有依赖此effect的全部清掉,deps是个数组,元素为Set,Set里面放的则是ReactiveEffect,也就是effect;
- 把当前effect入栈,并将当前effect置为当前活跃effect->activeEffect;后执行fn函数;
- finally,把effect出栈,执行完成了,把activeEffect还原到之前的状态;
- 其中涉及到调用轨迹栈的记录。和shouldTrack是否需要跟踪轨迹的处理。
effect.spec单元测试
- 部分注意点,非全部源码
// 传递给effect的方法,会立即执行一次
it('should run the passed function once (wrapped by a effect)', () => {const fnSpy = jest.fn(() => {})effect(fnSpy)expect(fnSpy).toHaveBeenCalledTimes(1)
})
// 在 effect 执行将 observe 对基本类型赋值,observe 进行改变时,将反应到基本类型上
it('should observe basic properties', () => {let dummyconst counter = reactive({ num: 0 })effect(() => (dummy = counter.num))expect(dummy).toBe(0)counter.num = 7expect(dummy).toBe(7)
})
// 在多个 effect 中处理 observe,当 observe 发生改变时,将同步到多个 effect
it('should handle multiple effects', () => {let dummy1, dummy2const counter = reactive({ num: 0 })effect(() => (dummy1 = counter.num))effect(() => (dummy2 = counter.num))expect(dummy1).toBe(0)expect(dummy2).toBe(0)counter.num++expect(dummy1).toBe(1)expect(dummy2).toBe(1)
})
// 嵌套的 observe 做出改变时,也会产生响应
it('should observe nested properties', () => {let dummyconst counter = reactive({ nested: { num: 0 } })effect(() => (dummy = counter.nested.num))expect(dummy).toBe(0)counter.nested.num = 8expect(dummy).toBe(8)
})
// 在 effect 执行将 observe 对基本类型赋值,observe 进行删除操作时,将反应到基本类型上
it('should observe delete operations', () => {let dummyconst obj = reactive({ prop: 'value' })effect(() => (dummy = obj.prop))expect(dummy).toBe('value')delete obj.propexpect(dummy).toBe(undefined)
})
// 在 effect 执行将 observe in 操作,observe 进行删除操作时,将反应到基本类型上
it('should observe has operations', () => {let dummyconst obj = reactive<{ prop: string | number }>({ prop: 'value' })effect(() => (dummy = 'prop' in obj))expect(dummy).toBe(true)delete obj.propexpect(dummy).toBe(false)obj.prop = 12expect(dummy).toBe(true)
})
// this 会被响应
it('should observe chained getters relying on this', () => {const obj = reactive({a: 1,get b() {return this.a}})let dummyeffect(() => (dummy = obj.b))expect(dummy).toBe(1)obj.a++expect(dummy).toBe(2)
})it('should observe methods relying on this', () => {const obj = reactive({a: 1,b() {return this.a}})let dummyeffect(() => (dummy = obj.b()))expect(dummy).toBe(1)obj.a++expect(dummy).toBe(2)
})
// 改变原始对象不产生响应
it('should not observe raw mutations', () => {let dummyconst obj = reactive<{ prop?: string }>({})effect(() => (dummy = toRaw(obj).prop))expect(dummy).toBe(undefined)obj.prop = 'value'expect(dummy).toBe(undefined)
})it('should not be triggered by raw mutations', () => {let dummyconst obj = reactive<{ prop?: string }>({})effect(() => (dummy = obj.prop))expect(dummy).toBe(undefined)toRaw(obj).prop = 'value'expect(dummy).toBe(undefined)
})it('should not be triggered by inherited raw setters', () => {let dummy, parentDummy, hiddenValue: anyconst obj = reactive<{ prop?: number }>({})const parent = reactive({set prop(value) {hiddenValue = value},get prop() {return hiddenValue}})Object.setPrototypeOf(obj, parent)effect(() => (dummy = obj.prop))effect(() => (parentDummy = parent.prop))expect(dummy).toBe(undefined)expect(parentDummy).toBe(undefined)toRaw(obj).prop = 4expect(dummy).toBe(undefined)expect(parentDummy).toBe(undefined)
})
// 可以避免隐性递归导致的无限循环
it('should avoid implicit infinite recursive loops with itself', () => {const counter = reactive({ num: 0 })const counterSpy = jest.fn(() => counter.num++)effect(counterSpy)expect(counter.num).toBe(1)expect(counterSpy).toHaveBeenCalledTimes(1)counter.num = 4expect(counter.num).toBe(5)expect(counterSpy).toHaveBeenCalledTimes(2)
})it('should avoid infinite loops with other effects', () => {const nums = reactive({ num1: 0, num2: 1 })const spy1 = jest.fn(() => (nums.num1 = nums.num2))const spy2 = jest.fn(() => (nums.num2 = nums.num1))effect(spy1)effect(spy2)expect(nums.num1).toBe(1)expect(nums.num2).toBe(1)expect(spy1).toHaveBeenCalledTimes(1)expect(spy2).toHaveBeenCalledTimes(1)nums.num2 = 4expect(nums.num1).toBe(4)expect(nums.num2).toBe(4)expect(spy1).toHaveBeenCalledTimes(2)expect(spy2).toHaveBeenCalledTimes(2)nums.num1 = 10expect(nums.num1).toBe(10)expect(nums.num2).toBe(10)expect(spy1).toHaveBeenCalledTimes(3)expect(spy2).toHaveBeenCalledTimes(3)
})
// 结果未发生变动时不做处理,发生改变时应该产生响应
it('should discover new branches while running automatically', () => {let dummyconst obj = reactive({ prop: 'value', run: false })const conditionalSpy = jest.fn(() => {dummy = obj.run ? obj.prop : 'other'})effect(conditionalSpy)expect(dummy).toBe('other')expect(conditionalSpy).toHaveBeenCalledTimes(1)obj.prop = 'Hi'expect(dummy).toBe('other')expect(conditionalSpy).toHaveBeenCalledTimes(1)obj.run = trueexpect(dummy).toBe('Hi')expect(conditionalSpy).toHaveBeenCalledTimes(2)obj.prop = 'World'expect(dummy).toBe('World')expect(conditionalSpy).toHaveBeenCalledTimes(3)
})it('should discover new branches when running manually', () => {let dummylet run = falseconst obj = reactive({ prop: 'value' })const runner = effect(() => {dummy = run ? obj.prop : 'other'})expect(dummy).toBe('other')runner()expect(dummy).toBe('other')run = truerunner()expect(dummy).toBe('value')obj.prop = 'World'expect(dummy).toBe('World')
})it('should not be triggered by mutating a property, which is used in an inactive branch', () => {let dummyconst obj = reactive({ prop: 'value', run: true })const conditionalSpy = jest.fn(() => {dummy = obj.run ? obj.prop : 'other'})effect(conditionalSpy)expect(dummy).toBe('value')expect(conditionalSpy).toHaveBeenCalledTimes(1)obj.run = falseexpect(dummy).toBe('other')expect(conditionalSpy).toHaveBeenCalledTimes(2)obj.prop = 'value2'expect(dummy).toBe('other')expect(conditionalSpy).toHaveBeenCalledTimes(2)
})
// 传入参数 scheduler, 支持自定义调度
it('scheduler', () => {let runner: any, dummyconst scheduler = jest.fn(_runner => {runner = _runner})const obj = reactive({ foo: 1 })effect(() => {dummy = obj.foo},{ scheduler })expect(scheduler).not.toHaveBeenCalled()expect(dummy).toBe(1)// should be called on first triggerobj.foo++expect(scheduler).toHaveBeenCalledTimes(1)// should not run yetexpect(dummy).toBe(1)// manually runrunner()// should have runexpect(dummy).toBe(2)
})
Vue3源码阅读(八)effect相关推荐
- mybatis源码阅读(八) ---Interceptor了解一下
转载自 mybatis源码阅读(八) ---Interceptor了解一下 1 Intercetor MyBatis 允许你在已映射语句执行过程中的某一点进行拦截调用.默认情况下,MyBatis允许 ...
- mochiweb 源码阅读(八)
看来昨天的大雨给北京确实带来了重创,早上出门去海淀驾校,北清路辛庄桥路段直接就堵死了,结果好不容易慢慢走到红绿灯那,才发现前方正抽水,封路了.唉,晚上回到家,上微博发现此次因灾遇难者37人,愿逝者安息 ...
- Vue3源码阅读指南——计算属性(effectcomputed)
在阅读Vue3响应式数据部分的源代码时,effect和computed部分的确有着其设计精巧之处.其代码实现是在packages/reactivity/effect.ts和packages/react ...
- 源码阅读:AFNetworking(八)——AFAutoPurgingImageCache
该文章阅读的AFNetworking的版本为3.2.0. AFAutoPurgingImageCache该类是用来管理内存中图片的缓存. 1.接口文件 1.1.AFImageCache协议 这个协议定 ...
- Soul网关源码阅读(八)路由匹配初探
Soul网关源码阅读(八)路由匹配初探 简介 今日看看路由的匹配相关代码,查看HTTP的DividePlugin匹配 示例运行 使用HTTP的示例,运行Soul-Admin,Sou ...
- 【vn.py学习笔记(八)】vn.py utility、BarGenerator、ArrayManager源码阅读
[vn.py学习笔记(八)]vn.py utility.BarGenerator.ArrayManager源码阅读 写在前面 1 工具函数 2 BarGenerator 2.1 update_tick ...
- 源码阅读:AFNetworking(十六)——UIWebView+AFNetworking
该文章阅读的AFNetworking的版本为3.2.0. 这个分类提供了对请求周期进行控制的方法,包括进度监控.成功和失败的回调. 1.接口文件 1.1.属性 /**网络会话管理者对象*/ @prop ...
- 源码阅读:SDWebImage(六)——SDWebImageCoderHelper
该文章阅读的SDWebImage的版本为4.3.3. 这个类提供了四个方法,这四个方法可分为两类,一类是动图处理,一类是图像方向处理. 1.私有函数 先来看一下这个类里的两个函数 /**这个函数是计算 ...
- 源码阅读:SDWebImage(十九)——UIImage+ForceDecode/UIImage+GIF/UIImage+MultiFormat
该文章阅读的SDWebImage的版本为4.3.3. 由于这几个分类都是UIImage的分类,并且内容相对较少,就写在一篇文章中. 1.UIImage+ForceDecode 这个分类为UIImage ...
- 超像素SLIC算法源码阅读
超像素SLIC算法源码阅读 超像素SLIC算法源码阅读 SLIC简介 源码阅读 实验结果 其他超像素算法对比 超像素SLIC算法源码阅读 SLIC简介 SLIC的全称Simple Linear Ite ...
最新文章
- ubuntu18.04.4 没有声音
- kettle的安装与连接mysql(包含mysql8)简单使用,
- ubuntu 中vi的使用方法
- 使用 outlet 在SAP Spartacus 的页面添加自定义 HTML 元素的一个例子
- [乐理知识] 第三章 拍子 节拍 节奏
- 西瓜书《机器学习》决策树IDW3, C4.5公式推导
- 乐山计算机学校新歌王,星歌王第二季乐山市计算机学校专场赛决赛完美落幕!...
- ASP.NET MVC + ADO.NET EF 项目实战(一):应用程序布局设计
- 《剑指Offer》面试题6 重建二叉树——勘误
- ES6 Symbol之浅入解读
- LCS(HDU_5495 循环节)
- Android-Universal-Image-Loader-master(图片浏览+缓存)
- java项目源码分享——适合新手练手的java项目
- 新浪云mysql_php连接mysql数据库(新浪云SAE)
- C#调用C++类库dll,无法找到函数入口(无法在“***.dll“中找到名为“***“的入口点)
- 分布式软总线模块总结
- note:记各种资源
- Haskell语言学习笔记(30)MonadCont, Cont, ContT
- WDK开发入门1-基础环境搭建和第一个驱动程序(VS2010)
- 服务器网口修改为百兆,服务器千兆网口能否设置为百兆
热门文章
- 使用esp8266前的网络基础
- Python绘制漫天的雪花,漫步天涯
- 深空摄影系列教程(昴星团摄星队)笔记
- c语言电子表格复制数据错误循环冗余检查,xp系统提示“数据错误(循环冗余检查)”如何解决...
- SurfacePro6解决亮度自动调节问题
- RTNETLINK answers: File exists的解决方案
- 二维曲线 去噪点 c++_二维介孔聚吡咯-氧化石墨烯异质结用于制备无枝晶的锂金属负极...
- Solidworks直接打开SWB文件报错怎么办
- 【思维题】Bazinga
- jdk重复安装,Error:Registry key ‘Software\JavaSoft\Java Runtime Environment\CurrentVersion(已解决)