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
在 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 中,不處理上下文,無論任何生命週期,只想表達 setup
以及 value
是如何工做的。api
首先考慮一下這個 value
,當這個 value
的類型爲 number
或者 string
等非對象的類型時,爲了建立一個響應式的對象,因此須要一層 wrapper
,這樣 Vue
才能建立一個響應式對象。這就是爲何 RFC 中使用 value
是 variable.value
的形式了。數組
可是若是 value
自己是一個對象的話,能夠不須要這層 wrapper
。可能爲了統一,因此都加上了這層。app
其次 setup
做爲一個 Vue
的配置,須要在 Vue 實例化的時候執行的,選擇 Vue 的第一個生命週期 beforeCreate
鉤子中執行這個函數,等於用 setup
函數來替代 beforeCreate
,這樣能夠在 setup
函數中使用其餘生命週期的鉤子。
最後是這個 setup
的返回值,如何 unwrapped
並將值掛到 this
上提供給 template
使用。
這裏提供一個最簡單的 DEMO 能夠看 vfp.js
的實現,僅僅在 beforeCreate
執行了一下 setup
並將返回值作一層 unwrapped
並掛載到 vm
上 提供給 template
使用。
注: vm => Vue實例
先來看幾個關於 wrapper
的類。
這個抽象類主要實現了一個 setVmProperty
的方法,主要用來將 value
這個掛載到 vm
上。
這個類就是 value
函數實際使用的類,繼承 AbstractWrapper
,主要實現了 value get
和 value set
而且約定使用 $$state
做爲響應式對象中的 key
這個類主要用於對 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
也可使用其餘的。
value
和 state
函數都比較簡單,目的就是建立響應式對象。
value
與 state
的區別在於,value
方法能夠將一個非對象的類型(number 、 string 、 boolean),包裝成一個響應式對象。而 state
能夠直接將一個非響應式對象包裝成一個響應式對象。
const A = state({ value: 0 })
const B = value(0)
// 這二者在某種意義上是等價的。
複製代碼
主要有3個方法 functionApiInit
initSetup
createSetupContext
在 Vue 生命週期中的 beforeCreate
執行 functionApiInit
。主要用於判斷是否在 Vue 的配置中存不存在 setup
方法,若是存在,就進行 initSetup
。
initSetup
方法中,先經過 createSetupContext
方法建立一個本身的上下文。而後經過一個全局變量,保存上一次的 vm
,再設置當前的 vm
,接下去將以前建立的 上下文以及props 做爲參數執行 setup
函數。執行結束後,將當前的 vm
還原爲上一次的 vm
。最後將 setup
的返回值,綁定到 vm
中。
這裏爲何須要保存以前所執行的上下文?issue
setup
是能夠動態注入生命週期的鉤子的,須要保證鉤子注入的是當前執行 setup
的 vm
。因此在執行這個 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。
在 setup
執行的過程當中,能夠動態插入生命週期
鉤子。這裏生命週期的代碼比較簡單,主要須要拿到當前執行上下文的 vm
,再插入一個 callback
到相應的生命週期中。
在 injectHookOption
裏面有一個 merge Function
這個函數主要是 Vue 某個配置的合併策略,默認是簡單的覆蓋。具體文檔
爲何這裏沒有 beforeCreate
的鉤子,由於 beforeCreate
的鉤子已經被使用了,因此能使用只能是 beforeCreate
以後生命週期的鉤子。
watch 支持3種模式pre
post
sync
pre mode
: 在 rerender 以前執行回調
post mode
: 在 rerender 以後執行回調
sync mode
: 同步執行回調
watch 中主要有 2 個方法,createSingleSourceWatcher
和 createMuiltSourceWatcher
對應的是 2 種模式,一種是隻 watch 一個數據源,一種是 watch 多個數據源。
watch 在 vm 上維護了 2 個隊列,WatcherPreFlushQueue
和 WatcherPostFlushQueue
這 2 個隊列是用來維護所須要執行的回調。每一次通過 flush
後,隊列會被清空。
installWatchEnv
方法中,對當前 vm
的進行初始化隊列和綁定生命週期的事件。
注: 在Vue生命週期的 callHook
調用時,會 emit
出相應的事件 Hook Event
該方法用來維護 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));
複製代碼
在這個方法裏,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
。
該方法用來建立對多個數據源的 watch
。
一樣的在全部數據源初始化結束完成後會當即執行一次回調函數,除非是 lazy
的。
多個數據源與單個數據源不一樣的是,多個數據源須要維護多個數據源的 newValue
和 oldValue
。以及 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個數組,newValues
和 oldValues
順序與所觀察的數據源的順序一一對應。
以上是閱讀源碼的一個小筆記。Vue Function API
的源碼不長,因此推薦你們仍是能夠去閱讀一下源碼。
在閱讀源碼的過程當中見識到了不少之前沒有接觸過,不知道的 API,好比 Vue 中 callHook 的事件。能夠利用 vm
作不少事情。以及對邏輯的抽離和複用有了更深的思考,是否有了 Function API
就已經不須要 vuex
。一塊兒期待 Vue 3.0
吧,完整的類型會讓整個開發體驗徹底不同。