(3)vue3 reactive源码解析
专栏前言
本文是vue3源码解析系列的第三篇文档,在前两篇文章中,我们了解了vue3源码的运行、调试,以及阅读前的一些前置知识点,从本节开始,我们就可以正式的开始vue3的源码阅读了。
我们首先阅读的模块是@vue/reactivity中的reactive以及相关api,effect的源代码。
在正文开始之前,我先将本节的简化版源码放出来,有兴趣的同学可以clone到本地,一边debug,一边阅读文章,这样效果更佳~
前言
reactive的含义如其名称,通过reactive创建的对象都是具备响应式的。即reactive对象的改变会造成副作用。
于是我们引出副作用API(effect),如果effect内部依赖了reactive,则reactive的改变会重新触发effect。
现在让我们走进案例与源码,看看究竟是如何实现响应式的。
案例
let { reactive, effect } = Vue
const obj = reactive({
name: '卖鱼强',
})
effect(() => {
document.querySelector('#app').innerText = obj.name
})
setTimeout(() => {
obj.name = '狂飙强'
}, 2000)
以上测试案例,我们涉及到了三个重要的阶段
- reactive初始化
- effect初始化
- reactive发生修改
最后形成了effect的自动触发,我们就从以上三个角度去切入源码实现。
reactive初始化
为了方便阅读与理解,以下仅贴出核心源码
packages/reactivity/src/reactive.ts
export function reactive(target) {
return createReactiveObject(
target, // reactive里面的值
false,
mutableHandlers,
mutableCollectionHandlers,
reactiveMap
)
}
function createReactiveObject(target, isReadonly, baseHandlers, collectionHandlers, proxyMap) {
// 判断是否已经被代理过了,如果是,则获取缓存中的值,并直接返回
// 我们这里第一次指定,必然是不存在的,所以跳过这个
const existingProxy = proxyMap.get(target)
if (existingProxy) {
return existingProxy
}
// 对reactive中的变量进行代理,我们这里的target类型是obejct,targetType为common,所以接下来进入baseHandlers逻辑
// 而baseHandlers从reactive被当做参数传递过来的,实际执行的是mutableHandlers
const proxy = new Proxy(
target,
baseHandlers
)
proxyMap.set(target, proxy)
return proxy
}
// reactive中变量类型为object场景下,proxy的监听逻辑会走到这里
export const mutableHandlers = {
get,
set,
}
通过源码 我们可以看得出来,使用reactive,内部实际执行的是createReactiveObject,函数就是新建了proxy,并最终返回。
不过要注意一点的是,经过reactive处理过的对象,都会以target为WeakMap键,proxy为值,进行一次缓存,这样同一个值再次进行reactive的时候就会读取缓存中的值。

接下来,让我们进入初始化阶段的mutableHandlers,也就是proxy中核心的get set函数,看看内部做了些什么。
初始化读取(get)
当触发obj.name的读取行为的时候,就会触发代理对象的get函数
packages/reactivity/src/baseHandlers.ts
const get = createGetter()
function createGetter() {
return function get(target, key, receiver) {
const res = Reflect.get(target, key, receiver) // 读取被代理对象
// 核心逻辑(track):依赖收集,后续单独看
// 如果当前值是reactive则递归proxy处理
if (isObject(res)) {
return reactive(res)
}
return res
}
}
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
const set = createSetter()
function createSetter(shallow = false) {
return function set(target, key, value, receiver) {
// 修改被代理数据,完成数据更新
const res = Reflect.set(target, key, value, receiver)
// 核心逻辑(trigger):依赖触发,后续单独看
return res // true
}
}
通过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
/**
* 当前被执行的effect
*/
export let activeEffect: ReactiveEffect | undefined
export function effect(fn) {
const _effect = new ReactiveEffect(fn) // 首先执行new ReactiveEffect,所以我们跳转到ReactiveEffect中
_effect.run() // 并立刻执行了run方法,run方法内实际执行的就是effect内部函数
}
export class ReactiveEffect {
parent: ReactiveEffect | undefined = undefined
constructor(
public fn: () => T, // 这里的fn就是effect内部的匿名函数
) {}
run() {
try {
activeEffect = this // 将effect对象,也就是new ReactiveEffect的结果,保存到activeEffect
shouldTrack = true // 表示开始依赖收集
return this.fn() // 这里的fn,实际上就是effect内部的匿名函数
}
}
}
vue3的依赖收集几乎都是通过ReactiveEffect进行完成的,简单来说就是ReactiveEffect.run一旦运行后,就会将当前正在运行的匿名函数保存到内存中,以便于proxy get事件触发的时候,收集保存在内存中的匿名函数,进而完成依赖收集。
effect方法内部,首先new ReactiveEffect 最终执行了一次fn,但是在执行之前,将activeEffect赋值为this,将自身保存到了公共变量activeEffect之中。
让我们来看看此时运行的fn是什么
() => {
document.querySelector('#app').innerText = obj.name
}
匿名函数的内部读取了obj.name,触发了被代理对象obj的get方法.
所以接下来我们回到get方法中,查看之前忽略的依赖收集逻辑。
packages/reactivity/src/baseHandlers.ts
function createGetter(isReadonly = false, shallow = false) {
return function get(target: Target, key: string | symbol, receiver: object) {
const res = Reflect.get(target, key, receiver) // 读取被代理对象
if (!isReadonly) { // obj为可读代码 所以isReadony一定为false 进入if中
track(target, TrackOpTypes.GET, key)
}
return res
}
}
// 依赖收集
function track(target, type, key) {
if (shouldTrack && activeEffect) { // 在effect中执行run方法的时候,我们确保了shouldTrack为true activeEffect 存在值,所以进入判断
let depsMap = targetMap.get(target) // targetMap是一个全局变量,实际上是一个new WeakMap 首次depsMap肯定是不存在的
if (!depsMap) {
// 这里的target为被代理对象,{name: '张三'},该值做为key,Map作为value
targetMap.set(target, (depsMap = new Map()))
}
let dep = depsMap.get(key) // 当前key为name 首次也是不存在的
if (!dep) {
// depsMap是一个Map结构,key是name value是createDep()的返回值,我们进入createDep
depsMap.set(key, (dep = createDep()))
}
// 将dep作为参数传递到trackEffects中,此时的dep为Set
trackEffects(dep, undefined)
}
}
export const createDep = (effects?) => {
const dep = new Set(effects) // 实际上就是生成了Set结构(Set我们简单理解为元素不可重复的数组)
dep.w = 0
dep.n = 0
return dep
}
export function trackEffects(
dep: Dep,
) {
// 一系列边界判断,合法的情况下shouldTrack为true
if (shouldTrack) {
dep.add(activeEffect!)
// 将全局变量activeEffect(包含effect的匿名函数)加入到dep(Set)中
// 到这里 我们将响应式数据与effect函数建立起了联系 标志着我们完成了依赖收集
}
}
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
function createSetter(shallow = false) {
return function set(target, key, value, receiver): boolean {
const result = Reflect.set(target, key, value, receiver) // 完成被代理对象的赋值操作
trigger(target, TriggerOpTypes.SET, key, value, oldValue)
return result
}
export function trigger(target, type, key?, newValue?, oldValue?, oldTarget?) {
// 通过全局变量targetMap(weakMap)获取value
// 在依赖收集阶段我们收集到了当前target,所以这时候 depsMap存在值 值为Map Map的key为name 值为Set Set内部是effect的fn
const depsMap = targetMap.get(target)
triggerEffects(depsMap.get(key))
}
export function triggerEffects(dep, debuggerEventExtraInfo?) {
const effects = isArray(dep) ? dep : [...dep] // 将set处理为数组
for (const effect of effects) {
triggerEffect(effect, debuggerEventExtraInfo)
}
}
function triggerEffect(
effect: ReactiveEffect, // 每一个effect都是ReactiveEffect,内部的fn都是effect的fn
) {
// 此时的activeEffect为undefined,一定进入if中
if (effect !== activeEffect || effect.allowRecurse) {
effect.run() // effect的run方法就是effect的fn,完成执行
}
}
经过以上代码,我们可以了解到,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。
转载自:https://juejin.cn/post/7202132390549553211