Vue3.0 的 reactive API 定義和源碼實現

引言

今年,對於從事前端開發的同學而言,很是期待的一件事就是 Vue3.0的發佈。但是,Vue3.0離發佈還是有點時間的,並且正式發佈也不代表我們就馬上就可以用於業務開發。它還需要完善相應的生態工具。不過正式使用是一碼事,我們自己玩又是一碼事(hh)。

Vue3.0特地準備了一個嚐鮮版的項目供大家體驗 Vue3.0即將會出現的一些 API,例如 setupreactivetoRefsreadonly等等, 順帶附上Composition API文檔 的地址,還沒看過的同學趕緊去 Get,別等到發佈才知道(笨鳥要先飛,聰明鳥那更要先飛是吧)。

同樣地,我也 Clone了下來玩了一會,對這個 reactive API頗感興趣。所以,今天我們就來看看 reactive API是什麼(定義)怎麼實現的(源碼實現)?

一、定義及優點

1.1 定義

reactive API的定義爲傳入一個對象並返回一個基於原對象的響應式代理,即返回一個 Proxy,相當於 Vue2x版本中的 Vue.observer

首先,我們需要知道在 Vue3.0中徹底廢掉了原先的 Options API,而改用 Composition API,簡易版的 Composition API看起來會是這樣的:

  setup() {
    const state = reactive({
      count: 0,
      double: computed(() => state.count * 2)
    })

    function increment() {
      state.count++
    }

    return {
      state,
      increment
    }
  }

可以看到,沒有了我們熟悉的datacomputedmethods等等。看起來,似乎有點 React風格,這個提出確實當時社區中引發了很多討論,說Vue越來越像React…很多人並不是很能接受,具體細節大家可以去閱讀 RFC 的介紹

1.2 優點

回到本篇文章所關注的,很明顯 reactive API對標 data選項,那麼相比較 data選項有哪些優點?

首先,在 Vue 2x中數據的響應式處理是基於 Object.defineProperty()的,但是它只會偵聽對象的屬性,並不能偵聽對象。所以,在添加對象屬性的時候,通常需要這樣:

    // vue2x添加屬性
    Vue.$set(object, 'name', wjc)

reactive API是基於 ES2015 Proxy實現對數據對象的響應式處理,即在 Vue3.0可以往對象中添加屬性,並且這個屬性也會具有響應式的效果,例如:

    // vue3.0中添加屬性
    object.name = 'wjc'

1.3 注意點

使用 reactive API需要注意的是,當你在 setup中返回的時候,需要通過對象的形式,例如:

    export default {
      setup() {
          const pos = reactive({
            x: 0,
            y: 0
          })

          return {
             pos: useMousePosition()
          }
      }
    }

或者,藉助 toRefs API包裹一下導出,這種情況下我們就可以使用展開運算符或解構,例如:

    export default {
      setup() {
          let state = reactive({
            x: 0,
            y: 0
          })
        
          state = toRefs(state)
          return {
             ...state
          }
      }
    } 

toRefs() 具體做了什麼,接下來會和 reactive 一起講解

二、源碼實現

首先,相信大家都有所耳聞,Vue3.0TypeScript重構了。所以,大家可能會以爲這次會看到一堆 TypeScript的類型之類的。出於各種考慮,本次我只是講解編譯後,轉爲 JS 的源碼實現(沒啥子門檻,大家放心 hh)。

2.1 reactive

1.先來看看 reactive函數的實現:

function reactive(target) {
    // if trying to observe a readonly proxy, return the readonly version.
    if (readonlyToRaw.has(target)) {
        return target;
    }
    // target is explicitly marked as readonly by user
    if (readonlyValues.has(target)) {
        return readonly(target);
    }
    if (isRef(target)) {
        return target;
    }
    return createReactiveObject(target, rawToReactive, reactiveToRaw, mutableHandlers, mutableCollectionHandlers);
}

可以,看到先有 3 個邏輯判斷,對 readonlyreadonlyValuesisRef分別進行了判斷。我們先不看這些邏輯,通常我們定義 reactive會直接傳入一個對象。所以會命中最後的邏輯 createReactiveObject()

2.那我們轉到 createReactiveObject()的定義:

function createReactiveObject(target, toProxy, toRaw, baseHandlers, collectionHandlers) {
    if (!isObject(target)) {
        if ((process.env.NODE_ENV !== 'production')) {
            console.warn(`value cannot be made reactive: ${String(target)}`);
        }
        return target;
    }
    // target already has corresponding Proxy
    let observed = toProxy.get(target);
    if (observed !== void 0) {
        return observed;
    }
    // target is already a Proxy
    if (toRaw.has(target)) {
        return target;
    }
    // only a whitelist of value types can be observed.
    if (!canObserve(target)) {
        return target;
    }
    const handlers = collectionTypes.has(target.constructor)
        ? collectionHandlers
        : baseHandlers;
    observed = new Proxy(target, handlers);
    toProxy.set(target, observed);
    toRaw.set(observed, target);
    return observed;
}

createReactiveObject()傳入了四個參數,它們分別扮演的角色:

  • target是我們定義 reactive時傳入的對象
  • toProxy是一個空的 WeakSet
  • toProxy是一個空的 WeakSet
  • baseHandlers是一個已經定義好 getset的對象,它看起來會是這樣:
    const baseHandlers = {
        get(target, key, receiver) {},
        set(target, key, value, receiver) {},
        deleteProxy: (target, key) {},
        has: (target, key) {},
        ownKey: (target) {}
    };
  • collectionHandlers是一個只包含 get 的對象。

然後,進入 createReactiveObject(), 同樣地,一些分支邏輯我們這次不會去分析。

看源碼時需要保持的一個平常心,先看主邏輯

所以,我們會命中最後的邏輯,即:

    const handlers = collectionTypes.has(target.constructor)
        ? collectionHandlers
        : baseHandlers;
    observed = new Proxy(target, handlers);
    toProxy.set(target, observed);
    toRaw.set(observed, target);

它首先判斷 collectionTypes中是否會包含我們傳入的 target的構造函數,而 collectionTypes是一個 Set集合,主要包含 Set, Map, WeakMap, WeakSet等四種集合的構造函數。

如果 collectionTypes包含它的構造函數,那麼將 handlers賦值爲只有 getcollectionHandlers對象,否則,賦值爲 baseHandlers對象。

這兩者的區別就在於前者只有 get,很顯然這個是留給不需要派發更新的變量定義的,例如我們熟悉的 props它就只實現了 get

然後,將 targethandlers傳入 Proxy,作爲參數實例化一個 Proxy對象。這也是我們看到一些文章常談的 Vue3.0ES2015 Proxy取代了 Object.defineProperty

最後的兩個邏輯,也是非常重要,toProxy()將已經定義好 Proxy對象的 target和 對應的 observed作爲鍵值對塞進 toProxy這個 WeakMap中,用於下次如果存在相同引用的 target 需要 reactive,會命中前面的分支邏輯,返回定義之前定義好的 observed,即:

    // target already has corresponding Proxy target 是已經有相關的 Proxy 對象
    let observed = toProxy.get(target);
    if (observed !== void 0) {
        return observed;
    }

toRaw()則是和 toProxy相反的鍵值對存入,用於下次如果傳進的 target已經是一個 Proxy對象時,返回這個 target,即:

    // target is already a Proxy target 已經是一個 Proxy 對象
    if (toRaw.has(target)) {
        return target;
    }

2.2 toRefs

前面講了使用 reactive需要關注的點,提及 toRefs可以讓我們方便地使用解構和展開運算符,其實是最近 Vue3.0 issue也有大神講解過這方面的東西。有興趣的同學可以移步 When it’s really needed to use toRefs in order to retain reactivity of reactive value瞭解。

我當時也湊了一下熱鬧,如下圖:

可以看到,toRefs是在原有 Proxy對象的基礎上,返回了一個普通的帶有 getset的對象。這樣就解決了 Proxy對象遇到解構和展開運算符後,失去引用的情況的問題。

結語

好了,對於 reactive API的定義和大致的源碼實現就如上面文章中描的述。而分支的邏輯,大家可以自行走不同的 case去閱讀。當然,需要說的是這次的源碼只是嚐鮮版的,不排除之後正式的會做諸多優化,但是主體肯定是保持不變的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章