前言:本文讲述vue源码中的reactivity模块,也就是响应式核心模块。
建议先看如下文章:
applyOptions
在组件的一系列初始化之后,来到参数初始化(applyOptions)
applyOptions
方法中,将传入的各个“选项”进行初始化构建,负责将组件的各种选项(如data、computed、methods、生命周期等)应用到组件实例上
我们例子是一个选项式API的代码,按照上面截图的代码,传入的“data选项”就被reactive
响应化了,下面具体看下reactive
内部的构建逻辑。
reactive
然后进入reactive
方法中,看看到底是如何响应式构建的。
- 首先判断上述传入的值是否为只读类型(也就是通过
readonly()
设置的变量https://cn.vuejs.org/api/reactivity-core.html#readonly) - 上述所传入的参数中,第三第四个都是代理处理函数,最后一个参数是全局Map依赖,具体如下
createReactiveObject
上述createReactiveObject
方法就是构建核心代码,具体看看内部逻辑
Proxy
是一个JavaScript全局方法,代理一个对象,简单理解就是当获取这个对象的属性值或者给这个对象设置值都可以拦截二次处理,详细可以查看以下文档:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Proxy
- 275行代码是响应式核心:条件判断的两个函数,这两个是对
data
原对象的代理拦截处理方法
collectionHandlers
baseHandlers
- 277行代码中的
proxyMap
参数,也就是reactive
主文件下的一个全局Map,主要用于依赖收集等工作
这个函数执行完以后,响应式的初始化构建就完成了(选项式API中的data),接下来具体看下上述的两个代理拦截处理方法。
baseHandlers
根据上述判断条件,我们先分析baseHandlers
拦截方法
Proxy内部进不去,所以我们直接在baseHandlers
方法中设置断点
具体的MutableReactiveHandler
类如下:
Reflect
可以简单理解为操作对象,可以设置值,取值等操作,具体详见:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Reflect
get(依赖收集(track))
在BaseReactiveHandler
中的get
方法中:
- 159行代码递归实现深层对象的响应化:当对象是一个深层嵌套对象,在这行代码则使用了递归来实现,当然,vue通过对当前代理是否为只读状态(
isReadonly
)来优化是否继续递归 - 143行代码依赖收集(track):是父类get方法里面的依赖收集方法,它记录下当前执行的副作用函数依赖了哪些数据。这是通过全局的
activeEffect
和数据特定的依赖列表(Dep)来实现的。每当副作用函数访问一个响应式数据时,就会将这个副作用函数添加到该数据的依赖列表中。
同样进入track函数:
- 获取或创建依赖集合(
dep
):接着,尝试从depsMap
中获取当前属性的依赖集合。如果这个属性还没有对应的依赖集合,就创建一个新的Set
来存储依赖于这个属性的所有副作用,并将其添加到depsMap
中。
- 跟踪副作用:最后,调用
trackEffect
函数将当前的activeEffect
添加到dep中。这表明当前正在执行的副作用函数依赖于访问的属性。trackEffect
函数还负责处理一些额外的逻辑,比如避免重复跟踪同一个副作用,以及在开发模式下提供额外的调试信息。
set(触发更新(trigger))
当响应式变量被改变会走该部分代码
- 触发更新(
trigger
):核心是上面204行代码(新增)和206行代码(更新),可以理解成副作用函数,当响应式变量更新或者新增响应式变量,会查找该数据的依赖列表,找到所有依赖于这个数据的副作用函数,并重新执行它们,以响应数据的变化,比如组件的渲染函数或计算属性,trigger
确保了当数据更新时,所有依赖于这些数据的视图或计算值也会相应地被更新
我们通过试图改变这个响应式变量,点击v2-compat
选项通过试图改变响应式变量,会走进206代码的debugger
贴上对应视图层源码部分
然后F9
走进以下的trigger
函数中看下源码
- 参数部分
target
:触发更新的响应式对象type
:触发更新的操作类型,在这里是set
key
:被操作的目标属性的键newValue
:新值oldValue
:旧值oldTarget
:暂不知道这旧集合对象的用处(TODO: )
- 73行代码:获取依赖映射
- 79行代码:定义需要被触发的依赖集合
deps
- 80行到124行代码:最终确定依赖集合
- 其中126行代码和145行代码的
pauseScheduling
和resetScheduling
函数:用于控制副作用(effects
)触发的调度行为,设计目的是为了提高性能和避免不必要的重复计算,尤其是在一次性执行多个响应式状态更新时。它们通过暂停和重置调度器(scheduler
)的运行状态,允许Vue批量处理响应式状态的更新,然后统一触发副作用的执行。 - 127行代码:触发副作用,核心则是
triggerEffects
方法
deleteProperty,has,ownKeys
这三个方法分别用于拦截对象属性的删除、属性检查和键的枚举操作
- has:in操作符的捕捉器
- deleteProperty:delete操作符的捕捉器
- ownKeys:Object.getOwnPropertyNames 方法和 Object.getOwnPropertySymbols 方法的捕捉器
具体也就是二次添加了触发更新/依赖收集的处理拦截方法,比如:
const { reactive, effect } = Vue;
const obj = reactive({
name: 'Vue',
version: '3.0'
});
effect(() => {
console.log(`The name is: ${obj.name}`);
});
// 删除属性前,effect 会打印: "The name is: Vue"
delete obj.name;
// 删除属性后,由于响应式系统的deleteProperty拦截,effect 会再次执行
// 打印: "The name is: undefined"
collectionHandlers
回到createReactiveObject
方法中,上面调用baseHandlers
是一个简单的选项式api下的data
数据,那么什么情况下会执行collectionHandlers
拦截方法呢?
可以看到上述269行代码,貌似是获取target的类型
所以我们得到:collectionHandlers是专门用于处理Map、Set、WeakMap和WeakSet这类集合类型的响应式操作的处理程序集。这些处理程序通过代理(Proxy)机制拦截对这些集合的操作,如添加、删除元素,以及遍历集合等操作,从而使得集合的操作也成为响应式的。
- 对于数组vue2需要进行对数组的方法进行拦截处理,同时还需要对数组或者嵌套对象进行递归处理响应化
- Proxy直接代理数组的所有操作
具体进入到collectionHandlers
方法中:
collectionHandlers
的执行时机与集合类型的响应式代理创建和集合操作紧密相关,它确保了集合的响应式操作能够触发视图更新和副作用函数的执行,是Vue 3响应式系统支持集合类型的关键机制
- 340行代码参数部分
- isReadonly:如果为true,则创建的响应式代理将是只读的,否则是可写的
- shallow:如果为true,则创建的响应式代理将是浅响应式的,否则是深响应式的
- 341行代码根据340行的两个参数最终得到的
instrumentations
对象,这个就是最后实现的代理函数
- 可以从截图看到这种类型的代理实现比普通对象来的简单
- 372行代码
collectionHandlers
通过createInstrumentationGetter
函数生成的get拦截器管理了对集合的响应式访问和修改
end
响应式对象的构建流程就结束了。
上述的很多细节(方法的具体实现)以及暴露出去的接口(computed、watch等)还可以进一步分析代码。
评论区