Vue 3.x 响应式原理——ref源码分析
source link: https://zhuanlan.zhihu.com/p/95010735
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
Vue 3.x 响应式原理——ref源码分析
在上一篇文章Vue 3.x 响应式原理——reactive源码分析中,笔者简述了Vue 3.x 的 reactive
API 的实现原理,了解过 Vue Composition API 的同学都知道reactive
和ref
创建响应式数据的区别,本文通过讲述ref
API 的实现原理,帮助更进一步了解 Vue 3.x 的响应式原理。
阅读此文之前,如果对以下知识点不够了解,可以先了解以下知识点:
笔者之前也写过相关文章,也可以结合相关文章:
Ref对象
ref
的作用是提供响应式包装对象,便于利用Vue Composition API 进行函数式的组装,首先我们通过isRef
函数入口,看看Vue 3.x 是如何标识ref
对象的:
const isRefSymbol = Symbol()
export interface Ref<T = any> {
// This field is necessary to allow TS to differentiate a Ref from a plain
// object that happens to have a "value" field.
// However, checking a symbol on an arbitrary object is much slower than
// checking a plain property, so we use a _isRef plain property for isRef()
// check in the actual implementation.
// The reason for not just declaring _isRef in the interface is because we
// don't want this internal field to leak into userland autocompletion -
// a private symbol, on the other hand, achieves just that.
[isRefSymbol]: true // 用一个symbol来标识ref对象,但是后面又被改成了通过_isRef属性来标识
value: UnwrapRef<T> // 响应式包装对象的value属性,是解包装的值
}
// ...
export function isRef(r: any): r is Ref {
// 通过_isRef属性判断一个对象是否是ref对象
return r ? r._isRef === true : false
}
看上面代码,我们可以认识到,ref
对象总会被挂载一个叫做_isRef
的属性,所以通过_isRef
这个属性是否存在就可以帮助我们判断一个对象是否是ref
对象。
此外,ref
对象含有一个属性叫value
,value
的类型是UnwrapRef<T>
,下面看看UnwrapRef<T>
:
export interface ComputedRef<T = any> extends WritableComputedRef<T> {
readonly value: UnwrapRef<T>
}
type UnwrapArray<T> = { [P in keyof T]: UnwrapRef<T[P]> }
// Recursively unwraps nested value bindings.
// 递归获取包装对象value的类型
// 因为ref不能是嵌套的ref,即value不能是一个ref对象
export type UnwrapRef<T> = {
// 如果遇到value是computedRef类型,解套求其value的类型
cRef: T extends ComputedRef<infer V> ? UnwrapRef<V> : T
// 如果遇到value是Ref类型,解套求其value的类型
ref: T extends Ref<infer V> ? UnwrapRef<V> : T
// 如果遇到value是数组,对数组里每一项解包装
array: T extends Array<infer V> ? Array<UnwrapRef<V>> & UnwrapArray<T> : T
// 如果遇到value是对象,对对象每一项遍历解包装
object: { [K in keyof T]: UnwrapRef<T[K]> }
}[T extends ComputedRef<any>
? 'cRef'
: T extends Ref
? 'ref'
: T extends Array<any>
? 'array'
: T extends Function | CollectionTypes
? 'ref' // bail out on types that shouldn't be unwrapped
: T extends object ? 'object' : 'ref']
通过上面的代码我们看到ref
响应式包装对象的value
的类型一定是一个解包装的对象,而不能是嵌套的ref
。对于数组和对象类型,需要对其进行遍历,保证其中每项都没有嵌套ref
对象,如果有嵌套的情况,需要再进行解包装。
下面来看ref
函数,ref
函数将一个普通对象转化为响应式包装对象:
export function ref<T extends Ref>(raw: T): T
export function ref<T>(raw: T): Ref<T>
export function ref<T = any>(): Ref<T>
export function ref(raw?: unknown) {
// 已经是ref对象了,直接返回原始值
if (isRef(raw)) {
return raw
}
// 转化为ref对象
raw = convert(raw)
const r = {
_isRef: true,
get value() {
// getter触发时,触发依赖收集,源码在effect部分,在笔者下一篇文章会有讲述
track(r, OperationTypes.GET, 'value')
return raw
},
set value(newVal) {
// setter触发时,首先调用convert转化为ref对象
raw = convert(newVal)
// trigger通知deps,通知依赖这一状态的对象更新
trigger(
r,
OperationTypes.SET,
'value',
__DEV__ ? { newValue: newVal } : void 0
)
}
}
return r
}
// convert的作用是创建响应式包装对象,这里直接使用reactive,其原理在上一节有讲过
const convert = <T extends unknown>(val: T): T =>
isObject(val) ? reactive(val) : val
通过ref
函数,我们了解到了ref
的底层就是reactive
,ref
对象具有对应的 getter 和 setter ,getter总是返回经过convert
转化后的响应式对象raw
,并触发 Vue 的依赖收集,对ref
对象赋值会调用setter
,setter
调用会通知deps,通知依赖这一状态的对象更新,并重新更新raw
,raw
被保存为新的响应式包装对象。
toRefs
最后我们来看toRefs
,toRefs
将reactive
对象转换为普通对象,其中结果对象上的每个属性都是指向原始对象中相应属性的ref
引用对象,这在组合函数返回响应式状态时非常有用,这样保证了开发者使用对象解构或拓展运算符不会丢失原有响应式对象的响应。
export function toRefs<T extends object>(
object: T
): { [K in keyof T]: Ref<T[K]> } {
const ret: any = {}
// 遍历对象的所有属性,都对其调用toProxyRef
for (const key in object) {
ret[key] = toProxyRef(object, key)
}
return ret
}
// toProxyRef相当于把对象的每个属性都变成一个包装对象,这样在结构和使用拓展运算符时,就不会丢失原有响应式对象的引用了
function toProxyRef<T extends object, K extends keyof T>(
object: T,
key: K
): Ref<T[K]> {
return {
_isRef: true,
get value(): any {
return object[key]
},
set value(newVal) {
object[key] = newVal
}
} as any
}
本文介绍了 Vue 3.x 的Ref
对象的原理,ref
的作用是便于我们使用 Vue 3.x 进行组件组合时,通过函数传参不会丢失响应式对象的原始引用,其原理的核心是包装和解包装,包装时,我们保证ref
对象的value
不能嵌套ref
对象,所以使用了UnwrapRef
,同时,对于对象和数组,包装和解包装时需要对其中每项进行遍历,以保证不会出现嵌套ref
对象的情况。
后面笔者会对effect
模块进行分析,讲述 Vue 3.x 在依赖收集原理。
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK