Composition API
解决: 传统的option api (选项式)配置方法写组件的时候问题,随着业务复杂度越来越高,代码量会不断的加大;由于相关业务的代码需要遵循option的配置写到特定的区域,导致后续维护非常的复杂,同时代码可复用性不高.
提供了一下几个函数:
setup
ref
reactive
watchEffect
computed
toRefs
生命周期的hooks
...
setup:
- 所有的组合API函数都在此使用, 只在初始化时执行一次
- 函数如果返回对象, 则对象中的属性或方法, 在
html 模板
中可以直接使用
执行时机:
在beforeCreate之前执行(就执行一次), 此时组件对象还没有创建: 即this是undefined, 不能通过this来访问data / computed / methods / props,所有的composition API相关回调函数中也都不可以.
参数:
-
setup(props, context) / setup(props, {attrs, slots, emit})
-
props: 包含props配置声明且传入了的所有属性的对象
-
attrs: 包含没有在props配置中声明的属性的对象, 相当于 this.$attrs
-
slots: 包含所有传入的插槽内容的对象, 相当于 this.$slots
-
emit: 用来分发自定义事件的函数, 相当于 this.$emit
返回值:
- 一般都返回一个对象: 为html模板提供数据;
- 返回对象中的属性会与data函数返回对象的属性合并成为组件对象的属性;
- 返回对象中的方法会与methods中的方法合并成功组件对象的方法,若有重名, setup优先;
注意:
- ‘尽量不要混合使用!’ - methods中可以访问setup提供的属性和方法, 但在setup方法中不能访问data和methods(没this对象);
- setup不能是一个async函数: 因为返回值不再是return的对象, 而是promise, 模板看不到return对象中的属性数据;
- 以下代码基于 @vue/cli 4.5.10
- 部分TS (Typescript 学习)语法
// App.vue
<template>
<h2>App 父级组件</h2>
<h4>{{msg}}</h4>
<hr>
<Child :msg=msg @XXX="XXX"/> <!-- msg2='真心' -->
<button @click="updateMsg">up-</button>
<button @click="msg += '======'">up=</button>
</template>
<script lang="ts">
import { defineComponent,ref, reactive } from 'vue';
import Child from './components/Child.vue'
export default defineComponent({
name: 'App',
components: {
Child
},
setup() {
const msg = ref('what are you doing?')
function updateMsg() {
msg.value+='-------'
}
function XXX(txt: string) {
msg.value+=txt
}
return{
msg,
updateMsg,
XXX
}
}
})
</script>
// Child.vue
<template>
<h3>Child子组件</h3>
<h5>{{msg}}</h5>
<h5>count:{{count}}</h5>
<button @click="emitXX">分发事件emit</button>
</template>
<script>
import {defineComponent} from 'vue'
export default defineComponent({
name: 'Child',
data() {
return{
count: 10
}
},
props: ['msg'],
beforeCreate() {
console.log('beforeCreate ...');
},
inheritAttrs: false, // 禁用组件的属性继承(消除vue warn)
setup(props, context) {
console.log('props:', props);console.log('props.msg:', props.msg);
console.log('context:', context);
// console.log('context.attrs:', context.attrs.msg2);
console.log('setup ... 此时this:', this);
const showMsg1 = ()=>{
console.log('setup 的showMsg1 ...');
}
// 按钮点击事件的回调函数
function emitXX() {
context.emit('XXX','1234') // 导致msg 父子组件都更新
}
return{
showMsg1,
emitXX
}
},
// 界面渲染后生命周期的回调
mounted() {
console.log('mounted ... 此时this:',this);
},
//
methods: {
showMsg2() {
console.log('methods 的showMsg2 ...');
}
}
})
</script>
ref 和 reactive
ref: 定义一个基本类型的响应式数据。返回一个包含响应式数据的引用(reference)对象,操作数据使用 .value 属性,模板中不需要.value
reactive: 定义多个数据的响应式.
- const proxy = reactive(obj): 接收一个普通对象obj然后返回该普通对象obj的响应式代理器对象;
- 响应式转换是“深层的”:会影响对象内部所有嵌套的属性;
- 内部基于 ES6 的 Proxy 实现,通过代理对象操作源对象内部数据都是响应式的.
// App.vue
<template>
<div>hello Vue3!</div>
<!-- <button @click="add">
Count is: {{ count }}
</button> -->
<button @click="updateuser">
updateuser
</button>
<h2>name:{{user.name}}</h2>
<h2>age:{{user.age}}</h2>
<h2>hsb:{{user.hsb}}</h2>
</template>
<script lang="ts">
// 引入api函数
import { defineComponent,ref, reactive } from 'vue';
export default defineComponent({
name: 'App',
// 点击按钮让页面中的数据变化
// vue2:
// data(){
// return {
// count: 0
// }
// },
// methods: {
// add (){
// this.count++;
// }
// }
// vue3:
setup(){ // 所有的组合API函数都在此使用, 只在初始化时执行一次
// let number = 10 // 此时的数据并非响应式的( 响应式数据: 数据变化,页面跟着渲染变化)
const number = ref(10) // ref函数需要引入
// console.log(number);
function add() {
number.value ++
}
/**
* 作用: 定义多个数据的响应式
const proxy = reactive(obj): 接收一个普通对象obj然后返回该普通对象obj的响应式代理器对象
响应式转换是“深层的”:会影响对象内部所有嵌套的属性
内部基于 ES6 的 Proxy 实现,通过代理对象操作源对象内部数据都是响应式的
*/
const obj ={
name: 'y',
age: 24,
hsb: {
name: 'z',
age: 24,
car: ['奔驰','BWM','法拉利']
}
}
// 若想要目标对象中的数据变化,同时界面跟着渲染,需要操作proxy代理对象,而不能操作obj对象
const user = reactive(obj)
// user是代理对象,obj是目标对象
console.log(user);
const updateuser = ()=>{
user.name+='--'
user.age-=1
user.hsb.name+='---'
user.hsb.car[0]='玛莎'
}
return { // setup函数如果返回对象, 对象中的属性或方法, 模板中可以直接使用
number,
add2:add,
user,
updateuser
}
}
});
</script>
ref 和 reactive 的细节点:
- ref用来处理基本类型数据, reactive用来处理对象(递归深度响应式)
- 如果用 ref 放入对象/数组, 内部会自动将对象/数组转换为reactive的代理对象
- ref内部: 通过给value属性添加
getter/setter
来实现对数据的劫持 - reactive内部: 通过使用
Proxy
来实现对对象内部所有数据的劫持, 并通过Reflect
操作对象内部数据 - ref的数据操作: 在js中要.value, 在模板中不需要(内部解析模板时会自动添加.value)
<template>
<h2>ref _ reactive 细节</h2>
<h3>m1:{{m1}}</h3>
<h3>m2:{{m2}}</h3>
<h3>m3:{{m3}}</h3>
<hr>
<button @click="update">upda</button>
</template>
<script lang="ts">
import { defineComponent,ref, reactive } from 'vue';
export default defineComponent({
name: 'App',
setup() {
const m1 = ref('abc')
const m2 = reactive({
name: 'xiaox',
wife: {
name: 'hah'
}
})
const m3 = ref({
name: 'aa',
wife: {
name: 'bb'
}
})
console.log('m3',m3); // ref内部会自动将对象/数组转换为reactive的代理对象
const update = () =>{
m1.value += '111111'
m2.wife.name += '222222'
m3.value.name += '333333'
}
return{
m1,
m2,
m3,
update
}
}
})
</script>
对比 vue2 和 vue3 的响应式
vue2的响应式:
对象: 通过defineProperty对对象的已有属性值的读取和修改进行劫持(监视/拦截)
数组: 通过重写数组更新数组一系列更新元素的方法来实现元素修改的劫持。
Object.defineProperty(data, 'count', {
get () {},
set () {}
})
Vue3的响应式:
通过 Proxy(代理): 拦截对data任意属性的任意(13种)操作, 包括属性值的读写, 属性的添加, 属性的删除等…
通过 Reflect(反射): 动态对被代理对象的相应属性进行特定的操作。
<script>
const user = {
name: '佐助',
age: 19,
wife: {
name: '小樱',
age: 18,
}
}
const proxyUser = new Proxy(user, {
get(target, prop) {
console.log('get ..');
return Reflect.get(target, prop)
},
set(target, prop, val) {
console.log('set ..');
return Reflect.set(target, prop, val)
},
deleteProperty(target, prop) {
console.log('delete ..');
return Reflect.deleteProperty(target, prop)
}
})
// 通过代理对象获取目标对象的属性值
console.log(proxyUser.name);
// 更新
proxyUser.name = '佐助2'
console.log(user.name);
// 添加
proxyUser.gender = '男'
console.log(user);
// 删除
delete proxyUser.age
console.log(user);
// 深度更新
proxyUser.wife.name = '小樱2'
console.log(user);
</script>
vue2响应式缺陷:
- 对象直接新添加的属性或删除已有属性, 界面不会自动更新
- 直接通过下标替换元素或更新length, 界面不会自动更新 arr[1] = {}
计算属性与监视
computed函数
watch函数
watchEffect函数
<template>
<h2>计算属性和监视</h2>
<fieldset>
<legend>姓名操作</legend>
姓氏:<input type="text" placeholder="请输入姓氏" v-model="user.lastName"><br>
姓名:<input type="text" placeholder="请输入姓名" v-model="user.firstName"><br>
</fieldset>
<fieldset>
<legend>计算属性和监视演示</legend>
fullName1:<input type="text" placeholder="显示姓名" v-model="fullName1"><br>
fullName2: <input type="text" placeholder="显示姓名" v-model="fullName2"><br>
fullName3:<input type="text" placeholder="显示姓名" v-model="fullName3"><br>
</fieldset>
</template>
<script lang="ts">
import { defineComponent,computed, watch, watchEffect, ref, reactive } from 'vue';
export default defineComponent({
name: 'App',
setup() {
const user = reactive({
// 姓氏
lastName: '张',
// 姓名
firstName: '三丰'
})
// 通过计算属性显示第一个姓名
// vue3: 若computed 只传入一个回调函数,代表get
const fullName1 = computed(()=>{ // 返回 ref 对象
console.log('fullName1 get...');
return `${user.lastName}_${user.firstName}`
})
// 若computed传入一个对象,代表get 和 set
const fullName2 = computed({
get() {
// console.log('fullName2 get...');
return `${user.lastName}_${user.firstName}`
},
set(val: string) {
console.log('fullName2 set...');
const name = val.split('_')
user.lastName = name[0]
user.firstName = name[1]
}
})
// 监视:
const fullName3 = ref('')
watch(user, ({lastName, firstName})=>{
fullName3.value = lastName + '_' + firstName
}, {immediate: true, deep: true}) // 启动时就执行一次watch,不然无法渲染出 且 开启深度监视
// watchEffect(()=>{ // 默认就执行一次
// fullName3.value = user.lastName + '_' + user.firstName
// })
watch(fullName3, ()=>{
const name = fullName3.value.split('_')
user.lastName = name[0]
user.firstName = name[1]
}, {immediate: true, deep: true})
// watchEffect(()=>{
// const name = fullName3.value.split('_')
// user.lastName = name[0]
// user.firstName = name[1]
// })
// watch 监视可多个数据:
// 1, 数据为响应式:
watch([user, fullName3], ()=>{
console.log('我是响应式数据...');
})
// 2, 数据为普通数据:
watch([()=>user.lastName, ()=>user.firstName], ()=>{
console.log('我是普通数据...');
})
return {
user,
fullName1,
fullName2,
fullName3
}
}
})
</script>
生命周期钩子函数
vue2 和 vue3 的对比:
beforeCreate -> 使用 setup()
created -> 使用 setup()
beforeMount -> onBeforeMount
mounted -> onMounted
beforeUpdate -> onBeforeUpdate
updated -> onUpdated
beforeDestroy -> onBeforeUnmount
destroyed -> onUnmounted
自定义hoos函数
所谓 hooks 函数:
在不使用class
的情况下管理状态数据且抽取出逻辑思维的可复用功能函数
// useMousePosition.ts
// 展示鼠标点击的横纵坐标
import { ref, onMounted, onBeforeUnmount } from 'vue'
export default function () {
const x = ref(-1)
const y = ref(-1)
const clickCB = (event: MouseEvent) => {
x.value = event.pageX
y.value = event.pageY
}
// 页面加载完毕再点击, 生命周期组合API
onMounted(() => {
window.addEventListener('click', clickCB)
})
// 页面关闭卸载点击生命周期组合API
onBeforeUnmount(() => {
window.removeEventListener('click', clickCB)
})
return{
x,
y
}
}
// useRequest.ts
// 发送请求
import axios from 'axios';
import { ref } from "vue";
// 发送ajax 请求
// 利用TS 泛型强化了请求数据类型的检查
export default function <T>(url: string) {
const loading = ref(true)
const data = ref<T | null>(null)
const errMsg = ref('')
//发送请求
axios.get(url).then(res=>{
loading.value = false
data.value = res.data
}).catch(err=>{
loading.value = false
errMsg.value = err.message || '未知错误'
})
return{
loading, // 加载状态
data, // 请求数据
errMsg // 错误信息
}
}
调用如下:
// App.vue
<template>
<h2>自定义hook函数</h2>
<h4>在不使用class的情况下管理状态数据且抽取出逻辑思维的可复用功能函数</h4>
<h3>x: {{x}}、y: {{y}}</h3>
<hr>
<h3 v-if="loading">正在加载中...</h3>
<h3 v-else-if="errMsg">加载失败: {{errMsg}}</h3>
<ul v-else>
<li>id: {{data.id}}</li>
<li>address: {{data.address}}</li>
<li>distance: {{data.distance}}</li>
</ul>
<hr>
<!-- 数组数据 -->
<ul v-for="item in data" :key="item.id">
<li>id: {{item.id}}</li>
<li>title: {{item.title}}</li>
<li>price: {{item.price}}</li>
</ul>
</template>
<script lang="ts">
// 定义接口约束请求的数据类型
interface AddressData{
id: number;
address: string;
distance: string;
}
interface ProductsData{
id: string;
title: string;
price: number;
}
import { defineComponent, watch } from 'vue'
import useMousePosition from './hooks/useMousePosition'
import useRequest from './hooks/useRequest'
export default defineComponent({
name: 'App',
setup(){
// 展示用户点击的横纵坐标
const {x, y} = useMousePosition()
// 发送请求
// const {loading, data, errMsg} = useRequest<AddressData>('/data/address.json') // 请求对象类型数据
const {loading, data, errMsg} = useRequest<ProductsData[]>('/data/products.json') // 请求数组类型数据
// 监视
watch(data, ()=>{
if (data.value) {
console.log(data.value.length);
}
})
return {
x,
y,
loading,
data,
errMsg
}
}
})
</script>
toRefs
把一个响应式对象转换成普通对象,该普通对象的每个 property 都是一个ref对象
<template>
<h2>toRefs 的使用</h2>
<h4>把一个响应式对象转换成普通对象,该普通对象的每个 property 都是一个ref对象</h4>
<h3>name: {{state.name}}</h3>
<h3>age: {{state.age}}</h3>
<h3>name: {{name}}</h3>
<h3>age: {{age}}</h3>
<hr>
</template>
<script lang="ts">
import {defineComponent, reactive, toRefs, ref} from 'vue'
export default defineComponent({
name: 'App',
setup(){
const state = reactive({
name: '佐助',
age: 20
})
// const state2 = toRefs(state)
// console.log('state: ', state);
// console.log('state2: ', state2);
const {name, age} = toRefs(state)
// 定时器确定数据为响应式的
setInterval(() => {
state.name += "1"
state.age += 1
// state2.name.value += "2"
// state2.age.value += 2
// name += '3' // error
// age += 3 // error
}, 1500);
return{
state,
// ...state, // 这种方式会导致数据不是响应式的
// ...state2,
name,
age
}
}
})
</script>