前言:上一篇文章记录了vue3组合式API的和vue2选项式API的区别,这篇文章则主要是对组合式API中setup使用注意项和ref的源码解读学习
setup
setup
组件选项在组件创建之前执行,一旦props
解决,并用作组合 API
的入口点。
注意:
setup
函数的执行时机是在beforeCreate
和created
之前- 由于
setup
执行时机是在created
之前,所以组件才刚刚被创建,而data
和methods
还没初始化好,所以无法在setup
中使用data
和methods
setup
中this
指向undefined
setup
只能是同步的,不能是异步的
ref源码导读
ref最重要的作用,其实是提供了一套Ref类型,我们先来看,它到底是个怎么样的数据类型。
// 生成一个唯一key,开发环境下增加描述符 'refSymbol'
export const refSymbol = Symbol(__DEV__ ? 'refSymbol' : undefined)
// 声明Ref接口
export interface Ref<T = any> {
// 用此唯一key,来做Ref接口的一个描述符,让isRef函数做类型判断
[refSymbol]: true
// value值,存放真正的数据的地方。关于UnwrapNestedRefs这个类型,我后续单独解释
value: UnwrapNestedRefs<T>
}
// 判断是否是Ref数据的方法
// 对于is关键词,若不熟悉,见:http://www.typescriptlang.org/docs/handbook/advanced-types.html#using-type-predicates
export function isRef(v: any): v is Ref {
return v ? v[refSymbol] === true : false
}
// 见下文解释
export type UnwrapNestedRefs<T> = T extends Ref ? T : UnwrapRef<T>
要想了解UnwrapNestedRefs
与UnwrapRef
,必须先要了解ts中的infer。如果之前不了解,请先阅读相关文档。看完文档,再建议去google
一些案例看看加深下印象。再来看源码:
// 不应该继续递归的引用数据类型
type BailTypes =
| Function
| Map<any, any>
| Set<any>
| WeakMap<any, any>
| WeakSet<any>
// 递归地获取嵌套数据的类型
// Recursively unwraps nested value bindings.
export type UnwrapRef<T> = {
// 如果是ref类型,继续解套
ref: T extends Ref<infer V> ? UnwrapRef<V> : T
// 如果是数组,循环解套
array: T extends Array<infer V> ? Array<UnwrapRef<V>> : T
// 如果是对象,遍历解套
object: { [K in keyof T]: UnwrapRef<T[K]> }
// 否则,停止解套
stop: T
}[T extends Ref
? 'ref'
: T extends Array<any>
? 'array'
: T extends BailTypes
? 'stop' // bail out on types that shouldn't be unwrapped
: T extends object ? 'object' : 'stop']
// 声明类型别名:UnwrapNestedRefs
// 它是这样的类型:如果该类型已经继承于Ref,则不需要解套,否则可能是嵌套的ref,走递归解套
export type UnwrapNestedRefs<T> = T extends Ref ? T : UnwrapRef<T>
Ref
是这样的一种数据结构:它有个key
为Symbol
的属性做类型标识,有个属性value
用来存储数据。这个数据可以是任意的类型,唯独不能是被嵌套了Ref类型的类型。 具体来说就是不能是这样 Array<Ref>
或者这样 { [key]: Ref }
。但很奇怪的是,这样Ref 又是可以的。
另外,Map、Set、WeakMap、WeakSet
也是不支持解套的。说明Ref数据的value
也有可能是Map<Ref>
这样的数据类型。Ref
类型的数据,是一种响应式的数据。然后我们看其具体实现:
// 从@vue/shared中引入,判断一个数据是否为对象
// Record<any, any>代表了任意类型key,任意类型value的类型
// 为什么不是 val is object 呢?可以看下这个回答:https://stackoverflow.com/questions/52245366/in-typescript-is-there-a-difference-between-types-object-and-recordany-any
export const isObject = (val: any): val is Record<any, any> =>
val !== null && typeof val === 'object'
// 如果传递的值是个对象(包含数组/Map/Set/WeakMap/WeakSet),则使用reactive执行,否则返回原数据
// 从上篇文章知道,这个reactive就是将我们的数据转成响应式数据
const convert = (val: any): any => (isObject(val) ? reactive(val) : val)
export function ref<T>(raw: T): Ref<T> {
// 转化数据
raw = convert(raw)
const v = {
[refSymbol]: true,
get value() {
// track的代码在effect中,暂时不看,能猜到此处就是监听函数收集依赖的方法。
track(v, OperationTypes.GET, '')
// 返回刚刚被转化后的数据
return raw
},
set value(newVal) {
// 将设置的值,转化为响应式数据,赋值给raw
raw = convert(newVal)
// trigger也暂时不看,能猜到此处就是触发监听函数执行的方法
trigger(v, OperationTypes.SET, '')
}
}
return v as Ref<T>
}
其实最难理解的就在于这个ref
函数。我们看到,这里也定义了get/set
,却没有任何Proxy
相关的操作。在之前的信息中我们知道reactive
能构建出响应式数据,但要求传参必须是对象。但ref
的入参是对象时,同样也需要reactive
做转化。那ref
这个函数的目的到底是什么呢?为什么需要有它?
参照vue3官方文档就可以比较清楚的了解了。
对于基本数据类型,函数传递或者对象解构时,会丢失原始数据的引用,换言之,我们没法让基本数据类型,或者解构后的变量(如果它的值也是基本数据类型的话),成为响应式的数据。我们是永远没办法让a
或x
这样的基本数据成为响应式的数据的,Proxy
也无法劫持基本数据。
但是有时候,我们确实就是想一个数字、一个字符串是响应式的,或者就是想利用解构的写法。那怎么办呢?只能通过创建一个对象,也即是源码中的Ref
数据,然后将原始数据保存在Ref
的属性value
当中,再将它的引用返回给使用者。既然是我们自己创造出来的对象,也就没必要使用Proxy
再做代理了,直接劫持这个value
的get/set
即可,这就是ref
函数与Ref
类型的由来。
不过单靠ref
还没法解决对象解构的问题,它只是将基本数据保持在一个对象的value
中,以实现数据响应式。对于对象的解构还需要另外一个函数:toRefs
。
export function toRefs<T extends object>(
object: T
): { [K in keyof T]: Ref<T[K]> } {
const ret: any = {}
// 遍历对象的所有key,将其值转化为Ref数据
for (const key in object) {
ret[key] = toProxyRef(object, key)
}
return ret
}
function toProxyRef<T extends object, K extends keyof T>(
object: T,
key: K
): Ref<T[K]> {
const v = {
[refSymbol]: true,
get value() {
// 注意,这里没用到track
return object[key]
},
set value(newVal) {
// 注意,这里没用到trigger
object[key] = newVal
}
}
return v as Ref<T[K]>
}
通过遍历对象,将每个属性值都转成Ref
数据,这样解构出来的还是Ref
数据,自然就保持了响应式数据的引用。但是源码中有一点要注意,toRefs
函数中引用的是toProxyRef
而不是ref
,它并不会在get/set
中注入track
跟trigger
,也就是说,**向toRefs
传入一个正常的对象,是不会返回一个响应式的数据的。**必须要传递一个已经被reactive
执行返回的对象才能有响应式的效果。
最后
源码顺序是经过调整的,方便理解阅读,到这里ref的源码已经读完了,如果觉得迷糊可以多看几遍,反正我也觉得有些迷糊。