Vue3 Beta版發佈了,離正式投入生產使用又更近了一步。此外,React Hook在社區的發展也是如火如荼。javascript
一時間你們都以爲Redux很low,都在研究各類各樣配合hook實現的新形狀態管理模式。
在React社區中,Context + useReducer的新型狀態管理模式廣受好評,那麼這種模式能不能套用到 Vue3 之中呢?html
這篇文章就從Vue3的角度出發,探索一下將來的Vue狀態管理模式。前端
vue-composition-api-rfc:
vue-composition-api-rfc.netlify.com/api.htmlvue
vue官方提供的嚐鮮庫:
github.com/vuejs/compo…java
能夠在這裏先預覽一下這個圖書管理的小型網頁:react
sl1673495.github.io/vue-bookshe…git
也能夠直接看源碼:github
Vue3中有一對新增的api,provide
和inject
,熟悉Vue2的朋友應該明白,vue-router
在上層組件經過provide提供一些變量,在子組件中能夠經過inject來拿到,可是必須在組件的對象裏面聲明,使用場景的也不多,因此以前我也並無往狀態管理的方向去想。
可是Vue3中新增了Hook,而Hook的特徵之一就是能夠在組件外去寫一些自定義Hook,因此咱們不光能夠在.vue組件內部使用Vue的能力, 在任意的文件下(如context.ts)下也能夠,
若是咱們在context.ts中
自定義並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的初衷。
在某些場景中,前端也須要對數據作分頁,配合Vue3的Hook,它會是怎樣編寫的呢?
進入Books
這個UI組件,直接在這裏把數據切分,而且引入Pagination
組件。
<template>
<section class="wrap">
<span v-if="loading">正在加載中...</span>
<section v-else class="content">
<Book v-for="book in pagedBooks" :key="book.id" :book="book" />
<el-pagination class="pagination" v-if="pagedBooks.length" :page-size="pageSize" :total="books.length" :current="elPagenationBindings.current" @current-change="elPagenationBindings.currentChange" />
</section>
<slot name="tips"></slot>
</section>
</template>
<script lang="ts"> import { createComponent } from "@vue/composition-api"; import { usePages } from "@/hooks"; import { Books } from "@/types"; import Book from "./Book.vue"; export default createComponent({ name: "books", setup(props) { const pageSize = 10; const { elPagenationBindings, data: pagedBooks } = usePages( () => props.books as Books, { pageSize } ); return { elPagenationBindings, pagedBooks, pageSize }; }, props: { books: { type: Array, default: () => [] }, loading: { type: Boolean, default: false } }, components: { Book } }); </script>
複製代碼
這裏主要的邏輯就是用了usePages
這個自定義Hook,有點奇怪的是第一項參數返回的是一個讀取props.books
的方法。
其實這個方法在Hook內部會傳給watch方法做爲第一個參數,因爲props是響應式的,因此對props.books
的讀取天然也能收集到依賴,從而在外部傳入的books
發生變化的時候,能夠通知watch
去從新執行回調函數。
看一下usePages
的編寫:
import { watch, ref, reactive } from "@vue/composition-api";
export interface PageOption {
pageSize?: number;
}
export function usePages<T>(watchCallback: () => T[], pageOption?: PageOption) {
const { pageSize = 10 } = pageOption || {};
const data = ref<T[]>([]);
// 提供給el-pagination組件的參數
const elPagenationBindings = reactive({
current: 1,
currentChange: (currnetPage: number) => {}
});
// 根據頁數切分數據
const sliceData = (currentData: T[], currentPage: number) => {
return currentData.slice(
(currentPage - 1) * pageSize,
currentPage * pageSize
);
};
watch(watchCallback, values => {
const currentChange = (currnetPage: number) => {
elPagenationBindings.current = currnetPage;
data.value = sliceData(values, currnetPage);
};
currentChange(1);
elPagenationBindings.currentChange = currentChange;
});
return {
data,
elPagenationBindings
};
}
複製代碼
Hook內部定義好了一些響應式的數據如分頁後的數據data
,以及提供給el-pagination
組件的props對象elPagenationBindings
,此後對於前端分頁的需求來講,就能夠經過在模板中使用Hook返回的值來輕鬆實現,而不用在每一個組件都寫一些data
、pageNo
之類的重複邏輯了。
const { elPagenationBindings, data: pagedBooks } = usePages(
() => props.books as Books,
{ pageSize: 10 }
);
複製代碼
如何判斷已閱後的圖書,也能夠經過在BookContext
中返回一個函數,在組件中加以判斷:
// 是否已閱
const hasReadedBook = (book: Book) => finishedBooks.value.includes(book)
provide(BookSymbol, {
books,
setBooks,
finishedBooks,
addFinishedBooks,
removeFinishedBooks,
hasReadedBook,
booksAvaluable,
})
複製代碼
在StatusButton
組件中:
<template>
<button v-if="hasReaded" @click="removeFinish">刪</button>
<button v-else @click="handleFinish">閱</button>
</template>
<script lang="ts"> import { createComponent } from "@vue/composition-api"; import { useBookListInject } from "@/context"; import { Book } from "../types"; interface Props { book: Book; } export default createComponent({ props: { book: Object }, setup(props: Props) { const { book } = props; const { addFinishedBooks, removeFinishedBooks, hasReadedBook } = useBookListInject(); const handleFinish = () => { addFinishedBooks(book); }; const removeFinish = () => { removeFinishedBooks(book); }; return { handleFinish, removeFinish, // 這裏調用一下函數,輕鬆的判斷出狀態。 hasReaded: hasReadedBook(book) }; } }); </script>
複製代碼
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;
removeFinishedBooks: (book: Book) => void;
hasReadedBook: (book: Book) => boolean;
booksAvaluable: 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 booksAvaluable = computed(() => {
return books.value.filter(
book => !finishedBooks.value.find(({ id }) => id === book.id)
);
});
// 是否已閱
const hasReadedBook = (book: Book) => finishedBooks.value.includes(book);
provide(BookSymbol, {
books,
setBooks,
finishedBooks,
addFinishedBooks,
removeFinishedBooks,
hasReadedBook,
booksAvaluable
});
};
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區分,而是按照邏輯關注點分隔,
這樣的好處顯而易見,咱們想要維護某一個功能的時候更加方便的能找到全部相關的邏輯,而再也不是在選項和文件之間跳來跳去。
邏輯聚合 咱們想要維護某一個功能的時候更加方便的能找到全部相關的邏輯,而再也不是在選項mutation,state,action的文件之間跳來跳去(通常跳到第三個的時候我可能就把第一個忘了)
和Vue3 api一致 不用像Vuex那樣記憶不少瑣碎的api(mutations, actions, getters, mapMutations, mapState ....這些甚至會做爲面試題),Vue3的api學完了,這套狀態管理機制天然就能夠運用。
跳轉清晰 在組件代碼裏看到useBookInject
,command + 點擊後利用vscode的能力就能夠跳轉到代碼定義的地方,一目瞭然的看到全部的邏輯。(想一下Vue2中vuex看到mapState,mapAction還得去對應的文件夾本身找,簡直是...)
本文相關的全部代碼都放在
這個倉庫裏了,感興趣的同窗能夠去看,
在以前剛看到composition-api,還有尤大對於Vue3的Hook和React的Hook的區別對比的時候,我對於Vue3的Hook甚至有了一些盲目的崇拜,可是真正使用下來發現,雖然不須要咱們再去手動管理依賴項,可是因爲Vue的響應式機制始終須要非原始的數據類型來保持響應式,所帶來的一些心智負擔也是須要注意和適應的。
另外,vuex-next也已經編寫了一部分,我去看了一下,也是選擇使用provide
和inject
做爲跨模塊讀取store
的方法。vue-router-next同理,將來這兩個api真的會大有做爲。
整體來講,Vue3雖然也有一些本身的缺點,可是帶給咱們React Hook幾乎全部的好處,並且還規避了React Hook的一些讓人難以理解坑,在某些方面還優於它,期待Vue3正式版的發佈!
若是本文對你有幫助,就點個贊支持下吧,你的「贊」是我持續進行創做的動力,讓我知道你喜歡看個人文章吧~
抽獎時間,關注公衆號有機會抽取「掘金小冊5折優惠碼」。
關注公衆號「前端從進階到入院」便可加我好友,我拉你進「前端進階交流羣」,你們一塊兒共同交流和進步。