当前位置: 首页 > 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数据响应式原理解析

相关文章:

  • 怎么用新浪云做网站女教师网课入侵录屏
  • 海南建设大厅网站百度查询
  • 网站开发未按合同约定工期完工seo优化方法网站快速排名推广渠道
  • 收录提交外汇seo公司
  • 招聘网站建设的目的郴州网站建设
  • 企业网站类型有哪些搜索引擎推广案例
  • 【CS创世SD NAND征文】基于全志V3S与CS创世SD NAND的物联网智能路灯网关数据存储方案
  • 闲庭信步使用SV搭建图像测试平台:第九课——初步使用类
  • 开疆智能CCLinkIE转ModbusTCP网关连接施耐德TCP从站配置案例
  • NEO4j的安装部署
  • P0/P1级重大故障根因分析:技术挑战与无指责复盘文化
  • From Tranformer to Decoder ONLY
  • SPSS再次使用
  • Linux零基础快速入门到精通
  • 使用Bash脚本RSA公钥加密算法对密码进行加密解密方法
  • vscode中vue自定义组件的标签失去特殊颜色高亮
  • 清华大学联合IDEA推出GUAVA:单幅图像生成实时可动画3D上半身,渲染速度突破0.1秒,可实现实时表情与动作同步。
  • [附源码+数据库+毕业论文]基于Spring+MyBatis+MySQL+Maven+jsp实现的超市库存商品管理系统,推荐!
  • 基于Qt和GDAL的多线程影像重采样工具
  • QT 学习笔记摘要(一)
  • 电动汽车定速巡航模式控制设计方法
  • Flask(六) 数据库操作SQLAlchemy
  • 【LUT技术专题】1D和3DLUT的高效组合-SepLUT
  • Java 线程池技术深度解析与代码实战
  • Petrel导入well数据
  • Nginx性能优化配置指南