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相关推荐

  1. mybatis源码阅读(八) ---Interceptor了解一下

    转载自  mybatis源码阅读(八) ---Interceptor了解一下 1 Intercetor MyBatis 允许你在已映射语句执行过程中的某一点进行拦截调用.默认情况下,MyBatis允许 ...

  2. mochiweb 源码阅读(八)

    看来昨天的大雨给北京确实带来了重创,早上出门去海淀驾校,北清路辛庄桥路段直接就堵死了,结果好不容易慢慢走到红绿灯那,才发现前方正抽水,封路了.唉,晚上回到家,上微博发现此次因灾遇难者37人,愿逝者安息 ...

  3. Vue3源码阅读指南——计算属性(effectcomputed)

    在阅读Vue3响应式数据部分的源代码时,effect和computed部分的确有着其设计精巧之处.其代码实现是在packages/reactivity/effect.ts和packages/react ...

  4. 源码阅读:AFNetworking(八)——AFAutoPurgingImageCache

    该文章阅读的AFNetworking的版本为3.2.0. AFAutoPurgingImageCache该类是用来管理内存中图片的缓存. 1.接口文件 1.1.AFImageCache协议 这个协议定 ...

  5. Soul网关源码阅读(八)路由匹配初探

    Soul网关源码阅读(八)路由匹配初探 简介      今日看看路由的匹配相关代码,查看HTTP的DividePlugin匹配 示例运行      使用HTTP的示例,运行Soul-Admin,Sou ...

  6. 【vn.py学习笔记(八)】vn.py utility、BarGenerator、ArrayManager源码阅读

    [vn.py学习笔记(八)]vn.py utility.BarGenerator.ArrayManager源码阅读 写在前面 1 工具函数 2 BarGenerator 2.1 update_tick ...

  7. 源码阅读:AFNetworking(十六)——UIWebView+AFNetworking

    该文章阅读的AFNetworking的版本为3.2.0. 这个分类提供了对请求周期进行控制的方法,包括进度监控.成功和失败的回调. 1.接口文件 1.1.属性 /**网络会话管理者对象*/ @prop ...

  8. 源码阅读:SDWebImage(六)——SDWebImageCoderHelper

    该文章阅读的SDWebImage的版本为4.3.3. 这个类提供了四个方法,这四个方法可分为两类,一类是动图处理,一类是图像方向处理. 1.私有函数 先来看一下这个类里的两个函数 /**这个函数是计算 ...

  9. 源码阅读:SDWebImage(十九)——UIImage+ForceDecode/UIImage+GIF/UIImage+MultiFormat

    该文章阅读的SDWebImage的版本为4.3.3. 由于这几个分类都是UIImage的分类,并且内容相对较少,就写在一篇文章中. 1.UIImage+ForceDecode 这个分类为UIImage ...

  10. 超像素SLIC算法源码阅读

    超像素SLIC算法源码阅读 超像素SLIC算法源码阅读 SLIC简介 源码阅读 实验结果 其他超像素算法对比 超像素SLIC算法源码阅读 SLIC简介 SLIC的全称Simple Linear Ite ...

最新文章

  1. ubuntu18.04.4 没有声音
  2. kettle的安装与连接mysql(包含mysql8)简单使用,
  3. ubuntu 中vi的使用方法
  4. 使用 outlet 在SAP Spartacus 的页面添加自定义 HTML 元素的一个例子
  5. [乐理知识] 第三章 拍子 节拍 节奏
  6. 西瓜书《机器学习》决策树IDW3, C4.5公式推导
  7. 乐山计算机学校新歌王,星歌王第二季乐山市计算机学校专场赛决赛完美落幕!...
  8. ASP.NET MVC + ADO.NET EF 项目实战(一):应用程序布局设计
  9. 《剑指Offer》面试题6 重建二叉树——勘误
  10. ES6 Symbol之浅入解读
  11. LCS(HDU_5495 循环节)
  12. Android-Universal-Image-Loader-master(图片浏览+缓存)
  13. java项目源码分享——适合新手练手的java项目
  14. 新浪云mysql_php连接mysql数据库(新浪云SAE)
  15. C#调用C++类库dll,无法找到函数入口(无法在“***.dll“中找到名为“***“的入口点)
  16. 分布式软总线模块总结
  17. note:记各种资源
  18. Haskell语言学习笔记(30)MonadCont, Cont, ContT
  19. WDK开发入门1-基础环境搭建和第一个驱动程序(VS2010)
  20. 服务器网口修改为百兆,服务器千兆网口能否设置为百兆

热门文章

  1. 使用esp8266前的网络基础
  2. Python绘制漫天的雪花,漫步天涯
  3. 深空摄影系列教程(昴星团摄星队)笔记
  4. c语言电子表格复制数据错误循环冗余检查,xp系统提示“数据错误(循环冗余检查)”如何解决...
  5. SurfacePro6解决亮度自动调节问题
  6. RTNETLINK answers: File exists的解决方案
  7. 二维曲线 去噪点 c++_二维介孔聚吡咯-氧化石墨烯异质结用于制备无枝晶的锂金属负极...
  8. Solidworks直接打开SWB文件报错怎么办
  9. 【思维题】Bazinga
  10. jdk重复安装,Error:Registry key ‘Software\JavaSoft\Java Runtime Environment\CurrentVersion(已解决)