Vue 3 组合式 API

相比较 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 所示。

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 所示。

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 所示。

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)">&times;</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 提供的数据。

相关推荐
酷酷的阿云7 分钟前
不用ECharts!从0到1徒手撸一个Vue3柱状图
前端·javascript·vue.js
aPurpleBerry1 小时前
JS常用数组方法 reduce filter find forEach
javascript
GIS程序媛—椰子1 小时前
【Vue 全家桶】7、Vue UI组件库(更新中)
前端·vue.js
ZL不懂前端2 小时前
Content Security Policy (CSP)
前端·javascript·面试
乐闻x2 小时前
ESLint 使用教程(一):从零配置 ESLint
javascript·eslint
我血条子呢2 小时前
[Vue]防止路由重复跳转
前端·javascript·vue.js
半开半落2 小时前
nuxt3安装pinia报错500[vite-node] [ERR_LOAD_URL]问题解决
前端·javascript·vue.js·nuxt
麦麦大数据2 小时前
基于vue+neo4j 的中药方剂知识图谱可视化系统
vue.js·知识图谱·neo4j
customer083 小时前
【开源免费】基于SpringBoot+Vue.JS医院管理系统(JAVA毕业设计)
java·vue.js·spring boot·后端·spring cloud·开源·intellij-idea