这是我参与11月更文挑战的10天,活动详情查看:2021最后一次更文挑战
当组件越来越大时,不同的逻辑关注点会相互分离,维护起来非常困难。
我们可以使用 Composition API 来解决这个问题。
Setup 函数
使用 this
setup 函数是在解析其它组件选项之前被调用的,因此我们在其中使用的 this 并不指向改组件的实例。
结合模板使用
setup 函数返回的内容会暴露在外部,我们可以在模板中直接使用其中的变量或者方法。
const app = Vue.createApp({
setup(props) {
return {
name: 'Jack',
handleClick() { alert('this is setup') }
}
},
template:/*html*/ `
<div @click='handleClick'>{{name}}</div>
`
})
响应式变量
ref
如下代码返回的变量并非响应式,因此 2 秒之后返回的 name 变量不变:
setup(props) {
let name = 'Jack';
setTimeout(() => {
name = 'Joe'
}, 2000);
return { name }
}
我们可以使用 ref 响应式变量。它通过 proxy 对数据进行封装,当数据变化时,触发模板等内容的更新。
ref 处理基础类型的数据,如字符串、数字等。下面的例子中,ref 通过 proxy 把 'Jack' 变为 proxy({value: 'Jack'}) 的响应式引用。使用 value property 来修改值,在组件中直接使用变量名:
const app = Vue.createApp({
setup(props) {
const { ref } = Vue;
let name = ref('Jack');
setTimeout(() => {
name.value = 'Joe'
}, 2000);
return { name }
},
template:/*html*/ `
<div>{{name}}</div>
`
})
reactive
reactive 可以处理非基础类型的数据,如对象、数组等。下面的例子中,reactive 通过 proxy 把 {name: 'Jack'} 变为 proxy({name: 'Jack'}) 的响应式引用:
const app = Vue.createApp({
setup(props) {
const { reactive } = Vue;
const nameObj = reactive({ name: 'Jack' });
setTimeout(() => {
nameObj.name = 'Joe'
}, 2000);
return { nameObj }
},
template:/*html*/ `
<div>{{nameObj.name}}</div>
`
})
我们可以使用 readonly 将响应式引用的对象进行复制,返回一个只读的数据。
const app = Vue.createApp({
setup(props) {
const { reactive, readonly } = Vue;
const nameObj = reactive([123]);
const copy = readonly(nameObj)
setTimeout(() => {
nameObj[0] = '456';
copy[0] = '456'; // 报错,copy 对象只读
}, 2000);
return { nameObj }
},
template:/*html*/ `
<div>{{nameObj[0]}}</div>
`
})
使用 ES6 解构的数据不具有响应性,因为此时返回的是一个值,而非对象,即 name === 'Jack':
const app = Vue.createApp({
setup(props) {
const { reactive } = Vue;
const nameObj = reactive({ name: 'Jack' });
setTimeout(() => {
nameObj.name = 'Joe';
}, 2000);
const { name } = nameObj; // ES6 解构
return { name } // name 的类型是 string,而不是 ref
},
template:/*html*/ `
<div>{{name}}</div>
`
})
toRefs
解构时使用 toRefs 可以解决这个问题:
const app = Vue.createApp({
setup(props) {
const { reactive, toRefs } = Vue;
const nameObj = reactive({ name: 'Jack' });
setTimeout(() => {
nameObj.name = 'Joe';
}, 2000);
const { name } = toRefs(nameObj);
return { name }
},
template:/*html*/ `
<div>{{name}}</div>
`
})
此时, toRefs 将 proxy({name: 'Jack'}) 转化为 {name: proxy({value: 'Jack'})}。解构之后的 name === proxy({value: 'Jack'})。
toRef
当 toRefs 对象使用 ES6 解构为不存在的 property 时,会返回 undefined。
我们可以使用 toRef 来解决这个问题,为源响应式对象上的某个 property 新创建一个 ref。
const app = Vue.createApp({
setup(props) {
const { reactive, toRef } = Vue;
const data = reactive({ name: 'Jack' });
const age = toRef(data, 'age');
setTimeout(() => {
age.value = '18';
}, 2000);
return { age }
},
template:/*html*/ `
<div>{{age}}</div>
`
})
得益于 ES6 的 Proxy 对象,相较于 Vue2 的 Object.defineProperty(),Vue3 使用 Proxy() 成功支持已有和新增的 key 的读取和修改。
Object.defineProperty(data, "count",{
get() {},
set() {},
})
Proxy(data, {
get(key) {},
set(key, value) {},
})
Setup 参数
setup 函数接收两个参数,分别是 props 和 context。
props 参数是父组件传来的响应式 prop attribute,不可用 ES6 解构,可以使用 toRefs 和 toRef。
context 参数中含有三个 property,分别是attrs、slots、emit。这三个属性相当于在组件对象中使用的 this.$attrs、this.$slots、this.$emit。
访问组件的 property
由于 setup 被执行时,组件实例没有被创建,所以我们只能访问父组件传递过来的参数,即 props、attrs、slots、emit。
使用渲染函数
如果 setup 函数返回的是渲染函数,组件会直接使用这个渲染函数生成 DOM,可结合 slots 实现:
const app = Vue.createApp({
template:/*html*/ `
<child><h1>parent</h1></child>
`
})
app.component('child', {
setup(props, context) {
const { h } = Vue;
const { attrs, slots, emit } = context;
return () => h('div', {}, slots.default())
}
})
可以在setup 函数中使用 emit:
const app = Vue.createApp({
methods: { handleChange() { alert('change'); } },
template:/*html*/ `<child @change='handleChange'></child>`
})
app.component('child', {
setup(props, context) {
const { attrs, slots, emit } = context;
function handleClick() { emit('change') };
return { handleClick }
},
template:/*html*/`<div @click='handleClick'>child</div>`
})
由此可见,setup 函数中的语法可以替代传统的语法。
TodoList
下面的代码通过 setup 函数实现了一个 TodoList:
const app = Vue.createApp({
setup(props) {
const { ref, reactive } = Vue;
const inputValue = ref('');
const list = reactive([]);
const handleChange = (e) => {
inputValue.value = e.target.value
}
const handleSubmit = () => {
list.push(inputValue.value)
}
return {
inputValue,
handleChange,
handleSubmit,
list
}
},
template:/*html*/ `
<div>
<input :value='inputValue' @input='handleChange'/>
<button @click='handleSubmit'>submit</button>
<ul>
<li v-for='(item, index) in list' :key='index'>{{item}}</li>
</ul>
</div>
`
})
但是我们发现实现输入功能和实现提交功能的代码结合到了一起,我们需要把它们拆分开使代码结构更加清晰。
// 对关于 list 的操作进行了封装
const listRelativeEffect = () => {
const { reactive } = Vue;
const list = reactive([]);
const handleSubmit = (item) => {
list.push(item)
}
return {
list,
handleSubmit
}
}
// 对关于 inputValue 的操作进行了封装
const inputRelativeEffect = () => {
const { ref } = Vue;
const inputValue = ref('');
const handleChange = (e) => {
inputValue.value = e.target.value
}
return {
inputValue,
handleChange
}
}
const app = Vue.createApp({
setup(props) {
// 流程调度中转
const { list, handleSubmit } = listRelativeEffect();
const { inputValue, handleChange } = inputRelativeEffect();
return {
inputValue, handleChange,
handleSubmit, list
}
},
template:/*html*/ `
<div>
<input :value='inputValue' @input='handleChange'/>
<button @click='handleSubmit(inputValue)'>submit</button>
<ul>
<li v-for='(item, index) in list' :key='index'>{{item}}</li>
</ul>
</div>
`
})
这样写提升了代码的可维护性。
computed 属性
我们可以使用从 Vue 导入的 computed 方法,传递一个参数,它是一个类似 getter 的回调函数,得到一个只读的响应式引用。为了访问新创建的计算变量的值,我们需要像 ref 一样使用 .value property。
const app = Vue.createApp({
setup(props) {
const { ref, computed } = Vue;
const count = ref(0);
const handleClick = () => { count.value += 1; };
const countAdd5 = computed(() => {
return count.value + 5;
})
return { count, handleClick, countAdd5 }
},
template:/*html*/ `
<div @click='handleClick'>{{count}}--{{countAdd5}}</div>
`
})
计算属性里也可以接收 setter 和 getter,它们以对象的形式作为参数:
const app = Vue.createApp({
setup(props) {
const { ref, computed } = Vue;
const count = ref(0);
const handleClick = () => { count.value += 1; };
const countAdd5 = computed({
get: () => {
return count.value + 5;
},
set: (param) => {
count.value = param - 5;
}
});
setTimeout(() => {
countAdd5.value = 100;
}, 1000);
return { count, handleClick, countAdd5 }
},
template:/*html*/ `
<div @click='handleClick'>{{count}}--{{countAdd5}}</div>
`
})
watch 响应式更改
watch函数接受 3 个参数
- 一个想要侦听的 ref 或 reactive 的响应式引用、 getter 或 effect 函数、或这些类型的数组
- 一个回调
- 可选的配置选项
const app = Vue.createApp({
setup(props) {
const { ref, watch } = Vue;
const name = ref('Jack');
watch(name, (newValue, oldValue) => {
console.log(newValue, oldValue)
})
return { name }
},
template:/*html*/`<input v-model='name'>`
})
watch的第一个参数是 reactive 的 property 时,我们需要先将其转为 getter 函数,否则它就不是响应式引用了:
const app = Vue.createApp({
setup(props) {
const { reactive, watch, toRefs } = Vue;
const nameObj = reactive({ name: 'Jack' });
// 第一个参数是 () => nameObj.name
watch(() => nameObj.name, (newValue, oldValue) => {
console.log(newValue, oldValue)
})
const { name } = toRefs(nameObj);
return { name }
},
template:/*html*/`<input v-model='name'>`
})
以数组的方式传参可以用一个侦听器同时监听多个参数:
const app = Vue.createApp({
setup(props) {
const { reactive, watch, toRefs } = Vue;
const nameObj = reactive({ name: 'Jack', age: '18' });
watch(
[() => nameObj.name, () => nameObj.age],
([newName, newAge], [oldName, oldAge]) => {
console.log(newName, newAge, '--', oldName, oldAge)
}
)
const { name, age } = toRefs(nameObj);
return { name, age }
},
template:/*html*/`
name: <input v-model='name'>
age: <input v-model='age'>
`
})
watch 函数的惰性的,渲染 DOM 之后不会立即执行。
watchEffect 没有惰性,渲染 DOM 之后立即执行,函数自动检测代码中对外部的依赖,当发生变化时重新执行整段代码。不需要传递很多参数,只需要传递回调函数。watchEffect 无法获取以前的数据。
const app = Vue.createApp({
setup(props) {
const { reactive, toRefs, watchEffect } = Vue;
const nameObj = reactive({ name: 'Jack', age: '18' });
watchEffect(() => {
console.log(nameObj.name);
});
const { name, age } = toRefs(nameObj);
return { name, age }
},
template:/*html*/`
name: <input v-model='name'>
age: <input v-model='age'>
`
})
可以将 watch 和 watchEffect 保存到一个函数对象中,调用这个函数可以取消监听。
const stop = watch(
[() => nameObj.name, () => nameObj.age],
([newName, newAge], [oldName, oldAge]) => {
console.log(newName, newAge, '--', oldName, oldAge);
setTimeout(() => {
stop();
}, 2000);
}
)
watch 接收第三个参数是可配置选项,可以在这里将设置为非惰性函数:
watch(
[() => nameObj.name, () => nameObj.age],
([newName, newAge], [oldName, oldAge]) => {
console.log(newName, newAge, '--', oldName, oldAge);
},
{ immediate: true }
)
结合 TypeScript 使用模块化
在 Vue3 中使用 TypeScript 时,用 defineComponent 方法来定义 component,它并没有实现操作逻辑,直接将传入的 object 返回。它的目的是让传入的对象能获得对应类型。
我们可以将重复的逻辑提取成一个单独的文件,这里我们以发送异步请求的逻辑为例:
// src/hooks/useURLLoader.ts
import { ref } from 'vue'
import axios from 'axios'
function useURLLoader<T>(url: string) {
// result.value 会推论为 null,使用泛型确定它的类型
const result = ref<T | null>(null);
const loading = ref(true);
const loaded = ref(false);
const error = ref(null);
axios.get(url).then((rawData) => {
loading.value = false;
loaded.value = true;
result.value = rawData.data;
}).catch(e => {
loading.value = false;
error.value = e;
})
return {
result, loaded, loading, error
}
}
export default useURLLoader;
这里请求 Dog API :
{
"message": "https://images.dog.ceo/breeds/vizsla/n02100583_2086.jpg",
"status": "success"
}
为了解决不同接口返回结果结构不同的问题,我们使用泛型来确定返回的类型:
<template>
<h1 v-if="loading">Loading...</h1>
<img :src="result.message" v-if="loaded" />
</template>
<script lang="ts">
import { defineComponent, watch } from "vue";
import useURLLoader from "./hooks/useURLLoader";
interface DogResult {
message: string;
status: string;
}
export default defineComponent({
name: "App",
setup() {
const { result, loaded, loading, error } = useURLLoader<DogResult>(
"https://dog.ceo/api/breeds/image/random"
);
watch(result, () => {
// 使用 type guard 检查,若类型不为 null,则是 DogResult
if (result.value) {
console.log("value", result.value.message);
}
});
return {
result,
loaded,
loading,
error,
};
},
});
</script>
<style>
#app {
text-align: center;
margin-top: 60px;
}
</style>
成功后页面如下:
我们再来使用不同的接口 Cat API ,可以感受到泛型带来的好处:
[{
"breeds": [],
"id": "6f0",
"url": "https://cdn2.thecatapi.com/images/6f0.jpg",
"width": 960,
"height": 575
}]
<template>
<h1 v-if="loading">Loading...</h1>
<img :src="result[0].url" v-if="loaded" />
</template>
<script lang="ts">
import { defineComponent, watch } from "vue";
import useURLLoader from "./hooks/useURLLoader";
interface CatResult {
id: string;
url: string;
width: number;
height: number;
}
export default defineComponent({
name: "App",
setup() {
const { result, loaded, loading, error } = useURLLoader<CatResult[]>(
"https://api.thecatapi.com/v1/images/search?limit=1"
);
watch(result, () => {
if (result.value) {
console.log("value", result.value[0].url);
}
});
return {
result,
loaded,
loading,
error,
};
},
});
</script>
<style>
#app {
text-align: center;
margin-top: 60px;
}
</style>
成功后的页面如下:
生命周期钩子
在选项式 API 中可以使用各种周期函数。由于 setup 函数啊在组件实例完全被初始化之前执行的函数,所以无需 beforeCreate 和 created 钩子。在这些钩子中的代码可以直接在 setup 中编写。
可以通过在生命周期钩子前面加上 “on” 来访问组件的生命周期钩子。例如 onMounted 对应组件的 mounted 钩子。这些钩子接收一个回调函数作为参数。
const app = Vue.createApp({
setup(props) {
const { onBeforeMount } = Vue;
onBeforeMount(() => {
console.log('beforeMount')
});
return {}
},
template:/*html*/`
<div>Hello World</div>
`
})
选项式 API 中的 renderTracked 钩子在每次渲染 DOM 之后重新收集响应式依赖, renderTriggerd 每次触发页面时自动执行。
Provide / Inject
可以在 setup 中这样使用 provide 和 inject:
const app = Vue.createApp({
setup(props) {
const { provide } = Vue;
provide('name', 'Jack')
return {}
},
template:/*html*/`
<child/>
`
})
app.component('child', {
setup(props) {
const { inject } = Vue;
const name = inject('name');
return { name }
},
template:/*html*/`
<div>{{name}}</div>
`
})
使用 Provide
Provide 函数接收两个参数,一个是 key,一个是 value。key 值以字符串的形式传递, value 值可以是字符串或者对象。
如果想传递多个值,则可以重构:
provide('location', 'North Pole')
provide('geolocation', {
longitude: 90,
latitude: 135
})
使用 Inject
inject 函数接收两个参数,一个是想要接收的 key,一个是默认 value。第二个参数是指当找不到 key 对应的值时,返回第二个参数的值。第二个参数可选。
响应性
我们可以在 provide 值的时候使用 ref 或 reactive 为值增加响应性。
const app = Vue.createApp({
setup(props) {
const { provide, ref } = Vue;
provide('name', ref('Jack'))
return {}
},
template:/*html*/`<child/>`
})
app.component('child', {
setup(props) {
const { inject } = Vue;
const name = inject('name');
const handleClick = () => {name.value = 'Joe'}
return { name, handleClick }
},
template:/*html*/`<div @click='handleClick'>{{name}}</div>`
})
但是我们为了数据的稳定性,对响应式数据的修改一般限制在 provide 的组件内部。
const app = Vue.createApp({
setup(props) {
const { provide, ref } = Vue;
const name = ref('Jack');
provide('name', name);
provide('changeName', (value) => { name.value = value; })
return {}
},
template:/*html*/`<child/>`
})
app.component('child', {
setup(props) {
const { inject } = Vue;
const name = inject('name');
const changeName = inject('changeName')
const handleClick = () => { changeName('Joe') }
return { name, handleClick }
},
template:/*html*/`<div @click='handleClick'>{{name}}</div>`
})
为了确保 provide 的数据不会被改变,我们可以对提供的数据使用 readonly:
const app = Vue.createApp({
setup(props) {
const { provide, ref, readonly } = Vue;
const name = ref('Jack');
provide('name', readonly(name));
provide('changeName', (value) => { name.value = value; })
return {}
},
template:/*html*/`<child/>`
})
模板引用
在组合式 API 中,可以这样使用 ref :
const app = Vue.createApp({
setup(props) {
const { ref, onMounted } = Vue;
const hello = ref(null);
onMounted(() => {
console.log(hello.value) // <div>Hello World</div>
});
return { hello }
},
template:/*html*/`<div ref='hello'>Hello World</div>`
})
这里我们在渲染上下文中暴露 hello,并通过 ref='hello',将其绑定到 div 作为其 ref。