当前位置: 首页 > news >正文

【源码】Reactive 源码

前言

用了很长时间的 componsition-api 了,最近想看看源码,抱着单纯的学习心态先从 reactive 开始吧。

个人习惯:

  • 看代码要带着问题去看,不要盲目的去看
  • 问题就是这次看源码的主线,要围绕着主线去展开,过程中和主线没有多大关系的该忽略掉就忽略掉
  • 开源项目一般都封装的比较好,有可能一个函数中会引用多个文件中的函数,每次跳转的时候将跳转的目录记下来,避免跳着跳着就不知道跳哪去了
  • 进入到一个文件中先将函数都收起来,便于查看

前置准备

  • vue-next 从 github 上把项目 clone 下来。

  • 通过 yarn install 安装依赖。

  • package.jsondev 脚本增加 sourcemap 最终命令为: "dev": "node scripts/dev.js --sourcemap"

  • 因为 vue3 是通过 rollup 打包的,所以还需要安装 rollup

  • 执行 npm run dev 在 package/vue/ 目录下会生成一个 dist 文件夹。

  • 在 package/vue/examples/ 目录下新建 init.html 文件。

    <!DOCTYPE html>
    <html lang="en">
    <body><div id="app"><h1>hello</h1></div><script src="../dist/vue.global.js"></script><script>const { watch, watchEffect, createApp, reactive } = Vuedebuggerconst data = reactive({a: 1,b: 2,count: 0})</script>
    </body>
    </html>
    
  • 然后将这个文件以服务的形式跑起来进入 debug,通过断点可以进入到 reactive 函数。

数据响应式 Reactive

reactive 函数一开始就判断了如果传入的 target 如果是一个只读的对象则 return target

紧接着调用了 createReactiveObject 函数,先来看一下函数对应的参数:

  1. target 将要被代理的对象
  2. 是否只读
  3. baseHandlerscollectionHandlers 都是 proxyhandler,对应的实参分别是 mutableHandlersmutableCollectionHandlers,根据 target 类型来决定 proxyhandler
  4. reactiveMap 是当前文件中声明的一个常量,用于存储依赖,现在只需要它是一个 weakMap 类型数据就好

函数一开始对 target 做了几种情况的判断,针对不同情况做了对应的 return

主要关注点是这一段代码

const proxy = new Proxy(target,targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
)

这里会根据 target 的类型来选择对应的 handlertargetType 是调用 getTargetType 函数的返回值,targetTypeMap/Set/WeakMap/WeakSet 的时候会将 collectionHandlers 传入 Proxy,反之采用 baseHandlers

baseHandlers

baseHandlers 对应的 mutableHandlers

get

get 对应的 createGetter 的返回值

createGetter 接收两个参数 isReadonlyshallow,在 get 方法中先判断了三种特殊情况,针对每种不同情况 return 了不同的值,其中在这几个判断中用到的 shallowReadonlyMap/readonlyMap/shallowReactiveMap 它们的类型和 reactiveMap 的类型都一样都是 weakMap,只不过是对应不同的状态。

下边判断了是不是数组,如果是数组并且 isReadonlyfalsekeyincludes/indexOf/lastIndexOf/push/pop/shift/unshift/splice 其中的一个的话则执行

return Reflect.get(arrayInstrumentations, key, receiver)

也就是对上述那些方法进行了重写。这段代码是为了解决边缘情况

includes/indexOf/lastIndexOf 是为了避免产生以下情况

const obj = {}
const arr = reactive([obj])
arr.indexOf(obj) // -1 正常情况下应该返回的是 0

push/pop/shift/unshift/splice 是为了避免这些改变数组长度的方法在某些情况下进入死循环

再往下获取了 keytarget 中对应的值

const res = Reflect.get(target, key, receiver)

接着判断 keysymbol 的情况。如果 isReadonlyfalse 则调用 track 函数进行依赖收集。

track 这个函数往后放一下,看完这个 get 函数后再回头来看 track

接着又判断了 createGetter 传入的 shallowtrueresref 类型的这两种情况。

如果 res 还是一个对象并且 isReadonlyfalse 则递归调用 reactive 函数,反之调用 readonly 函数。

从调用 reactive 函数这里可以看出,vue3 中的响应式和 vue2 的差别不仅在 definePropertyproxy 上,在处理响应式的时机上也有变化,defineProperty 是一上来就将 target 上的所有属性都变成响应式的,但是 vue3 是在当你去读取这个 key 的时候,采取将 key 对应的 value 转换成响应式的。

接下来去看一下 track 函数,这个函数的作用是用来收集依赖的。

一开始是一个判断条件,接下来的这段代码是为了获取当前 key 对应的数据同时构造一个数据结构

let depsMap = targetMap.get(target)
if (!depsMap) {targetMap.set(target, (depsMap = new Map()))
}
let dep = depsMap.get(key)
if (!dep) {depsMap.set(key, (dep = new Set()))
}

构造出来的数据结构是这样的

targetMap = {target: {key: [dep,...] // dep, set 类型} // map 类型
} // weakMap 类型

后边将 activeEffect 添加到 dep 中。这里的 activeEffect 其实就是我们要收集的依赖。

后边又将 dep 添加到了 activeEffectdeps 中,这一块当时在看的时候并不明白,后来是查资料明白的,我们放到后边说。如果是开发环境还调用了 onTrack 函数,这个函数在文档中有说,用于调试侦听器的行为。

至此,track 函数就结束了。

遇到的问题
  • activeEffect.deps.push(dep) 是什么,为什么要这样做

    语义上来看是将当前 key 所对应的 dep push 到了 activeEffectdeps 中,但是为什么要这样做还有待思考。网上查了一番,这篇文章写的还是很不错的,对这个问题点也有一定的解析。

    文章内的总结: 这个操作是在为在 reactiveEffect 方法中提到的 cleanup 方法做准备,每次收集 activeEffect 之前,会先将 activeEffect 中的 deps 清空,然后再进行收集依赖。先清空再收集就是为了避免如果 activeEffect 中有在特定条件下才会触发的依赖收集,之前已经收集过了,但是这次不需要收集,所以会先把之前收集的清空掉,然后再针对当前这次需要收集的依赖进行收集,保证当前收集的依赖肯定是当前需要被收集的。这里又引申除了 reactiveEffect 方法,这个方法在 watchEffect 中会用到,本文不涉及,所以不展开说。

    set

set 对应的 createSetter 的返回值

createSetter 接收一个参数 shallow 用来表示是不是浅层对象,然后直接 returnset 函数。

首先判断了不是浅层对象则处理 oldValueref 类型,但 newValue 不是 ref 类型的情况,因为 ref 类型的数据已经是响应式的了,所以不需要再次通过 trigger 函数来再次触发依赖。

refreactive 差不多,都可以返回一个响应式的数据,但是 ref 需要通过 .value 的形式来获取值。

后边的代码就是判断 target 是不是数组,用 Reflect.set()value 添加到 target 中,后续又判断了 key 在不在 target 中,如果 key 不在 target 中则按 ADD 类型调用 trigger 函数触发依赖,反之以 SET 类型调用 trigger 函数。调用完之后将 Reflect.set() 的返回值 return 出去。

接下来看一下 trigger 函数

targetMap 中获取 target 对应的依赖,没有则 return

定义了 set 类型的 effectsadd 方法,add 方法主要是将传入的 deps 遍历,然后将每个 effect 添加到 effects 中,这样 effects 中就保存了所有待执行的 effect

下边就是根据传入的 type 来执行对应的 add 函数。

再往下就定义了 run 函数,它负责来执行 effect,如果是开发环境 effect 中有 onTrigger 方法的话会先执行 onTrigger 方法,这个方法和 track 函数中调用的 onTrack 函数是一个意思,文档中有说,用于调试侦听器的行为。如果 effect 中有调度器的话会选择用调度器来执行 effect 否则直接执行 effect

run 函数的定义完了就是遍历 effects,将 run 函数传入并执行。

以上就是 trigger 函数的所有了。

deleteProperty

这个方法就没什么好说的了,用 Reflect.deleteProperty() 执行删除,判断这个 keytarget 自身的则调用 trigger 触发依赖,然后 return Reflect.deleteProperty() 的返回值。

has

执行 Reflect.has(),在 key 是非 Symbol 类型的时候调用 track 函数收集依赖,然后 return Reflect.has() 的返回值。

ownKeys

调用 track 收集依赖,return Reflect.ownKeys(target)

collectionHandler

collectionHandlers 对应的 mutableCollectionHandlers

get

get 对应的 createInstrumentationGetter 的返回值

createInstrumentationGetter 函数定义了 isReadonlyshallow 两个参数,这里在调用的时候传入的都是 false

方法内也根据这两个参数去获取了对应的 handlers 定义为 instrumentations,后边判断了 key 的三种特殊情况,这里的 key 代表的是调用 get 方法时的 key,根据 key 的不同 return 对应的值。

最后判断 key 是不是 instrumentations 自身的属性,如果是则 Reflect.get() 传入的第一个值是 instrumentations,反之直接将 target,最后将 Reflect.get() 的返回值 return。

这个 collectionHandlers 不进行具体展开,主要还是围绕着我们的主题进行。

collectionHandlers 到此结束。

总结

  1. vue3 的响应式将 target 对象传入 proxy,然后利用 handlers,在执行 get/has/ownKeys 等获取值方法的时候进行依赖收集,在执行 set/deleteProperty 等更改值方法的时候进行触发依赖。

  2. vue3 对于 target 的某一个 keyvalue 值还是对象的时候,只有在读取到这个 key 的时候才将 value 进行响应式处理,而 vue2 的处理是初始化的时候直接将所有属性及属性值进行响应式处理。

    以上就是我关于 reactive 源码的阅读过程,在我最终读完之后去跑 demo 的时候发现进入到 track 函数的时候,在一开始那个判断的地方就被 return 出去了,并没有真正的收集依赖。这也就产生了我的另外一个问题:什么时候才会收集依赖呢。有了这个问题,可以继续把源码看下去了

参考链接

  • 源码系列:Vue3深入浅出(一)
  • Vue3 文档
  • Vue3最啰嗦的Reactivity数据响应式原理解析
http://www.lqws.cn/news/511669.html

相关文章:

  • c++ 空指针,悬挂指针(悬空指针),野指针
  • 总结汇报思路
  • 重点解析(软件工程)
  • 使用markRaw实例化echarts对象
  • RAG实战 第三章:知识库构建与管理
  • OSS安全合规实战:金融行业敏感数据加密+KMS自动轮转策略(满足等保2.0三级要求)
  • 宝塔服务器调优工具 1.1(Opcache优化)
  • 跟着chrome面板优化页面性能
  • 中断控制与实现
  • 《C++》命名空间简述
  • AutoGPT,自主完成复杂任务
  • 安卓9.0系统修改定制化____安卓9.0修改 默认开启开发者选项与usb调试的操作步骤解析 十一
  • 2025.6.24总结
  • # Python中等于号的使用
  • 创建首个 Spring Boot 登录项目
  • Linux零基础快速入门到精通
  • 大模型本地部署,拥有属于自己的ChatGpt
  • Vue 英雄列表搜索与排序功能实现
  • Verilog基础:编译指令`default_nettype
  • Harmony状态管理@Event
  • ubuntu16编译paho.mqtt.c 及 paho.mqtt.cpp编译问题
  • 屠龙刀策略
  • Web攻防-CSRF跨站请求伪造Referer同源Token校验复用删除置空联动上传或XSS
  • 统计学纯基础(1)
  • C++ 快速回顾(一)
  • 学习记录:DAY33
  • linux操作系统的软件架构分析
  • Redis 分布式锁原理与实战-学习篇
  • 我的字节一面
  • DeepSeek智能总结 | 邓紫棋音乐版权纠纷核心梳理