這是我參與8月更文挑戰的第4天,活動詳情查看:8月更文挑戰javascript
最近一直從新學習Vue3
,看到composition API
了,嘗試結合源碼看看,理解深入一些。本文先來看看 reactive 和 ref 兩個API
html
咱們先來看官方對於reactive
的解釋,官方的解釋也很是簡單vue
返回對象的響應式副本java
但從這句話咱們能夠獲得如下信息react
reactive
接受一個對象做爲參數reactive
函數包裝事後的數據對象,這個對象具備響應式但一樣會有一些疑問web
好比,reactive
的參數只能傳遞一個對象嗎,若是傳遞其餘值會怎麼樣?api
好比,返回的響應式數據的本質是什麼,爲啥就能讓數據變成響應式?markdown
好比,"副本"是否是意味着響應式數據與原始數據沒有關聯?app
好比,返回的響應式副本里頭的數據是深度響應式嗎,便是否遞歸監聽對象的全部屬性?等等函數
帶着這些疑問咱們一塊兒來看 首先,經過reactive
建立一個響應數據
import { reactive } from "vue";
export default {
setup() {
const state = reactive({
count: 0,
});
},
};
複製代碼
如上代碼就能夠建立一個響應式數據state
,我具體來看一下這個
console.log(state)
複製代碼
能夠看見,返回的響應副本state
其實就是Proxy
對象。因此reactive
實現響應式就是基於ES2015 Proxy
的實現的。那咱們知道Proxy
有幾個特色:
Proxy
包裝的對象之間是有關聯的。即當原始對象裏頭數據發生改變時,會影響代理對象;代理對象裏頭的數據發生變化對應的原始數據也會發生變化。須要記住:是對象裏頭的數據變化,並不能將原始變量的從新賦值,那是大換血了
所以,既然reactive
實現響應式是基於Proxy
的實現的,那咱們大膽猜想,原始數據與相應數據也是有關聯的。那咱們來測試一下
<template>
<button @click="change"> {{ state.count }} </button>
</template>
<script> import { reactive } from "vue"; export default { setup() { const obj = { count: 0, }; const state = reactive(obj); function change(){ ++state.count console.log(obj); console.log(state); } return { state,change}; }, }; </script>
複製代碼
以上代碼測試結果以下
驗證,確實當響應式對象裏頭數據變化的時候原始對象的數據也會變化
若是反過來,結果也是同樣
// ++state.count
++obj.count;
複製代碼
當響應式對象裏頭數據變化的時候原始對象的數據也會變化
那問題來了,咱們操做數據的時候經過誰來操做呢?
官方的建議是
建議只使用響應式代理,避免依賴原始對象
再來解決另一個問題看看reactive
是否會深度監聽每一層呢?
const state = reactive({
a:{
b:{
c:{name:'c'}
}
}
});
console.log(state);
console.log(state.a);
console.log(state.a.b);
console.log(state.a.b.c);
複製代碼
能夠看到結果reactive
是遞歸會將每一層包裝成Proxy
對象的,深度監聽每一層的property
最後測試一下若是reactive
傳遞是非對象而是原始值會怎麼樣
const state = reactive(0);
console.log(state)
複製代碼
結果是,原始值並不會被包裝,因此也沒有響應式特色
下面,咱們看看reactive
的源碼吧
源碼目錄位置:vue-next\packages\reactivity\src\reactive.ts
直接找到reactive
的類型聲明:
export function reactive<T extends object>(target: T): UnwrapNestedRefs<T> 複製代碼
能夠看到reactive
接受一個參數target
,target
的類型是泛型T
,而T
類型是extends object
,簡單來講接受的參數target
的類型是object
類型或者時繼承自object
類的子類類型
返回值的類型的UnwrapNestedRefs<T>
看看UnwrapNestedRefs<T>
類型
type UnwrapNestedRefs<T> = T extends Ref ? T : UnwrapRef<T>
複製代碼
使用type
關鍵字聲明類型UnwrapNestedRefs<T>
,這裏有個三目運算符,用於進一步判斷T
;若是傳入的T
屬於Refs
類或者其子類,那麼返回傳入的T
,否者就是UnwrapRef<T>
下面具體看看reactive
方法的定義
export function reactive(target: object) {
// if trying to observe a readonly proxy, return the readonly version.
if (target && (target as Target)[ReactiveFlags.IS_READONLY]) {
return target
}
return createReactiveObject(
target,
false,
mutableHandlers,
mutableCollectionHandlers
)
}
複製代碼
接受一個類型爲object
參數,當傳入對象是隻讀,返回自己。這裏的as
關鍵字是斷言,表示傳入的值必定是Target
類型,裏頭有個ReactiveFlags.IS_READONLY
,用於判斷是不是隻讀的屬性
export interface Target {
[ReactiveFlags.SKIP]?: boolean
[ReactiveFlags.IS_REACTIVE]?: boolean
[ReactiveFlags.IS_READONLY]?: boolean
[ReactiveFlags.RAW]?: any
}
複製代碼
若是傳遞的對象是普通對象(不是readonly
),則執行建立響應式對象函數createReactiveObject(target,false,mutableHandlers,mutableCollectionHandlers)
該方法比較長,是reactive
的核心方法,因此仍是得讀一下源碼
function createReactiveObject( target: Target, isReadonly: boolean, baseHandlers: ProxyHandler<any>, collectionHandlers: ProxyHandler<any> ) {
if (!isObject(target)) {
if (__DEV__) {
console.warn(`value cannot be made reactive: ${String(target)}`)
}
return target
}
// target is already a Proxy, return it.
// exception: calling readonly() on a reactive object
if (
target[ReactiveFlags.RAW] &&
!(isReadonly && target[ReactiveFlags.IS_REACTIVE])
) {
return target
}
// target already has corresponding Proxy
const proxyMap = isReadonly ? readonlyMap : reactiveMap
const existingProxy = proxyMap.get(target)
if (existingProxy) {
return existingProxy
}
// only a whitelist of value types can be observed.
const targetType = getTargetType(target)
if (targetType === TargetType.INVALID) {
return target
}
const proxy = new Proxy(
target,
targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
)
proxyMap.set(target, proxy)
return proxy
}
複製代碼
能夠看到除了幾種特殊狀況返回target
自己以外,就返回proxy
,proxy
就是經過new Proxy
構造函數構建出來的。這裏也進一步證實了reactive
的響應式功能確實是經過Proxy
實現的
能夠看同樣Proxy
的定義
interface ProxyHandler<T extends object> {
getPrototypeOf? (target: T): object | null;
setPrototypeOf? (target: T, v: any): boolean;
isExtensible? (target: T): boolean;
preventExtensions? (target: T): boolean;
getOwnPropertyDescriptor? (target: T, p: PropertyKey): PropertyDescriptor | undefined;
has? (target: T, p: PropertyKey): boolean;
get? (target: T, p: PropertyKey, receiver: any): any;
set? (target: T, p: PropertyKey, value: any, receiver: any): boolean;
deleteProperty? (target: T, p: PropertyKey): boolean;
defineProperty? (target: T, p: PropertyKey, attributes: PropertyDescriptor): boolean;
enumerate? (target: T): PropertyKey[];
ownKeys? (target: T): PropertyKey[];
apply? (target: T, thisArg: any, argArray?: any): any;
construct? (target: T, argArray: any, newTarget?: any): object;
}
interface ProxyConstructor {
revocable<T extends object>(target: T, handler: ProxyHandler<T>): { proxy: T; revoke: () => void; };
new <T extends object>(target: T, handler: ProxyHandler<T>): T;
}
declare var Proxy: ProxyConstructor;
複製代碼
裏面的具體實現方法,在createReactiveObject
傳參的時候就傳入進來了 mutableHandlers和mutableCollectionHandlers
,具體能夠去`vue-next\packages\reactivity\src\baseHandlers.ts文件中看
通過上面的瞭解,咱們能夠總結和回答一下最開始幾個疑問了
reactive的參數能夠傳遞對象也能夠傳遞原始值。可是原始值並不會包裝成響應式數據
返回的響應式數據的本質Proxy對象
返回的響應式"副本"與原始數據有關聯,當原始對象裏頭的數據或者響應式對象裏頭的數據發生,會彼此相互影響。兩種均可以觸發界面更新,操做時建議只使用響應式代理對象
返回的響應式對象裏頭時深度遞歸監聽每一層的,每一層都會被包裝成Proxy對象
關於ref
,官方的解釋是:
接受一個內部值並返回一個響應式且可變的
ref
對象
爲了方便理解,下文中將內部值都稱爲原始數據(orgin
)
簡單來講ref
就是:原始數據=>響應式數據 的過程
但有幾個問題得搞明白
ref
接受的原始數據是什麼類型?是原始值仍是引用值,仍是都行?示例代碼1:
let origin = 0; //原始數據爲原始值
let count = ref(origin);
function add() {
count.value++;
}
複製代碼
示例代碼2:
let origin = { val: 0 };//原始數據爲對象
let count = ref(origin);
function add() {
count.value.val++;
}
複製代碼
經測試,咱們發現,傳遞的原始數據orgin
能夠是原始值也能夠是引用值,可是須要注意,若是傳遞的是原始值,指向原始數據的那個值保存在返回的響應式數據的.value
中,如上count.value;若是傳遞的一個對象,返回的響應式數據的.value
中對應有指向原始數據的屬性,如上count.value.val
爲了測試第二個問題,咱們將上述示例中的count
打出來,看返回的具體是什麼
console.log(count)
console.log(count.constructor)
複製代碼
對比發現,無論傳遞數據類型的數據給ref
,不管是原始值仍是引用值,返回的響應式數據對象本質都是由RefImpl
類構造出來的對象。但不一樣的是裏頭的value
,一個是原始值,一個是Proxy
對象
到這裏,不妨來讀一下RefImpl
類的源碼
目錄:vue-next\packages\reactivity\src\ref.ts
class RefImpl<T> {
private _value: T
public readonly __v_isRef = true
constructor(private _rawValue: T, private readonly _shallow = false) {
this._value = _shallow ? _rawValue : convert(_rawValue)
}
get value() {
track(toRaw(this), TrackOpTypes.GET, 'value')
return this._value
}
set value(newVal) {
if (hasChanged(toRaw(newVal), this._rawValue)) {
this._rawValue = newVal
this._value = this._shallow ? newVal : convert(newVal)
trigger(toRaw(this), TriggerOpTypes.SET, 'value', newVal)
}
}
}
複製代碼
能夠看見RefImpl class
傳遞了一個泛型類型T
,裏頭具體包含:
_value
,類型爲T
,有個公開只讀屬性__v_isRef
值爲true
get value(){}
和set value(){}
,分別對應私有屬性的讀寫操做,用於供外界操做value
constructor
,用於構造對象。構造函數接受兩個參數:
_rawValue
,要求是T
類型_shallow
,默認值爲true
當經過它構建對象時,會給對象的_value
屬性賦值爲 _rawValue
或者convert(_rawValue)
再看convert
源碼以下:
const convert = <T extends unknown>(val: T): T => isObject(val) ? reactive(val) : val 複製代碼
經過源碼咱們發現,最終Vue
會根據傳入的數據是否是對象isObject(val)
,若是是對象本質調用的是reactive
,不然返回原始數據
下面再來驗證最後一個問題就是:經過ref
包裝的結果,當原始數據改變時會觸發界面更新嗎?即原始數據和返回的響應式數據是否有關聯?
示例代碼3
let origin = 0; //原始值
let count = ref(origin);
function add() {
origin++
console.log(count.value)
}
複製代碼
示例代碼4
let origin = { val: 0 }; //引用值
let count = ref(origin);
function add() {
origin++
console.log(count.value.val)
}
複製代碼
發現,不管傳入給ref
的原始數據是原始值仍是引用值,當原始數據發生修改時,並不會影響響應式數據,更不會觸發界面UI
的更新
實例代碼5
let origin = 0;
let count = ref(origin);
function add() {
count.value++
console.log(origin)
}
複製代碼
上述代碼,不管count修改多少次,origin一直是0
即若是響應式數據發生改變,對應界面UI
是會自動更新的,注意不影響原始數據
簡單小結一下:
ref本質是將一個數據變成一個對象,這個對象具備響應式特色
ref接受的原始數據能夠是原始值也能夠是引用值,返回的對象本質都是RefImpl類的實例`
不管傳入的原始數據時什麼類型,當原始數據發生改變時,並不會影響響應數據,更不會觸發UI的更新。但當響應式數據發生改變,對應界面UI是會自動更新的,注意不影響原始數據。因此ref中,原始數據和通過ref包裝後的響應式數據是無關聯的
以上就是關於和reactive
和ref
全部內容~
源碼看得比較少,若有問題歡迎留言告知~