在2.0的时候使用的是ES5 中Object.defineproperty()做的数据劫持, 不过Object.defineproperty是对所有的属性做的数据劫持, vue3 中使用ES6 proxy 进行数据代理,实现起来更加容易,并且可以对数组,对象等数据进行更深层的监视
ref 介绍
- 可以将变量转换成响应式数据
- 用法: const age = ref(18)
- ref(x) 中 x 不可以是对象 { } 类型,其他类型都可以比如 数组,数字,字符串,布尔等
- ref 可以记录 dom
reative 介绍
- 可以将对象转换成响应式数据
ref 使用
<script setup> import { ref } from 'vue' const name = ref('张三') const eleDom = ref(null) </script> <template> <p ref="eleDom">{{ name }}</p> </template>
reative 使用
<script setup> import { reactive, ref } from '@vue/reactivity' const count = ref(0) const dataList = reactive({ count, arr: [], obj: { name: '吴八哥', age: 18 } }) const handleCount1Change = () => count.value++ const handleCount2Change = () => dataList.count++ const handleAgeChange = () => dataList.obj.age++ </script>
toRef 使用
<script setup> import { reactive, toRef } from '@vue/reactivity' const dataList = reactive({ count: 0, str: '示例' }) const count = toRef(dataList, 'count') const handleCountChange = () => count.value++ </script>
toRefs 使用
<script setup> import { reactive, toRefs } from '@vue/reactivity' /* const dataList = reactive({ count: 0, str: '示例' }) let { count, str } = toRefs(dataList) const handleCountChange = () => count.value++ */ // 封装一个函数(组合式 API 函数) const useSomeData = () => { const someData = reactive({ count: 0, str: '示例' }) // 数据处理 someData.count += 3 someData.str += '123' // 将被外界需要的数据返回 return toRefs(someData) } // 外界使用功能 let { count, str } = useSomeData() const handleCountChange = () => count.value </script>
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,下面看看UnwrapRef:
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函数,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 }