vue3的 reactivity 是一個獨立的包,這是一個比較大的改動,全部響應式相關的實現都在裏面,我主要講的也就是這一塊的。vue
1.proxy: es6的代理實現方式 2.reflect: 將object對象一些明顯屬於語言內部方法,放到Reflect上, 3.weakMap: WeakMap 的 key 只能是 Object 類型。 4.weakSet: WeakSet 對象是一些對象值的集合, 而且其中的每一個對象值都只能出現一次. 響應式簡要實現 咱們曾經的書寫響應式數據是這樣的react
data () {
return {
count: 0
}
}複製代碼
而後vue3新的響應式書寫方式(老的也兼容)es6
數組
setup() { const state = { count: 0, double: computed(() => state.count * 2) } function increment() { state.count++ }複製代碼onMounted(() => { console.log(state.count) }) watch(() => { document.title = `count ${state.count}` 複製代碼}) return { state, increment } }複製代碼onMounted(() => { console.log(state.count) }) watch(() => { document.title = `count ${state.count}` 複製代碼
感受setup這塊就有點像 react hooks 理解成一個帶有數據的邏輯複用模塊,再也不以vue組件爲單位的代碼複用了 和React鉤子不一樣,setup()函數僅被調用一次。 因此新的響應書數據兩種聲明方式: 1.Ref 前提:聲明一個類型 Ref 函數
export interface Ref<T> {
[refSymbol]: true
value: UnwrapNestedRefs<T>
}複製代碼
ref()函數源碼:ui
function ref(raw: unknown) {
if (isRef(raw)) {
return raw
}
// convert 內容:判斷 raw是否是對象,是的話 調用reactive把raw響應化
raw = convert(raw)
const r = {
_isRef: true,
get value() {
// track 理解爲依賴收集
track(r, OperationTypes.GET, '')
return raw
},
set value(newVal) {
raw = convert(newVal)
// trigger 理解爲觸發監聽,就是觸發頁面更新好了
trigger(r, OperationTypes.SET, '')
}
}
return r as Ref
}複製代碼
仍是看下 convert 吧spa
const convert = val => isObject(val) ? reactive(val) : val複製代碼
能夠看得出 ref類型 只會包裝最外面一層,內部的對象最終仍是調用reactive,生成Proxy對象進行響應式代理。 疑問 可能有人想問,爲何不都用proxy, 內部對象都用proxy,最外層還要搞個 Ref類型,畫蛇添足嗎? 理由可能比較簡單,那就是proxy代理的都是對象,對於基本數據類型,函數傳遞或對象結構是,會丟失原始數據的引用。 官方解釋:prototype
However, the problem with going reactive-only is that the consumer of a composition function must keep the reference to the returned object at all times in order to retain reactivity. The object cannot be destructured or spread:設計
2.Reactive 前提:先了解下 weakMap代理
// WeakMaps that store {raw <-> observed} pairs.
const rawToReactive = new WeakMap<any, any>() // key:原始對象 value: Proxy
const reactiveToRaw = new WeakMap<any, any>()
const rawToReadonly = new WeakMap<any, any>()
const readonlyToRaw = new WeakMap<any, any>()
reactive(target)複製代碼
源碼以下: 注:target必定是一個對象,否則會報警告
function reactive(target) {
// 若是target是一個只讀響應式數據
if (readonlyToRaw.has(target)) {
return target
}
// 若是是被用戶標記的只讀數據,那經過readonly函數去封裝
if (readonlyValues.has(target)) {
return readonly(target)
}
// go ----> step2
return createReactiveObject(
target,
rawToReactive,
reactiveToRaw,
mutableHandlers, // 注意傳遞
mutableCollectionHandlers
)
}複製代碼
createReactiveObject(target,toProxy,toRaw,baseHandlers,collectionHandlers)
function createReactiveObject(
target: unknown,
toProxy: WeakMap<any, any>,
toRaw: WeakMap<any, any>,
baseHandlers: ProxyHandler<any>,
collectionHandlers: ProxyHandler<any>
) {
// 判斷target不是對象就 警告 並退出
if (!isObject(target)) {
if (__DEV__) {
console.warn(`value cannot be made reactive: ${String(target)}`)
}
return target
}
// 經過原始數據 -> 響應數據的映射,獲取響應數據
let observed = toProxy.get(target)
if (observed !== void 0) {
return observed
}
// 若是原始數據自己就是個響應數據了,直接返回自身
if (toRaw.has(target)) {
return target
}
// 若是是不可觀察的對象,則直接返回原對象
if (!canObserve(target)) {
return target
}
// 集合數據與(對象/數組) 兩種數據的代理處理方式不一樣
const handlers = collectionTypes.has(target.constructor)
? collectionHandlers
: baseHandlers
// 聲明一個代理對象 ----> step3
observed = new Proxy(target, handlers)
// 兩個weakMap 存target observed
toProxy.set(target, observed)
toRaw.set(observed, target)
if (!targetMap.has(target)) {
targetMap.set(target, new Map())
}
return observed
}複製代碼
baseHandles (咱們以對象類型爲例,集合類型的handlers稍複雜點) handlers以下,new Proxy(target, handles)的 handles就是下面這個對象
export const mutableHandlers = {
get: createGetter(false),
set,
deleteProperty,
has,
ownKeys
}複製代碼
createGetter(false) 問題:如何代理多層嵌套的對象 關鍵詞:利用 proxy 的 get 思路:當咱們代理get獲取到res時,判斷res 是不是對象,若是是那麼 繼續reactive(res),能夠說是一個遞歸
reactive(target) -> createReactiveObject(target,handlers) -> new Proxy(target, handlers) -> createGetter(readonly) -> get() -> res -> isObject(res) ? reactive(res) : res
function createGetter(isReadonly: boolean) {
// isReadonly 用來區分是不是隻讀響應式數據
// receiver便是被建立出來的代理對象
return function get(target: object, key: string | symbol, receiver: object) {
// 獲取原始數據的響應值
const res = Reflect.get(target, key, receiver)
if (isSymbol(key) && builtInSymbols.has(key)) {
return res
}
if (isRef(res)) {
return res.value
}
// 收集依賴
track(target, OperationTypes.GET, key)
// 這裏判斷上面獲取的res 是不是對象,若是是對象 則調用reactive而且傳遞的是獲取到的res,
// 則造成了遞歸
return isObject(res)
? isReadonly
? // need to lazy access readonly and reactive here to avoid
// circular dependency
readonly(res)
: reactive(res)
: res
}
}複製代碼
set set的一個主要做用去觸發監聽,使試圖更新,須要注意的是控制何時纔是視圖須要真的更新
function set(
target: object,
key: string | symbol,
value: unknown,
receiver: object
): boolean {
// 拿到新值的原始數據
value = toRaw(value)
// 獲取舊值
const oldValue = (target as any)[key]
// 若是舊值是Ref類型,新值不是,那麼直接更新值,並返回
if (isRef(oldValue) && !isRef(value)) {
oldValue.value = value
return true
}
const hadKey = hasOwn(target, key)
const result = Reflect.set(target, key, value, receiver)
// 若是是原始數據原型鏈上的數據操做,不作任何觸發監聽函數的行爲。
if (target === toRaw(receiver)) {
// 更新的兩種條件
// 1. 不存在key,即當前操做是在新增屬性
// 2. 舊值和新值不等
if (!hadKey) {
trigger(target, OperationTypes.ADD, key)
} else if (hasChanged(value, oldValue)) {
trigger(target, OperationTypes.SET, key)
}
}
return result
}複製代碼
問題2: 對於數據的set操做會出發屢次traps, 這裏有個前提了解:就是咱們平常修改數組,好比 let a = [1], a.push(2), 這個push操做,咱們是其實是對a作了2個屬性的修改,1,set length 1; 2. set value 2 因此咱們的set traps會出發屢次 思路:經過屬性值和value控制,好比當 set key是 length的時候,咱們能夠判斷當前數組 已經有此屬性,因此不須要出發更新,當新設置的值和老值同樣是也不須要更新(說辭不夠嚴謹)
問題3: set的源碼裏面有 有一個 target === toRaw(receiver)條件下才繼續操做 trigger更新視圖 這裏就暴露出一個東西,即存在 target !== toRaw(receiver) Receiver: 最初被調用的對象。一般是 proxy 自己,但 handler 的 set 方法也有可能在原型鏈上或以其餘方式被間接地調用(所以不必定是 proxy 自己) 其實源碼有註釋
// don't trigger if target is something up in the prototype chain of original
即若是咱們的操做是操做原始數據原型鏈上的數據操做,target 就不等於 toRaw(receiver) 什麼狀況下 target !== toRaw(receiver) 例如:
const child = new Proxy( {}, { // 其餘 traps 省略 set(target, key, value, receiver) { Reflect.set(target, key, value, receiver) console.log('child', receiver) return true } } )const parent = new Proxy( { a: 10 }, { // 其餘 traps 省略 set(target, key, value, receiver) { Reflect.set(target, key, value, receiver) console.log('parent', receiver) return true } } )
Object.setPrototypeOf(child, parent) // child.proto === parent true
child.a = 4
複製代碼// 結果 // parent Proxy {a: 4} // Proxy {a: 4}複製代碼
從結果能夠看出,理論上 parent的set應該不會觸發,但實際是觸發了,此時
target: {a: 10}
receiver: Proxy {a: 4}
// 在vue3中
toRaw(receiver): {a: 4} 複製代碼
爲何有了proxy作響應式還須要一個Ref呢? 由於Proxy沒法劫持基礎數據類型,因此設計了這麼一個對象——Ref,其實仍是有不少設計細節,就不一一贅述了,官網也給了他們不一樣點,能夠本身去好好了解。