专栏前言
本文是vue3源码解析系列的第三篇文档,在前两篇文章中,我们了解了vue3源码的运行、调试,以及阅读前的一些前置知识点,从本节开始,我们就可以正式的开始vue3的源码阅读了。
我们首先阅读的模块是@vue/reactivity 中的reactive以及相关api,effect的源代码。
在正文开始之前,我先将本节的简化版源码放出来,有兴趣的同学可以clone到本地,一边debug,一边阅读文章,这样效果更佳~
https://github.com/BlueDancers/vue3-mini/tree/reactive
前言
reactive的含义如其名称,通过reactive创建的对象都是具备响应式的。即reactive对象的改变会造成副作用。
于是我们引出副作用API(effect),如果effect内部依赖了reactive,则reactive的改变会重新触发effect。
现在让我们走进案例与源码,看看究竟是如何实现响应式的。
案例
1 | let { reactive, effect } = Vue |
以上测试案例,我们涉及到了三个重要的阶段
- reactive初始化
- effect初始化
- reactive发生修改
最后形成了effect的自动触发,我们就从以上三个角度去切入源码实现。
reactive初始化
为了方便阅读与理解,以下仅贴出核心源码
packages/reactivity/src/reactive.ts
1 | export function reactive(target) { |
通过源码 我们可以看得出来,使用reactive,内部实际执行的是createReactiveObject,函数就是新建了proxy,并最终返回。
不过要注意一点的是,经过reactive处理过的对象,都会以target为WeakMap键,proxy为值,进行一次缓存,这样同一个值再次进行reactive的时候就会读取缓存中的值。
接下来,让我们进入初始化阶段的mutableHandlers,也就是proxy中核心的get set函数,看看内部做了些什么。
初始化读取(get)
当触发obj.name的读取行为的时候,就会触发代理对象的get函数
packages/reactivity/src/baseHandlers.ts
1 | const get = createGetter() |
get内部的逻辑很简单,通过Reflect完成被代理对象的读取操作。
如果被读取对象的属性是object则会再次进入reactive逻辑中进行proxy处理,确保嵌套对象的响应式。
也许有的人会说了proxy不是自身就实现了对象的拦截了吗?为什么我们还是要递归处理嵌套obj呢?
这里我给大家解释一下,proxy确实会拦截到所有操作,但是他也只能拦截当前层级的。
如果没有递归处理, obj.name.abc = 123的时候,只会触发obj.name的get事件,但是不会触发obj.name.abc的set事件。
初始化修改(set)
当触发obj.name
的修改行为,将会触发代理对象的set函数
packages/reactivity/src/baseHandlers.ts
1 | const set = createSetter() |
通过Reflect完成被代理对象值的更新,最后返回本次Reflect.set的结果,完成逻辑。
总体就是对proxy的简单利用,还是很简单的嘛
小结
以上代码是去除所有边界判断,以及响应式逻辑后,reactive的核心代码;我们可以发现,其实就是proxy + Reflect的基础使用。
目前数据已经具备响应式,但是数据变化后,引用数据的effect如何实现自动执行呢?接下来我们就去看看effect初始化的时候究竟做了什么。
effect初始化
读取 - 依赖收集(track)
我们回到测试demo中,根据我们使用vue3的预期,在初始化完成后,effect会触发一次,若干时间后,setTimeout内set触发,依赖obj.name
的 effect的函数还会被触发一次,这又是如何实现的呢?
这里我要提到Vue3中第一个非常非常非常重要的概念,依赖收集(track),整个reactivity都利用到了这个概念。
接下来,我们就要通过源码去了解,effect的初始化的时候,到底发生了什么,Vue3在此阶段是如何完成依赖收集的。
packages/reactivity/src/effect.ts
1 | /** |
vue3的依赖收集几乎都是通过ReactiveEffect进行完成的,简单来说就是ReactiveEffect.run一旦运行后,就会将当前正在运行的匿名函数保存到内存中,以便于proxy get事件触发的时候,收集保存在内存中的匿名函数,进而完成依赖收集。
effect方法内部,首先new ReactiveEffect 最终执行了一次fn,但是在执行之前,将activeEffect赋值为this,将自身保存到了公共变量activeEffect之中。
让我们来看看此时运行的fn是什么
1 | () => { |
匿名函数的内部读取了obj.name,触发了被代理对象obj的get方法.
所以接下来我们回到get方法中,查看之前忽略的依赖收集逻辑。
packages/reactivity/src/baseHandlers.ts
1 | function createGetter(isReadonly = false, shallow = false) { |
effect内部的fn被触发,fn执行中触发了obj的get,get内部触发了依赖收集(track),track内部通过构建targetMap,来维护变量与effect之间的关系,进而实现所谓的依赖收集。
我们来梳理一下他的数据结构
WeakMap
- key:被代理对象({name:’张三’})
- value:Map对象
- key:响应式对象的指定属性(name)
- value:指定对象的指定属性的使用函数(effect的匿名函数)
在WeakMap中,我们不仅仅收集了effect的匿名函数,还将effect与effect中具体读取的变量建立起了联系。
在未来的依赖触发逻辑中,weakMap将会发挥巨大作用。
到此为止,effect内的匿名函数执行完毕,同时我们也完成了重要的依赖收集。
修改 - 依赖触发(trigger)
继续回到demo中,2s后,obj.name赋值为狂飙强,此时的现象是effect中的函数自动执行了,这又是如何实现的呢?
此处首先一定是触发了代理对象obj.name的set,所以我们由此处开始分析。
packages/reactivity/src/baseHandlers.ts
1 | function createSetter(shallow = false) { |
经过以上代码,我们可以了解到,obj.name的改变在触发了proxy的set方法的同时,也触发了依赖触发(trigger)。
trigger中,我们首先通过**{name: ‘狂飙强’},找到了Map,再通过name找到Set,最终找到对应的effect的fn**,并进行匿名函数的执行,于是我们便看到了effect函数自动触发。
到此为止完成了整个响应式过程。
reactive源码总结
我们简单总结一下,reactive中依赖收集与依赖触发的过程
- 通过proxy处理reactive包裹的对象,被返回proxy代理对象
- effect初始化,生成了类ReactiveEffect,并执行了其run方法
- run方法执行后,当前effect的fn函数本身被保存到了activeEffect(公共变量),随后执行了effect的fn
- effect的fn触发,函数内使用到了obj.name,触发了代理对象的get
- get方法内部触发了依赖收集(track),配合保存到局部的activeEffect,最终通过WeakMap,建立了effect的fn与当前get的属性的联系,完成了依赖收集。
- 若干时间后,obj.name = ‘狂飙强’,触发proxy的set,同时触发了依赖触发(trigger)
- trigger内部通过当前代理对象以及具体修改的属性,在依赖收集阶段保存的WeakMap中,找到所有需要触发的effect的fn。
- 触发effect的fn函数,完成响应式。
最后反映在我们眼前,就是obj.name改变的同时,所有使用到obj.name的effet都被自动触发其匿名函数,完成响应式。
关于vue3 reactive的面试题
为什么Vue3的响应式使用WeakMap实现?
还记得我们前一篇文章谈到的WeakMap吗,一旦被代理对象被置为null,weakMap中该key将会被垃圾回收,达到性能最大化的目的
简述Vue3的响应式的核心实现逻辑?
通过proxy递归代理对象,然后在get中完成依赖收集,在set中完成依赖触发
Vue3的reactive为什么不能代理简单类型?
reactive底层依赖proxy,但是proxy只能代理对象,无法代理基础类型。
为什么reactive解构会失去响应式?
这里要明确一点,只有解构出来的变量是基础类型的时候,才会失去响应式,失去响应式的主要原因是基础类型无法被proxy代理。
总结
到此为止,我们的vue3中的响应式模块的第一个API,reactive源码解读就完成了;
总的来说逻辑还是比较复杂的,尽管我已经很努力的去反复修改与简化,但是还是能可以感觉到,有些东西很难用文字讲清楚。
也不知道是否可以帮助到正在阅读文章的你,如果你觉得还不错的话,还麻烦你动动小手点个赞,关注专栏,这是我输出优质文章最大的动力。
如果有小伙伴存在视频教程诉求的话,请评论区告诉我,我会评估出几期视频的必要性~
下一站,我们将前往ref。