相比较 Vue.js 2.x
版本传统的方法组织形式,
Vue.js 3.x
提出了组合式
API
的格式。本
章带领读者系统学习
Vue.js 3.x
新增的有关组合式
API
的知识。
本章主要涉及到如下知识。
-
组合式 API 与选项式 API 的语法区别。
-
在组合式 API 中使用生命周期的钩子函数。
-
在组合式 API 中使用计算属性 computed 。
-
在组合式 API 中使用侦听属性 watch 。
-
响应性 API 的使用。
-
在组件中使用组合式 API。
1、组合式 API 的基本用法
组合式 API
是
Vue.js 3.x
版本的新增语法,该语法可以在大型组件中按照组件的各个功能对代码进行功能区域的划分,这样可以更加方便的管理并维护代码,同时也更好地体现了代码之间的逻辑关系。
1.1 选项式 API 与组合式 API
通过前面各个章节知识的讲解,我们可以认识到一个 Vue
应用实例的创建可以包含以
下几个选项:
➢
数据区
data
➢
方法区
methods
➢
钩子函数
➢
计算属性
computed
➢
侦听属性
watch
➢
局部组件
components
➢
局部自定义指令
directives
若是一个全局组件,除了上述选项之外,还可能具备
template
选项或
render
选项。
上面这种用各个选项将组件装配完成的语法模式被称为“选项式 API
”(
Options API
)。 这种语法模式规定:组件中所有的方法必须都书写在 methods
方法区,所有的计算属性都必须书写在 computed 选项中,以此类推。也就是组件是按照不同的选项来进行规划的,而不是按照组件的功能组成来进行规划的。这样一来,即使是不同功能的方法,也必须混杂在
methods 方法区中,这样势必遮盖了潜在的代码逻辑。
Vue.js 3.x 提出了组合式
API
(
Composition API
)的概念。组合式
API
中不在具备上述
各个组件,而是按照组件的功能划分来组织代码的书写。
组合式 API
摒弃了创建
Vue
应用实例时的各个选项,改用一个名为
setup
的函数来完成业务逻辑。该函数是组合式 API
的入口,会在
beforeCreate
钩子函数之前自动执行,即在
setup中无法访问到数据区,也无法访问到方法区,所以这些以选项格式出现的功能区也就不需要再书写了。
Vue 代码采用组合式 API 后变为如下所示的格式。
let app=Vue.createApp({
setup(){
// 组合式 API 代码
}
});
app.mount('#app')
那么setup
函数要如何书写呢?没有数据区我们如何绑定数据呢?这就需要从
Vue
对象中解构对应的功能来实现数据的绑定。
Vue.js 3.x 框架从
Vue
对象中解构出名为
ref
的函数来为绑定的变量创建响应性引用。该函数的使用格式如下所示。
let 变量的响应性引用
=ref(
初始值
);
ref 函数返回的结果是一个对象,该对象的名称应该和
HTML
文档中文本插值或双向绑定的变量名一致。同时该对象具备一个名为 value
的属性,用来存储具体的数据值。
最终 setup
函数要求将所有的变量、函数名合称为对象,并作为
setup
函数的返回值,这样才能在页面中有渲染效果。
【示例 7-1
】
第一个组合式
API
程序
。在页面中具备一个命令按钮,单击该命令按钮后,将“欢迎学习组合式 API
”的欢迎语显示在页面中。
HTML 文档中挂载点代码如下所示。
<div id="app">
<h1>组合式 API</h1> <hr />
<button @click="btnClick">单击</button>
<p>{{message}}</p>
</div>
在上述代码中,为<button></button>
按钮绑定了
click
事件,事件处理函数名为
btnClick,
同时为<p></p>
标记对实现了文本插值,文本插值的数据变量名为
message
。这在选项式
API中应该分别放在 methods
方法区和
data
数据区中。
使用组合式
API
实现的
Vue
代码如下所示。
let {ref}=Vue;
let app=Vue.createApp({
setup(){
let message=ref('');
btnClick=()=>{
message.value='欢迎学习组合式 API';
}
return {message,btnClick}
}
});
app.mount('#app')
在上述代码中,第 01
句从
Vue
对象中解构出组合式
API
需要用到的
ref
函数。观察代码,从 Vue.createApp()
方法中可以看到,原来选项式
API
中的
data
和
methods
都不复存在了,替代的是 setup
函数。
第 04
句使用
ref
函数定义了一个初值为空串的数据,并将该数据赋给
message
对象, message 对象的
value
属性记录了初始的空串值。这里需要注意,
ref
函数返回值赋给的对象名与 HTML
文档中
<p></p>
标记对文本插值的变量名是一致的。
第 05
句实现了
HTML
文档中
<button></button>
按钮绑定的单击事件的事件处理函数,类似于过去 methods
方法区中的代码。这里需要注意,函数名
btnClick
需要和
HTML
文档中绑定的事件处理函数名一致。同时这里可以使用 function
格式,也可以使用箭头函数格式。由于 setup
是一个函数,不是对象,所以不能使用
btnClick(){}
格式。
第 06
句将欢迎语赋值给
<p></p>
标记对文本插值的变量,这里
message
对象已经是<p></p>标记对文本插值的变量的一个引用,其
value
属性记录了该变量的取值。
第 08
行是
setup
函数的返回值,这里需要将
setup
函数中定义的
message
对象、
btnClick
函数作为返回值返回。
【示例 7-2
】
鼠标事件改变容器背景颜色
。在页面中有一个
<div></div>
容器,当鼠标经过时该容器的北京颜色变为#ff4e00
;当鼠标离开该容器时背景颜色变为
#3385ff
。
HTML 文档中挂载点代码如下所示。
<div id="app">
<h1>组合式 API</h1> <hr />
<div
class="box"
:class="divClass"
@mouseover="divOver"
@mouseout="divOut">
</div>
</div>
使用组合式
API
实现的
Vue
代码如下所示。
let {ref}=Vue;
let app=Vue.createApp({
setup(){
let divClass=ref('bgc01');
divOver=()=>{
divClass.value='bgc02';
}
divOut=()=>{
divClass.value='bgc01';
}
return {divClass,divOver,divOut};
}
});
app.mount('#app')
在上述代码中,第 04
行使用
ref
函数创建了
divClass
变量的引用,同时赋初值为字符串“
bgc01
”
,这样页面中的
<div></div>
标记对会首先让
bgc01
样式生效。第
05
行至第
07
行完成<div></div>
标记对鼠标经过时的事件处理函数。第
08
行至第
10
行完成
<div></div>
标记对鼠标离开时的事件处理函数。
本例中需要用到以下两个类名。
.bgc01{
background-color: #3385ff;
}
.bgc02{
background-color: #ff4e00;
}
1.2 在组合式 API 中使用生命周期钩子
我们在 Vue
组件的生命周期中讲述了八个钩子函数,其中
beforeCreate
和
created
是不需要在组合式 API
中使用的,因为
setup
函数本身就是围绕这两个钩子函数运行的。即在beforeCreate 和
created
钩子函数中书写的代码可以直接书写在
setup
函数中。
在 setup
函数中使用其他钩子函数,需要在钩子函数名前添加
on_
前缀,同时将钩子函数名的首字母大写。例如 mounted
钩子函数应该书写为
onMounted
钩子函数。
在组合式 API
中使用生命周期的钩子函数有如下两点语法要求。
➢ 钩子函数名需要首先从
Vue
对象中解构出来。
➢ 钩子函数的参数是一个回调函数,具体的代码要书写在这个回调函数中。
【示例 7-3
】
在组合式
API
中使用钩子函数
。在页面中有一个显示单价的文本插值,同
时还有一个用于修改单价的文本框和命令按钮。默认情况下单价显示一个初始值。当用户在文本框中录入了修改后的单价并单击命令按钮后,单价被修改并显示在文本插值中。在整个示例过程中,为组合式 API 绑定 onBeforeMount、onMounted、onBeforeUpdate、onUpdated四个钩子函数,并观察控制台中显示的文本提示。
示例效果如图 7-1
所示。
![](https://i-blog.csdnimg.cn/direct/71625056872a40b1bf9855e9e824f838.png)
HTML
文档中挂载点代码如下所示。
<div id="app">
<h1>组合式
API</h1> <hr />
<p>单价:¥
{{price}}</p>
<p>
<input type="text" v-model.number="newPrice" />
<button @click="setPrice">更改单价</button>
</p>
</div>
使用组合式
API
实现的
Vue
代码如下所示。
let {ref,onBeforeMount,onMounted,onBeforeUpdate,onUpdated}=Vue;
let app=Vue.createApp({
setup(){
let price=ref('150.50');
let newPrice=ref('');
function setPrice(){
price.value=newPrice.value.toFixed(2);
}
onBeforeMount(()=>{
console.log('BeforeMount 钩子函数触发了');
})
onMounted(()=>{
console.log('Mounted 钩子函数触发了');
})
onBeforeUpdate(()=>{
console.log('BeforeUpdate 钩子函数触发了');
})
onUpdated(()=>{
console.log('Updated 钩子函数触发了');
})
return {price,newPrice,setPrice};
}
});
app.mount('#app')
在上述代码中,第 01
行先从
Vue
对象中解构出
setup
函数要用到的四个钩子函数。第04行创建了文本插值的变量引用
price
,第
05
行创建了文本框双向绑定的变量引用
newPrice
。第 21
行将
HTML
文档中要用到的
price
、
newPrice
、
setPrice
整合成对象,并作为
setup
函数的返回值。
第 09
行至第
11
行书写了
onBeforeMounte
钩子函数的代码。可以看出,钩子函数的内容书写在了 onBeforeMount
钩子函数的回调函数中,回调函数是一个使用箭头函数书写的格式。请读者在具体操作的过程中注意观察控制台输出的内容。
1.3 在组合式 API 中使用计算属性
在组合式 API
中使用计算属性,也需要首先从
Vue
对象中解构出
computed
成员,来完成计算属性的书写。
在组合式 API
的
setup
函数中使用计算属性的语法格式如下所示。
let {ref,computed}=Vue;
setup(){
let 计算属性名=computed({
get(){
// 计算属性的 Getter()函数
return 计算属性表达式
},
set(newValue){};
// 计算属性的 Setter()函数
})
}
若项目功能不需要
Setter()
函数,则语法格式如下所示。
let {ref,computed}=Vue;
setup(){
let 计算属性名
=computed(function(){
return 计算属性表达式;
})
}
【示例 7-4
】
在组合式
API
中使用计算属性
。在页面中有如图
7-2
所示的界面。当用户调整商品的购买数量时,自动计算共需支付的总价。其中购买数量是 HTML5
技术提供的type 属性取值为
number
的
<input />
标记,即微调器。
示例效果如图
7-1
所示。
![](https://i-blog.csdnimg.cn/direct/f54c9fd6def646c890ac535233b8a906.png)
HTML
文档中挂载点代码如下所示。
<div id="app">
<h1>在组合式
API
中使用计算属性
</h1> <hr />
<p>商品单价:¥
{{price}}</p>
<p>购买数量:
<input type="number" v-model="count"></p>
<p>共需支付:¥
{{total}}</p>
</div>
使用组合式
API
实现的
Vue
代码如下所示。
let {ref,computed}=Vue;
let app=Vue.createApp({
setup(){
let price=ref(25.6);
let count=ref(1);
let total=computed(()=>(price.value*count.value).toFixed(2));
return {price,count,total};
}
});
app.mount('#app')
在上述代码中,第 04
行定义了表示单价的变量引用,即
price
,并赋初值为
25.6
。第05 行定义了双向绑定微调器的变量
count
的引用,并赋初值为
1
。第
06
行定义了计算属性total,该属性的依赖变量是
price
和
count
。当
price
或
count
发生变化时,要对
total
进行重新计算。这里读者需要注意,使用 computed
定义的计算属性,若需要在代码中访问该值,也需要使用 value
属性来访问,即
total.value
。
1.4 在组合式 API 中使用侦听属性
在组合式 API
中使用侦听属性,也需要首先从
Vue
对象中解构出
watch
成员,来完成侦听属性的书写。
在组合式 API
的
setup
函数中使用侦听属性的语法格式如下所示。
let {ref,watch}=Vue;
setup(){
let 变量名
=ref(
初值
);
watch(变量名
,(newValue,oldValue)=>{
// 当变量名发生变化时,执行该函数体
},{
deep:true/false,
immediate:true/false
})
}
【示例 7-5】
在组合式
API
中使用侦听属性
。在页面中有如图
7-3
所示的界面。当用户在十六进制颜色代码的文本框中输入一个 6
位颜色代码值时,系统会自动计算出该颜色代码值的各个基色成分,即红色、绿色、蓝色各占十进制的数值。
示例效果如图
7-3
所示。
![](https://i-blog.csdnimg.cn/direct/9da086b844d240f2abf3165a6a4aa430.png)
HTML
文档中挂载点代码如下所示。
<div id="app">
<h1>在组合式
API
中使用侦听属性
</h1> <hr />
<p>十六进制颜色代码:
#<input type="text" v-model="color" /></p>
<p>红色:
{{red}}
;绿色:
{{green}}
;蓝色:
{{blue}}
。
</p>
</div>
使用组合式
API
实现的
Vue
代码如下所示。
let {ref,watch}=Vue;
let app=Vue.createApp({
setup(){
let color=ref('ff5857');
let red=ref(0);
let green=ref(0);
let blue=ref(0);
watch(color,newValue=>{
if(newValue.length===6){
red.value=Number.parseInt(newValue.slice(0,2),16);
green.value=Number.parseInt(newValue.slice(2,4),16);
blue.value=Number.parseInt(newValue.slice(4),16);
}
},{
immediate:true
})
return {color,red,green,blue};
}
});
app.mount('#app')
在上述代码中,第 08
行至第
16
行对
color
数据进行了侦听,同时使用了侦听属性的immediate 属性,即立即执行的侦听属性特性。
2、 响应性 API
在组合式 API
的
setup
函数中,
Vue.js 3.x
框架提供了大量的可以实现响应性功能的命
令,这些命令可以轻易的实现数据绑定,并且可以为数据指定特定的初始值。
2.1 响应性基础 API
1. reactive()的使用。
从 Vue
对象中解构出
reactive
之后,可以利用
reactive()
方法创建对象的响应性代理。该
方法的使用有如下特点。
➢ reactive()
方法的参数只能接收对象类型的数据和
ref
数据。
➢ reactive()
方法可以实现对象的深度响应。
reactive()方法的语法格式如下所示。
let {reactive}=Vue;
let 变量
=reactive({});
setup(){
let product={
brand:'Apple', productName:'iPad', type:'Air 4', size:'256G'
}
let pro=reactive(product);
function btnClick(){
pro.brand='HuaWei'
console.log(product)
}
return {pro,btnClick};
}
上述代码中,第 05
行为对象
product
创建了一个响应性代理
pro
,这样在第
10
行将 pro
返回后,在
HTML
文档中就可以通过文本插值显示
pro
代理的成员了。第
06
行至第
09
行设置了一个函数,该函数若绑定到按钮的单击事件中,则单击按钮后,会更改 pro
代理中的brand 成员的值,同时也会影响到
product
源对象中
brand
的值。
2 .readonly 的使用。
从 Vue
对象中解构出
readonly
之后,可以利用
readonly()
方法创建对象的只读代理。该方法的使用有如下特点。
➢ readonly()
方法的参数只能接收对象类型的数据和
ref
数据。
➢ readonly()
方法可以实现对象的深度只读。
readonly()方法的语法格式如下所示。
let {readonly}=Vue;
let 变量
=readonly({});
setup(){
let obj={ a:100,b:200,c:300 };
let objRead=readonly(obj);
console.log(objRead.a);
objRead.a=500;
console.log(objRead.a);
}
在上述代码中,第 03
行为对象
obj
创建了一个只读代理
objRead
。第
04
行在控制台输出 a
成员的值,结果为
100
。第
05
行改变
a
成员的值为
500
,系统会报出提示并认定修改失败,提升内容为:Set operation on key "a" failed: target is readonly.
所以第
06
行再次在控制台输出 a
成员的值时,结果仍为
100
。
3 .toRaw 的使用。
从 Vue
对象中解构出
toRaw
之后,可以利用
toRaw()
方法得到
reactive()
方法和
readonly()
方法代理的原始对象。
toRaw()方法的语法格式如下所示。
let {toRaw}=Vue;
let 变量
=toRaw(reactive
代理
/readonly
代理
);
setup(){
let obj={ a:100,b:200,c:300 };
let objRead=readonly(obj);
console.log(objRead);
let objValue=toRaw(objRead);
console.log(objValue)
}
在上述代码中,第 03
行为对象
obj
创建了一个只读代理
objRead
,第
04
行在控制台直接输出 objRead
,结果为:
Proxy{a:100,b:200,c:300}
。第
05
行通过
toRaw()
方法可以得到objRead 只读代理的原始对象
objValue
,即
obj
,第
06
行在控制台中输出
objValue
,结果为: {a:100,b:200,c:300}。
若在控制台显示表达式 objValue===obj
的结果,应该显示
true
。
4 .markRaw 的使用。
从 Vue
对象中解构出
markRaw
之后,可以利用
markRaw()
方法基于对象创建一个无法
转换为代理的对象。
markRaw()方法的语法格式如下所示。
let {markRaw}=Vue;
let 变量
=markRaw({});
01 setup(){
02 let obj={ a:100,b:200,c:300 };
03 let temp=markRaw(obj);
04 let p1=reactive(temp);
05 let p2=readonly(temp);
06 }
在上述代码中,第 03
行为
obj
对象创建了一个无法转换为代理的对象
temp
。这样一来,
想基于
temp
创建
reactive()
方法或
readonly()
方法代理都是无法成功的。最终第
04
行和第
05
行创建的
reactive()
方法代理
p1
和
readonly()
方法代理
p2
都是对象,并没有成功创建
Proxy
代理。
5 .判断响应性。
从 Vue
对象中解构出
isProxy
、
isReactive
、
isReadonly
,可以利用这些方法判断一个变
量是否为响应性代理。
➢ isProxy()
方法用于判断变量是否是由
reactive
或
readonly
创建的代理
➢ isReactive()
方法用于判断变量是否是由
reactive
方法创建的响应性代理。
➢ isReadonly()
方法用于判断变量是否是由
readonly
方法创建的只读性代理。
01 setup(){
02 let obj={ a:100,b:200,c:300 };
03 let objReactive=reactive(obj);
04 let objRead=readonly(obj);
05 let objRaw=markRaw(obj);
06 console.log(isProxy(objReactive)); // true
07 console.log(isProxy(objRead)); // true
08 console.log(isReactive(objReactive)); // true
09 console.log(isReadonly(objRead)); // true
10 console.log(isProxy(objRaw)); // false
11 }
2.2 Refs
1. ref 的使用。
从 Vue
对象中解构出
ref
,可以利用
ref()
方法将数据转换为一个响应性且可变的
ref
对
象。该方法的使用有如下特点。
➢ 最终生成的
ref
对象具备
value
属性,该属性返回最终的数据取值。
➢ ref
方法接收一个引用数据类型的数据,其
value
属性返回该对象的代理。
01 setup(){
02 let number=ref(150);
03 let string=ref('welcome');
04 let boolean=ref(true);
05 console.log(number.value,string.value,boolean.value);
06 let object=ref({a:100,b:200,c:300});
07 console.log(object.value);
08 }
在上述代码中,第 02
行到第
04
行为基本数据类型生成
ref
对象,第
05
行输出这三个
基本数据类型生成的
ref
对象的
value
属性取值,分别是:
150 'welcome' true
。第
06
行为引
用数据类型生成
ref
对象,第
07
行输出这个引用数据类型生成的
ref
对象的
value
属性取值,
结果为:
Proxy{a:100,b:200,c:300}
。
2 .unref 的使用。
从 Vue 对象中解构出 unref,可以利用 unref()方法返回 ref 对象的原始值。
01 setup(){
02 let number=ref(150);
03 let string=ref('welcome');
04 let boolean=ref(true);
05 let num=unref(number);
06 let str=unref(string);
07 let bool=unref(boolean);
08 console.log(num,str,bool);
09 let object=ref({a:100,b:200,c:300});
10 let obj=unref(object)
11 console.log(obj);
12 }
第 05
行到第
07
行为第
02
行到第
04
行生成的
ref
对象返回原始值。第
08
行输出这些
原始值,结果分别是:
150 'welcome' true
。第
09
行依据一个对象创建了
ref
对象,第
10
行
使用
unref()
方法企图返回
ref
对象的原始值,第
11
行输出该原始值时会发现,结果依然为
Proxy{a:100,b:200,c:300}
。
从上面的代码中我们可以得出结论,基于基本数据类型生成的 ref
对象,使用
unref()
方法可以得到其原始值。基于引用数据类型生成的
ref
对象,使用
unref()
方法无法得到其原
始值,依然是引用数据类型的代理。
鉴于上述原因,在项目开发过程中,组合式 API
绑定数据时尽量遵循下列原则。
➢ 对于基本数据类型的响应性引用,使用
ref()
方法。
➢ 对于引用数据类型的响应性引用,使用
reactive()
方法。
3 .toRef 的使用。
从 Vue
对象中解构出
toRef
,可以利用
toRef()
方法为响应性对象上的某个属性新建一个
ref
响应性对象。同时这个新建的
ref
响应性对象与源响应性对象还是双向绑定的。
toRef()方法的语法格式如下所示。
let {reactive,toRef}=Vue;
let 源响应性对象
=reactive(
对象数据
);
let 新响应性对象
=toRef(
源响应性对象
,
属性名
);
01 setup(){
02 let object={a:100,b:200,c:300};
03 let obj=reactive(object);
04 let aRef=toRef(obj,'a');
05 console.log(obj.a,aRef.value); // 100 100
06 aRef.value=300;
07 console.log(obj.a,aRef.value); // 300 300
08 obj.a=200;
09 console.log(obj.a,aRef.value); // 200 200
10 }
在上述代码中,第 02
行定义了一个普通对象,第
03
行使用
reactive()
方法为其生成了
一个响应性对象
obj
,第
04
行将响应性对象
obj
的
a
属性生成了一个新响应性对象
aRef
。在
第
05
行输出
obj.a
和
aRef.value
时,都是
100
。
这样一来,obj
是源响应性对象,
aRef
是从
obj
中的
a
属性里生成的新响应性对象。即
aRef
和
obj.a
双向绑定。
第 06
行和第
07
行说明:修改
aRef.value
的值,
obj.a
也跟着发生变化。
第 08
行和第
09
行说明:修改
obj.a
的值,
aRef.value
也跟着发生变化。
4 .toRefs 的使用。
从 Vue 对象中解构出 toRefs,可以利用 toRefs()方法为响应性对象上的所有属性新建为ref 响应性对象。同时这个新建的 ref 响应性对像中的成员分别与源响应性对象的对应成员双向绑定。
toRefs()方法的语法格式如下所示。
let {reactive,toRef}=Vue;
let 源响应性对象
=reactive(
对象数据
);
let 新响应性对象
=toRefs(
源响应性对象
);
01 setup(){
02 let object={a:100,b:200,c:300};
03 let obj=reactive(object);
03 let objRefs=toRefs(obj);
04 console.log(objRefs); // {a:objectRefImpl,b:objectRefImpl,c:objectRefl}
05 objRefs.a.value=200;
06 console.log(objRefs.a.value,obj.a); // 200
07 obj.a=300;
08 console.log(objRefs.a.value,obj.a); // 300
09 }
在上述代码中,第 03
行为响应性对象
obj
中的所有属性生成新的响应性对象
objRefs
。第 04
行在控制台中输出
objRefs
。新的响应性对象
objRefs
的结构如下所示。
{
a:{value:100,_key:"a",_object:Proxy{a:300,b:200,c:300},
b:{value:200,_key:"a",_object:Proxy{a:300,b:200,c:300},
c:{value:300,_key:"a",_object:Proxy{a:300,b:200,c:300}
}
5 .isRef 的使用。
从 Vue
对象中解构出
isRef
,可以利用
isRef()
方法判断参数是否是一个
ref
响应性对象。
3、 在组件中使用组合式 API
3.1 组件中的 setup 函数
setup 函数在组件中使用时可以接受两个参数:
props
和
context
。其中
props
参数用于在
setup
函数中访问父组件传递过来的数据,这是一个响应性代理。
context
参数是一个对象,暴露了其它可能在 setup
函数中有用的值。
【示例 7-6
】
使用组合式
API
开发
my-list
组件
。从
Vue
应用实例中向组件
my-list
传递一个数组,数组中显示六个星座的名称,my-list
组件循环遍历该数组并形成一个无序列表显示在页面中。当单击页面中的命令按钮时,无序列表显示另外六个星座的名称。
在
HTML
文档中使用
my-list
组件的代码如下所示。
<div id="app">
<h1>使用组合式
API
开发
my-list
组件
</h1> <hr />
<my-list :lists="lists"
></my-list>
<button @click="btnClick">下一组
</button>
</div>
01 let {reactive}=Vue;
02 let app=Vue.createApp({
03 setup(){
04 let lists=reactive(['白羊座','金牛座','双子座','巨蟹座','狮子座','处女座']);
05 function btnClick(){
06 let arr=['天秤座','天蝎座','射手座','摩羯座','水瓶座','双鱼座'];
07 lists.forEach((item,index,array)=>{
08 array[index]=arr[index];
09 })
10 }
11 return {lists,btnClick};
12 }
13 })
14 app.component('my-list',{
15 template:`
16 <ul>
17 <template v-for="(item,index) in lists">
18 <li>{{item}}</li>
19 </template>
20 </ul>
21 `,
22 props:['lists'],
23 setup(props){
24 let lists=props.lists;
25 console.log(lists);
26 }
27 })
28 app.mount('#app')
在上述代码中,第 02
行至第
13
行为
Vue
应用实例的代码,即
my-list
组件的父组件。
第
14
行至第
27
行为注册
my-list
组件的代码。
在 Vue
应用实例中,第
04
行使用
reactive()
方法生成了一个响应性数组,并将该数组作
为组件的自定义属性传递给
my-list
组件。在
my-list
组件的第
22
行,依然使用了
props
选项
来接收父组件传递过来的数据,此时在
my-list
组件的
template
模板区已经可以访问到
lists
数组了,第
17
行至第
19
行的数组遍历已经能够实现了。
但是子组件通过 props
选项接收到的父组件传递过来的数据,若要在
setup
函数中访问
时不能使用
this.lists
这种表述方式的。只能使用
setup
函数的
props
属性访问到
lists
数组中
的数据。
当 Vue
应用实例中的命令按钮单击时,将响应性数据
lists
中的数组元素更换为另外六
个星座的名称,这样子组件中对父组件传递过来的数据的渲染会自动发生变化。
除此之外,setup
函数还具备一个名为
context
的参数,该参数是一个对象,具备以下四
个成员。
➢ context.attrs
:
context
对象的属性,用于访问父组件传递过来的非
Props
属性,相当
于选项式
API
中的
this.$attrs
属性。
➢ context.slots
:
context
对象的属性,用于在使用
render
渲染函数创建组件模板时,
在组件中安置插槽。
➢ context.emit
:
context
对象的方法,用于子组件触发父组件的自定义事件,相当于选
项式
API
中的
this.$emit()
方法。
➢ context.expose
:
context
对象的方法,用于在使用
render
渲染函数创建组件模板时,
向父组件中暴露使用了
ref
的子组件的数据。
3.2 使用 setup 函数从子组件向父组件传递数据
我们知道,子组件向父组件传递数据应该使用 this.$emit()
方法来结合自定义事件实现。
在
setup
函数中,
setup
函数的参数
context
对象中具备
emit()
方法,可以直接使用
context.emit()
来触发父组件的自定义事件,也可以从
context
对象中解构出来的再使用。
【示例 7-7
】
在
setup
函数中向父组件传递数据
。向实例
7-6
中的
my-list
组件的每一个
无序列表项之前添加一个关闭按钮,用户单击该按钮时可以删除该列表项。
子组件 my-list
代码变为如下所示。
01 app.component('my-list',{
02 template:`
03 <ul>
04 <template v-for="(item,index) in lists">
05 <li><span class="close" @click="close(index)">×</span> {{item}}</li>
06 </template>
</ul>
08 `,
09 props:['lists'],
10 setup(props,context){
11 let {emit}=context;
12 function close(index){
13 emit('custom',index);
14 }
15 return {close};
16 }
17 })
在上述代码中,子组件 my-list
的
setup
函数在第
11
行首先从
context
中解构出
emit()
方
法,在第
13
行使用
emit()
方法触发了父组件的
custom
自定义事件,并将单击了关闭按钮的
无序列表项的索引值传递给了父组件。
HTML
文档中使用
my-list
组件的代码如下所示。
<my-list :lists="lists" @custom="listCustom"></my-list>
Vue
应用实例代码变为如下所示。
01 let {reactive}=Vue;
02 let app=Vue.createApp({
03 setup(){
04 let lists=reactive(['白羊座','金牛座','双子座','巨蟹座','狮子座','处女座']);
05 function btnClick(){
06 let arr=['天秤座','天蝎座','射手座','摩羯座','水瓶座','双鱼座'];
07 lists.forEach((item,index,array)=>{
08 array[index]=arr[index];
09 })
10 }
11 function listCustom(index){
12 lists.splice(index,1);
13 }
14 return {lists,btnClick,listCustom};
15 }
16 })
在上述代码中,Vue
应用实例作为父组件,第
11
行至第
13
行定义了自定义事件
custom
的事件处理函数
listCustom
。该函数接收子组件传递过来的无序列表项的索引值,并将该无
序列表项从
lists
数组中删除掉。
3.3 在 setup 函数使用 render 渲染函数
在组件中若不希望使用 template
选项设置组件的
DOM
模板,也可以使用
render
渲染函
数来完成。此时
setup
函数的返回值将返回创建的虚拟
DOM
节点。这种情况下,
setup
函数
可以使用
context
参数的
slots
属性为虚拟
DOM
节点安置插槽。
【示例 7-8
】
在
setup
函数中使用渲染函数并安置插槽
。全局注册一个名为
my-title
组件,
该组件用于为页面设置主标题和副标题。最终效果如图
7-4
所示。
子组件
my-title
代码如下所示。
01 let app=Vue.createApp({
02 setup(){}
03 })
04 app.component('my-title',{
05 setup(props,context){
06 let {h} =Vue;
07 return ()=>{
08 return h('div',{
09 class:'title'
10 },[
11 h('h1',context.slots.primary()),
12 h('p',{
13 class:'desc'
14 },context.slots.secondary()),
15 h('hr')
16 ])
17 }
18 }
19 })
20 app.mount('#app')
在上述代码中,第 06
行从
Vue
对象中解构出用于创建虚拟
DOM
节点的
h
函数,第
07行至第 17
行,
setup
函数使用
return
语句返回整个组件的
DOM
结构。此时
setup
函数的
return语句已经不在为 template
模板暴露数据了。
在创建虚拟 DOM
节点的过程中,第
11
行为标记对
h1
安置了名为
primary
的具名插槽,用于书写主标题,第 14
行为标记对
p
安置了名为
secondary
的具名插槽,用于书写副标题。即在渲染函数中安置插槽要用到 setup
函数的
context
参数来实现。
HTML
文档中使用
my-list
组件的代码如下所示。
<div id="app">
<my-title>
<template v-slot:primary>
新生信息录入
</template>
<template v-slot:secondary>
在该页面您可以通过对表单的操作完成新生信息的录入。
</template>
</my-title>
</div>
这样一来,使用 render
渲染函数形成的虚拟
DOM
节点就可以直接使用
setup
函数中定
义的数据了,也就不再需要使用
return
向虚拟
DOM
节点暴露数据了。
3.4 父组件使用子组件的 ref 数据
在 setup
函数中使用渲染函数生成虚拟
DOM
节点时,若父组件要使用子组件的
ref
数
据,则应该使用
setup
函数的
context
参数的
expose
方法,该方法可以将
setup
函数中的数
据暴露给父组件,而父组件需要使用
ref
引用子组件,并访问子组件中使用
context.expose()
方法暴露出去的数据。
【示例 7-9
】
用渲染函数实现的超级链接组件
。全局注册一个名为
my-link
组件,该组
件用于在页面中实现超级链接。
HTML
文档中使用
my-link
组件的代码如下所示。
<div id="app">
<my-link
ref="link"
url="https://www.baidu.com"
title="欢迎使用百度"
target="_blank">
百度一下
</my-link>
</div>
从上述代码中可以看到,组件 my-link
在使用时,父组件设置了
3
个自定义属性:
url
属性用来指定超级链接的地址,
title
用来设置超级链接的标题,
target
用来设置超级链接打
开时的方式。
这里规定:url
作为
Props
属性传递给子组件,
title
和
target
作为非
Props
属性传递给子
组件。
同时,组件 my-link
还需要设置一个匿名插槽,用来输入超级链接的文本。
注册 my-link
组件的代码如下所示。
01 app.component('my-link',{
02 props:['url'],
03 setup(props,context){
04 const {h}=Vue;
05 return ()=>h('a',{
06 href:props.url,
07 title:context.attrs.title,
08 target:context.attrs.target
09 },context.slots.default());
10 }
11 })
从上述代码中,可以看出使用渲染函数开发组件,在书写时有如下所示的规定。
➢ 父组件传递过来的
Props
属性,要通过
setup
函数的
props
参数获取。
➢ 父组件传递过来的非
Props
属性,要通过
setup
函数的
context
参数的
attrs
属性获取。
➢ 为组件安置匿名插槽要是用
setup
函数的
context
参数的
slots.default()
方法实现。
在 HTML
代码中,子组件
<my-link></my-link>
标记对设置了
ref
属性的取值,名为
link
。
这样在父组件中(
Vue
应用实例)就可以通过
this.$refs.link
来访问子组件中的内容。对于子
组件而言,要想让父组件通过
this.$refs.link
来访问自身的数据,必须使用
setup
函数的
context
参数的
expose()
方法将数据暴露给父组件。
注册 my-link
组件的代码改为如下所示。
01 app.component('my-link',{
02 props:['url'],
03 setup(props,context){
04 const {h}=Vue;
05 context.expose({
06 url:props.url,
07 title:context.attrs.title
08 })
09 return ()=>h('a',{
10 href:props.url,
11 title:context.attrs.title,
12 target:context.attrs.target
13 },context.slots.default());
14 }
15 })
在上述代码中,第 05
行使用
context.expose()
方法将
Props
属性
href
和非
Props
属性
title
暴露给了父组件。这里需要读者注意,父组件使用
this.$refs.link
无法在父组件的
setup
函数中访问到这些子组件暴露的数据,只能在父组件的 mounted
钩子函数中访问到这些数据。
3.5 在 setup 函数中使用 Provide/Inject 技术
要想在组件的 setup
函数中使用
Provide/Inject
技术,需要先从
Vue
对象中解构出
provide
方法和
inject
方法。
用于提供数据的父组件,在 setup
函数中可以使用如下所示的格式实现
Provider
功能。
const {provide}=Vue;
setup(){
provide(name,value
); // name
提供数据名,字符型格式;
value
提供数据值
}
用于接收数据的子组件,在
setup
函数中可以使用如下所示的格式实现
Inject
功能。
const {inject}=Vue;
setup(){
let 变量名
= inject(name); // name
指定数据名
}
这样就可以在多级父子组件之间传递数据了。
【示例 7-10
】
Provide
与
Inject
的使用
。创建两个全局组件,组件名分别为
com-parent
和
com-child
,其中
com-parent
是
com-child
的父组件。在
Vue
应用实例的数据区声明变量,
利用
Provide
与
Inject
技术让
com-child
组件能够访问
Vue
应用实例的数据区变量。
HTML
代码如下所示。
<div id="app">
<com-parent>
<com-child></com-child>
</com-parent>
</div>
这是第四章的示例
4-19
,该示例用组合式
API
实现的
Vue
代码如下所示。
01 const {h,reactive,provide,inject}=Vue;
02 let app=Vue.createApp({
03 setup(){
04 let student=reactive({
05 studentName:'张三',
06 sex:'男',
07 age:25
08 })
09 provide('student',student); // Vue 应用实例提供数据
10 }
11 })
12 app.component('com-parent',{
13 setup(props,context){
14 return ()=>h('div',{
15 class:'parent'
16 },[context.slots.default()])
17 }
18 })
19 app.component('com-child',{
20 setup(props,context){
21 let info=inject('student'); // 组件 com-child 接收数据
22 return ()=>h('button',{
23 onClick(){
24 console.log(info);
25 }
26 },'我是 com-child 组件')
27 }
28 })
29 app.mount('#app')
在上述代码中,第 01
行先将所有要用到的方法从
Vue
对象中解构出来。第
02
行至第
11
行是
Vue
应用实例的代码。第
12
行至第
18
行为注册
com-parent
组件的代码。第
19
行
至第
28
行为注册
com-child
组件的代码。
第 16
行为
com-parent
组件安置了一个匿名插槽,因为该组件在使用时要内部嵌套
com-child
组件。
第 09
行
Vue
应用实例使用
provide()
方法提供了名为
'
student
'
的数据。第
21
行
com-child
组件接收了
Vue
应用实例提供的名为的
'
student
'
数据,并赋值给
info
变量。
需要注意,Vue
应用实例使用
provide()
函数提供的数据是通过
reactive()
方法实现
的响应性对象,所以若
Vue
应用实例修改了
student
响应性对象的成员取值,则
com-child
组件中使用
inject()
方法接收到的数据也会立即发生变化。
在多级组件之间使用 Provide/Inject
技术时,需要注意一下几个方面。
➢ 为了实现响应性,若要传递对象或数组,建议使用
reactive()
方法生成该对象。
➢ 为了实现响应性,若要传递普通数据类型的数据,建议使用
ref()
方法生成数据。
➢ 若希望通过
Provide
提供的数据在注入数据的子组件中无法修改,建议使用
readonly()
方法生成
Provide
提供的数据。