Vue3 中的 Composition API
为什么要有 Composition API?
当一个组价越来越大的时候,data、methods、computed、directives、mixin 等混杂在一起。里面的功能是混杂的,在一堆混杂的逻辑里面查找如 name 相关的逻辑的话,就会有一定的困难。这样的话会造成可维护性降低。
Composition API 就是解决这个问题的有效方案。
1. setup 函数的使用
setup 函数是在 created 时候(实例被初始化前)执行的。里边返回数据或者方法,插值函数可以检测到。
const app = Vue.createApp({
template: `
<div>
<div @click="handleClick">hello world</div>
<div>name: {{ name }}, age: {{ age }}</div>
</div>
`,
setup(props, context) {
return {
name: "sjh",
age: 28,
handleClick: () => {
alert(123)
}
}
}
})
app.mount("#root")
因为是实例初始化前执行,因此 setup 里不能使用 this 访问到实例。
setup 函数里有两个参数,第一个是 props,指组件从父组件那儿接收到的参数,第二个是 context,暴露了 attrs、slot、emit 等实用方法。
上面的数据
2. ref,reactive 响应式引用的用法和原理
上面看着很合理,但是 name 和 age 都是非响应式的,即数据改变并不会触发渲染。
如何解决问题呢?现在需要响应式的引用:通过 proxy 对数据进行封装。当数据变化时,触发模板等内容的更新。
后面的例子都是通过一秒后修改 name 的值来查看是否有响应式。如果页面数据没有改变,说明没有响应式。
2.1 ref:响应式封装基础类型数据
使用 ref 函数,如 ref("sjh") ,对基础类型的数据进行封装,变成 proxy({value: "sjh"}) 这样的一个响应式引用。
const { ref } = Vue;
const app = Vue.createApp({
template: `
<div>
<div>name: {{ name }}, age: {{ age }}</div>
</div>
`,
setup(props, context) {
const name = ref("sjh")
const age = ref(28)
setTimeout(() => {
name.value = "hhhhh"
}, 1000)
return { name, age }
}
})
app.mount("#root")
改成响应式引用后,数据变化 ,页面上的内容也会相应改变。
2.2 reactive:响应式封装非基础类型数据
使用 reactive 函数,将非基础类型数据转化为响应式的。一般就是对象或者数组。
原理: reactive({ name: "sjh" }) 转化成 proxy({ name: "sjh" })
const { ref, reactive } = Vue;
const app = Vue.createApp({
template: `
<div>
<div>name: {{ obj.name }}, age: {{ obj.age }}</div>
</div>
`,
setup(props, context) {
const obj = reactive({ name: "sjh", age: 20 })
setTimeout(() => {
obj.name = "hhhhh"
}, 1000)
return { obj }
}
})
app.mount("#root")
2.3 readonly:数据变成只读形式
用 readonly 包裹响应式引用,该响应式数据变成只读,即不可修改该数据。
2.4 toRefs:解构时不丢失响应
使用过对象解构的同学会觉得插值函数里的写法好麻烦啊,所以会用对象解构。但是这里需要注意的一点,如果直接使用结构后的值,这些值不是响应式的。
解构会将对象或数组解构成普通的数据,丢失了响应。因此解构之前需要用 toRefs 包裹一下,使解构后所有值变成响应式的。
原理:toRefs({ name: "sjh", age: 20 }) 转化成 { name: proxy({ value: "sjh" }), age: proxy({ value: 20 }) } ,将内部的所有数据变成响应式的。现在使用解构后就不会丢失响应式了。
const { ref, reactive, toRefs } = Vue;
const app = Vue.createApp({
template: `
<div>
<div>name: {{ name }}, age: {{ age }}</div>
</div>
`,
setup(props, context) {
const obj = reactive({ name: "sjh", age: 20 })
setTimeout(() => {
obj.name = "hhhhh"
}, 1000)
const { name, age } = toRefs(obj)
return { name, age }
}
})
app.mount("#root")
3. toRef 以及 context 参数
3.1 toRef:从响应式对象中获取单个响应式数据
有一种使用场景,当解构的时候并非解构响应式对象里所拥有的 key,则解构出来的数据没有响应式,值为 undefined。
如下代码:data 里边只有 name,但是想解构 age 出来,此时前端页面会因为 age 是 undefined 而报错。
const { ref, reactive, toRefs } = Vue;
const app = Vue.createApp({
template: `
<div>
<div>{{ age }}</div>
</div>
`,
setup(props, context) {
const data = reactive({ name: "sjh" })
const { age } = toRefs(data)
setTimeout(() => {
age.value = 20
}, 1000)
return { age }
}
})
app.mount("#root")
toRef 就可以有效地解决问题。
语法:toRef(data, 'age') ,意思是,尝试从 data 里取 age 数据,如果没找到,则返回一个值为 null 的响应式数据。
const { ref, reactive, toRefs, toRef } = Vue;
const app = Vue.createApp({
template: `
<div>{{ age }}</div>
`,
setup(props, context) {
const data = reactive({ name: "sjh" })
const age = toRef(data, "age")
setTimeout(() => {
age.value = 20
}, 1000)
return { age }
}
})
app.mount("#root")
3.2 context 参数的意义
在上文已经有稍微讲一下 context 了,在这里深入讲一下。
之前说过,context 里边有三个值,分别为 attrs,slots,emit。
-
attrs:为父组件传过来的 non-props,即没有通过 props 接收的属性。 -
slots:父组件传递过来的插槽内容。例如,通过 slots.default() 可以获取默认插槽的虚拟 DOM 形式。 例如,想玩些花的,用 h 函数来渲染通过父组件传过来的默认插槽: const app = Vue.createApp({
template: `
<child>parent</child>
`,
})
app.component("child", {
setup(props, context) {
const data = reactive({ name: "sjh" })
const { name } = toRefs(data)
const { attrs, slots, emit } = context
return () => Vue.h('div', {}, slots.default())
}
})
app.mount("#root")
如果不使用 Composition API,就需要用 this.$slots.default() 来获取默认插槽 -
emit:触发自定义事件的方法。 例如,子组件点击的时候,触发父组件的自定义事件: const app = Vue.createApp({
template: `
<child @change="handleChange">parent</child>
`,
setup(props) {
const handleChange = () => alert("change")
return { handleChange }
}
})
app.component("child", {
template: `
<div @click="handleClick">child</div>
`,
setup(props, context) {
const { attrs, slots, emit } = context
const handleClick = () => emit("change")
return { handleClick }
}
})
app.mount("#root")
3. 示例:使用 Composition API 开发 todolist
现在写一个 todolist 来感受一下 Composition API 的优势:
const { ref, reactive, toRefs, toRef } = Vue;
const listRelativeEffect = () => {
const list = reactive([])
const addItemToList = (item) => {
list.push(item)
}
return { list, addItemToList }
}
const inputRelativeEffect = () => {
const inputValue = ref("")
const handleInputValueChange = (e) => {
inputValue.value = e.target.value
}
return { inputValue, handleInputValueChange }
}
const app = Vue.createApp({
template: `
<div>
<div>
<input :value="inputValue" @input="handleInputValueChange" />
<button @click="() => addItemToList(inputValue)">提交</button>
</div>
<ul>
<li v-for="(item, index) in list" :key="index">
{{ item }}
</li>
</ul>
</div>
`,
setup(props) {
const { list, addItemToList } = listRelativeEffect()
const { inputValue, handleInputValueChange } = inputRelativeEffect()
return {
list,
addItemToList,
inputValue,
handleInputValueChange,
}
}
})
app.mount("#root")
使用了 Composition API 后,相关的逻辑可以分离出来,使得代码可维护性和可读性都有了很大的提升。
4. computed 方法生成计算属性
之前在 vue 基础中已经学了传统的 computed 语法了,现在讲一下 Composition API 里的计算属性。
4.1 基本使用
实现一个简单的计数器,并且有一行计算属性是计数的结果乘以 2 的结果:
const { ref, computed } = Vue;
const app = Vue.createApp({
template: `
<div>
<span @click="handleClick">{{ count }}</span> --
<span>{{ countAddFive }}</span>
</div>
`,
setup(props) {
const count = ref(0)
const handleClick = () => {
count.value += 1
}
const countAddFive = computed(() => {
return 2 * count.value
})
return { count, countAddFive, handleClick }
}
})
app.mount("#root")
可以看到,在 Composition API 中,需要先引入 computed 函数,然后正常使用即可。
4.2 进阶使用
其实更详细的,computed 方法接收一个对象,里面接收 get 和 set 两个属性。
get 和 set 的值均为方法。get 方法返回提供的数据的值,set 方法用来处理属性设置的时候要执行的相应逻辑。
直接上代码,看的更清晰:
const app = Vue.createApp({
template: `
<div>
<span @click="handleClick">{{ count }}</span> --
<span>{{ countAddFive }}</span>
</div>
`,
setup(props) {
const count = ref(0)
const handleClick = () => {
count.value += 1
}
const countAddFive = computed({
get: () => count.value + 5,
set: (param) => count.value = param - 5,
})
return { count, countAddFive, handleClick }
}
})
5. watch 和 watchEffect 的使用和差异性
5.1 watch 基本用法
Composition API 里提供了 watch 方法,和传统的 watch 方法类似,写在 setup 函数里。
Composition API 中使用 watch 的方式:
watch(name, (currentValue, prevValue) => {
console.log(currentValue, prevValue)
})
可以看到 watch 方法接收两个参数,第一个参数指的是要监听的数据,第二个参数是监听到数据变化后的回调。回调函数有两个参数,第一个参数是数据改变后的值,第二个参数是数据改变前的值。
可以看出,watch 有两个特性:
- 具备一定的惰性,只有在数据变化的时候才会执行
- 参数可以拿到改变前和改变后的值
5.2 watch 监听非响应式数据
注意:监听的数据必须具有响应式,或者是个有返回值的函数
如 reactive 返回的响应式对象,里边的属性值时没有响应式的,如果要监听里面的单个属性而非整个响应式对象,就需要写成函数,或者将属性变成响应式。
-
解法一:写成函数 const { ref, watch, reactive } = Vue;
const app = Vue.createApp({
template: `
<div>
<div>
Name: <input v-model="nameObj.name" />
</div>
<div>
Name is {{ nameObj.name }}
</div>
</div>
`,
setup(props) {
const nameObj = reactive({ name: "sjh" })
watch(() => nameObj.name, (currentValue, prevValue) => {
console.log(currentValue, prevValue)
})
return {
nameObj
}
}
})
app.mount("#root")
-
解法二:将属性变成响应式 const { ref, watch, reactive, toRefs } = Vue;
const app = Vue.createApp({
template: `
<div>
<div>
Name: <input v-model="name" />
</div>
<div>
Name is {{ name }}
</div>
</div>
`,
setup(props) {
const nameObj = reactive({name: "sjh"})
const { name } = toRefs(nameObj)
watch(name, (currentValue, prevValue) => {
console.log(currentValue, prevValue)
})
return {
name
}
}
})
app.mount("#root")
5.3 watch 监听多个数据
监听多个数据的时候,没必要重复写,写成数组复用即可。
const { name, age } = toRefs(data)
watch([name, age], ([curName, curAge], [prevName, prevAge]) => {
console.log([curName, curAge], [prevName, prevAge])
})
5.4 watchEffect 基本用法
watchEffect 侦听器相比于 watch 侦听器,更偏向于 effect。
watchEffect 和 watch 的区别
- 只要写了 watchEffect,回调就立即执行,没有惰性
- watchEffect 会自己感知内部的代码数据是否因为相关依赖发生了改变,如果发生了改变就会执行,因此不需要传递很多参数。watch 需要通过参数指定监听哪个数据。
- watchEffect 无法获取之前的数据和当前的数据。watch 可以获取之前和当前的数据。
watchEffect(() => {
console.log(nameObj.name)
})
暂停 watchEffect
例如,五秒后结束监听
const stop = watchEffect(() => {
console.log(nameObj.name)
setTimeout(() => {
stop()
}, 5000)
})
watch 暂停监听的语法和 watchEffect 一模一样
watch 立即执行
设置参数 immediate: true 即可
const stop = watch(name, (currentValue, prevValue) => {
console.log(currentValue, prevValue)
}, {
immediate: true
})
6. 生命周期钩子
现在用 Composition API 来写生命周期钩子,直接摘官网的:
选项式 API | Hook inside setup |
---|
beforeCreate | Not needed* | created | Not needed* | beforeMount | onBeforeMount | mounted | onMounted | beforeUpdate | onBeforeUpdate | updated | onUpdated | beforeUnmount | onBeforeUnmount | unmounted | onUnmounted | errorCaptured | onErrorCaptured | renderTracked | onRenderTracked | renderTriggered | onRenderTriggered | activated | onActivated | deactivated | onDeactivated |
选项式 API 就是 vue2 的那种 API,因为 vue 组件是通过对象内的配置新建出来的。
因为 setup 函数执行的时间就在 beforeCreate 和 created 之间,因此 Composition API 没有提供响应的钩子。
export default {
setup() {
onMounted(() => {
console.log('Component is mounted!')
})
}
}
7. Provide, inject, 模板 Ref 的用法
provide 和 inject
provide 提供了可跨层级的数据的提供和获取。在 Composition API 里是这样使用的:
const app = Vue.createApp({
setup(props) {
const { provide } = Vue
provide("name", ref("sjh"))
},
template: `
<div>
<child />
</div>
`
})
app.component("child", {
setup(props) {
const { inject } = Vue
const name = inject("name", "defaultName")
return {
name
}
},
template: `<div>{{ name }}</div>`
})
用 provide 和 inject 进行子组件通知父组件
因为单向数据流的原则,子组件不能直接修改父组件的数据,因此是通过子组件通知父组件,然后父组件本身来修改自己的数据。原理和以前的 emit 类似,见下面的代码:
const app = Vue.createApp({
setup(props) {
const { provide, ref, readonly } = Vue
const name = ref("name before change")
provide("name", readonly(name))
provide("changeName", (value) => {
name.value = value
})
},
template: `
<div>
<child />
</div>
`
})
app.component("child", {
setup(props) {
const { inject } = Vue
const name = inject("name")
const changeName = inject("changeName")
const handleClick = () => {
changeName("changed name")
}
return {
name,
handleClick
}
},
template: `<div @click="handleClick">{{ name }}</div>`
})
模板 ref 用法
这里的 ref 是获取 dom 节点,和之前的响应式 ref 冲突了。这里有个固定的写法:const nodeName = ref(null) ,此时会根据声明的变量名去 template 上寻找名称匹配的 ref 属性。
const app = Vue.createApp({
setup(props) {
const { ref, onMounted } = Vue
const hello = ref(null)
onMounted(() => {
console.log(hello.value)
})
return {
hello
}
},
template: `
<div>
<div ref="hello">hello world</div>
</div>
`
})
|