写作是对自己思想的开发和研究。
前言
先赞后看,已成习惯。大叫好我是奉旨撸码的胖大海。
本文旨在通俗易懂的描述“复合式API”的概念、语法和作用,作为自己学习的延续和阶段性成果展示。
一、什么是复合式API?
复合式API是Vue3.0新增的、相对于”选项式API“而言的,一种新的组件编写形式。语法层面,主要由setup函数和在其内部调用的生命周期钩子构成,使用时一般还会搭配一些响应式API(下期内容)。
setup函数
组合式 API 的入口,一般做为组件选项使用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| export default { props: { name: { type: String, }, }, setup(props,context) { console.log(props); const defaultName = props.name||'码农胖大海' return { defaultName }; }, };
|
需要注意的是在 setup 中你应该避免使用 this,因为setup 选项在组件创建之前执行,此时组件实例还没有生产。
生命周期钩子
可以通过直接导入 onX
函数来注册生命周期钩子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| import { onMounted, onUpdated, onUnmounted } from 'vue'
const MyComponent = { setup() { onMounted(() => { console.log('mounted!') }) onUpdated(() => { console.log('updated!') }) onUnmounted(() => { console.log('unmounted!') }) } }
|
和Vue的生命周期是一致的,除了create,因为setup本身就是这个阶段。
详情可以参看选项式 API 的生命周期选项和组合式 API 之间的映射
<script setup>
Vue3.2新增的复合式API新写法,是在单文件组件中使用组合式 API 的编译时语法糖。
1 2 3
| <script setup> console.log('hello script setup') </script>
|
里面的代码会被编译成组件 setup()
函数的内容,在每次组件实例被创建的时候执行。内置有defineProps
、 defineEmits
等方法,以提供和setup()函数中props和context相似的能力。
其间定义的所有变量都可以直接在模板使用,包括improt导入的组件或方法(官方:顶层的绑定会被暴露给模板)。但是,当需要通过模板 ref 或者 $parent访问该组件实例时,需要使用defineExpose明确要暴露出去的属性。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| <template> <div class="c-count-wrap"> <span class="c-count-num">{{ currentTime }}</span> </div> </template>
<script setup> let currentTime = ref(props.initialValue); let timer = null; // 定时器
// ...省略很多代码,具体的参看最后一个例子 const stop = () => { clearInterval(timer); };
// 使用defineExpose明确要暴露出去的属性和方法 defineExpose({ stop, }); </script>
|
相比于普通的 script 语法,它具有更多优势:
- 更少的样板内容,更简洁的代码。
- 能够使用纯 Typescript 声明 props 和抛出事件。
- 更好的运行时性能 (其模板会被编译成与其同一作用域的渲染函数,没有任何的中间代理)。
- 更好的 IDE 类型推断性能 (减少语言服务器从代码中抽离类型的工作)。
二、它的出现是为了解决什么问题?
可以帮助我们更好的进行代码逻辑的聚合和复用。
当一个组件包含功能较多,变得越来越复杂的时候,选项式API的方式有一个弊端。它会导致逻辑关注点分散,继而使得理解和维护组件变得困难。官方文档中的这个大型组件的示例,很好的展示了这点(图中逻辑关注点按颜色进行了分组)。
复合式API允许我们将这些分散在data、computed、methods、filters……中的相关逻辑拎出来写在一起,以实现代码逻辑的聚合和复用。类似mixins,但比mixins要灵活,且没有变量覆盖、数据来源不明的问题。
三、举个例子
通过一个例子感受下“选项式”和“复合式”的区别。
这里拿很久之前实现过的倒计时组件举例。代码做了简化,但麻雀虽小,五脏俱全,非常适合练手。Github上有完整代码
模板部分很简单,由两个颜色的svg图片和中间的数字构成。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| <template> <div class="c-count-wrap"> <svg xmlns="http://www.w3.org/200/svg" height="110" width="110"> <circle cx="55" cy="55" r="50" fill="none" stroke="#ccc" stroke-width="5" stroke-linecap="round"/> <circle class="c-count-process" cx="55" cy="55" r="50" fill="none" stroke="#ff9800" stroke-width="5" :stroke-dasharray="`${process},10000`"/> </svg> <span class="c-count-num">{{ currentTime }}</span> </div> </template> <style> .c-count-wrap { display: inline-block; position: relative; font-size: 0; } .c-count-wrap .c-count-num { position: absolute; display: inline-block; top: 50%; left: 0; width: 100%; text-align: center; transform: translateY(-50%); font-size: 14px; white-space: nowrap; } .c-count-wrap .c-count-process { transform-origin: 55px 55px; transform: rotate(-90deg); } </style>
|
Js部分则是有2个入参、1个计算属性、3个方法和1个事件。
选项式API的实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
| <script> export default { props: { initialValue: { type: Number, default: 10, }, autoPlay: { type: Boolean, default: true, }, }, data() { return { currentTime: 0, timer: null, }; }, computed: { // 环形进度条 process() { const totalTime = this.initialValue; const currentPercent = parseFloat(this.currentTime / totalTime).toFixed( 2 ); const circleLength = Math.floor(2 * Math.PI * 50); return currentPercent * circleLength; }, }, created() { this.currentTime = this.initialValue; if (this.autoPlay) { this.start(); } }, methods: { start() { clearInterval(this.timer); this.timer = setInterval(() => { if (this.currentTime <= 0) { clearInterval(this.timer); // 派发事件-倒计时结束 this.$emit("turnOver"); return; } this.currentTime -= 1; }, 1000); }, stop() { clearInterval(this.timer); }, reset() { this.stop(); this.currentTime = this.initialValue; }, }, }; </script>
|
复合式API的实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60
| <script> import { ref, computed } from 'vue' export default { props: { initialValue: { type: Number, default: 10, }, autoPlay: { type: Boolean, default: true, }, }, setup(props,context){ let currentTime = ref(props.initialValue) let timer = null
const start = ()=> { clearInterval(timer); timer = setInterval(() => { if (currentTime.value <= 0) { clearInterval(timer); context.emit("turnOver"); return; } currentTime.value -= 1; }, 1000); } const stop = () => { clearInterval(timer); } const reset = ()=> { stop() currentTime.value = props.initialValue; } const process = computed(()=>{ const totalTime = props.initialValue; const currentPercent = parseFloat(currentTime.value / totalTime).toFixed( 2 ); const circleLength = Math.floor(2 * Math.PI * 50); return currentPercent * circleLength; })
if (props.autoPlay) { start(); }
return { currentTime, process, start, stop, reset } } }; </script>
|
script setup版实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60
| <script setup>
import { ref, computed } from 'vue'
const props = defineProps({ initialValue: { type: Number, default: 10, }, autoPlay: { type: Boolean, default: true, }, });
const emit = defineEmits(["turnOver"]);
let currentTime = ref(props.initialValue); let timer = null;
const start = () => { clearInterval(timer); timer = setInterval(() => { if (currentTime.value <= 0) { clearInterval(timer); emit("turnOver"); return; } currentTime.value -= 1; }, 1000); }; const stop = () => { clearInterval(timer); }; const reset = () => { stop() currentTime.value = props.initialValue; };
const process = computed(() => { const totalTime = props.initialValue; const currentPercent = parseFloat(currentTime.value / totalTime).toFixed(2); const circleLength = Math.floor(2 * Math.PI * 50); return currentPercent * circleLength; });
if (props.autoPlay) { start(); }
defineExpose({ start, stop, reset, }); </script>
|
总结
复合式API是Vue3新增的、相较于“选项式API”而言的,一种新的组件编写形式。用以解决选项式API,在大型复杂组件中存在的逻辑关注点分散问题。它可以帮助我们更好的进行代码聚合和复用。
参考资料
- https://v3.cn.vuejs.org/guide/composition-api-introduction.html