Effect

前言:effect类似于vue2源码中的watch,观察者/订阅者。
以下过程中,effect为观察者,target的属性值为被观察者,effect观察target的属性值,target的属性值被修改通知effect,确保以这个思想看待下面源码。

Effect与Target映射

target ===> effect

// 存放effect集合,使用Set去重
type Dep = Set<ReactiveEffect>
// any为监控对象的属性,属性对应的Dep
// 存放监控对象所有属性与对应Dep
type KeyToDepMap = Map<any, Dep>
// any为target
const targetMap = new WeakMap<any, KeyToDepMap>()

上述代码目的:使用Proxy监控是target,每次触发handler都是属性维度,因此需要维护属性与effect集合的映射。

Dep == 》effect集合

keyToDepMap ===》 target的所有属性对应Dep

targetMap ===》target对应的所有属性监控

修改target对应属性,可以通过以上映射找到该属性对应的effect进行触发执行。

effect的类型

export interface ReactiveEffect<T = any> {(): T// effect的标志属性_isEffect: true// 唯一标识id: numberactive: boolean// 原始函数raw: () => T// 关联effect的响应数据的观察者集合// effect与对应的响应数据指向的观察者集合是同一个对象// 目的就是为了当该effect需要与响应数据分开时,直接从deps中移除该effect,同时对应的响应数据的观察者集合也移除了该effect,因为指向同一份数据deps: Array<Dep>// effect的配置选项options: ReactiveEffectOptions
}
// active属性作用是当为true时,会清楚effect的依赖并且执行onStop事件
// stop会在unmountComponent内部进行触发
// 因此是组件消亡的时候会触发stop,如果active为true会清除依赖触发onStrop
stop(effect: ReactiveEffect) {if (effect.active) {cleanup(effect)if (effect.options.onStop) {effect.options.onStop()}effect.active = false}
}

大家可以从后面的effect创建函数里面了解到,active为false时,effect就是一个普通的传入函数或undefined,没有监听的依赖项

effect的配置属性

export interface ReactiveEffectOptions {// 是否立即执行lazy?: boolean// 调度器,是否自定义调度effectscheduler?: (job: ReactiveEffect) => void// 是否收集的监听函数onTrack?: (event: DebuggerEvent) => void// 是否触发的监听函数onTrigger?: (event: DebuggerEvent) => void// 是否停止的监听函数onStop?: () => void
}
// 事件类型
export type DebuggerEvent = {// effecteffect: ReactiveEffect// 监听对象target: object// 类型,这个主要是看何种方式,例如ADD、DELETE、CLEARtype: TrackOpTypes | TriggerOpTypes// porxy监听的属性key: any
} & DebuggerEventExtraInfo
export interface DebuggerEventExtraInfo {newValue?: anyoldValue?: anyoldTarget?: Map<any, any> | Set<any>
}

effect构造函数

export function effect<T = any>(fn: () => T,options: ReactiveEffectOptions = EMPTY_OBJ
): ReactiveEffect<T> {// 如果fn是监听函数,则获取原始函数,重新创建// 从这可以看出,effect每次会返回一个新的监控函数if (isEffect(fn)) {fn = fn.raw}// 核心就是createReactiveEffectconst effect = createReactiveEffect(fn, options)// lazy属性就是标识是否默认执行一次if (!options.lazy) {effect()}return effect
}

传入一个函数,返回一个effect的构造函数

3.createReactiveEffect函数

function createReactiveEffect<T = any>(fn: () => T,options: ReactiveEffectOptions
): ReactiveEffect<T> {// 创建ReactiveEffect类型的函数const effect = function reactiveEffect(): unknown {// 如果active为false,则不会进行依赖收集if (!effect.active) {return options.scheduler ? undefined : fn()}// effectStack指的是effect栈// 如果effectStack中没有effect,则进行执行// 为了防止循环依赖// effect1中触发effect2,此时触发effect2的调用,此时effect2中又触发effect1,但是此时effect1还在effectStack中,则不会进入if (!effectStack.includes(effect)) {// 清除effect中对应的监控数据// 并且从监控数据依赖中清除该effect观察者// 双向进行清除,主要通过deps属性cleanup(effect)try {// 向trackStack推入一个shouldTrack// shouldTrack主要是是否进行依赖收集标志位enableTracking()// 向effectStack栈推入一个effecteffectStack.push(effect)// 活动effect指向该effectactiveEffect = effect// 执行fn,进行触发内部监控数据对应的getter来收集该effect依赖return fn()} finally {// 弹出该effecteffectStack.pop()// 弹出shouldTrackresetTracking()activeEffect = effectStack[effectStack.length - 1]}}} as ReactiveEffect// 设置effect的基础属性effect.id = uid++effect._isEffect = trueeffect.active = trueeffect.raw = fneffect.deps = []effect.options = optionsreturn effect
}
// 记录上一次的shouldTrack状态
// 并且现在置为false
// 例如当wraning时会执行该函数
export function pauseTracking() {trackStack.push(shouldTrack)shouldTrack = false
}
// 记录上一次的shouldTrack状态
// 并且现在依赖可进行收集,因为要执行fn,获取
export function enableTracking() {trackStack.push(shouldTrack)shouldTrack = true
}
// effect执行完毕,则将shoudTrack置为之前状态
export function resetTracking() {const last = trackStack.pop()shouldTrack = last === undefined ? true : last
}

将原始函数fn重新定义为effect,主要作用类似watch当作为观察者

cleanup函数

作用:为了清除该effect依赖项

function cleanup(effect: ReactiveEffect) {// 获取与该effect有关的被观察者的effect集合// 从前面可知,该effect关联的监控对象指向的dep是在同一个存储地址const { deps } = effectif (deps.length) {// 从dep中删除该effectfor (let i = 0; i < deps.length; i++) {deps[i].delete(effect)}// 并且去除deps的每一项deps.length = 0}
}

听讲解还是很懵逼?一张图搞定。

是不是非常清晰,deps[i].delete操作切除了被观察与effect的联系,deps.length=0,操作切除了effect与被观察的联系。

track函数

作用:依赖收集

export function track(target: object, type: TrackOpTypes, key: unknown) {// 如果不进行依赖收集或目标观察者为undefined则直接返回if (!shouldTrack || activeEffect === undefined) {return}// 获取该target的属性与观察者收集箱集合// depsMap<key, dep>,key为tarfet属性let depsMap = targetMap.get(target)// 如果不存在则重新创建if (!depsMap) {targetMap.set(target, (depsMap = new Map()))}// 获取该key对应的依赖集合let dep = depsMap.get(key)// 如果不存在则重新创建if (!dep) {depsMap.set(key, (dep = new Set()))}// 判断是否存在该effect观察者if (!dep.has(activeEffect)) {// 对应属性值的dep添加该effectdep.add(activeEffect)// 并且该effect中的deps也添加被观察的属性值的depactiveEffect.deps.push(dep)if (__DEV__ && activeEffect.options.onTrack) {// 触发onTrack函数activeEffect.options.onTrack({effect: activeEffect,target,type,key})}}
}

很简单,主要是收集目标targetEffect,并且该targetEffect的deps也添加被观察属性值对应的dep,做双映射

trigget函数

作用:通知对应依赖执行回调函数

export function trigger(target: object,type: TriggerOpTypes,key?: unknown,newValue?: unknown,oldValue?: unknown,oldTarget?: Map<unknown, unknown> | Set<unknown>
) {// 获取与target有关的所有观察者const depsMap = targetMap.get(target)if (!depsMap) {// never been trackedreturn}// 添加一个依赖集合变量,Set确保唯一性const effects = new Set<ReactiveEffect>()// 从名字看出来添加函数const add = (effectsToAdd: Set<ReactiveEffect> | undefined) => {if (effectsToAdd) {effectsToAdd.forEach(effect => {// 避免循环依赖if (effect !== activeEffect) {effects.add(effect)}})}}// clear指的是target清除所有属性触发的事件// 类似target = nullif (type === TriggerOpTypes.CLEAR) {// collection being cleared// trigger all effects for target// 遍历所有属性的观察集合,添加到effects中depsMap.forEach(add)} else if (key === 'length' && isArray(target)) {// 监控数组中length的长度变化,pop、shift、push、unshift等都会造成length变化depsMap.forEach((dep, key) => {// 将属性length与key大于等于length的属性,全部添加到effects中// 小于length的key自动进行依赖收集了,在这不需要收集// 这里主要针对当直接对Array.length进行赋值时,要收集大于等于length的所有属性的依赖if (key === 'length' || key >= (newValue as number)) {add(dep)}})} else {// schedule runs for SET | ADD | DELETE// 针对对应属性的dep进行添加activeEffectif (key !== void 0) {add(depsMap.get(key))}// ITERATE_KEY的收集是以下函数,主要是proxy中handler的第五个参数,监控对象属性集合的获取,例如Object.keys、for(key in target)等// function ownKeys(target: object): (string | number | symbol)[] {//     track(target, TrackOpTypes.ITERATE, ITERATE_KEY)//  return Reflect.ownKeys(target)// }// also run for iteration key on ADD | DELETE | Map.SET// 删除添加都会改变对象与数组的keys长度与length// 举个例子,使用watch深层次监控target时,会进行key in target进行递归监控,因此会触发ownKeys,当target进行删除与添加属性时,同样会进行监控到// 可能会有其它作用,待使用后继续补充const isAddOrDelete =type === TriggerOpTypes.ADD ||(type === TriggerOpTypes.DELETE && !isArray(target))if (isAddOrDelete ||(type === TriggerOpTypes.SET && target instanceof Map)) {add(depsMap.get(isArray(target) ? 'length' : ITERATE_KEY))}if (isAddOrDelete && target instanceof Map) {add(depsMap.get(MAP_KEY_ITERATE_KEY))}}const run = (effect: ReactiveEffect) => {if (__DEV__ && effect.options.onTrigger) {effect.options.onTrigger({effect,target,key,type,newValue,oldValue,oldTarget})}// 如果存在scheduler,则运行scheduler调度器// 否则直接执行effectif (effect.options.scheduler) {effect.options.scheduler(effect)} else {effect()}}effects.forEach(run)
}

总结

effect相当于watch,target的属性与effect有一个映射表KeyToDepMap
target的属性值获取会触发track,此时会收集activeEffect
target的属性值修改会触发trigger,此时利用target从targetMap中获取与该对象有关的KeyToDepMap,然后使用属性key从KeyToDepMap获取dep,遍历dep进行添加到执行队列中(简化版)

vue3源码effect相关推荐

  1. 推荐 7 个 Vue2、Vue3 源码解密分析的开源项目

    大家好,我是你们的 猫哥,那个不喜欢吃鱼.又不喜欢喵 的超级猫 ~ 1. 为什么要学习源码 ? 阅读优秀的代码的目的是让我们能够写出优秀的代码. 不给自己设限,不要让你周围人的技术上限成为你的上限.其 ...

  2. 学习尤雨溪写的 Vue3 源码中的简单工具函数

    大家好,我是若川.最近组织了源码共读活动.每周读 200 行左右的源码.很多第一次读源码的小伙伴都感觉很有收获,感兴趣可以加我微信ruochuan12,拉你进群学习. 初学者也能看懂的 Vue3 源码 ...

  3. 初学者也能看懂的 Vue3 源码中那些实用的基础工具函数

    1. 前言 大家好,我是若川.最近组织了源码共读活动.每周读 200 行左右的源码.很多第一次读源码的小伙伴都感觉很有收获,感兴趣可以加我微信ruochuan12,拉你进群学习. 写相对很难的源码,耗 ...

  4. Vue3源码解析01--Vue3初探

    Vue3 源码解析 01 - Vue3 浅谈 前言 最近几个月一直忙于公司搬砖,导致都没有时间学习了.正好这段时间慢慢恢复了正常工作的作息,赶紧学习一下最新发布的 Vue3 框架. 为什么有 Vue3 ...

  5. vue3源码分析--真的有必要掌握框架的细枝末节吗?

    古人云:工欲善其事必先利其器,磨刀不误砍柴工.但是砍柴的人需要知道怎么制作刀吗? 注意:本文先分析要不要学源码,然后分析要不要掌握源码的每一个细枝末节(深究技术)!!! 为什么要学源码 为了面试被迫学 ...

  6. Vue3源码分析之打包原理

    Vue3源码分析之打包原理 如果之前你已经看过我的<Vue3源码分析之入门>,那么你可以直接阅读此篇文章 Vue3源码分析之入门 一.配置环境 1. 全局安装yarn Monorepo 管 ...

  7. Vue3源码解析之入门

    Vue3源码分析之入门 本文主要是针对想自学Vue3之类的框架源码的,却不知道如何上手的小伙伴们~ Vue3源码GitHub地址 Vue3源码克隆路径 :git@github.com:vuejs/co ...

  8. vue3源码分析——看看complier是怎么来解析的

    引言 <<往期回顾>> vue3源码分析--手写diff算法 vue3源码分析--实现组件更新 vue3源码分析--解密nextTick的实现 想知道vue3-complier ...

  9. vue3源码分析——实现slots

    引言 <<往期回顾>> vue3源码分析--rollup打包monorepo vue3源码分析--实现组件的挂载流程 vue3源码分析--实现props,emit,事件处理等 ...

  10. 【Vue3源码学习】响应式源码解析:reactive、effect、ref

    源码版本 Vue3.2.24 废话不多说,直接开始!!! reactive响应式 源码地址:packages/reactivity/reactive.ts 先看一下在 Vue3 中定义的几个用来标记目 ...

最新文章

  1. Qt基于QGraphicsObject自定义图元并实现简单的动画
  2. Mongodb固定集合
  3. Java实现Huffman哈夫曼树(数组实现)
  4. 博士笔记 | 周志华《机器学习》手推笔记第二章-模型评估与选择
  5. 禁用计算机服务LanmanServer,[如何]在Windows 10中启用或禁用SMB协议 | MOS86
  6. IOS Animation-KeyPath值
  7. STM32 + RT Thread OS 学习笔记[四]
  8. 幅频特性曲线的绘制(2)
  9. 雷锋科普:联发科MT6577,国产平民双核手机的芯
  10. 网络篇 OSPF的DR与BDR的选举-48
  11. 宏康 HY17 时钟 串口
  12. ZooKeeper命令行
  13. R语言3.15 综合评价方法
  14. Cast-Designer Weld人工智能参与的多道焊工艺参数设计
  15. 跨平台应用开发进阶(五十一):HTML5(富文本内容)连续数字、字母不自动换行问题分析及解决
  16. 从此以后谁也别说我不懂LDO了
  17. Android手机截图代码(针对root设备,可截取任意屏幕)
  18. 数据结构与算法分析:哈希表
  19. 基于AM335x裸机开发例程使用手册
  20. 长期熬夜——真的不好

热门文章

  1. Feescale K64开发笔记1: 开发环境的建立
  2. 品质担当,有效提升办公生产力,永艺XY人体工学椅开箱实测
  3. [一直更新中]WerKeyTom的口胡
  4. nividia 自定义分辨率整合进了显示器原生支持的分辨率,如何删除
  5. 关于大地测量领域常用的角度知识汇总(方位角,竖直角)
  6. 【金猿产品展】沃丰科技GaussMind——用技术提升客户体验
  7. 计算机 调剂 学校,考研调剂应该怎样联系学校?这三点一定要注意
  8. Android 视频播放器
  9. 程序人生——苏嵌第六天
  10. Android内部存储和外部存储以及缓存清理和内存清理!