您现在的位置是:网站首页> 编程资料编程资料
Vue3 源码解读之副作用函数与依赖收集_vue.js_
2023-05-24
419人已围观
简介 Vue3 源码解读之副作用函数与依赖收集_vue.js_
版本:3.2.31
副作用函数
副作用函数是指会产生副作用的函数,如下面的代码所示:
function effect(){ document.body.innerText = 'hello vue3' }当 effect 函数执行时,它会设置 body 的文本内容,但除了 effect 函数之外的任何函数都可以读取或设置 body 的文本内容。也就是说,effect 函数的执行会直接或间接影响其他函数的执行,这时我们说 effect 函数产生了副作用。副作用很容易产生,例如一个函数修改了全局变量,这其实也是一个副作用。
// 全局变量 let val = 1 function effect() { val = 2 // 修改全局变量,产生副作用 }副作用函数的全局变量
在副作用模块中,定义了几个全局的变量,提前认识这些变量有助与我们了解副作用函数的生成以及调用的过程。
// packages/reactivity/src/effect.ts export type Dep = Set& TrackedMarkers type KeyToDepMap = Map // WeakMap 集合存储副作用函数 const targetMap = new WeakMap () // 用一个全局变量存储当前激活的 effect 函数 export let activeEffect: ReactiveEffect | undefined // 标识是否开启了依赖收集 export let shouldTrack = true const trackStack: boolean[] = []
targetMap
targetMap 是一个 WeakMap 类型的集合,用来存储副作用函数,从类型定义可以看出 targetMap的数据结构方式:
- WeakMap 由
target --> Map构成 - Map 由
key --> Set构成
其中 WeakMap 的键是原始对象 target,WeakMap 的值是一个 Map 实例,Map 的键是原始对象 target 的 key,Map 的值是一个由副作用函数组成的 Set。它们的关系如下:

targetMap 为什么使用 WeakMap
我们来看下面的代码:
const map = new Map(); const weakMap = new WeakMap(); (function() { const foo = {foo: 1}; const bar = {bar: 2}; map.set(foo, 1); // foo 对象是 map 的key weakMap.set(bar, 2); // bar 对象是 weakMap 的 key })在上面的代码中,定义了 map 和 weakMap 常量,分别对应 Map 和 WeakMap 的实例。在立即执行的函数表达式内部定义了两个对象:foo 和 bar,这两个对象分别作为 map 和 weakMap 的key。
当函数表达式执行完毕后,对于对象 foo 来说,它仍然作为 map 的 key 被引用着,因此垃圾回收器不会把它从内存中移除,我们仍然可以通过 map.keys 打印出对象 foo 。
对于对象 bar 来说,由于 WeakMap 的 key 是弱引用,它不影响垃圾收集器的工作,所以一旦表达式执行完毕,垃圾回收器就会把对象 bar 从内存中移除,并且我们无法获取 weakMap 的 key 值,也就无法通过 weakMap 取得对象 bar 。
简单地说,WeakMap 对 key 是弱引用,不影响垃圾回收器的工作**。根据这个特性可知,一旦 key 被垃圾回收器回收,那么对应的键和值就访问不到了。所以 WeakMap 经常用于存储那些只有当 key 所引用的对象存在时 (没有被回收) 才有价值的信息**。
例如在上面的场景中,如果 target 对象没有任何引用了,说明用户侧不再需要它了,这时垃圾回收器会完成回收任务。但如果使用 Map 来代替 WeakMap,那么即使用户侧的代码对 target 没有任何引用,这个 target 也不会被回收,最终可能导致内存溢出。
activeEffect
activeEffect 变量用来维护当前正在执行的副作用
shouldTrack
shouldTrack 变量用来标识是否开启依赖搜集,只有 shouldTrack 的值为 true 时,才进行依赖收集,即将副作用函数添加到依赖集合中。
副作用的实现
effect 函数
effect API 用来创建一个副作用函数,接受两个参数,分别是用户自定义的fn函数和options 选项。源码如下所示:
// packages/reactivity/src/effect.ts export function effect( fn: () => T, options?: ReactiveEffectOptions ): ReactiveEffectRunner { // 当传入的 fn 中存在 effect 副作用时,将这个副作用的原始函数赋值给 fn if ((fn as ReactiveEffectRunner).effect) { fn = (fn as ReactiveEffectRunner).effect.fn } // 创建一个副作用 const _effect = new ReactiveEffect(fn) if (options) { extend(_effect, options) if (options.scope) recordEffectScope(_effect, options.scope) } // 如果不是延迟执行的,则立即执行一次副作用函数 if (!options || !options.lazy) { _effect.run() } // 通过 bind 函数返回一个新的副作用函数 const runner = _effect.run.bind(_effect) as ReactiveEffectRunner // 将副作用添加到新的副作用函数上 runner.effect = _effect // 返回这个新的副作用函数 return runner }
由上面的代码可以知道,当传入的参数 fn 中存在 effect 副作用时,将这个副作用的原始函数赋值给 fn。然后调用 ReactiveEffect 类创建一个封装后的副作用函数。
在有些场景下,我们不希望 effect 立即执行,而是希望它在需要的时候才执行,我们可以通过在 options 中添加 lazy 属性来达到目的。在 effect 函数源码中,判断 options.lazy 选项的值,当值为true 时,则不立即执行副作用函数,从而实现懒执行的 effect。
接着通过 bind 函数返回一个新的副作用函数runner,这个新函数的this被指定为 _effect,并将 _effect 添加到这个新副作用函数的 effect 属性上,最后返回这个新副作用函数。
由于 effect API 返回的是封装后的副作用函数,原始的副作用函数存储在封装后的副作用函数的effect属性上,因此如果想要获取用户传入的副作用函数,需要通过 fn.effect.fn 来获取。
在 effect 函数中调用了 ReactiveEffect 类创建副作用,接下来看看 ReactiveEffect 类的实现。
ReactiveEffect 类
// packages/reactivity/src/effect.ts export class ReactiveEffect{ active = true deps: Dep[] = [] parent: ReactiveEffect | undefined = undefined /** * Can be attached after creation * @internal */ computed?: ComputedRefImpl /** * @internal */ allowRecurse?: boolean onStop?: () => void // dev only onTrack?: (event: DebuggerEvent) => void // dev only onTrigger?: (event: DebuggerEvent) => void constructor( public fn: () => T, public scheduler: EffectScheduler | null = null, scope?: EffectScope ) { recordEffectScope(this, scope) } run() { // 如果 effect 已停用,返回原始副作用函数执行后的结果 if (!this.active) { return this.fn() } let parent: ReactiveEffect | undefined = activeEffect let lastShouldTrack = shouldTrack while (parent) { if (parent === this) { return } parent = parent.parent } try { // 创建一个新的副作用前将当前正在执行的副作用存储到新建的副作用的 parent 属性上,解决嵌套effect 的情况 this.parent = activeEffect // 将创建的副作用设置为当前正则正在执行的副作用 activeEffect = this // 将 shouldTrack 设置为 true,表示开启依赖收集 shouldTrack = true trackOpBit = 1 << ++effectTrackDepth if (effectTrackDepth <= maxMarkerBits) { // 初始化依赖 initDepMarkers(this) } else { // 清除依赖 cleanupEffect(this) } // 返回原始副作用函数执行后的结果 return this.fn() } finally { if (effectTrackDepth <= maxMarkerBits) { finalizeDepMarkers(this) } trackOpBit = 1 << --effectTrackDepth // 重置当前正在执行的副作用 activeEffect = this.parent shouldTrack = lastShouldTrack this.parent = undefined } } // 停止(清除) effect stop() { if (this.active) { cleanupEffect(this) if (this.onStop) { this.onStop() } this.active = false } } }
在 ReactiveEffect 类中,定义了一个 run 方法,这个 run 方法就是创建副作用时实际运行方法。每次派发更新时,都会执行这个run方法,从而更新值。
全局变量 activeEffect 用来维护当前正在执行的副作用,当存在嵌套渲染组件的时候,依赖收集后,副作用函数会被覆盖,即 activeEffect 存储的副作用函数在嵌套 effect 的时候会被内层的副作用函数覆盖。为了解决这个问题,在 run 方法中,将当前正在执行的副作用activeEffect保存到新建的副作用的 parent 属性上,然后再将新建的副作用设置为当前正在执行的副作用。在新建的副作用执行完毕后,再将存储到 parent 属性的副作用重新设置为当前正在执行的副作用。
在 ReactiveEffect 类中,还定义了一个 stop 方法,该方法用来停止并清除当前正在执行的副作用。
track 收集依赖
当使用代理对象访问对象的属性时,就会触发代理对象的 get 拦截函数执行,如下面的代码所示:
const obj = { foo: 1 } const p = new Proxy(obj, { get(target, key, receiver) { track(target, key) return Reflect.get(target, key, receiver) } }) p.foo在上面的代码中,通过代理对象p 访问 foo 属性,便会触发 get 拦截函数的执行,此时就在 get 拦截函数中调用 track 函数进行依赖收集。源码中 get 拦截函数的解析可阅读《Vue3 源码解读之非原始值的响应式原理》一文中的「访问属性的拦截」小节。
下面,我们来看看 track 函数的实现。
track 函数
// packages/reactivity/src/effect.ts // 收集依赖 export function track(target: object, type: TrackOpTypes, key: unknown) { // 如果开启了依赖收集并且有正在执行的副作用,则收集依赖 if (shouldTrack && activeEffect) { // 在 targetMap 中获取对应的 target 的依赖集合 let depsMap = targetMap.get(target) if (!depsMap) { // 如果 target 不在 targetMap 中,则加入,并初始化 value 为 new Map() targetMap.set(target, (depsMap = new Map())) } // 从依赖集合中获取对应的 key 的依赖 let dep = depsMap.get(key) if (!dep) { // 如果 key 不存在,将这个 key 作为依赖收集起来,并初始化 value 为 new Set() depsMap.set(key, (dep = createDep())) } const eventInfo = __DEV__ ? { effect: activeEffect, target, type, key } : undefined trackEffects(dep, eventInfo) } }在 track 函数中,通过一个 if 语句判断是否进行依赖收集,只有当 shouldTrack 为 true 并且存在 activeEffect,即开启了依赖收集并且存在正在执行的副作用时,才进行依赖收集。
然后通过 target 对象从 targetMap 中尝试获取对应 target 的依赖集合depsMap,如果 targetMap 中不存在当前target的依赖集合,则将当前 target 添加进 targetMap 中,并将 targetMap 的 value 初始化为 new Map()。
// 在 targetMap 中获取对应的 target 的依赖集合 let depsMap = targetMap.get(target) if (!depsMap) { // 如果 target 不在 targetMap 中,则加入,并初始化 value 为 new Map() targetMap.set(target, (depsMap = new Map())) }接着根据target中被读取的 key,从依赖集合depsMap中获取对应 key 的依赖,如果依赖不存在,则将这个 key 的依赖收集到依赖集合depsMap中,并将依赖初始化为 new Set()。
// 从依赖集合中获取对应的 key 的依赖 let dep = depsMap.get(key) if (!dep) { // 如果 key 不存在,将这个 key 作为依赖收集起来,并初始化 value 为 new Set() depsMap.set(key, (dep = createDep())) }最后调用 trackEffects 函数,将副作用函数收集到依赖集合depsMap中。
const eventInfo = __DEV__ ? { effect: activeEffect, target, type, key } : undefined trackEffects(dep, eventInfo)trackEffects 函数
// 收集副作用函数 export function trackEffects( dep: Dep, debuggerEventExtraInfo?: DebuggerEventExtraInfo ) { let shouldTrack = false if (effectTrackDepth <= maxMarkerBits) { if (!newTracked(dep)) { dep.n |= trackOpBit // set newly tracked shouldTrack = !
相关内容
- Vue 3.0的attribute强制行为理解学习_vue.js_
- Javascript中的对象属性是有序的吗_javascript技巧_
- 使用Express+Node.js对mysql进行增改查操作 _node.js_
- vue中设置滚动条方式_vue.js_
- Windows下安装NodeJS的详细步骤_node.js_
- 8个鲜为人知但很实用的Web API用法总结_javascript技巧_
- vue使用axios导出后台返回的文件流为excel表格详解_vue.js_
- 浅析一下Vue3的响应式原理_vue.js_
- React中父组件如何获取子组件的值或方法_React_
- Vue中实现过渡动画效果示例代码_vue.js_
点击排行
本栏推荐
