- 组合式api(Composition API)算是vue3对我们开发者来说非常有价值的一个api更新,我们先不关注具体语法,先对它有一个大的感知
上面我们通过图示简单了解了一下vue3带来的全新的api形式,下面我们通过一个具体的小案例更加深入的体会一下俩种api下的开发模式对比,我们先暂时忽略语法细节,只关注代码编写形式
俩个独立的功能:
<template>
<div>
<button @click="show">显示button>
<button @click="hide">隐藏button>
<div v-if="showDiv">一个被控制显隐的divdiv>
div>
<div>
<button @click="changeRed">红色button>
<button @click="changeYellow">蓝色button>
<div :style="`color:${fontColor}`">一个被控制字体颜色的的divdiv>
div>
template>
<script>
export default {
name: 'App',
data() {
return {
showDiv: true, // 功能一数据
fontColor: '' // 功能二数据
}
},
methods: {
// 功能一方法
show() {
this.showDiv = true
},
hide() {
this.showDiv = false
},
// 功能二方法
changeRed() {
this.fontColor = 'red'
},
changeYellow() {
this.fontColor = 'blue'
}
}
}
script>
<template>
<div>
<button @click="show">显示button>
<button @click="hide">隐藏button>
<div v-if="showDivFlag">一个被控制显隐的divdiv>
div>
<div>
<button @click="changeRed">红色button>
<button @click="changeBlue">蓝色button>
<div :style="`color:${fontColor}`">一个被控制字体颜色的的divdiv>
div>
template>
<script>
import { ref } from 'vue'
export default {
name: 'App',
setup() {
// 功能一
const showDivFlag = ref(true)
function show() {
showDivFlag.value = true
}
function hide() {
showDivFlag.value = false
}
// 功能二
const fontColor = ref('')
function changeRed() {
fontColor.value = 'red'
}
function changeBlue() {
fontColor.value = 'blue'
}
return { showDivFlag, show, hide, fontColor, changeRed, changeBlue }
}
}
script>
在这里可能会有疑惑,那我们现在是把功能相关的所有数据和行为放到一起维护了,如果应用很大功能很多的情况下,setup函数不会变得很大吗?岂不是又会变得比较难维护,接下来我们就来拆解一下庞大的setup函数
<script>
import { ref } from 'vue'
function useShow() {
const showDivFlag = ref(true)
function show() {
showDivFlag.value = true
}
function hide() {
showDivFlag.value = false
}
return { showDivFlag, show, hide }
}
function useColor() {
const fontColor = ref('')
function changeRed() {
fontColor.value = 'red'
}
function changeBlue() {
fontColor.value = 'blue'
}
return { fontColor, changeRed, changeBlue }
}
export default {
name: 'App',
setup() {
// 功能一
const { showDivFlag, show, hide } = useShow()
// 功能二
const { fontColor, changeRed, changeBlue } = useColor()
return { showDivFlag, show, hide, fontColor, changeRed, changeBlue }
}
}
script>
以上,我们通过定义功能函数,把俩个功能相关的代码各自抽离到一个独立的小函数中,然后通过在setUp函数中再把俩个小功能函数组合起来,这样一来,我们既可以把setup函数变得清爽,又可以方便维护快速定位功能位置
到此我们没有关注api细节,只是体会组合式api给到我们的好处,接下来我们就要深入到api细节,看看全新的api都该如何使用 ↓
export default {
setup () {
console.log('setup执行了')
console.log(this)
},
beforeCreate() {
console.log('beforeCreate执行了')
console.log(this)
}
}
- 作用:reactive是一个函数,接收一个普通的对象传入,把对象数据转化为响应式对象并返回
使用步骤
代码落地
<template>
<div>{{ state.name }}</div>
<div>{{ state.age }}</div>
<button @click="state.name = 'pink'">改值</button>
</template>
<script>
import { reactive } from 'vue'
export default {
setup () {
const state = reactive({
name: 'cp',
age: 18
})
return {
state
}
}
}
</script>
- 作用:ref是一个函数,接受一个简单类型或者复杂类型的传入并返回一个响应式且可变的 ref 对象
使用步骤
<template>
<div>{{ money }}</div>
<button @click="changeMondy">改值</button>
</template>
<script>
import { ref } from 'vue'
export default {
setup() {
let money = ref(100)
console.log(money.value)
return {
money
}
}
}
</script>
总结说明:
- 场景: 经过reactive函数处理之后返回的对象,如果给这个对象解构或者展开,会让数据丢失响应式的能力,为了解决这个问题需要引入toRefs函数,使用 toRefs函数 可以保证该对象展开的每个属性都是响应式的
还是之前的案例,如果我们想在模板中省略到state,直接书写name和age,你可能会想到,那我在return出去的时候把state中的属性解构出来不就好了
修改前
<template>
<div>{{ state.name }}</div>
<div>{{ state.age }}</div>
<button @click="state.name = 'pink'">改值</button>
</template>
<script>
import { reactive } from 'vue'
export default {
setup() {
const state = reactive({
name: 'cp',
age: 18
})
return {
state
}
}
}
</script>
解构修改后
<template>
<div>{{ name }}</div>
<div>{{ age }}</div>
<button @click="name = 'pink'">改值</button>
</template>
<script>
import { reactive } from 'vue'
export default {
setup() {
const state = reactive({
name: 'cp',
age: 18
})
return {
...state
}
}
}
</script>
<template>
<div>{{ name }}</div>
<div>{{ age }}</div>
<button @click="name = 'pink'">改值</button>
</template>
<script>
import { reactive,toRefs } from 'vue'
export default {
setup() {
const state = reactive({
name: 'cp',
age: 18
})
return {
...toRefs(state)
}
}
}
</script>
作用:根据现有响应式数据经过一定的计算得到全新的数据
使用步骤
<template>
{{ list }}
{{ filterList }}
<button @click="changeList">change listbutton>
template>
<script>
import { computed, ref } from 'vue'
export default {
setup() {
const list = ref([1, 2, 3, 4, 5])
// 输入大于3的数字
const filterList = computed(() => {
return list.value.filter(item => item > 3)
})
// 修改list的函数
function changeList() {
list.value.push(6, 7, 8)
}
return {
list,
filterList,
changeList
}
}
}
script>
作用:基于响应式数据的变化执行回调逻辑,和vue2中的watch的功能完全一致
- 普通监听
- 立即执行
- 深度监听
使用步骤
<template>
{{ age }}
<button @click="age++">change agebutton>
template>
<script>
import { ref, watch } from 'vue'
export default {
setup() {
const age = ref(18)
watch(() => {
// 返回你想要监听的响应式属性(ref产生的对象必须加.value)
return age.value
}, () => {
// 数据变化之后的回调函数
console.log('age发生了变化')
})
return {
age
}
}
}
script>
watch的效果默认状态下,只有监听的数据发生变化才会执行回调,如果你需要在一上来的时候就立刻执行一次,需要配置一下
immediate
属性
<template>
{{ age }}
<button @click="age++">change agebutton>
template>
<script>
import { ref, watch } from 'vue'
export default {
setup() {
const age = ref(18)
watch(() => {
// 返回你想要监听的响应式属性(ref产生的对象必须加.value)
return age.value
}, () => {
// 数据变化之后的回调函数
console.log('age发生了变化')
},{ immediate: true})
return {
age
}
}
}
script>
当我们监听的数据是一个对象的时候,默认状态下,对象内部的属性发生变化是不会引起回调函数执行的,如果想让对象下面所有属性都能得到监听,需要开启
deep
配置
<template>
{{ name }}
{{ info.age }}
<button @click="name = 'pink'">change namebutton>
<button @click="info.age++">change agebutton>
template>
<script>
import { reactive, toRefs, watch } from 'vue'
export default {
setup() {
const state = reactive({
name: 'cp',
info: {
age: 18
}
})
watch(() => {
return state
}, () => {
// 数据变化之后的回调函数
console.log('age发生了变化')
}, {
deep: true
})
return {
...toRefs(state)
}
}
}
script>
使用watch的时候,尽量详细的表明你到底要监听哪个属性,避免使用deep引起的性能问题,比如我仅仅只是想在state对象的age属性变化的时候执行回调,可以这么写
<template>
{{ name }}
{{ info.age }}
<button @click="name = 'pink'">change namebutton>
<button @click="info.age++">change agebutton>
template>
<script>
import { reactive, toRefs, watch } from 'vue'
export default {
setup() {
const state = reactive({
name: 'cp',
info: {
age: 18
}
})
watch(() => {
// 详细的告知你要监听谁
return state.info.age
}, () => {
// 数据变化之后的回调函数
console.log('age发生了变化')
})
return {
...toRefs(state)
}
}
}
script>
使用步骤
<template>
<div>生命周期函数div>
template>
<script>
import { onMounted } from 'vue'
export default {
setup() {
// 时机成熟 回调函数自动执行
onMounted(() => {
console.log('mouted生命周期执行了')
})
onMounted(() => {
console.log('mouted生命周期函数又执行了')
})
}
}
script>
选项式API | 组合式API |
---|---|
beforeCreate |
不需要(直接写到setup函数中) |
created |
不需要(直接写到setup函数中) |
beforeMount |
onBeforeMount |
mounted |
onMounted |
beforeUpdate |
onBeforeUpdate |
updated |
onUpdated |
beforeDestroyed |
onBeforeUnmount |
destroyed |
onUnmounted |
在vue3的组合式API中,父传子的基础套路完全一样,基础思想依旧为:父传子是通过prop进行传入,子传父通过调用自定义事件完成
实现步骤
代码落地
app.vue
<template>
<son :name="name" @get-msg="getMsg">son>
template>
<script>
import { ref } from 'vue'
import Son from './components/son'
export default {
components: {
Son
},
setup() {
const name = ref('cp')
function getMsg(msg) {
console.log(msg)
}
return {
name,
getMsg
}
}
}
script>
components/son.vue
<template>
<div>
{{name}}
<button @click="setMsgToSon">setbutton>
div>
template>
<script>
export default {
props: {
name: {
type: String
}
},
emits: ['get-msg'], // 声明当前组件触发的自定义事件
setup(props,{emit}) {
console.log(props.name)
function setMsgToSon(){
emit('get-msg','这是一条来自子组件的msg信息')
}
return {
setMsgToSon
}
}
}
script>
通常我们使用props进行父子之间的数据传递,但是如果组件嵌套层级较深,一层一层往下传递将会变的非常繁琐,有没有一种手段可以把这个过程简化一下呢,有的,就是我们马上要学习的provide 和 inject,它们配合起来可以方便的完成跨层传递数据
来个需求: 爷组件中有一份数据 传递给孙组件直接使用
实现步骤:
代码落地
爷爷组件 - app.vue
<template>
<father>father>
template>
<script>
import Father from '@/components/Father'
import { provide } from 'vue'
export default {
components: {
Father
},
setup() {
let name = '柴柴老师'
// 使用provide配置项注入数据 key - value
provide('name', name)
}
}
script>
孙组件 - components/Son.vue
<template>
我是子组件
{{ name }}
template>
<script>
import { inject } from 'vue'
export default {
setup() {
const name = inject('name')
return {
name
}
}
}
script>
事实上,只要是后代组件,都可以方便的获取顶层组件提供的数据
provide默认情况下传递的数据不是响应式的,也就是如果对provide提供的数据进行修改,并不能响应式的影响到底层组件使用数据的地方,如果想要传递响应数据也非常简单,只需要将传递的数据使用ref或者reactive生成即可
app.vue
<template>
<father>father>
<button @click="changeName">change namebutton>
template>
<script>
import Father from '@/components/Father'
import { provide, ref } from 'vue'
export default {
components: {
Father
},
setup() {
// 使用ref转换成响应式再传递
let name = ref('柴柴老师')
function changeName(){
name.value = 'pink'
}
provide('name', name)
return {
changeName
}
}
}
script>
在模板中使用ref,我们都很清楚,它一般有三种使用场景
- ref + 普通dom标签 获取真实dom对象
- ref + 组件标签 获取组件实例对象
- ref + v-for 获取由dom对象(实例对象)组成的数组 (不经常使用)
实现步骤
const hRef = ref(null)
hRef.value
代码落地
components/RefComponent.vue
<template>
我是一个普通的组件
template>
app.vue
<template>
<h1 ref="h1Ref">我是普通dom标签h1>
<ref-comoonent ref="comRef">ref-comoonent>
template>
<script>
import { onMounted, ref } from 'vue'
import RefComoonent from '@/components/RefComponent'
export default {
components: {
RefComoonent
},
setup() {
const h1Ref = ref(null)
const comRef = ref(null)
onMounted(() => {
console.log(h1Ref.value)
console.log(comRef.value)
})
// 必须return
return {
h1Ref,
comRef
}
}
}
script>
核心功能
<template>
<section class="todoapp">
<header class="header">
<h1>todosh1>
<input
class="new-todo"
placeholder="请输入要完成的任务"
autofocus
v-model="curTask"
@keyup.enter="add"
/>
header>
<section class="main">
<input id="toggle-all" class="toggle-all" type="checkbox" v-model="isAll"/>
<label for="toggle-all">标记所有已经完成label>
<ul class="todo-list">
<li v-for="(item, index) in list" :key="item.id">
<div class="view">
<input class="toggle" type="checkbox" v-model="item.flag" />
<label>{{ item.name }}label>
<button class="destroy" @click="del(index)">button>
div>
li>
ul>
section>
<footer class="footer">
<span class="todo-count"> 还未完成的任务有:<strong>{{count}}strong>项 span>
footer>
section>
template>
<script>
import { computed, ref } from 'vue'
export default {
setup() {
const list = ref([
{ id: 1, name: '吃饭', flag: false },
{ id: 2, name: '睡觉', flag: false },
{ id: 3, name: '打豆豆', flag: true }
])
// 删除函数
function del(index) {
// index 要删除项的下标值
// splice
list.value.splice(index, 1)
}
const curTask = ref('')
function add() {
// 添加逻辑
list.value.unshift({
id: new Date(),
name: curTask.value,
flag: false
})
curTask.value = ''
}
// 全选取消全选
// {name:"cp"} console.log(info.name) info.name = 'pink'
const isAll = computed({
// 获取isAll数据的时候会执行get函数
get() {
// 当list列表中所有项的flag属性都为true 就为true
// every
return list.value.every(item => item.flag === true)
},
set(val) {
// 拿到isAll最新值 遍历一下list 把里面的flag属性设置为最新值
list.value.forEach(item => {
item.flag = val
})
}
})
// 计算未完成的任务
const count = computed(()=>{
return list.value.filter(item=>item.flag === false).length
})
return {
list,
del,
curTask,
add,
isAll,
count
}
}
}
script>
<style>
html,
body {
margin: 0;
padding: 0;
}
button {
margin: 0;
padding: 0;
border: 0;
background: none;
font-size: 100%;
vertical-align: baseline;
font-family: inherit;
font-weight: inherit;
color: inherit;
-webkit-appearance: none;
appearance: none;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
font: 14px "Helvetica Neue", Helvetica, Arial, sans-serif;
line-height: 1.4em;
background: #f5f5f5;
color: #111111;
min-width: 230px;
max-width: 550px;
margin: 0 auto;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
font-weight: 300;
}
:focus {
outline: 0;
}
.hidden {
display: none;
}
.todoapp {
background: #fff;
margin: 130px 0 40px 0;
position: relative;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1);
}
.todoapp input::-webkit-input-placeholder {
font-style: italic;
font-weight: 300;
color: rgba(0, 0, 0, 0.4);
}
.todoapp input::-moz-placeholder {
font-style: italic;
font-weight: 300;
color: rgba(0, 0, 0, 0.4);
}
.todoapp input::input-placeholder {
font-style: italic;
font-weight: 300;
color: rgba(0, 0, 0, 0.4);
}
.todoapp h1 {
position: absolute;
top: -140px;
width: 100%;
font-size: 80px;
font-weight: 200;
text-align: center;
color: #b83f45;
-webkit-text-rendering: optimizeLegibility;
-moz-text-rendering: optimizeLegibility;
text-rendering: optimizeLegibility;
}
.new-todo,
.edit {
position: relative;
margin: 0;
width: 100%;
font-size: 24px;
font-family: inherit;
font-weight: inherit;
line-height: 1.4em;
color: inherit;
padding: 6px;
border: 1px solid #999;
box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
box-sizing: border-box;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.new-todo {
padding: 16px 16px 16px 60px;
border: none;
background: rgba(0, 0, 0, 0.003);
box-shadow: inset 0 -2px 1px rgba(0, 0, 0, 0.03);
}
.main {
position: relative;
z-index: 2;
border-top: 1px solid #e6e6e6;
}
.toggle-all {
width: 1px;
height: 1px;
border: none; /* Mobile Safari */
opacity: 0;
position: absolute;
right: 100%;
bottom: 100%;
}
.toggle-all + label {
width: 60px;
height: 34px;
font-size: 0;
position: absolute;
top: -52px;
left: -13px;
-webkit-transform: rotate(90deg);
transform: rotate(90deg);
}
.toggle-all + label:before {
content: "❯";
font-size: 22px;
color: #e6e6e6;
padding: 10px 27px 10px 27px;
}
.toggle-all:checked + label:before {
color: #737373;
}
.todo-list {
margin: 0;
padding: 0;
list-style: none;
}
.todo-list li {
position: relative;
font-size: 24px;
border-bottom: 1px solid #ededed;
}
.todo-list li:last-child {
border-bottom: none;
}
.todo-list li.editing {
border-bottom: none;
padding: 0;
}
.todo-list li.editing .edit {
display: block;
width: calc(100% - 43px);
padding: 12px 16px;
margin: 0 0 0 43px;
}
.todo-list li.editing .view {
display: none;
}
.todo-list li .toggle {
text-align: center;
width: 40px;
/* auto, since non-WebKit browsers doesn't support input styling */
height: auto;
position: absolute;
top: 0;
bottom: 0;
margin: auto 0;
border: none; /* Mobile Safari */
-webkit-appearance: none;
appearance: none;
}
.todo-list li .toggle {
opacity: 0;
}
.todo-list li .toggle + label {
background-image: url("data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23ededed%22%20stroke-width%3D%223%22/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: center left;
}
.todo-list li .toggle:checked + label {
background-image: url("data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23bddad5%22%20stroke-width%3D%223%22/%3E%3Cpath%20fill%3D%22%235dc2af%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22/%3E%3C/svg%3E");
}
.todo-list li label {
word-break: break-all;
padding: 15px 15px 15px 60px;
display: block;
line-height: 1.2;
transition: color 0.4s;
font-weight: 400;
color: #4d4d4d;
}
.todo-list li.completed label {
color: #cdcdcd;
text-decoration: line-through;
}
.todo-list li .destroy {
display: none;
position: absolute;
top: 0;
right: 10px;
bottom: 0;
width: 40px;
height: 40px;
margin: auto 0;
font-size: 30px;
color: #cc9a9a;
margin-bottom: 11px;
transition: color 0.2s ease-out;
}
.todo-list li .destroy:hover {
color: #af5b5e;
}
.todo-list li .destroy:after {
content: "×";
}
.todo-list li:hover .destroy {
display: block;
}
.todo-list li .edit {
display: none;
}
.todo-list li.editing:last-child {
margin-bottom: -1px;
}
.footer {
padding: 10px 15px;
height: 20px;
text-align: center;
font-size: 15px;
border-top: 1px solid #e6e6e6;
}
.footer:before {
content: "";
position: absolute;
right: 0;
bottom: 0;
left: 0;
height: 50px;
overflow: hidden;
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), 0 8px 0 -3px #f6f6f6,
0 9px 1px -3px rgba(0, 0, 0, 0.2), 0 16px 0 -6px #f6f6f6,
0 17px 2px -6px rgba(0, 0, 0, 0.2);
}
.todo-count {
float: left;
text-align: left;
}
.todo-count strong {
font-weight: 300;
}
.filters {
margin: 0;
padding: 0;
list-style: none;
position: absolute;
right: 0;
left: 0;
}
.filters li {
display: inline;
}
.filters li a {
color: inherit;
margin: 3px;
padding: 3px 7px;
text-decoration: none;
border: 1px solid transparent;
border-radius: 3px;
}
.filters li a:hover {
border-color: rgba(175, 47, 47, 0.1);
}
.filters li a.selected {
border-color: rgba(175, 47, 47, 0.2);
}
.clear-completed,
html .clear-completed:active {
float: right;
position: relative;
line-height: 20px;
text-decoration: none;
cursor: pointer;
}
.clear-completed:hover {
text-decoration: underline;
}
.info {
margin: 65px auto 0;
color: #4d4d4d;
font-size: 11px;
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
text-align: center;
}
.info p {
line-height: 1;
}
.info a {
color: inherit;
text-decoration: none;
font-weight: 400;
}
.info a:hover {
text-decoration: underline;
}
/*
Hack to remove background from Mobile Safari.
Can't use it globally since it destroys checkboxes in Firefox
*/
@media screen and (-webkit-min-device-pixel-ratio: 0) {
.toggle-all,
.todo-list li .toggle {
background: none;
}
.todo-list li .toggle {
height: 40px;
}
}
@media (max-width: 430px) {
.footer {
height: 50px;
}
.filters {
bottom: 10px;
}
}
style>