vue3的響應式設計——proxy

概要

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}`
複製代碼
複製代碼onMounted(() => { console.log(state.count) }) watch(() => { document.title = `count ${state.count}` 複製代碼}) return { state, increment } }複製代碼

感受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,其實仍是有不少設計細節,就不一一贅述了,官網也給了他們不一樣點,能夠本身去好好了解。

相關文章
相關標籤/搜索