Vue3 源码解析 - reactive 响应式工作原理
前言
从 reactive
响应式函数入手,探究 Vue3 响应式原理实现。首先从官方文档的基本使用开始,然后解读 reactive
函数源码,理解它的功能是怎么实现,知其然到知其所以然。最后是源码调试,同时理解 Vue3 渲染和更新流程,理论和实践结合,理解响应式工作原理
为了更好理解掌握,可以打开 链接 根据下面的调试部分动手试试
reactive 基本使用
reactive
是用于创建响应式数据的函数,接收一个引用对象,并返回一个响应式的 Proxy
对象。当我们修改这个 Proxy
对象时,Vue3会自动追踪这些变化,并触发视图相应的更新
1、reactive
函数的基本使用方法
reactive
只能用于对象类型 (对象、数组和如 Map
、Set
这样的集合类型)。不支持 string
、number
或 boolean
这样的原始类型
import { reactive } from 'vue';
let state = reactive({
count: 0
});
// 在组件中使用 state.count 并且当修改值时,界面会自动更新
2、不能替换整个对象 state
,不能直接解构
如果直接赋值 state
会改变它的引用指向,不再是 proxy
对象,丢失了响应式
state = {
count: 1
}
state.count = 2 // 视图不会更新,丢失了响应式
直接解构,同样断开了响应式连接
const state = reactive({ count: 0 })
// 当解构时,count 已经与 state.count 断开连接
let { count } = state
3、reactive
创建集合对象
reactive 函数可以支持 Set
和 Map
数据结构
import { reactive } from 'vue'
const state = reactive(new Map())
state.set('count', 1)
console.log(state.get('count')) // 1
4、Reactive Proxy 对象 vs 原对象
reactive
创建的 Proxy
对象和原对象是不相等的,如果重复创建响应式对象,和原来的对象是相等的
import { reactive } from 'vue'
const data = { count: 0 }
const state = reactive(data)
// 代理对象和原始对象不是全等的
console.log(data === state) // false
const state1 = reactive(data) // 在同一个对象上调用 reactive() 会返回相同的代理
console.log(state1 === state) // true
5、reactive
传入 ref
对象 value
会自动解包
ref
创建对象传入 reactive
函数,不需要访问 .value
会自动解包,并且修改 state.count
的值,ref
值也会发生变化
import { reactive, ref } from 'vue'
const data = ref(0)
const state = reactive({ count: data }) // 自动解构 data.value
state.count = 1
console.log(state.count, data.value) // 1 1
data.value += 1
console.log(state.count) // 2
注意,数组和集合不会自动解包
const books = reactive([ref('Vue 3 Guide')])
// 这里需要 .value
console.log(books[0].value)
const map = reactive(new Map([['count', ref(0)]]))
// 这里需要 .value
console.log(map.get('count').value)
reactive 源码解析
访问链接,F12 打开控制台,刷新页面
<script setup>
debugger
import { reactive } from 'vue'
const state = reactive({
count: 0
})
</script>
<template>
<h1>{{ state.count }}</h1>
<button @click="state.count++"> add </button>
</template>
单步进入 reactive
函数调试
reactive 函数
reactive
API 接受一个对象参数,通过 createReactiveObject
函数处理后,直接返回一个 proxy
对象
export function reactive(target: object) {
// 如果试图去观察一个只读的代理对象,会直接返回只读版本
if (target && (target as Target)[ReactiveFlags.IS_READONLY]) {
return target
}
// 创建一个代理对象并返回
return createReactiveObject(
target,
false,
mutableHandlers,
mutableCollectionHandlers,
reactiveMap
)
}
其中调用 isReadonly
判断 target
中是否有只读对象 key ReactiveFlags.IS_READONLY
,如果是直接返回
ReactiveFlags
枚举对象会在源码中多个地方引用,isRef
, isReadonly
等工具方法都是根据对象是否有这些 key
判断的
export const enum ReactiveFlags {
SKIP = '__v_skip', // 是否跳过响应式 返回原始对象
IS_REACTIVE = '__v_isReactive', // 标记一个响应式对象
IS_READONLY = '__v_isReadonly', // 标记一个只读对象
RAW = '__v_raw' // 标记获取原始值
}
createReactiveObject
createReactiveObject
函数执行返回一个 proxy
实例对象,函数接收 5 个参数
target
:传入reactive
的目标对象isReadonly
:生成的代理对象是否只读。shallowReadonly
,readonly
传入 truebaseHandlers
:生成代理对象的handler
参数。当 target 类型是 Array 或 Object 时使用该handler
。collectionHandlers
:当target
类型是 Map、Set、WeakMap、WeakSet 时使用该handler
。proxyMap
:存储生成代理对象后的Map
对象,防止重复创建
function createReactiveObject(
target,
isReadonly,
baseHandlers,
collectionHandlers,
proxyMap
) {
// 目标必须是对象类型(包括数组、set、map结构)
if (!isObject(target)) {
if (__DEV__) {
console.warn(`value cannot be made reactive: ${String(target)}`)
}
return target
}
// target 已经是 Proxy类型对象,直接返回
// 有个例外,如果 readonly 是响应式对象,则继续
if (
target[ReactiveFlags.RAW] &&
!(isReadonly && target[ReactiveFlags.IS_REACTIVE])
) {
return target
}
// 目标已经存在对应的代理对象
const existingProxy = proxyMap.get(target)
if (existingProxy) {
return existingProxy
}
// 只有白名单里的类型才能被创建响应式对象,1是对象或数组类型,2是 Set/Map/WeakSet/WeakMap类型
const targetType = getTargetType(target)
if (targetType === TargetType.INVALID) {
return target
}
// 利用 Proxy 创建响应式对象,对象或数组类型使用 baseHandlers,Set/Map/WeakSet/WeakMap类型collectionHandlers
const proxy = new Proxy(
target,
targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
)
// 缓存已经代理的对象
proxyMap.set(target, proxy)
return proxy
}
可以看到,在该函数中基础数据类型会直接返回原始值。
将已生成的代理对象缓存进 proxyMap
,当这个代理对象已存在时不会重复生成,会直接返回已有对象。
然后通过 TargetType
来判断 target
目标对象的类型,仅支持 Array、Object、Map、Set、WeakMap、WeakSet 生成代理,其他对象会被标记为 INVALID
,并返回原始值。
当目标对象通过类型校验后,会通过 new Proxy()
生成一个代理对象 proxy
,Proxy handler
参数根据 target
的类型进行判断,对象和数组使用 baseHandlers
,集合类型使用 collectionHandlers
假如 reactive
函数传入的是对象,baseHandlers
取的是 mutableHandlers
,它是从 baseHandlers 文件引入
import {
mutableHandlers, // reactive 函数
readonlyHandlers, // readonly 函数
shallowReactiveHandlers, // shallowReactive 函数
shallowReadonlyHandlers // shallowReadonly 函数
} from './baseHandlers'
mutableHandlers
对象定义了 5 个拦截方法,分别是 get
,set
,deleteProperty
,has
,ownKeys
方法
export const mutableHandlers: ProxyHandler<object> = {
get, // 用于拦截对象的读取属性操作
set, // 用于拦截对象的设置属性操作
deleteProperty, // 用于拦截对象的删除属性操作
has, // 检查一个对象是否拥有某个属性
ownKeys // 针对 getOwnPropertyNames, getOwnPropertySymbols, keys 的代理方法
}
get
和 set
都是通过 createGetter
工厂函数生成的,以便适配除 reactive
外的其他 api,例如 shallowReactive
、readonly
、shallowReadonly
等。
const get = createGetter()
const shallowGet = createGetter(false, true)
const readonlyGet = createGetter(true)
const shallowReadonlyGet = createGetter(true, true)
createGetter
根据不同参数生成不同的功能函数,运用到了函数柯里化思想
get 函数
对特殊 key 处理
get
函数对 key
进行特殊处理
// 如果 get 访问的 key 是 '__v_isReactive',返回 createGetter 的 isReadonly 参数取反结果
if (key === ReactiveFlags.IS_REACTIVE) {
return !isReadonly
} else if (key === ReactiveFlags.IS_READONLY) {
// 如果 get 访问的 key 是 '__v_isReadonly',返回 createGetter 的 isReadonly 参数
return isReadonly
} else if (
// 如果 get 访问的 key 是 '__v_raw',并且 receiver 与原始标识相等,则返回原始值
key === ReactiveFlags.RAW &&
receiver ===
(isReadonly
? shallow
? shallowReadonlyMap
: readonlyMap
: shallow
? shallowReactiveMap
: reactiveMap
).get(target)
) {
return target
}
数组特殊方法处理
判断 target
不是只读对象,并且目标对象是个数组,访问的 key
又在数组需要劫持的方法里,直接调用修改后的数组方法执行
const targetIsArray = isArray(target)
if (!isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) {
return Reflect.get(arrayInstrumentations, key, receiver)
}
arrayInstrumentations
对象包括的数组方法
- 索引数组方法:
includes
、indexOf
、lastIndexOf
- 改变数组长度 length 方法:
push
、pop
、shift
、unshift
、splice
const arrayInstrumentations = /*#__PURE__*/ createArrayInstrumentations()
function createArrayInstrumentations() {
const instrumentations = {}
;(['includes', 'indexOf', 'lastIndexOf'] as const).forEach(key => {
instrumentations[key] = function (this: unknown[], ...args: unknown[]) {
// toRaw 可以把响应式对象转成原始数据
const arr = toRaw(this) as any
for (let i = 0, l = this.length; i < l; i++) {
// 依赖收集
track(arr, TrackOpTypes.GET, i + '')
}
// 先尝试用参数本身,可能是响应式数据
const res = arr[key](...args)
if (res === -1 || res === false) {
// 如果失败,再尝试把参数转成原始数据
return arr[key](...args.map(toRaw))
} else {
return res
}
}
})
;(['push', 'pop', 'shift', 'unshift', 'splice'] as const).forEach(key => {
instrumentations[key] = function (this: unknown[], ...args: unknown[]) {
pauseTracking()
const res = (toRaw(this) as any)[key].apply(this, args)
resetTracking()
return res
}
})
return instrumentations
}
之所以 get
函数跳过数组这些内置处理方法,原因是它们都会访问和修改数组的 length
属性,如果重复访问,又同时被修改了,会造成循环引用
对象类型处理
createGetter 函数传入的两个参数 isReadonly
,shallow
-
readonly
函数isReadonly
为 true 是只读对象,不进行track
依赖收集 -
shallowReactive
函数shallow
为 true 是浅层响应式,直接返回 get 结果,不做递归执行 -
如果传入的是
ref
值,不是数组类型,会自动解包 -
最后判断如果是嵌套引用类型,会进行递归处理,将最终结果返回
/**
* @description: 用于拦截对象的读取属性操作
* @param {isReadonly} 是否只读
* @param {shallow} 是否浅观察
*/
function createGetter(isReadonly = false, shallow = false) {
return function get(target, key, receiver) {
// 省略这段判断
const res = Reflect.get(target, key, receiver)
// 如果是 key 是 Symbol,并且 key 是 Symbol 对象中的 Symbol 类型的 key
// 或者 key 是不需要追踪的 key: __proto__,__v_isRef,__isVue
// 直接返回 get 结果
if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {
return res
}
// 只读不进行依赖收集
if (!isReadonly) {
track(target, TrackOpTypes.GET, key)
}
// 如果是 shallow 浅层响应式,直接返回 get 结果
if (shallow) {
return res
}
// 如果是 ref ,则返回解包后的值 - 当 target 是数组,key 是 int 类型时,不需要解包
if (isRef(res)) {
return targetIsArray && isIntegerKey(key) ? res : res.value
}
// 值是引用类型,递归reactive处理响应式
if (isObject(res)) {
return isReadonly ? readonly(res) : reactive(res)
}
return res
}
}
set 函数
set
函数相对简单点,它也有一个 createSetter
的工厂函数,通过柯里化的方式返回一个 set
函数。
const set = /*#__PURE__*/ createSetter()
const shallowSet = /*#__PURE__*/ createSetter(true)
set
函数判断 target
是否相同,然后判断是否是存在的数组内点下标和对象的 key,调用 trigger
进行派发更新
function createSetter(shallow = false) {
return function set(
target: object,
key: string | symbol,
value: unknown,
receiver: object
): boolean {
let oldValue = (target as any)[key]
if (isReadonly(oldValue) && isRef(oldValue) && !isRef(value)) {
return false
}
// 如果模式不是浅观察
if (!shallow) {
if (!isShallow(value) && !isReadonly(value)) {
oldValue = toRaw(oldValue)
value = toRaw(value)
}
// 目标对象不是数组,旧值是ref,新值不是ref,则直接赋值
if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
oldValue.value = value
return true
}
} else {
// in shallow mode, objects are set as-is regardless of reactive or not
}
const hadKey =
isArray(target) && isIntegerKey(key)
? Number(key) < target.length
: hasOwn(target, key)
const result = Reflect.set(target, key, value, receiver)
// 如果目标是原型链的某个属性,通过 Refect.set 修改它会再次触发 setter,这种情况没必要触发两次 trigger
if (target === toRaw(receiver)) {
if (!hadKey) {
trigger(target, TriggerOpTypes.ADD, key, value)
} else if (hasChanged(value, oldValue)) {
trigger(target, TriggerOpTypes.SET, key, value, oldValue)
}
}
return result
}
}
这时可能有一个疑问, reactive
只负责创建数据对象,那么它是怎么和视图渲染和更新进行关联的呢,接下来从组件的挂载和渲染分析揭晓
组件挂载渲染
源码调试
在 vue 文件搜索 createAppAPI
,在 mount
挂载方法上打上断点
它首先执行 createVNode
创建虚拟 DOM 对象
然后执行 render
函数,调用 patch
方法,目标是将虚拟 DOM 转化为真实的 DOM
如果是渲染组件而非元素,调用 processComponent
方法
单步进入,调用 mountComponent
方法,继续往下执行
mountComponent
方法调用 createComponentInstance
创建组建实例 instance
,将其作为参数传入 setupComponent
,主要是调用我们定义的 setup
函数,所以 setup
函数执行时机是在 mount
方法
执行 setup
函数,会返回一个函数,在 handleSetupResult
函数,赋值给组件实例的 render 作为渲染函数
setup 函数执行了,reactive
对象也创建了,接下来就是将 reactive 数据和render
函数做关联
当执行 render
函数访问 reactive
数据,触发 get
拦截方法进行依赖追踪,当数据发现变化,触发 set
进行视图更新,那么起中间桥梁的是 ReactiveEffect
ReactiveEffect
ReactiveEffect 类实例化主要传入两个参数,第一个是 effect
副作用函数,第二个参数是调度配置对象
重点理解 run
方法,它执行后,将副作用函数收集到 reactive
对应数据依赖集合上,这时数据和模版 render 关联上了,当修改 reactive
数据,自然可以从依赖中取出,然后执行副作用函数更新视图
export class ReactiveEffect<T = any> {
constructor(fn, scheduler, scope) {
recordEffectScope(this, scope)
}
run() {
if (!this.active) {
return this.fn()
}
try {
this.parent = activeEffect
// 当前激活的副作用对象
activeEffect = this
shouldTrack = true
trackOpBit = 1 << ++effectTrackDepth
if (effectTrackDepth <= maxMarkerBits) {
initDepMarkers(this)
} else {
cleanupEffect(this)
}
// 执行副作用函数
return this.fn()
} finally {
if (effectTrackDepth <= maxMarkerBits) {
// 完成依赖标记
finalizeDepMarkers(this)
}
trackOpBit = 1 << --effectTrackDepth
activeEffect = this.parent
shouldTrack = lastShouldTrack
this.parent = undefined
if (this.deferStop) {
this.stop()
}
}
}
stop() {
//
if (activeEffect === this) {
this.deferStop = true
} else if (this.active) {
cleanupEffect(this)
if (this.onStop) {
this.onStop()
}
this.active = false
}
}
}
继续往下执行 setupRenderEffect
,实例化 ReactiveEffect
,传入第一个参数 componentUpdateFn
作为副作用函数 fn
在函数最后执行 update
方法,执行的是 ReactiveEffect 实例的 run
方法
搜索 ReactiveEffect
在 run
打上断点
执行 run
方法,将当前 this
实例对象赋值给了全局变量 activeEffect
,然后执行副作用函数 fn
,也就是组件的更新函数 `
单步进入 fn
执行,也就是执行 componentUpdateFn
组件函数,调用 renderComponentRoot
,执行 render
函数
这时就会到了 setup
返回的 渲染函数,内部就会访问到 state.count
,触发它的 get
函数,在 get 函数上打上断点(可以搜索 createGetter)
track 函数进行依赖收集了
track 依赖收集
track
函数会将当前激活的 activeEffect
添加到 targetMap
映射上
上面的 onTrack
函数可以用于辅助调试收集依赖
逻辑上,创建一个全局的 WeakMap targetMap
映射,target
作为 key
,Map
作为 value
,然后为 target
的每一个 key
又创建映射,收集的依赖存储到 Set 集合中
const targetMap = new WeakMap(); // 为每个响应式对象存储依赖关系
// 然后track()函数就需要首先拿到targetMap的depsMap
function track(target, key) {
let depsMap = targetMap.get(target); // target是响应式对象的名称,key是对象中属性的名称
if (!depsMap){ // 不存在则为这个对象创建一个新的deps图
targetMap.set(target, (depsMap = new Map())
};
let dep = depsMap.get(key); // 获取属性的依赖对象,和之前的一致了
if(!dep){ //同样不存在就创建一个新的
depsMap.set(key, (dep = new Set()))
};
dep.add(activeEffect);
}
依赖收集好了,最后执行 patch
挂载,组件的渲染就执行完了
trigger 派发更新
当点击按钮,修改 state.count
值,触发了 set
函数,执行 trigger
派发更新
继续往下执行,根据 target 之前的映射,取出之前收集的依赖集合,遍历执行,然后调用依赖对象副作用函数 componentUpdateFn
进行更新视图
Vue 的更新并不是同步的,而是放到 Promise
微任务队列中,做批量更新
任务队列 flushJobs
执行 ReactiveEffect run
方法,走 patch 的更新 vnode
拿到最新的数据,最后又回到开始渲染的流程,再走一遍,此时 patch
是更新 DOM
总结
reactive
函数通过 Proxy
创建一个代理对象,定义了拦截方法
在组件渲染时,会触发 reactive get
方法,通过 track
的处理器函数来收集依赖,将组件更新函数添加到数据的依赖集合中
当数据发现变化,会触发 set
方法,通过 trigger
的处理器函数来派发更新,每个依赖的使用都会被包裹到一个副作用(effect)函数中,而派发更新后就会执行副作用函数,这样视图依赖的值就被更新了
转载自:https://juejin.cn/post/7264073604496228411