刚刚分享了一篇关于 Vue3 组合式 API,不过觉得不够全面和具体,没有提及到如何 compute、watch、methods 和 prop 等从 Option-API 迁移到组合式 API。所以今天再来聊一聊,通过 code 帮助大家理解组合式 API 以及如何迁移。
我们先看一看 Vue2 的局限性,然后 Vue3 是解决这个问题,局限性如下
- 随着你的组件不断扩展,其可读性就会变得 hard,这也就意味着难于维护
- 组件间共享代码的模式不是那么优雅
- vue2 存在类型系统
可读性
这里就拿搜索页面,这里先实现搜索功能,搜索功能分布在 data 还有 methods 中,随后可能还添加搜索过滤器和分页功能。逻辑上关注特点通过组件 option 来组织。也就是对于功能他们逻辑散落在 data、computed、 props、methods 和 lifecycle。其实我们希望将功能逻辑都集中在一起。
为什么叫 composition functions 组合函数,所谓组合式 Api 就是可以在 setup 组合这些函数来,其实主要有了 setup 我们就不会将功能拆分到各个 Option,
通过一个实例解释如何将 vue2 的中 Option-API 替换为组合式 API(composition API)。
- 组合式 API 替换 data(基本类型和复合类型)
- 组合式 API 替换 methods
- 组合式 API 替换 compute
- 组合式 API 替换 watcher
- 组合式 API 如何范围prop 属性
用于组合式 API 的响应式数据
在 Vue2 我们可以在 data 中定义一个基本类型数据或者一个复合类型数据,例如对象或者数组,然后将数据绑定到界面上,data 中定义数据可以被其他属性 computed 或者 methods 访问到,这些数据是响应式数据,
data(){
return{
title:"machine leanring"
}
}
下面是组合式 API(composition API) 实现方式,在 setup 定义个变量后将其作为一个对象的属性返回,便可以通过 {{title}} 显示在界面上
setup(){
const title = "machine learning"
return {
title
}
}
<h1 class="title">{{title}}</h1>
响应式的基础类型数据
但是这样定义数据并不是响应式数据,不过 Vue3 提供两种方式在 setup 中定义响应式数据,分别是 ref 和 reactive 分别用于定义基本类型数据和复合数据。我们先看基础数据类型响应式方式为 ref(基础数据),例如 String、Number 或者 Boolean
setup(){
const title = ref("machine learning")
return {
title
}
}
响应式的复合类型数据
对于对象可以用 reactive来定义响应式对象,然后再 setup 作为返回对象的一个属性将其返回供调用。
setup(){
const title = ref("machine learning");
let count = ref(0)
const tut = reactive({
title:'machine learning',
lesson:12,
chapters:[
{id:0,title:"introducion"},
{id:2,title:"regression"},
{id:3,title:"classification"},
]
})
return {
title,
count,
tut
}
}
在 Vue2 通过defineProperty API 提供 getter 和 setter 来监听数据来实现响应式数据,不过这个中实现方式存在一些问题就是如果有大量属性值作为
compute
看一看将 Option-API 的 compute,从 vue 引入 computed 函数接受一个函数,这个函数接受一些基础值的组合(操作)。
const refTitlContent = computed(function(){
return `${refTitle.value} ${refContent.value}`
})
完整代码
<template>
<h1 class="title">{{refTitlContent}}</h1>
<div class="card">
<div class="card-header">
<div class="card-header-title">{{title}}</div>
</div>
<div class="card-content">
<div class="card-content-body">{{content}}</div>
</div>
</div>
<div class="section">
<div class="control">
<input class="input" type="text" v-model="title"/>
</div>
<div class="control">
<input class="input" type="text" v-model="content"/>
</div>
</div>
<div class="section">
<div class="control">
<input class="input" type="text" v-model="refTitle"/>
</div>
<div class="control">
<input class="input" type="text" v-model="refContent"/>
</div>
</div>
</template>
<script>
import { ref,computed } from 'vue'
export default {
name:"ReplaceComputed",
setup(){
const refTitle = ref('')
const refContent = ref('')
const refTitlContent = computed(function(){
return `${refTitle.value} ${refContent.value}`
})
return {
refTitle,
refContent,
refTitlContent
}
},
data(){
return {
title:"",
content:""
}
}
}
</script>
watcher
watch 接受一个观察值,第一个参数接受可以是一个要观察变量或者由观察变量数组,第二个 handler 回调函数接受 newVal 和 oldVal 的函数。
setup(){
const refTitle = ref('');
const refContent = ref('content');
watch([refTitle,refContent],(newVals,oldVals)=>{
console.log('Old refValue',oldVals[0]);
console.log('New refValue',newVals[0]);
console.log('New refValue',newVals[1]);
console.log('New refValue',newVals[1]);
})
return {
refTitle,
refContent
}
},
也可以检测一个响应值,这里第一个参数是接受一个函数,这个函数返回的值是要监测的对象。
const tut = reactive({
tutTitle:'',
tutContent:''
});
watch(()=>{return {...tut}}, function(newVal,oldVal){
console.log('tut title',oldVal.tutTitle)
console.log('tut title',newVal.tutTitle)
// console.log('new tut title',newVal.tutContent)
// console.log('new tut Content',newVal.tutContent)
})
watch(()=>tut.tutTitle,function(newVal,oldVal){
console.log('old tut title',newVal);
console.log('new tut title',oldVal);
})
如果要想要深度检测对象就可以在第二参数中指定deep:true。
const tut = reactive({
tutTitle:'',
tutContent:'',
category:{
name:''
}
});
const tut = reactive({
tutTitle:'',
tutContent:'',
category:{
name:''
}
});
watch(()=>tut.category,function(newVal,oldVal){
console.log('new tut category',newVal)
console.log('old tut category',oldVal)
},{
deep:true
})
provide 和 inject
首先创建 3 个 Component 分别为 ChildA、ChildB 和 ChildC,依赖关系如下图
在 App.vue 文件中
- 引入 ChildA 组件
- 然后从 vue 引入
provide函数,函数接受两个参数,第一个参数为提供变量名称,第二个参数为该变量的值
<template>
<div class="container">
<ChildA/>
</div>
</template>
<script>
import ChildA from './components/Option/ChildA.vue'
import {provide} from 'vue';
export default {
name: 'App',
setup(){
provide('refProvideTitle','machine learning 2');
},
data(){
return {
title:"machine learning",
}
},
components:{
ChildA
},
provide(){
return{
title:this.title
}
}
}
</script>
在 ChildC 文件中
- 从 vue 中引入
inject - 然后函数
inject中,第一个参数接受变量名为refProvideTitle第二个参数为该参数的默认值 inject返回接受到injectedTitle一个响应式数据,然后将setup返回供模板使用
<template>
<div>
<h1 class="title">Child C</h1>
<h1 class="title">{{title}}</h1>
<h1 class="title"> injected Title:{{injectedTitle}}</h1>
</div>
</template>
<script>
import {inject} from 'vue';
export default {
name:"ChildC",
setup(){
const injectedTitle = inject('refProvideTitle','default machine leaerning2');
return {
injectedTitle
}
},
inject:['title']
}
</script>
上面我们完成如何通过组合式 API 在不同组件之间进行传值,
const count = ref('0');
const refTut = reactive({
title:"machine learning title",
content:"machine learning content"
});
provide('count',count);
provide('tut',refTut);
<template>
<div class="container">
<div class="control">
<button class="button" @click="incrementCount" > increment </button>
</div>
<ChildA/>
</div>
</template>
<script>
import ChildA from './components/Option/ChildA.vue'
import {provide, ref, reactive} from 'vue';
export default {
name: 'App',
setup(){
provide('refProvideTitle','machine learning 2');
const count = ref('10');
const refTut = reactive({
refTitle: "machine learning title",
refContent:"machine learning content"
});
function incrementCount(){
count.value ++;
}
provide('count',count);
provide('refTut',refTut);
return {
count,
incrementCount,
}
},
data(){
return {
title:"machine learning",
}
},
components:{
ChildA
},
provide(){
return{
title:this.title
}
}
}
</script>
完整代码
<template>
<div class="container">
<div class="control">
<button class="button" @click="incrementCount" > increment </button>
</div>
<ChildA/>
</div>
</template>
<script>
import ChildA from './components/Option/ChildA.vue'
import {provide, ref, reactive} from 'vue';
export default {
name: 'App',
setup(){
provide('refProvideTitle','machine learning 2');
const count = ref('10');
const refTut = reactive({
refTitle: "machine learning title",
refContent:"machine learning content"
});
function incrementCount(){
count.value ++;
}
provide('count',count);
provide('refTut',refTut);
provide('incrementCount',incrementCount)
return {
count,
incrementCount,
}
},
data(){
return {
title:"machine learning",
}
},
components:{
ChildA
},
provide(){
return{
title:this.title
}
}
}
</script>
不但可以通过 Provide 和 Injection 来跨组件层级传递数据还可以传递方法,不过这个还是从上层至下进行传递。
<template>
<div>
<h1 class="title">Child C</h1>
<h1 class="title">{{title}}</h1>
<h1 class="title"> injected Title:{{injectedTitle}}</h1>
<h1 class="title"> injected count:{{childCount}}</h1>
<h1 class="title"> injected refTitle:{{refTitle}}</h1>
<h1 class="title"> injected refContent:{{refContent}}</h1>
<div class="control">
<button class="button" @click="incrementCount" > increment </button>
</div>
</div>
</template>
<script>
import {inject, toRefs} from 'vue';
export default {
name:"ChildC",
setup(){
const injectedTitle = inject('refProvideTitle','default machine leaerning2');
const childCount = inject('count',0);
const childTut = inject('refTut',{});
const incrementCount = inject('incrementCount')
console.log(childTut)
return {
injectedTitle,
childCount,
... toRefs(childTut),
incrementCount
}
},
inject:['title']
}
</script>
生命周期对比
| Option API | Hook inside(setup) | |
|---|---|---|
| beforeCreate | 无需 | |
| created | 无需 | |
| beforeMount | onBeforemount | |
| mounted | onMounted | |
| beforeUpdate | onBeforeUpdate | |
| update | onUpdated | |
| beforeUnmount | onBeforeUnmount | |
| errorCaptured | onErrorCaptured | |
| renderTracked | onRenderTracked | |
| renderTtriggered | onRenderTtriggered |
template 引用
<template>
<div class="section">
<div class="control">
<input type="text" class="input" ref="inputRef">
</div>
</div>
</template>
<script>
export default {
name:"ReplaceTemplateRef",
mounted(){
this.$refs.inputRef.focus()
}
}
</script>
这个大家都比较熟悉在 vue2 通过为 dom 元素指定一个 ref 便可以在 dom 元素绑定到 DOM tree 之后通过引用 $refs.inputRef来获取这个 DOM 元素的引用了。有点类似 jQuery 来操作 DOM 的方法。
接下来我们组合式 API 来实现一下,注意这里需要在 setup 函数经该 inputRef 返回
<script>
import {ref, onMounted} from 'vue';
export default {
name:"ReplaceTemplateRef",
setup(){
const inputRef = ref(null);
onMounted(()=>{
console.log(inputRef.value)
inputRef.value.focus();
})
return {
inputRef
}
},
// mounted(){
// this.$refs.inputRef.focus()
// }
}
</script>
prop
接下来聊一聊如何在 setup 函数访问 prop 属性值,其实 setup 第一个参数是 prop 子组件的自定义属性可以通过这个参数进行访问。
<template>
<div class="container">
<Person/>
</div>
</template>
<script>
import Person from './components/Option/Person.vue';
export default {
name: 'App',
components:{
Person
}
}
</script>
<template>
<div class="container">
<div class="control">
<input class="input" type="text" placeholder="tut title" v-model="tutTitle">
</div>
<div class="control">
<input class="input" type="text" placeholder="tut content" v-model="tutContent">
</div>
<PersonGreeting :title="tutTitle" :content="tutContent" />
</div>
</template>
<script>
import {ref} from 'vue';
import PersonGreeting from './PersonGreeting.vue'
export default {
name:"Person",
components:{
PersonGreeting
},
setup(){
const tutTitle = ref('');
const tutContent = ref('');
return {
tutTitle,
tutContent,
}
}
}
</script>
<template>
<div class="section">
<h1 class="title">{{title}}</h1>
<h1 class="subtitle">{{content}}</h1>
</div>
</template>
<script>
export default {
name:'PersonGreeting',
props:['title','content']
}
</script>
<template>
<div class="section">
<h1 class="title">{{title}}</h1>
<h1 class="subtitle">{{content}}</h1>
<h1 class="title"> description {{tutDescription}}</h1>
</div>
</template>
<script>
import { computed } from 'vue'
export default {
name:'PersonGreeting',
setup(props){
const tutDescription = computed(()=>{
return `${props.title} ${props.content}`
})
return {
tutDescription
}
},
props:['title','content']
}
</script>
<style scoped>
</style>
context
在 setup 接受两个参数第一个参数已经介绍过了 props,从这个参数 props 可以获取从父组件传入的参数,第二个参数 context 是对象有 attrs、emit 和 slots。可以通过 emit 从子组件向父组件传递数据。在父组件 Person 接受子组件发出事件 sendDescription 处理函数。
<template>
<div class="container">
<div class="control">
<input class="input" type="text" placeholder="tut title" v-model="tutTitle">
</div>
<div class="control">
<input class="input" type="text" placeholder="tut content" v-model="tutContent">
</div>
<PersonGreeting :title="tutTitle" :content="tutContent" @sendDescription="descriptionHandler" />
</div>
</template>
<script>
import {ref} from 'vue';
import PersonGreeting from './PersonGreeting.vue'
export default {
name:"Person",
components:{
PersonGreeting
},
setup(){
const tutTitle = ref('');
const tutContent = ref('');
function descriptionHandler(decription){
alert(decription)
}
return {
tutTitle,
tutContent,
descriptionHandler,
}
}
}
</script>
通过 context 的 emit 方法发送事件,第一个参数是事件名,第二个参数是要通过事件传递的值。
<template>
<div class="section">
<h1 class="title">{{title}}</h1>
<h1 class="subtitle">{{content}}</h1>
<h1 class="title"> description {{tutDescription}}</h1>
<button @click="sendEvent">send Description</button>
</div>
</template>
<script>
import { computed } from 'vue'
export default {
name:'PersonGreeting',
setup(props,context){
const tutDescription = computed(()=>{
return `${props.title} ${props.content}`
})
function sendEvent(){
context.emit('sendDescription',tutDescription.value)
}
return {
tutDescription,
sendEvent
}
},
props:['title','content']
}
</script>