前段時間,Vue 官方釋出了 Composition API RFC 的文檔,我也在收到消息的第一時間上手嚐鮮。javascript
雖然 Vue 3.0 還沒有發佈,可是其處於 RFC 階段的 Composition API 已經能夠經過插件 @vue/composition-api 進行體驗了。接下來的內容我將以構建一個 TODO LIST 應用來體驗 Composition API 的用法。vue
本文示例的代碼: https://github.com/jrainlau/v...
這個 TODO LIST 應用很是簡單,僅有一個輸入框、一個狀態切換器、以及 TODO 列表構成:java
你們也能夠在這裏體驗。react
藉助 vue-cli
初始化項目之後,咱們的項目結構以下(僅討論 /src
目錄):git
. ├── App.vue ├── components │ ├── Inputer.vue │ ├── Status.vue │ └── TodoList.vue └── main.js
從 /components
裏文件的命名不難發現,三個組件對應了 TODO LIST 應用的輸入框、狀態切換器,以及 TODO 列表。這三個組件的代碼都很是簡單就不展開討論了,此處只討論核心的 App.vue
的邏輯。github
App.vue
<template> <div class="main"> <Inputer @submit="submit" /> <Status @change="onStatusChanged" /> <TodoList :list="onShowList" @toggle="toggleStatus" @delete="onItemDelete" /> </div> </template> <script> import Inputer from './components/Inputer' import TodoList from './components/TodoList' import Status from './components/Status' export default { components: { Status, Inputer, TodoList }, data () { return { todoList: [], showingStatus: 'all' } }, computed: { onShowList () { if (this.showingStatus === 'all') { return this.todoList } else if (this.showingStatus === 'completed') { return this.todoList.filter(({ completed }) => completed) } else if (this.showingStatus === 'uncompleted') { return this.todoList.filter(({ completed }) => !completed) } } }, methods: { submit (content) { this.todoList.push({ completed: false, content, id: parseInt(Math.random(0, 1) * 100000) }) }, onStatusChanged (status) { this.showingStatus = status }, toggleStatus ({ isChecked, id }) { this.todoList.forEach(item => { if (item.id === id) { item.completed = isChecked } }) }, onItemDelete (id) { let index = 0 this.todoList.forEach((item, i) => { if (item.id === id) { index = i } }) this.todoList.splice(index, 1) } } } </script>
在上述的代碼邏輯中,咱們使用 todoList
數組存放列表數據,用 onShowList
根據狀態條件 showingStatus
的不一樣而展現不一樣的列表。在 methods
對象中定義了添加項目、切換項目狀態、刪除項目的方法。整體來講仍是很是直觀簡單的。vue-cli
按照 Vue 的官方說法,2.x 的寫法屬於 Options-API 風格,是基於配置的方式聲明邏輯的。而接下來咱們將使用 Composition-API 風格重構上面的邏輯。element-ui
下載了 @vue/composition-api
插件之後,按照文檔在 main.js
引用便開啓了 Composition API 的能力。小程序
main.js
import Vue from 'vue' import App from './App.vue' import VueCompositionApi from '@vue/composition-api' Vue.config.productionTip = false Vue.use(VueCompositionApi) new Vue({ render: h => h(App), }).$mount('#app')
回到 App.vue
,從 @vue/composition-api
插件引入 { reactive, computed, toRefs }
三個函數:微信小程序
import { reactive, computed, toRefs } from '@vue/composition-api'
僅保留 components: { ... }
選項,刪除其餘的,而後寫入 setup()
函數:
export default { components: { ... }, setup () {} }
接下來,咱們將會在 setup()
函數裏面重寫以前的邏輯。
首先定義數據。
爲了讓數據具有「響應式」的能力,咱們須要使用 reactive()
或者 ref()
函數來對其進行包裝,關於這兩個函數的差別,會在後續的章節裏面闡述,如今咱們先使用 reactive()
來進行。
在 setup()
函數裏,咱們定義一個響應式的 data
對象,相似於 2.x 風格下的 data()
配置項。
setup () { const data = reactive({ todoList: [], showingStatus: 'all', onShowList: computed(() => { if (data.showingStatus === 'all') { return data.todoList } else if (data.showingStatus === 'completed') { return data.todoList.filter(({ completed }) => completed) } else if (data.showingStatus === 'uncompleted') { return data.todoList.filter(({ completed }) => !completed) } }) }) }
其中計算屬性 onShowList
通過了 computed()
函數的包裝,使得它能夠根據其依賴的數據的變化而變化。
接下來定義方法。
在 setup()
函數裏面,對以前的幾個操做選項的方法稍加修改便可直接使用:
function submit (content) { data.todoList.push({ completed: false, content, id: parseInt(Math.random(0, 1) * 100000) }) } function onStatusChanged (status) { data.showingStatus = status } function toggleStatus ({ isChecked, id }) { data.todoList.forEach(item => { if (item.id === id) { item.completed = isChecked } }) } function onItemDelete (id) { let index = 0 data.todoList.forEach((item, i) => { if (item.id === id) { index = i } }) data.todoList.splice(index, 1) }
與在 methods: {}
對象中定義的形式所不一樣的地方是,在 setup()
裏的方法不能經過 this
來訪問實例上的數據,而是經過直接讀取 data
來訪問。
最後,把剛剛定義好的數據和方法都返回出去便可:
return { ...toRefs(data), submit, onStatusChanged, toggleStatus, onItemDelete, }
這裏使用了 toRefs()
給 data
對象包裝了一下,是爲了讓它的數據保持「響應式」的,這裏面的原委會在後續章節展開。
重構完成後,發現其運行的結果和以前的徹底一致,證實 Composition API 是能夠正確運行的。接下來咱們來聊聊 reactive()
和 ref()
的問題。
咱們知道 Vue 的其中一個賣點,就是其強大的響應式系統。不管是哪一個版本,這個核心功能都貫穿始終。而說到響應式系統,每每離不開響應式數據,這也是被你們所津津樂道的話題。
回顧一下,在2.x版本中 Vue 使用了 Object.defineProperty()
方法改寫了一個對象,在它的 getter 和 setter 裏面埋入了響應式系統相關的邏輯,使得一個對象被修改時可以觸發對應的邏輯。在即將到來的 3.0 版本中,Vue 將會使用 Proxy
來完成這裏的功能。爲了體驗所謂的「響應式對象」,咱們能夠直接經過 Vue 提供的一個 API Vue.observable()
來實現:
const state = Vue.observable({ count: 0 }) const Demo = { render(h) { return h('button', { on: { click: () => { state.count++ }} }, `count is: ${state.count}`) } }
上述代碼引用自 官方文檔
從代碼能夠看出,經過 Vue.observable()
封裝的 state
,已經具有了響應式的特性,當按鈕被點擊的時候,它裏面的 count
值會改變,改變的同時會引發視圖層的更新。
回到 Composition API,它的 reactive()
和 ref()
函數也是爲了實現相似的功能,而 @vue/composition-api
插件的核心也是來自 Vue.observable()
:
function observe<T>(obj: T): T { const Vue = getCurrentVue(); let observed: T; if (Vue.observable) { observed = Vue.observable(obj); } else { const vm = createComponentInstance(Vue, { data: { $$state: obj, }, }); observed = vm._data.$$state; } return observed; }
節選自 插件源碼
在理解了 reactive()
和 ref()
的目的以後,咱們就能夠去分析它們的區別了。
首先咱們來看兩段代碼:
// style 1: separate variables let x = 0 let y = 0 function updatePosition(e) { x = e.pageX y = e.pageY } // --- compared to --- // style 2: single object const pos = { x: 0, y: 0 } function updatePosition(e) { pos.x = e.pageX pos.y = e.pageY }
假設 x
和 y
都是須要具有「響應式」能力的數據,那麼 ref()
就至關於第一種風格,單獨地爲某個數據提供響應式能力;而 reactive()
則至關於第二種風格,給一整個對象賦予響應式能力。
可是在具體的用法上,經過 reactive()
包裝的對象會有一個坑。若是想要保持對象內容的響應式能力,在 return 的時候必須把整個 reactive()
對象返回出去,同時在引用的時候也必須對整個對象進行引用而沒法解構,不然這個對象內容的響應式能力將會丟失。這麼提及來有點繞,能夠看看官網的例子加深理解:
// composition function function useMousePosition() { const pos = reactive({ x: 0, y: 0 }) // ... return pos } // consuming component export default { setup() { // reactivity lost! const { x, y } = useMousePosition() return { x, y } // reactivity lost! return { ...useMousePosition() } // this is the only way to retain reactivity. // you must return `pos` as-is and reference x and y as `pos.x` and `pos.y` // in the template. return { pos: useMousePosition() } } }
舉一個不太恰當的例子。「對象的特性」是賦予給整個「對象」的,它裏面的內容若是也想要擁有這部分特性,只能和這個對象捆綁在一塊,而不能單獨拎出來。
可是在具體的業務中,若是沒法使用解構取出 reactive()
對象的值,每次都須要經過 .
操做符訪問它裏面的屬性會是很是麻煩的,因此官方提供了 toRefs()
函數來爲咱們填好這個坑。只要使用 toRefs()
把 reactive()
對象包裝一下,就可以經過解構單獨使用它裏面的內容了,而此時的內容也依然維持着響應式的特性。
至於什麼時候使用 reactive()
和 ref()
,都是按照具體的業務邏輯來選擇。對於我我的來講,會更傾向於使用 reactive()
搭配 toRefs()
來使用,由於通過 ref()
封裝的數據必須經過 .value
才能訪問到裏面的值,寫法上要注意的地方相對更多一些。
Vue 其中一個被人詬病得很嚴重的問題就是邏輯複用。隨着項目愈加的複雜,能夠抽象出來被複用的邏輯也愈加的多。可是 Vue 在 2.x 階段只能經過 mixins 來解決(固然也能夠很是繞地實現 HOC,這裏再也不展開)。mixins 只是簡單地把代碼邏輯進行合併,若是須要對邏輯進行追蹤將會是一個很是痛苦的過程,由於繁雜的業務邏輯裏面每每很難一眼看出哪些數據或方法是來自 mixins 的,哪些又是來自當前組件的。
另一點則是對 TypsScript 的支持。爲了更好地進行類型推斷,雖然 2.x 也有使用 Class 風格的 ts 實現方案,但其冗長繁雜和依賴不穩定的 decorator 的寫法,並不是一個好的解決方案。受到 React Hooks 的啓發,Vue Composition API 以函數組合的方式完成邏輯,天生就適合搭配 TypeScript 使用。
至於 Options API 和 Composition API 孰優孰劣的問題,在本文所展現的例子中實際上是比較難區分的,緣由是這個例子的邏輯實在是太過簡單。可是若是深刻思考的話不難發現,若是項目足夠複雜,Composition API 可以很好地把邏輯抽離出來,每一個組件的 setup()
函數所返回的值都可以方便地被追蹤(好比在 VSCode 裏按着 cmd 點擊變量名便可跳轉到其定義的地方)。這樣的能力在維護大型項目或者多人協做項目的時候會很是有用,通用的邏輯也能夠更細粒度地共享出去。
關於 Composition API 的設計理念和優點能夠參考官網的 Motivation 章節。
若是腦洞再開大一點,Composition API 可能還有更酷的玩法。
reactive()
方法能夠把一個對象變得響應式,搭配 watch()
方法能夠很方便地處理 side effects:
import { reactive, watch } from 'vue' const state = reactive({ count: 0 }) watch(() => { document.body.innerHTML = `count is ${state.count}` })
上述例子中,當響應式的 state.count
被修改之後,會觸發 watch()
函數裏面的回調。基於此,也許咱們能夠利用這個特性去處理其餘平臺的視圖更新問題。微信小程序開發框架 mpvue 就是經過魔改 Vue 的源碼來實現小程序視圖的數據綁定及更新的,若是擁有了 Composition API,也許咱們就能夠經過 reactive()
和 watch()
等方法來實現相似的功能,此時 Vue 將會是位於數據和視圖中間的一層,數據的綁定放在 reactive()
,而視圖的更新則統一放在 watch()
當中進行。
本文經過一個 TODO LIST 應用,按照官網的指導完成一次對 Composition API 的嚐鮮式探索,學習了新的 API 的用法並討論了當中的一些設計理念,分析了當中的一些問題,最後腦洞大開對立面的用法進行了探索。因爲相關資料較少且 Composition API 仍在 RFC 階段,因此文章當中可能會有難以免的謬誤,若是有任何的意見和見解都歡迎和我交流。