作者:Mingle
前言
背景:2019年2月6号,React 发布 「16.8.0」 版本,vue紧随其后,发布了「vue3.0 RFC」
Vue3.0受React16.0 推出的hook抄袭启发(咳咳...),提供了一个全新的逻辑复用方案。使用基于函数的 API,我们可以将相关联的代码抽取到一个 "composition function"(组合函数)中 —— 该函数封装了相关联的逻辑,并将需要暴露给组件的状态以相应式的数据源的方式返回出来。
本文目的
本文会介绍Vue3.0「组合api的用法和注意点」。最后会用一个 Todolist 的项目实战,向大家介绍「Vue3.0的逻辑复用写法以及借用provide和inject的新型状态管理方式」
本文提纲:
- 如何新建一个使用vue3.0的项目
- conposition api
- 逻辑复用(hook)和状态管理(provide+inject)
- 结合项目实战,做一个todo list
正文
如何新建一个使用vue3.0的项目
接下来向大家简单介绍下如何尝鲜 -- 自己创建一个vue3.0的项目。
- 安装vue0-cli
我这边使用的是最新版本的vue-cli 4.4.0
npm install -g @vue/cli# ORyarn global add @vue/cli
- 将vue升级到bata版本
vue add vue-next
ok了。就这么简单!
conposition api
#### 目录
- 基本例子
- setup()
- reactive
- ref
- computed
- watchEffect
- watch
- 生命周期
- 依赖注入
基本例子
count is {{ count.count }}
plusOne is {{ plusOne }}
count++
setup
❝
该setup功能是新的组件选项。它是组件内部暴露出所有的属性和方法的统一API。
❞
调用时机
创建组件实例,然后初始化 props ,紧接着就调用setup 函数。从生命周期钩子的视角来看,它会在 beforeCreate 钩子之前被调用
模板中使用
如果 setup 返回一个对象,则对象的属性将会被合并到组件模板的渲染上下文
{{ count }} {{ object.foo }}
setup 参数
- 「props」第一个参数接受一个响应式的props,这个props指向的是外部的props。如果你没有定义props选项,setup中的第一个参数将为undifined。props和vue2.x并无什么不同,仍然遵循以前的原则;
- 不要在子组件中修改props;如果你尝试修改,将会给你警告甚至报错。
- 不要结构props。结构的props会失去响应性。
2.「上下文对象」第二个参数提供了一个上下文对象,从原来 2.x 中 this 选择性地暴露了一些 property。
const MyComponent = { setup(props, context) { context.attrs context.slots context.emit },}
Tip:
由于vue3.x向下兼容vue2.x,所以我在尝试之后发现,一个vue文件中你可以同时写两个版本的东西。
import { reactive, computed, watch, onMounted } from 'vue'export default { name: 'HelloWorld', props: { count: Number, }, data () { return { msg: "我是vue2.x中的this" } }, methods: { test () { console.log(this.msg) } }, mounted () { console.log('vue2.x mounted') }, // eslint-disable-next-line no-unused-vars setup (props, val) { console.log(this, 'this') // undefined onMounted(() => { console.log('vue3.x mounted') }) return { ...props } }}
当然这边不推荐你在项目中这么用,但是抱着尝鲜和探究的态度,我们势必要弄清如果这么写要注意哪些?
- 如果我写了mounted(2.x),在setup函数中又写了onMounted(3.x),谁先执行?
setup中的先执行。因为setup() 在解析 2.x 选项前被调用;
- 我在vue2.x选项中中定义在this上的变量,在setup上可以通过this访问吗?可以重复定义吗?可以return吗?
首先在setup中的this将不再指向vue,而是undefined;所以在setup函数内部自然无法访问到vue实例上的this。
setup内部定义的变量和外表的变量并无冲突;
但是如果你要将其return 暴露给template,那么就会产生冲突。
reactive
❝
接收一个普通对象然后返回该普通对象的响应式代理。等同于 2.x 的 Vue.observable()
❞
const obj = reactive({ count: 0 })
ref
❝
接受一个参数值并返回一个响应式且可改变的 ref 对象。ref 对象拥有一个指向内部值的单一属性 value。
❞
const count = ref(0)console.log(count.value) // 0count.value++console.log(count.value) // 1
tip:
- ref常用于基本类型,reactive用于引用类型。如果ref传入对象,其实内部会自动变为reactive.
- 当 ref 作为渲染上下文的属性返回(即在setup() 返回的对象中)并在模板中使用时,它会自动解套,无需在模板内额外书写 .value;
{{ count }}
- 当 ref 作为 reactive 对象的 property 被访问或修改时,也将自动解套 value 值,其行为类似普通属性。
const count = ref(0)const state = reactive({ count,})console.log(state.count) // 0state.count = 1console.log(count.value) // 1
- 注意当嵌套在 reactive Object 中时,ref 才会解套。从 Array 或者 Map 等原生集合类中访问 ref 时,不会自动解套:
const arr = reactive([ref(0)])// 这里需要 .valueconsole.log(arr[0].value)const map = reactive(new Map([['foo', ref(0)]]))// 这里需要 .valueconsole.log(map.get('foo').value)
computed
computed和vue2.x版本保持一致,支持getter和setter
- 传入一个 getter 函数,返回一个默认不可手动修改的 ref 对象。
const count = ref(1)const plusOne = computed(() => count.value + 1)console.log(plusOne.value) // 2plusOne.value++ // 错误!
- 或者传入一个拥有 get 和 set 函数的对象,创建一个可手动修改的计算状态。
const count = ref(1)const plusOne = computed({ get: () => count.value + 1, set: (val) => { count.value = val - 1 },})plusOne.value = 1console.log(count.value) // 0
watchEffect
❝
传入的一个函数,并且立即执行,响应式追踪其依赖,并在其依赖变更时重新运行该函数。
❞
注册监听
import {watchEffect}from 'vue' // 导入apiconst count = ref(0) // 定义响应数据watchEffect(() => console.log(count.value)) // 注册监听函数// -> 打印出 0setTimeout(() => { count.value++ // -> 打印出 1}, 100)
注销监听
- 默认情况下是在**组件卸载**的时候停止监听;- 也可以显示**调用返回值**以停止侦听;
const stop = watchEffect(() => { /* ... */})// 之后stop()
清除副作用
> 有时副作用函数会执行一些异步的副作用, 这些响应需要在其失效时清除(即完成之前状态已改变了)。所以侦听副作用传入的函数可以接收一个 onInvalidate 函数作入参, 用来注册清理失效时的回调。
当以下情况发生时,这个失效回调会被触发:
- 副作用即将重新执行时
- 侦听器被停止
const count = ref(0)watchEffect( (onInvalidate) => { console.log(count.value, '副作用') const token = setTimeout(() => { console.log(count.value, '副作用') }, 4000) onInvalidate(() => { // id 改变时 或 停止侦听时 // 取消之前的异步操作 token.cancel() }) })
副作用刷新时机
> Vue 的响应式系统会缓存副作用函数,并异步地刷新它们,这样可以避免同一个 tick 中多个状态改变导致的不必要的重复调用。在核心的具体实现中, 组件的更新函数也是一个被侦听的副作用。当一个用户定义的副作用函数进入队列时, 会在所有的组件更新后执行:
{{ count }}
在这个例子中:
- count 会在初始运行时同步打印出来
- 更改 count 时,将在组件更新后执行副作用。
如果副作用需要同步或在组件更新之前重新运行,我们可以传递一个拥有 flush 属性的对象作为选项(默认为 'post'):
// 同步运行watchEffect( () => { /* ... */ }, { flush: 'sync', })// 组件更新前执行watchEffect( () => { /* ... */ }, { flush: 'pre', })
watch
> watch API 完全等效于 2.x this.$watch (以及 watch 中相应的选项)。watch 需要侦听特定的数据源,并在回调函数中执行副作用。默认情况是懒执行的,也就是说仅在侦听的源变更时才执行回调。
- 对比 watchEffect,watch 允许我们:
- 懒执行副作用;
- 更明确哪些状态的改变会触发侦听器重新运行副作用;
- 访问侦听状态变化前后的值。
- 侦听单个数据源
侦听器的数据源可以是一个拥有返回值的 getter 函数,也可以是 ref:
// 侦听一个 getterconst state = reactive({ count: 0 })watch( () => state.count, (count, prevCount) => { /* ... */ })// 直接侦听一个 refconst count = ref(0)watch(count, (count, prevCount) => { /* ... */})
- 侦听多个数据源
watch([fooRef, barRef], ([foo, bar], [prevFoo, prevBar]) => { /* ... */})
- 与 watchEffect 共享的行为
watch 和 watchEffect 在停止侦听, 清除副作用 (相应地 onInvalidate 会作为回调的第三个参数传入),副作用刷新时机 和 助听器调试 等方面行为一致.
生命周期钩子函数
❝
可以直接导入 onXXX 一组的函数来注册生命周期钩子,这些生命周期钩子注册函数只能在 setup() 期间同步使用,在卸载组件时,在生命周期钩子内部同步创建的侦听器和计算状态也将自动删除。
❞
- 「与 2.x 版本生命周期相对应的组合式 API」
- beforeCreate -> 使用 setup()
- created -> 使用 setup()
- beforeMount -> onBeforeMount
- mounted -> onMounted
- beforeUpdate -> onBeforeUpdate
- updated -> onUpdated
- beforeDestroy -> onBeforeUnmount
- destroyed -> onUnmounted
- errorCaptured -> onErrorCaptured
- 新增的钩子函数
- onRenderTracked
- onRenderTriggered
两个钩子函数都接收一个DebuggerEvent,与 watchEffect 参数选项中的 onTrack 和 onTrigger 类似:
export default { onRenderTriggered(e) { debugger // 检查哪个依赖性导致组件重新渲染 },}
依赖注入
❝
provide 和 inject 提供依赖注入,功能类似 2.x 的 provide/inject。两者都只能在当前活动组件实例的 setup() 中调用。
❞
这是本篇文章的重点。结合项目实战以此来探索一下未来的 Vue 状态管理模式和逻辑复用模式。
「用法」
❝
provide 和 inject 提供依赖注入,功能类似 2.x 的 provide/inject。两者都只能在当前活动组件实例的 setup() 中调用。
❞
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, } },}
inject 接受一个可选的的默认值作为第二个参数。如果未提供默认值,并且在 provide 上下文中未找到该属性,则 inject 返回 undefined。
- 「注入的响应性」
可以使用 ref 来保证 provided 和 injected 时间值的响应:
// 提供者:const themeRef = ref('dark')provide(ThemeSymbol, themeRef)// 使用者:const theme = inject(ThemeSymbol, ref('light'))watchEffect(() => { console.log(`theme set to: ${theme.value}`)})
如果注入一个响应式对象,则它的状态变化也可以被侦听。
逻辑组合与复用
引出问题:
我们通常会基于一堆相同的数据进行花样呈现,有列表展示、有饼图占比、有折线图趋势、有热力图说明频次等等,这些组件使用的是相同的一些数据和数据处理逻辑。对于数据处理逻辑,目前vue有
- Mixins
- 高阶组件 (Higher-order Components, aka HOCs)
- Renderless Components (基于 scoped slots / 作用于插槽封装逻辑的组件)
但是上面的方案是存在一些弊端:
- 模版中的数据来源不清晰
- 命名空间冲突。
- 需要额外的组件实例嵌套来封装逻辑(性能问题);
##### 基于组合api 的解决方案function useMouse() { const x = ref(0) const y = ref(0) const update = e => { x.value = e.pageX y.value = e.pageY } onMounted(() => { window.addEventListener('mousemove', update) }) onUnmounted(() => { window.removeEventListener('mousemove', update) }) return { x, y }}// 在组件中使用该函数const Component = { setup() { const { x, y } = useMouse() // 与其它函数配合使用 const { z } = useOtherLogic() return { x, y, z } }, template: `
{{ x }} {{ y }} {{ z }}
`}
项目预览
源码:https://github.com/961998264/todolist-vue-3.0
项目介绍
- 已完成事件列表
- 未完成事件列表
- 查看事件详情
- 修改事件完成状态和事件详情
项目src目录
hooks文件夹是专门放hook的
context文件夹以模块划分
先来看下context编写(我这边是用的ts)
import { provide, ref, Ref, inject, computed, } from 'vue' //vue apiimport { getListApi } from 'api/home' // mock的api// 以下为定义的ts类型,你也可以单独建一个专门定义类型的文件。type list = listItem[]interface listItem { title: string, context: string, id: number, status: number,}interface ListContext { list: Ref, getList: () => {}, changeStatus: (id: number, status: number) => void, addList: (item: listItem) => void, delList: (id: number) => void, finished: Ref, unFinish: Ref, setContext: (id: number, context: string) => void, setActiveItem: () => void,}
provide名称,推荐用Symbol
const listymbol = Symbol()
提供provide的函数
export const useListProvide = () => { // 全部事件 const list = ref([]); // 当前查看的事件id const activeId = ref(null) // 当前查看的事件 const activeItem = computed(() => { if (activeId.value || activeId.value === 0) { const item = list.value.filter((item: listItem) => item.id === activeId.value) return item[0] } else { return null } }) // 获取list const getList = async function () { const res: any = await getListApi() console.log("useListProvide -> res", res) if (res.code === 0) { list.value = res.data } } // 新增list const addList = (item: listItem) => { list.value.push(item) } //修改状态 const changeStatus = (id: number, status: number) => { console.log('status', status) const removeIndex = list.value.findIndex((listItem: listItem) => listItem.id === id) if (removeIndex !== -1) { list.value[removeIndex].status = status } }; // 修改事件信息 const setContext = (id: number, context: string) => { const Index = list.value.findIndex((listItem: listItem) => listItem.id === id) if (Index !== -1) { list.value[Index].context = context } } // 删除事件 const delList = (id: number) => { console.log("delList -> id", id) for (let i = 0; i { return list.value.filter(item => item.status === 0) }) // 已完成事件列表 const finished = computed(() => { return list.value.filter(item => item.status === 1) }) provide(listymbol, { list, unFinish, finished, changeStatus, getList, addList, delList, setContext, activeItem, activeId })}
在这个函数中定义 待办事件,并且定义一系列增删改查函数,通过provide暴露出去。
提供inject的函数
export const useListInject = () => { const listContext = inject(listymbol); if (!listContext) { throw new Error(`useListInject must be used after useListProvide`); } return listContext};
全局状态肯定不止一个模块,所以在 context/index.ts 下做统一的导出
import { useListProvide, useListInject } from './home/index'console.log("useListInject", useListInject)export { useListInject }export const useProvider = () => { useListProvide()}
然后在 App.vue 的根组件里使用 provide,在最上层的组件中注入全局状态。
import { useProvider } from './context/index'export default { name: 'App', setup () { useProvider() return { } }}
在组件中获取数据:
import { useListInject } from '../../context/home/index'setup () { const { list, changeStatus, getList, unFinish, finished, addList, a ctiveItem, setContext } = useListInject()}
不管是父子组件还是兄弟组件,或者是比关系套更深的组件,我们都可以通过useListInject来获取到相应式的数据。
- 「逻辑聚合」 同一份数据的相关逻辑我们可以写在一个usexxxx的函数中,不再像以前,按照选择将逻辑分开。在methods,computed,watch,created,mounted中来回跳转。
- 「取代vuex」 在比较小的项目中,你可以用这种状态管理的方式取代vuex。(反正我用react基本不用redux,不管项目大小)。
作者:Mingle