Vue 3.0 RFC API 的實現

Vue3.0 的 RFC 已經發布了幾個月了,Vue 底層幾乎沒有變更,仍是沿用原來響應式的。因此一直在思考能不能使用如今的版本,實現 RFC 中的 API,直到看到了 Vue Function API 這個庫,這個庫讓開發者提早嚐鮮到了RFC 中的 API,固然做爲 RFC,因此最終 3.0 的 API 仍是未知的,以及底層的實現也還未知。html

Dreamacro大佬說 類型纔是 functional 的最大意義。我以爲很是有道理,在這個 ts 盛行的年代。vue

一個思考

想複用邏輯和狀態,關鍵在於如何建立一個能夠被 Vue 觀察的對象(響應式對象)。當響應式的對象發生了變化時,Vue 會開始它的更新邏輯,至於它是怎麼更新了,這裏不做討論。其次就是,怎麼將這個狀態綁定到 vm 上,除了使用 computed 來手動綁定以外,還能夠用什麼方法。git

Ovservable

在 Vue 2.6 以前,想建立一個響應式對象須要實例化一個 Vue,但在 Vue 2.6 以後,能夠經過Vue.observable 來建立一個響應式的對象。github

Vue 2.6 之前vue-router

const create = (obj: object) => {
  const vm = new Vue({ data: () => obj })
  return vm.$data
}
複製代碼

Vue 2.6 以後vuex

const create = (obj: object) => Vue.observable(obj)
複製代碼

這裏有一個 DEMO,能夠看出,普通的對象更新是不會觸發 Vue 的更新邏輯的。響應式的對象,即便不在該 Vue 實例中去更改值,也會觸發 Vue 的更新,能夠在 DEMO 的控制檯中嘗試一下輸入 x.value++typescript

簡單的DEMO

DEMO 中,不處理上下文,無論任何生命週期,只想表達 setup 以及 value 是如何工做的。api

首先考慮一下這個 value,當這個 value 的類型爲 number 或者 string 等非對象的類型時,爲了建立一個響應式的對象,因此須要一層 wrapper ,這樣 Vue 才能建立一個響應式對象。這就是爲何 RFC 中使用 valuevariable.value 的形式了。數組

可是若是 value 自己是一個對象的話,能夠不須要這層 wrapper 。可能爲了統一,因此都加上了這層。app

其次 setup 做爲一個 Vue的配置,須要在 Vue 實例化的時候執行的,選擇 Vue 的第一個生命週期 beforeCreate 鉤子中執行這個函數,等於用 setup 函數來替代 beforeCreate,這樣能夠在 setup 函數中使用其餘生命週期的鉤子。

最後是這個 setup 的返回值,如何 unwrapped 並將值掛到 this 上提供給 template 使用。

這裏提供一個最簡單的 DEMO 能夠看 vfp.js 的實現,僅僅在 beforeCreate 執行了一下 setup並將返回值作一層 unwrapped 並掛載到 vm 上 提供給 template 使用。

Vue-Function-API 源碼解析

注: vm => Vue實例

value和state

先來看幾個關於 wrapper 的類。

AbstractWrapper.ts

這個抽象類主要實現了一個 setVmProperty 的方法,主要用來將 value 這個掛載到 vm上。

ValueWrapper.ts

這個類就是 value 函數實際使用的類,繼承 AbstractWrapper,主要實現了 value getvalue set而且約定使用 $$state 做爲響應式對象中的 key

ComputedWrapper.ts

這個類主要用於對 computed 作一層 wrapper,繼承 AbstractWrapper

state 函數主要就是將對象轉換成響應式的對象,因此方法也及其簡單。

export function state<T>(value: T): T {
  return observable(value);
}
複製代碼

value 函數須要將響應式對象經過 類ValueWrapper 包裝一層

export function value<T>(value: T): Wrapper<T> {
  return new ValueWrapper(state({ $$state: value }));
}
複製代碼

這裏的 key: $$state 也可使用其餘的。

valuestate 函數都比較簡單,目的就是建立響應式對象。

valuestate 的區別在於,value 方法能夠將一個非對象的類型(number 、 string 、 boolean),包裝成一個響應式對象。而 state 能夠直接將一個非響應式對象包裝成一個響應式對象。

const A = state({ value: 0 })
const B = value(0)

// 這二者在某種意義上是等價的。
複製代碼

setup

setup.ts

主要有3個方法 functionApiInit initSetup createSetupContext

初始化

在 Vue 生命週期中的 beforeCreate 執行 functionApiInit。主要用於判斷是否在 Vue 的配置中存不存在 setup 方法,若是存在,就進行 initSetup

執行

initSetup方法中,先經過 createSetupContext 方法建立一個本身的上下文。而後經過一個全局變量,保存上一次的 vm,再設置當前的 vm,接下去將以前建立的 上下文以及props 做爲參數執行 setup 函數。執行結束後,將當前的 vm 還原爲上一次的 vm。最後將 setup 的返回值,綁定到 vm 中。

這裏爲何須要保存以前所執行的上下文?issue

setup 是能夠動態注入生命週期的鉤子的,須要保證鉤子注入的是當前執行 setupvm。因此在執行這個 setup 函數時,須要保存當前執行的 vm

建立上下文

這裏的上下文不是 vm,而是對 vm 提取了一些關鍵信息而成的 ctx

ctx 包含的 props 有這些:

const props: Array<string | [string, string]> = [
      'root',
      'parent',
      'refs',
      ['slots', 'scopedSlots'],
      'attrs',
    ];
const methodReturnVoid = ['emit'];
// vm.$root === ctx.root
// vm.$refs === ctx.refs
// ...
複製代碼

建立出來的 ctx 對象做爲 setup 函數的第二個參數傳入。

當使用 Vue-router vuex 等插件,這裏的上下文就會缺失 router store 等。

若是須要使用 vue-router,一個比較簡單的方法是經過 ctx.root.$router 這樣來使用。

在官方倉庫中有一個相關的PR被Close掉了。

feat: add an option to bind other props #37

爲了考慮與RFC一致,方便遷移,因此做者不添加除了RFC之外的API。

lifeCycle

lifecycle.ts

setup 執行的過程當中,能夠動態插入生命週期 鉤子。這裏生命週期的代碼比較簡單,主要須要拿到當前執行上下文的 vm,再插入一個 callback 到相應的生命週期中。

injectHookOption 裏面有一個 merge Function 這個函數主要是 Vue 某個配置的合併策略,默認是簡單的覆蓋。具體文檔

爲何這裏沒有 beforeCreate 的鉤子,由於 beforeCreate 的鉤子已經被使用了,因此能使用只能是 beforeCreate 以後生命週期的鉤子。

watch

watch 支持3種模式pre post sync

pre mode: 在 rerender 以前執行回調

post mode: 在 rerender 以後執行回調

sync mode: 同步執行回調

watch 中主要有 2 個方法,createSingleSourceWatchercreateMuiltSourceWatcher

對應的是 2 種模式,一種是隻 watch 一個數據源,一種是 watch 多個數據源。

watch 在 vm 上維護了 2 個隊列,WatcherPreFlushQueueWatcherPostFlushQueue

這 2 個隊列是用來維護所須要執行的回調。每一次通過 flush 後,隊列會被清空。

installWatchEnv

installWatchEnv 方法中,對當前 vm 的進行初始化隊列和綁定生命週期的事件。

注: 在Vue生命週期的 callHook 調用時,會 emit 出相應的事件 Hook Event

flushWatcherCallback

該方法用來維護 watch 的回調隊列。

會根據 mode 選擇插入回調的隊列或者當即執行。

方法中有一個函數 fallbackFlush 用於當所 watch 的值發生了變化,可是沒有觸發 Vue 的 update 時進行一個兜底的 flush。例如在 setup 函數中改變某個 watch 的值。

其餘發生 update 的變動,會交給生命週期的 event 去執行隊列的回調。

vm.$on('hook:beforeUpdate', () => flushQueue(vm, WatcherPreFlushQueueKey));
vm.$on('hook:updated', () => flushQueue(vm, WatcherPostFlushQueueKey));
複製代碼
createSingleSourceWatcher

在這個方法裏,value 會被轉換成 getter 的形式。

if (isWrapper<T>(source)) {
    getter = () => source.value;
  } else {
    getter = source as () => T;
  }
複製代碼

在 RFC 中,有這樣的一句話**Unlike 2.x $watch, the callback will be called once when the watcher is first created.**全部 watch 的回調在建立的時候會先執行一遍,除非他是 lazy 的,也就是說,watch 默認是 immediate 的。若是是 lazy 的,會將回調放到 2 個隊列的其中一個。

除了第一次執行回調的時機比較特殊,其他的回調執行時機都交給 flushWatcherCallback

createMuiltSourceWatcher

該方法用來建立對多個數據源的 watch

一樣的在全部數據源初始化結束完成後會當即執行一次回調函數,除非是 lazy 的。

多個數據源與單個數據源不一樣的是,多個數據源須要維護多個數據源的 newValueoldValue。以及 stop 函數,須要對全部 watch 的數據源 stop

function execCallback() {
    cb.apply(
      vm,
      watcherContext.reduce<[T[], T[]]>(
        (acc, ctx) => {
          acc[0].push((ctx.value === initValue ? ctx.getter() : ctx.value) as T);
          acc[1].push((ctx.oldValue === initValue ? undefined : ctx.oldValue) as T);
          return acc;
        },
        [[], []]
      )
    );
  }

function stop() {
    watcherContext.forEach(ctx => ctx.watcherStopHandle());
  }
複製代碼

多個數據源的回調參數爲2個數組,newValuesoldValues 順序與所觀察的數據源的順序一一對應。

總結

以上是閱讀源碼的一個小筆記。Vue Function API 的源碼不長,因此推薦你們仍是能夠去閱讀一下源碼。

在閱讀源碼的過程當中見識到了不少之前沒有接觸過,不知道的 API,好比 Vue 中 callHook 的事件。能夠利用 vm 作不少事情。以及對邏輯的抽離和複用有了更深的思考,是否有了 Function API 就已經不須要 vuex。一塊兒期待 Vue 3.0 吧,完整的類型會讓整個開發體驗徹底不同。

參考

  1. Vue 插件
  2. vm-watch
  3. Vue 3.0 RFC
  4. Vue Function API Repository
相關文章
相關標籤/搜索