前言
Vue Composition API 是 Vue3.0 RFC 中提出的一个重大的新功能,随着浏览器的升级和前端圈函数式编程的火热趋势,Vue3 和它的 Composition API,会在将来被越来越多的开发者选择,因此了解和学习 Composition API 非常重要。目前大部分新版浏览器已经兼容了 Proxy, 而且也提供了兼容版本的 Composition API,可以在 Vue 2.0 中使用,我们可以通过 Composition API 应用在我们的新项目中,以为了将来迁移和函数式编程模式开发做准备。
什么是 Vue Composition API
Vue Composition API: 是一组附加的、基于函数的 API,可以灵活组合组件逻辑。
a set of additive, function-based APIs that allow flexible composition of component logic.
从官方的介绍来看,它有三个特点:
- 附加的
- 基于函数
- 可以组合组件逻辑
附加的
它是一项新功能,但它不会也不会取代您从 Vue 1 和 2 中了解和喜爱的优秀的“选项 API”。只需将此新 API 视为您工具箱中的另一个工具,它可能在某些使用 Options API 解决起来有点笨拙的情况下会派上用场。
总的来说,就是 Composition API 不会影响你继续使用 Options API, Composition API 更适合用于组织你的代码逻辑。
基于函数
Composition API 都是函数式的 API,可以利用函数的特性自由地去使用诸如 Vue 响应式、生命周期、监听和计算属性等功能。这种变化在使用过程有以下几点好处:
- 不再局限于在 Vue 选项中使用,可以聚焦逻辑关注点,避免反复横跳地阅读代码
比如一个业务逻辑实现需要用到 data、methods、computed 的选项,如果很多其他业务逻辑也用到这些功能时,那么就会影响我们阅读。当不再局限在 Vue 选项中使用这些功能时,我们就可以将实现该逻辑点的代码写在一起了!
- 把各自功能拆分成函数,我们可以单独地去引入具体的功能。这意味着,我们可以通过外部文件去分功能模块,把功能内聚在各自的模块当中。
- 大多数 API 函数符合单一责任原则,而且没有外部的函数副作用,这提高了代码可读性。
可以组合组件逻辑
Composition API 可以在外部 js 文件或其他地方使用,只需要引入你需要用到的 Composition API,因此可以通过拆分成 js 功能模块,最后去组合逻辑。
举个例子,一个筛选框 + 列表 + 打印的页面,打印、筛选配置和列表的列配置之间是没有关系的,但在 Options API 中,我们可能会写在一个 Vue 单文件中。而 Composition API 基于函数的方式,可以让我们根据逻辑功能拆分,重新组合组件整体逻辑。
如下图:组件逻辑被拆分成四个模块,分别是筛选、权限、打印和表格的功能。
最后在组件内引用,把需要模版引用的值通过 setup 函数返回给模版
常用 API 的使用方法
setup
参数:
[props={}](Observer): 传递给组件的 props[context](Object): 上下文对象,包含 attrs、 slots、emit、root,其中 root 为根实例,在root 可以找到注册到全局的方法和属性,打印结果如下:
返回值:返回一个对象,暴露给 template
官方示例:
<template>
<div>{{ collectionName }}: {{ readersNumber }} {{ book.title }}</div>
</template>
<script>
import { ref, reactive } from 'vue'
export default {
props: {
collectionName: String
},
setup(props) {
const readersNumber = ref(0)
const book = reactive({ title: 'Vue 3 Guide' })
return {
readersNumber,
book
}
}
}
</script>
响应式 API
ref
接受一个内部值并返回一个响应式且可变的 ref 对象。ref 对象仅有一个 .value property,指向该内部值。
如果将对象分配为 ref 值,则它将被 reactive 函数处理为深层的响应式对象。
const readersNumber = ref(0);
console.log(readersNumber.value); // 注意要用 .value 获取值
reactive
返回对象的响应式副本,一般用于包装引用类型的数据
setup() {
const obj = reactive({ count: 0 })
return {
obj,
}
}
toRefs
解构会让数据失去响应式,可以用 toRefs 来解构赋值
setup(props) {
const {name, sex} = toRefs(Props)
const user = reactive({ username: 'xxx', age: 11 });
return {
...toRefs(user),
}
}
解构为什么会让数据失去响应式?
因为解构赋值只是将值拷贝了,但是没有经过 Proxy 代理处理响应式,因此解构赋值后,得到的数据会失去响应式。
Methods
方法在 Comosiotion API 没有特殊处理,直接声明函数即可,如果 template 有引用,就需要在 setup 中返回
setup() {
const changeTitle = () => {
book.title = 'Vue Composition Api 编程指北';
};
return {
changeTitle,
}
}
watch
watch(
() => book.title,
(newVal, oldVal) => {
console.log(`监听到标题从 ${oldVal} 更改为 ${newVal}`);
},
{ immediate: true }
);
watchEffect
立即执行传入的一个函数,同时响应式追踪其依赖,并在其依赖变更时重新运行该函数。
官方示例:
const count = ref(0)
watchEffect(() => console.log(count.value))
// -> logs 0
setTimeout(() => {
count.value++
// -> logs 1
}, 100)
从上面的示例中,可以发现 watch 与 watchEffect 之间的区别:
watch 会明确监听具体的响应式数据并且能获取数据更新前后的值 (newVal, oldVal),watchEffect 只需依赖更新就是执行这个 Effect 函数
computed
setup() {
const book = reactive({ title: 'Vue 3 Guide' });
const titleLen = computed(() => book.title.length);
return {
book,
titleLen
}
}
getCurrentInstance
getCurrentInstance 支持访问内部组件实例。
const vm = getCurrentInstance();
console.log(vm)
打印输出当前组件实例, 就和 Options API 中的 this 是一样的,虽然我们通过这种方式得到组件实例,但不建议使用在应用代码中。
getCurrentInstance只暴露给高阶使用场景,典型的比如在库中。强烈反对在应用的代码中使用getCurrentInstance。请不要把它当作在组合式 API 中获取this的替代方案来使用。
{
attrs: (...)
data: (...)
emit: ƒ ()
emitted: (...)
isDeactivated: (...)
isMounted: (...)
isUnmounted: (...)
parent: {proxy: VueComponent, type: {…}, uid: 90, update: ƒ, emit: ƒ, …}
props: (...)
proxy: VueComponent {_uid: 124, _isVue: true, $options: {…}, _renderProxy: Proxy, _self: VueComponent, …}
refs: (...)
root: {proxy: Vue, type: {…}, uid: 4, update: ƒ, emit: ƒ, …}
scope: EffectScopeImpl {active: true, effects: Array(0), cleanups: Array(0), vm: VueComponent}
setupContext: {slots: {…}, …}
slots: (...)
type: {parent: VueComponent, _parentVnode: VNode, propsData: undefined, _parentListeners: undefined, _renderChildren: undefined, …}
uid: 124
update: ƒ ()
vnode:
}
组件通信
组件通信可以分为父子组件之间的通信和跨组件之间的通信
父子组件之间的通信
-
父组件通过
props传给子组件, 子组件通过emit传给父组件- 因为 Composition API 是附加的,我们依然可以用选项式 API 声明
props emit可以从 setup 的第二个参数context获得,
- 因为 Composition API 是附加的,我们依然可以用选项式 API 声明
// 子组件
export default defineComponent({
props: {
message: String,
},
setup(props, { emit }) {
console.log('props', props.message);
const handleFeedBack = () => {
emit('onFeedBack', '收到');
};
return {
handleFeedBack,
};
},
});
- 实现 v-model
在 Options API 中,可以使用 .sync 实现 Prop 的“双向绑定”,还可以使用 model 选项实现 v-model,那么如何使用 Composition API 实现数据双向绑定呢?
实际上用法也是一样的,通过 emit('update:propname', newVal)
export default defineComponent({
props: {
visible: Boolean,
},
setup(props, { emit }) {
const handleClose = () => {
emit('update:visible', false);
};
return {
handleClose,
};
},
});
<ChildComponent :title.sync="pageTitle" />
<!-- 替换为 -->
<ChildComponent v-model:title="pageTitle" />
至于 Model 选项也是非常好用的,我在 Composition API 中没有找到相关的 API,可以直接结合一起使用。
export default defineComponent({
model: {
prop: 'visible',
event: 'change',
},
setup(props, { emit }) {
const handleClose = () => {
emit('change', false);
};
return {
handleClose,
};
},
});
数据和事件透传
在 Vue2.0 中我们可以使用 $attrs 和 $listeners 来实现数据透传和事件传递.
$attrs
包含了父作用域中不作为 prop 被识别 (且获取) 的 attribute 绑定 (
class和style除外)。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定 (class和style除外),并且可以通过v-bind="$attrs"传入内部组件——在创建高级别的组件时非常有用。
在 Vue3.0 中 $attrs 仍然保留这个功能,而 $listeners 被移除, 事件监听器是 $attrs 的一部分。因此事件传递在 Vue3.0 中使用变得更加方便了,不需要写 $listeners, 只需要写上 v-bind="$attrs" 就可以了。
父组件
<template>
<div>
<s-button @click="handleOpenDialog">打开弹窗</s-button>
<Child v-model="dialogVisible" message="回家吃饭!" @onGrandChildAnswer="onGrandChildAnswer" />
</div>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue'
import Child from './child.vue'
export default defineComponent({
components: {
Child
},
setup() {
const dialogVisible = ref(false)
const handleOpenDialog = () => {
dialogVisible.value = true
}
const onGrandChildAnswer = (answer: string) => {
// eslint-disable-next-line no-alert
alert(`来自孙子的回应:${answer}`)
}
return {
dialogVisible,
handleOpenDialog,
onGrandChildAnswer
}
}
})
</script>
子组件:需要将 $attrs 传给孙子组件
<template>
<el-dialog :visible="visible" :append-to-body="false" title="标题" :show-default-footer="false" @close="handleClose">
<grand-child v-bind="$attrs"></grand-child>
</el-dialog>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import GrandChild from './grandChild.vue'
export default defineComponent({
components: {
GrandChild
},
model: {
prop: 'visible',
event: 'change'
},
props: {
visible: Boolean
},
emits: ['change'],
setup(props, { emit }) {
const handleClose = () => {
emit('change', false)
}
return {
handleClose
}
}
})
</script>
孙子组件: 能接收到来自祖父的 message,并且做出回应
<template>
<div @click="grandChildAnswer">来自祖父的消息 {{ message }}</div>
</template>
<script lang="ts">
import { defineComponent } from '@vue/composition-api';
export default defineComponent({
props: {
message: {
type: String,
default: '',
},
},
setup(props, { emit }) {
const grandChildAnswer = () => {
emit('onGrandChildAnswer', '我收到了,祖父!');
};
return {
grandChildAnswer,
};
},
});
</script>
使用 Vuex
跨组件的通信方式,可以使用 Vuex 和 事件中心,Vuex 也有 Composition API, 使用 useStore 可以访问 store, 具体使用可以参照 Vuex 组合式 API
总结
本文主要介绍了 Composition API、常用 API 使用介绍、还有比较常见的组件通信功能实现。这已经满足大部分开发的需求,通过使用 Composition API,我感受到了使用其带来的诸多好处,特别是提高复用性和分模块处理业务逻辑方面,使用起来比 Vue2 方便了许多,希望大家读完有所收获并且可以使用起来来感受其极佳的使用体验。