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

引言

今年,對於從事前端開發的同窗而言,非常期待的一件事就是 Vue3.0 的發佈。可是,Vue3.0 離發佈仍是有點時間的,而且正式發佈也不表明咱們就立刻就能夠用於業務開發。它還須要完善相應的生態工具。不過正式使用是一碼事,咱們本身玩又是一碼事(hh)。javascript

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

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

1、定義及優勢

1.1 定義

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

首先,咱們須要知道在 Vue3.0 中完全廢掉了原先的 Options API,而改用 Composition API,簡易版的 Composition API 看起來會是這樣的:java

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

    function increment() {
      state.count++
    }

    return {
      state,
      increment
    }
  }

能夠看到,沒有了咱們熟悉的datacomputedmethods等等。看起來,彷佛有點 React風格,這個提出確實當時社區中引起了不少討論,說Vue愈來愈像React....不少人並非很能接受,具體細節你們能夠去閱讀 RFC 的介紹react

1.2 優勢

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

首先,在 Vue 2x 中數據的響應式處理是基於 Object.defineProperty() 的,可是它只會偵聽對象的屬性,並不能偵聽對象。因此,在添加對象屬性的時候,一般須要這樣:git

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

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

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

1.3 注意點

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

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 一塊兒講解

2、源碼實現

首先,相信你們都有所耳聞,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 去閱讀。固然,須要說的是此次的源碼只是嚐鮮版的,不排除以後正式的會作諸多優化,可是主體確定是保持不變的。

推薦閱讀下一篇《4k+ 字分析 Vue 3.0 響應式原理(依賴收集和派發更新)》

寫做不易,若是你以爲有收穫的話,能夠帥氣三連擊!!!
相關文章
相關標籤/搜索