學習Vue3,ref
跟reactive
是整個源碼中的核心,經過這兩個方法建立了響應式數據。要想徹底吃透reactivity,必須先吃透這兩個。javascript
接收一個普通對象而後返回該普通對象的響應式代理。等同於 2.x 的
Vue.observable()
html
const obj = reactive({ count: 0 })
複製代碼
響應式轉換是「深層的」:會影響對象內部全部嵌套的屬性。基於 ES2015 的 Proxy 實現,返回的代理對象__不等於__原始對象。建議僅使用代理對象而避免依賴原始對象vue
Proxy
對象是目標對象的一個代理器,任何對目標對象的操做(實例化,添加/刪除/修改屬性等等),都必須經過該代理器。所以咱們能夠把來自外界的全部操做進行攔截和過濾或者修改等操做java
function reactive<T extends object>(raw: T): T 複製代碼
reactive
類的 api 主要提供了將複雜類型的數據處理成響應式數據的能力,其實這個複雜類型是要在object
array
map
set
weakmap weakset 這六種之中【以下源碼,他會判斷是不是五類以及是否被凍結】react
const collectionTypes = new Set<Function>([Set, Map, WeakMap, WeakSet])
const isObservableType = /*#__PURE__*/ makeMap(
'Obeject,Array,Map,Set,WeakMap,WeakSet'
)
const canObserve = (value: Target): boolean => {
return (
!value[ReactiveFlags.SKIP] &&
isObservableType(toRawType(value)) &&
!Object.isFrozen(value)
)
}
複製代碼
由於是組合函數【對象】,因此必須始終保持對這個所返回對象的引用以保持響應性【不能解構該對象或者展開】例如 const { x, y } = useMousePosition()或者return { ...useMousePosition() }typescript
function useMousePosition() {
const pos = reactive({
x: 0,
y: 0,
})
return pos
}
複製代碼
接受一個參數值並返回一個響應式且可改變的 ref 對象。ref 對象擁有一個指向內部值的單一屬性 .value。api
const count = ref(0)
console.log(count.value) // 0
count.value++
console.log(count.value) // 1
複製代碼
若是傳入 ref 的是一個對象,將調用 reactive 方法進行深層響應轉換。數組
模板中訪問
當ref
做爲渲染上下文的屬性返回(即在setup()
返回的對象中)並在模板中使用時,它會自動解套,無需在模板內額外書寫.value
:
<template>
<div>{{ count }}</div>
</template>
<script> export default { setup() { return { count: ref(0), } }, } </script>
複製代碼
做爲響應式對象的屬性訪問
當 ref 做爲 reactive 對象的 property 被訪問或修改時,也將自動解套 value 值,其行爲相似普通屬性:
const count = ref(0)
const state = reactive({
count,
})
console.log(state.count) // 0
state.count = 1
console.log(count.value) // 1
複製代碼
注意若是將一個新的 ref 分配給現有的 ref, 將替換舊的 ref:markdown
const otherCount = ref(2)
state.count = otherCount
console.log(state.count) // 2
console.log(count.value) // 1
複製代碼
注意當嵌套在 reactive Object 中時,ref 纔會解套。從 Array 或者 Map 等原生集合類中訪問 ref 時,不會自動解套:數據結構
const arr = reactive([ref(0)])
// 這裏須要 .value
console.log(arr[0].value)
const map = reactive(new Map([['foo', ref(0)]]))
// 這裏須要 .value
console.log(map.get('foo').value)
複製代碼
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
// 不該該繼續遞歸的引用數據類型
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<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
和Reactive
相關實現和使用方法,能夠看出二者都是將普通類型數據轉爲響應式數據。
能夠理解的是,用戶會糾結用
ref
仍是reactive
。而首先你要知道的是,這二者你都必需要了解,纔可以高效地使用組合式 API。只用其中一個極可能會使你的工做無謂地複雜化,或反覆地造輪子。
使用 ref 和 reactive 的區別,能夠經過如何撰寫標準的 JavaScript 邏輯來比較:
// 風格 1: 將變量分離
let x = 0
let y = 0
function updatePosition(e) {
x = e.pageX
y = e.pageY
}
// --- 與下面的相比較 ---
// 風格 2: 單個對象
const pos = {
x: 0,
y: 0,
}
function updatePosition(e) {
pos.x = e.pageX
pos.y = e.pageY
}
複製代碼
- 若是使用
ref
,咱們實際上就是將風格 (1) 轉換爲使用 ref (爲了讓基礎類型值具備響應性) 的更細緻的寫法。
reactive
和風格 (2) 一致。咱們只須要經過 reactive
建立這個對象。而只使用 reactive 的問題是,使用組合函數時必須始終保持對這個所返回對象的引用以保持響應性。這個對象不能被解構或展開:
// 組合函數:
function useMousePosition() {
const pos = reactive({
x: 0,
y: 0,
})
// ...
return pos
}
// 消費者組件
export default {
setup() {
// 這裏會丟失響應性!
const { x, y } = useMousePosition()
return {
x,
y,
}
// 這裏會丟失響應性!
return {
...useMousePosition(),
}
// 這是保持響應性的惟一辦法!
// 你必須返回 `pos` 自己,並按 `pos.x` 和 `pos.y` 的方式在模板中引用 x 和 y。
return {
pos: useMousePosition(),
}
},
}
複製代碼
toRefs
API 用來提供解決此約束的辦法——它將響應式對象的每一個 property 都轉成了相應的 ref。
function useMousePosition() {
const pos = reactive({
x: 0,
y: 0,
})
// ...
return toRefs(pos)
}
// x & y 如今是 ref 形式了!
const { x, y } = useMousePosition()
複製代碼
- 當使用組合式 API 時, 咱們須要一直區別「響應式值引用」與普通的基本類型值與對象,這無疑增長了使用本套 API 的心智負擔.
這一層心智負擔能夠經過名稱規範來大大下降 (例如爲全部的 ref 名加相似 xxxRef 的後綴),亦或者是使用類型系統。另外,因爲代碼組織方面的靈活性增長了,組件邏輯會更多地分解成一些短小精悍的函數,它們的上下文都比較簡單,這些 ref 的上限也易於控制。
複製代碼
- 讀寫 ref 的操做比普通值的更冗餘,由於須要訪問
.value
。