Vue3的熱度還沒過去,React Hook在社區的發展也是如火如荼。html
一時間你們都以爲Redux很low,都在研究各類各樣配合hook實現的新形狀態管理模式。vue
在React社區中,Context + useReducer的新型狀態管理模式廣受好評。react
這篇文章就從Vue3的角度出發,探索一下將來的Vue狀態管理模式。git
vue-composition-api-rfc:
vue-composition-api-rfc.netlify.com/api.htmlgithub
vue官方提供的嚐鮮庫:
github.com/vuejs/compo…vue-cli
Vue3中有一對新增的api,provide
和inject
,熟悉Vue2的朋友應該明白,api
在上層組件經過provide提供一些變量,在子組件中能夠經過inject來拿到,可是必須在組件的對象裏面聲明,使用場景的也不多,因此以前我也並無往狀態管理的方向去想。app
可是Vue3中新增了Hook,而Hook的特徵之一就是能夠在組件外去寫一些自定義Hook,因此咱們不光能夠在.vue組件內部使用Vue的能力, 在任意的文件下(如context.ts)下也能夠,異步
若是咱們在context.ts中async
自定義並export一個hook叫useProvide
,而且在這個hook中使用provide而且註冊一些全局狀態,
再自定義並export一個hook叫useInject
,而且在這個hook中使用inject返回剛剛provide的全局狀態,
而後在根組件的setup函數中調用useProvide
。
就能夠在任意的子組件去共享這些全局狀態了。
順着這個思路,先看一下這兩個api的介紹,而後一塊兒慢慢探索這對api。
import { provide, inject } from 'vue'
const ThemeSymbol = Symbol()
const Ancestor = {
setup() {
provide(ThemeSymbol, 'dark')
}
}
const Descendent = {
setup() {
const theme = inject(ThemeSymbol, 'light' /* optional default value */)
return {
theme
}
}
}
複製代碼
這個項目是一個簡單的圖書管理應用,功能很簡單:
首先使用vue-cli搭建一個項目,在選擇依賴的時候手動選擇,這個項目中我使用了TypeScript,各位小夥伴能夠按需選擇。
而後引入官方提供的vue-composition-api庫,而且在main.ts裏註冊。
import VueCompositionApi from '@vue/composition-api';
Vue.use(VueCompositionApi);
複製代碼
按照剛剛的思路,我創建了src/context/books.ts
import { provide, inject, computed, ref, Ref } from '@vue/composition-api';
import { Book, Books } from '@/types';
type BookContext = {
books: Ref<Books>;
setBooks: (value: Books) => void;
};
const BookSymbol = Symbol();
export const useBookListProvide = () => {
// 所有圖書
const books = ref<Books>([]);
const setBooks = (value: Books) => (books.value = value);
provide(BookSymbol, {
books,
setBooks,
});
};
export const useBookListInject = () => {
const booksContext = inject<BookContext>(BookSymbol);
if (!booksContext) {
throw new Error(`useBookListInject must be used after useBookListProvide`);
}
return booksContext;
};
複製代碼
全局狀態確定不止一個模塊,因此在context/index.ts下作統一的導出
import { useBookListProvide, useBookListInject } from './books';
export { useBookListInject };
export const useProvider = () => {
useBookListProvide();
};
複製代碼
後續若是增長模塊的話,就按照這個套路就好。
而後在main.ts的根組件裏使用provide,在最上層的組件中注入全局狀態。
new Vue({
router,
setup() {
useProvider();
return {};
},
render: h => h(App),
}).$mount('#app');
複製代碼
在組件view/books.vue中使用:
<template>
<Books :books="books" :loading="loading" /> </template>
<script lang="ts">
import { createComponent } from '@vue/composition-api';
import Books from '@/components/Books.vue';
import { useAsync } from '@/hooks';
import { getBooks } from '@/hacks/fetch';
import { useBookListInject } from '@/context';
export default createComponent({
name: 'books',
setup() {
const { books, setBooks } = useBookListInject();
const loading = useAsync(async () => {
const requestBooks = await getBooks();
setBooks(requestBooks);
});
return { books, loading };
},
components: {
Books,
},
});
</script>
複製代碼
這個頁面須要初始化books的數據,而且從inject中拿到setBooks的方法並調用,以後這份books數據就能夠供全部組件使用了。
在setup裏引入了一個useAsync
函數,我編寫它的目的是爲了管理異步方法先後的loading狀態,看一下它的實現。
import { ref, onMounted } from '@vue/composition-api';
export const useAsync = (func: () => Promise<any>) => {
const loading = ref(false);
onMounted(async () => {
try {
loading.value = true;
await func();
} catch (error) {
throw error;
} finally {
loading.value = false;
}
});
return loading;
};
複製代碼
能夠看出,這個hook的做用就是把外部傳入的異步方法func
在onMounted
生命週期裏調用
而且在調用的先後改變響應式變量loading
的值,而且把loading返回出去,這樣loading就能夠在模板中自由使用,從而讓loading這個變量和頁面的渲染關聯起來。
Vue3的hooks讓咱們能夠在組件外部調用Vue的全部能力,
包括onMounted,ref, reactive等等,
這使得自定義hook能夠作很是多的事情,
而且在組件的setup函數把多個自定義hook組合起來完成邏輯,
這恐怕也是起名叫composition-api的初衷。
import { provide, inject, computed, ref, Ref } from '@vue/composition-api';
import { Book, Books } from '@/types';
type BookContext = {
books: Ref<Books>;
setBooks: (value: Books) => void;
finishedBooks: Ref<Books>;
addFinishedBooks: (book: Book) => void;
booksAvailable: Ref<Books>;
};
const BookSymbol = Symbol();
export const useBookListProvide = () => {
// 待完成圖書
const books = ref<Books>([]);
const setBooks = (value: Books) => (books.value = value);
// 已完成圖書
const finishedBooks = ref<Books>([]);
const addFinishedBooks = (book: Book) => {
if (!finishedBooks.value.find(({ id }) => id === book.id)) {
finishedBooks.value.push(book);
}
};
const removeFinishedBooks = (book: Book) => {
const removeIndex = finishedBooks.value.findIndex(({ id }) => id === book.id);
if (removeIndex !== -1) {
finishedBooks.value.splice(removeIndex, 1);
}
};
// 可選圖書
const booksAvailable = computed(() => {
return books.value.filter(book => !finishedBooks.value.find(({ id }) => id === book.id));
});
provide(BookSymbol, {
books,
setBooks,
finishedBooks,
addFinishedBooks,
removeFinishedBooks,
booksAvailable,
});
};
export const useBookListInject = () => {
const booksContext = inject<BookContext>(BookSymbol);
if (!booksContext) {
throw new Error(`useBookListInject must be used after useBookListProvide`);
}
return booksContext;
};
複製代碼
最終的books模塊就是這個樣子了,能夠看到在hooks的模式下,
代碼再也不按照state, mutation和actions區分,而是按照邏輯關注點分隔,
這樣的好處顯而易見,咱們想要維護某一個功能的時候更加方便的能找到全部相關的邏輯,而再也不是在選項和文件之間跳來跳去。
本文相關的全部代碼都放在
這個倉庫裏了,感興趣的同窗能夠去看,
在以前剛看到composition-api,還有尤大對於Vue3的Hook和React的Hook的區別對比的時候,我對於Vue3的Hook甚至有了一些盲目的崇拜,可是真正使用下來發現,雖然不須要咱們再去手動管理依賴項,可是因爲Vue的響應式機制始終須要非原始的數據類型來保持響應式,所帶來的一些心智負擔也是須要注意和適應的。
舉個簡單的例子
setup() {
const loading = useAsync(async () => {
await getBooks();
});
return {
isLoading: !!loading.value
}
},
複製代碼
這一段看似符合直覺的代碼,卻會讓isLoading
這個變量失去響應式,可是這也是性能和內部實現設計的一些取捨,咱們選擇了Vue,也須要去學習和習慣它。
整體來講,Vue3雖然也有一些本身的缺點,可是帶給咱們React Hook幾乎全部的好處,並且還規避了React Hook的一些讓人難以理解坑,在某些方面還優於它,期待Vue3正式版的發佈!