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正式版的发布!