背景: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的项目。
安装vue0-cli
我这边使用的是最新版本的vue-cli 4.4.0
npm install -g @vue/cli
# OR
yarn global add @vue/cli
将vue升级到bata版本
vue add vue-next
ok了。就这么简单!
#### 目录
基本例子
setup()
reactive
ref
computed
watchEffect
watch
生命周期
依赖注入
count is {{ count.count }}
plusOne is {{ plusOne }}
❝该setup功能是新的组件选项。它是组件内部暴露出所有的属性和方法的统一API。
❞
创建组件实例,然后初始化 props ,紧接着就调用setup 函数。从生命周期钩子的视角来看,它会在 beforeCreate 钩子之前被调用
如果 setup 返回一个对象,则对象的属性将会被合并到组件模板的渲染上下文
{{ count }} {{ object.foo }}
「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
},
}
由于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,那么就会产生冲突。
❝接收一个普通对象然后返回该普通对象的响应式代理。等同于 2.x 的 Vue.observable()
❞
const obj = reactive({ count: 0 })
❝接受一个参数值并返回一个响应式且可改变的 ref 对象。ref 对象拥有一个指向内部值的单一属性 value。
❞
const count = ref(0)
console.log(count.value) // 0
count.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) // 0
state.count = 1
console.log(count.value) // 1
注意当嵌套在 reactive Object 中时,ref 才会解套。从 Array 或者 Map 等原生集合类中访问 ref 时,不会自动解套:
const arr = reactive([ref(0)])
// 这里需要 .value
console.log(arr[0].value)
const map = reactive(new Map([['foo', ref(0)]]))
// 这里需要 .value
console.log(map.get('foo').value)
computed和vue2.x版本保持一致,支持getter和setter
传入一个 getter 函数,返回一个默认不可手动修改的 ref 对象。
const count = ref(1)
const plusOne = computed(() => count.value + 1)
console.log(plusOne.value) // 2
plusOne.value++ // 错误!
或者传入一个拥有 get 和 set 函数的对象,创建一个可手动修改的计算状态。
const count = ref(1)
const plusOne = computed({
get: () => count.value + 1,
set: (val) => {
count.value = val - 1
},
})
plusOne.value = 1
console.log(count.value) // 0
❝传入的一个函数,并且立即执行,响应式追踪其依赖,并在其依赖变更时重新运行该函数。
❞
import {watchEffect}from 'vue' // 导入api
const count = ref(0) // 定义响应数据
watchEffect(() => console.log(count.value)) // 注册监听函数
// -> 打印出 0
setTimeout(() => {
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 API 完全等效于 2.x this.$watch (以及 watch 中相应的选项)。watch 需要侦听特定的数据源,并在回调函数中执行副作用。默认情况是懒执行的,也就是说仅在侦听的源变更时才执行回调。
对比 watchEffect,watch 允许我们:
懒执行副作用;
更明确哪些状态的改变会触发侦听器重新运行副作用;
访问侦听状态变化前后的值。
侦听单个数据源
侦听器的数据源可以是一个拥有返回值的 getter 函数,也可以是 ref:
// 侦听一个 getter
const state = reactive({ count: 0 })
watch(
() => state.count,
(count, prevCount) => {
/* ... */
}
)
// 直接侦听一个 ref
const 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 }}`
}
已完成事件列表
未完成事件列表
查看事件详情
修改事件完成状态和事件详情
hooks文件夹是专门放hook的context文件夹以模块划分
先来看下context编写(我这边是用的ts)
import { provide, ref, Ref, inject, computed, } from 'vue' //vue api
import { 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 < list.value.length; i++) {
if (list.value[i].id === id) {
list.value.splice(i, 1)
break
}
}
}
// 未完成事件列表
const unFinish = computed(() => {
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,不管项目大小)。