Vue3 学习笔记:Composition API
- 参考王红元老师Vue课程
setup
setup概念
- setup其实就是组件的另外一个选项:
- 只不过这个选项强大到我们可以用它来替代之前所编写的大部分其他选项;
- 比如methods、computed、watch、data、生命周期等等;
setup函数的参数
- 主要有两个参数
- 第一个参数:props
- 第二个参数:context
- props它其实就是父组件传递过来的属性会被放到props对象中,我们在setup中如果需要使用,那么就可以直接通过props参数获取:
- 对于定义props的类型,我们还是和之前的规则是一样的,在props选项中定义;
- 并且在template中依然是可以正常去使用props中的属性,比如message;
- 如果我们在setup函数中想要使用props,那么不可以通过 this 去获取(
- 因为props有直接作为参数传递到setup函数中,所以我们可以直接通过参数来使用即可;
- context,也称之为是一个SetupContext,它里面包含三个属性:
- attrs:所有的非prop的attribute;
- slots:父组件传递过来的插槽(这个在以渲染函数返回时会有作用,后面会讲到);
- emit:当我们组件内部需要发出事件时会用到emit(因为我们不能访问this,所以不可以通过 this.$emit发出事件);
- 示例:
<template>
<div>
Home Page
<h2>{{ message }}</h2>
<h2>{{ title }}</h2>
<h2>当前计数: {{ counter }}</h2>
<button @click="increment">+1</button>
</div>
</template>
<script>
export default {
props: {
message: {
type: String,
required: true,
},
},
data() {
return {
counter: 100,
};
},
// setup(props, context) {
setup(props, { attrs, slots, emit }) {
console.log(props.message);
console.log(attrs.id, attrs.class);
console.log(slots);
console.log(emit);
return {
title: "Hello Home",
counter: 100,
};
},
};
</script>
<style scoped></style>
- 组件调用:
- 输出:
setup函数的返回值
- setup的返回值可以在模板template中被使用,也就是说我们可以通过setup的返回值来替代data选项;
- 至是我们可以返回一个执行函数来代替在methods中定义的方法:
- 但是,如果我们将 counter 在 increment 或者 decrement进行操作时,是否可以实现界面的响应式呢?答案是不可以;
- 这是因为对于一个定义的变量来说,默认情况下,Vue并不会跟踪它的变化,来引起界面的响应式操作;
Reactive API
- 如果想为在setup中定义的数据提供响应式的特性,那么我们可以使用reactive的函数
- 那么这是什么原因呢?为什么就可以变成响应式的呢?
- 这是因为当我们使用reactive函数处理我们的数据之后,数据再次被使用时就会进行依赖收集;
- 当数据发生改变时,所有收集到的依赖都是进行对应的响应式操作(比如更新界面);
- 事实上,我们编写的data选项,也是在内部交给了reactive函数将其编程响应式对象的;
- 示例:
<template>
<div>
Home Page
<h2>{{message}}</h2>
<h2>当前计数: {{state.counter}}</h2>
<button @click="increment">+1</button>
</div>
</template>
<script>
import { reactive } from 'vue';
export default {
props: {
message: {
type: String,
required: true
}
},
setup() {
const state = reactive({
counter: 100
})
// 局部函数
const increment = () => {
state.counter++;
console.log(state.counter);
}
return {
state,
increment
}
}
}
</script>
<style scoped>
</style>
Ref API
- ref 会返回一个可变的响应式对象,该对象作为一个 响应式的引用 维护着它内部的值,这就是ref名称的来源;
- 它内部的值是在ref的 value 属性中被维护的;
- 这里有两个注意事项:
- 在模板中引入ref的值时,Vue会自动帮助我们进行解包操作,所以我们并不需要在模板中通过 ref.value 的方式
来使用; - 但是在 setup 函数内部,它依然是一个 ref引用, 所以对其进行操作时,我们依然需要使用 ref.value的方式;
- 在模板中引入ref的值时,Vue会自动帮助我们进行解包操作,所以我们并不需要在模板中通过 ref.value 的方式
- 模板中的解包是浅层的解包。
- 如果我们将ref放到一个reactive的属性当中,那么在模板中使用时,它会自动解包:
- 示例:
<template>
<div>
Home Page
<h2>{{message}}</h2>
<!-- 当我们在template模板中使用ref对象, 它会自动进行解包 -->
<h2>当前计数: {{counter}}</h2>
<!-- ref的解包只能是一个浅层解包(info是一个普通的JavaScript对象) -->
<h2>当前计数: {{info.counter.value}}</h2>
<!-- 当如果最外层包裹的是一个reactive可响应式对象, 那么内容的ref可以解包 -->
<h2>当前计数: {{reactiveInfo.counter}}</h2>
<button @click="increment">+1</button>
</div>
</template>
<script>
import { ref, reactive } from 'vue';
export default {
props: {
message: {
type: String,
required: true
}
},
setup() {
let counter = ref(100);
const info = {
counter
}
const reactiveInfo = reactive({
counter
})
// 局部函数
const increment = () => {
counter.value++;
console.log(counter.value);
}
return {
counter,
info,
reactiveInfo,
increment
}
}
}
</script>
<style scoped>
</style>
readonly
- 通过reactive或者ref可以获取到一个响应式的对象,但是某些情况下,我们传入给其他地方(组件)的这个响应式对象希望在另外一个地方(组件)被使用,但是不能被修改,这个时候如何防止这种情况的出现呢?
- Vue3为我们提供了readonly的方法;
- readonly会返回原生对象的只读代理(也就是它依然是一个Proxy,这是一个proxy的set方法被劫持,并且不能对其进行修改);
- 示例:
<template>
<div>
<button @click="updateState">修改状态</button>
</div>
</template>
<script>
import { reactive, ref, readonly } from 'vue';
export default {
setup() {
// 1.普通对象
const info1 = {name: "why"};
const readonlyInfo1 = readonly(info1);
// 2.响应式的对象reactive
const info2 = reactive({
name: "why"
})
const readonlyInfo2 = readonly(info2);
// 3.响应式的对象ref
const info3 = ref("why");
const readonlyInfo3 = readonly(info3);
const updateState = () => {
// readonlyInfo3.value = "coderwhy"
info3.value = "coderwhy";
}
return {
updateState,
}
}
}
</script>
<style scoped>
</style>
Reactive判断的API
- isProxy
- 检查对象是否是由 reactive 或 readonly创建的 proxy。
- isReactive
- 检查对象是否是由 reactive创建的响应式代理:
- 如果该代理是 readonly 建的,但包裹了由 reactive 创建的另一个代理,它也会返回 true;
- isReadonly
- 检查对象是否是由 readonly 创建的只读代理。
- toRaw
- 返回 reactive 或 readonly 代理的原始对象(不建议保留对原始对象的持久引用。请谨慎使用)。
- shallowReactive
- 创建一个响应式代理,它跟踪其自身 property 的响应性,但不执行嵌套对象的深层响应式转换 (深层还是原生对象)。
- shallowReadonly
- 创建一个 proxy,使其自身的 property 为只读,但不执行嵌套对象的深度只读转换(深层还是可读、可写的)。
toRefs
- 如果我们使用ES6的解构语法,对reactive返回的对象进行解构获取值,那么之后无论是修改结构后的变量,还是修改reactive
- 返回的state对象,数据都不再是响应式的:
- Vue为我们提供了一个toRefs的函数,可以将reactive返回的对象中的属性都转成ref;
- 这种做法相当于已经建立了 链接,任何一个修改都会引起另外一个变化;
toRef
- 如果我们只希望转换一个reactive对象中的属性为ref, 那么可以使用toRef的方法:
- 示例:
<template>
<div>
<h2>{{name}}-{{age}}</h2>
<button @click="changeAge">修改age</button>
</div>
</template>
<script>
import { reactive, toRefs, toRef } from 'vue';
export default {
setup() {
const info = reactive({name: "why", age: 18});
// 1.toRefs: 将reactive对象中的所有属性都转成ref, 建立链接
// let { name, age } = toRefs(info);
// 2.toRef: 对其中一个属性进行转换ref, 建立链接
let { name } = info;
let age = toRef(info, "age");
const changeAge = () => {
age.value++;
}
return {
name,
age,
changeAge
}
}
}
</script>
<style scoped>
</style>
ref其他的API
- unref
- 如果我们想要获取一个ref引用中的value,那么也可以通过unref方法:
- 如果参数是一个 ref,则返回内部值,否则返回参数本身;
- 这是 val = isRef(val) ? val.value : val 的语法糖函数;
- isRef
- 判断值是否是一个ref对象。
- shallowRef
- 创建一个浅层的ref对象;
- triggerRef
- 手动触发和 shallowRef 相关联的副作用:
- 示例:
customRef
- 创建一个自定义的ref,并对其依赖项跟踪和更新触发进行显示控制:
- 它需要一个工厂函数,该函数接受 track 和 trigger 函数作为参数;
- 并且应该返回一个带有 get 和 set 的对象;
- useDebounceRef.js
import { customRef } from 'vue';
// 自定义ref
export default function(value, delay = 300) {
let timer = null;
return customRef((track, trigger) => {
return {
get() {
track();
return value;
},
set(newValue) {
clearTimeout(timer);
timer = setTimeout(() => {
value = newValue;
trigger();
}, delay);
}
}
})
}
- 使用:
<template>
<div>
<input v-model="message"/>
<h2>{{message}}</h2>
</div>
</template>
<script>
import debounceRef from './hook/useDebounceRef';
export default {
setup() {
const message = debounceRef("Hello World");
return {
message
}
}
}
</script>
computed
- 如何使用computed呢?
- 方式一:接收一个getter函数,并为 getter 函数返回的值,返回一个不变的 ref 对象;
- 方式二:接收一个具有 get 和 set 的对象,返回一个可变的(可读写)ref 对象;
<template>
<div>
<h2>{{fullName}}</h2>
<button @click="changeName">修改firstName</button>
</div>
</template>
<script>
import { ref, computed } from 'vue';
export default {
setup() {
const firstName = ref("Kobe");
const lastName = ref("Bryant");
// 1.用法一: 传入一个getter函数
// computed的返回值是一个ref对象
const fullName = computed(() => firstName.value + " " + lastName.value);
// 2.用法二: 传入一个对象, 对象包含getter/setter
const fullName = computed({
get: () => firstName.value + " " + lastName.value,
set(newValue) {
const names = newValue.split(" ");
firstName.value = names[0];
lastName.value = names[1];
}
});
const changeName = () => {
// firstName.value = "James"
fullName.value = "coder why";
}
return {
fullName,
changeName
}
}
}
</script>
侦听数据的变化
- 在Composition API中,我们可以使用watchEffect和watch来完成响应式数据的侦听;
- watchEffect用于自动收集响应式数据的依赖;
- watch需要手动指定侦听的数据源;
watchEffect
- 当侦听到某些响应式数据变化时,我们希望执行某些操作,这个时候可以使用 watchEffect。
- 首先,watchEffect传入的函数会被立即执行一次,并且在执行的过程中会收集依赖;
- 其次,只有收集的依赖发生变化时,watchEffect传入的函数才会再次执行
watchEffect的停止侦听
- 如果在发生某些情况下,我们希望停止侦听,这个时候我们可以获取watchEffect的返回值函数,调用该函数即可。
- 比如在上面的案例中,我们age达到20的时候就停止侦听:
watchEffect清除副作用
- 在我们给watchEffect传入的函数被回调时,其实可以获取到一个参数:onInvalidate
- 当副作用即将重新执行 或者 侦听器被停止 时会执行该函数传入的回调函数;
- 我们可以在传入的回调函数中,执行一些清楚工作
- 综合示例:
<template>
<div>
<h2>{{name}}-{{age}}</h2>
<button @click="changeName">修改name</button>
<button @click="changeAge">修改age</button>
</div>
</template>
<script>
import { ref, watchEffect } from 'vue';
export default {
setup() {
// watchEffect: 自动收集响应式的依赖
const name = ref("why");
const age = ref(18);
const stop = watchEffect((onInvalidate) => {
const timer = setTimeout(() => {
console.log("网络请求成功~");
}, 2000)
// 根据name和age两个变量发送网络请求
onInvalidate(() => {
// 在这个函数中清除额外的副作用
// request.cancel()
clearTimeout(timer);
console.log("onInvalidate");
})
console.log("name:", name.value, "age:", age.value);
});
const changeName = () => name.value = "kobe"
const changeAge = () => {
age.value++;
if (age.value > 25) {
stop();
}
}
return {
name,
age,
changeName,
changeAge
}
}
}
</script>
setup中使用ref
- 其实非常简单,我们只需要定义一个ref对象,绑定到元素或者组件的ref属性上即可;
watchEffect的执行时机
- 它的默认值是pre,它会在元素 挂载 或者 更新 之前执行
- flush 选项还接受 sync,这将强制效果始终同步触发。然而,这是低效的,应该很少需要。
<template>
<div>
<h2 ref="title">哈哈哈</h2>
</div>
</template>
<script>
import { ref, watchEffect } from 'vue';
export default {
setup() {
const title = ref(null);
watchEffect(() => {
console.log(title.value);
}, {
flush: "post"
})
return {
title
}
}
}
</script>
Watch的使用
- watch的API完全等同于组件watch选项的Property:
- watch需要侦听特定的数据源,并在回调函数中执行副作用;
- 默认情况下它是惰性的,只有当被侦听的源发生变化时才会执行回调;
- 与watchEffect的比较,watch允许我们:
- 懒执行副作用(第一次不会直接执行);
- 更具体的说明当哪些状态发生变化时,触发侦听器的执行;
- 访问侦听状态变化前后的值;
侦听单个数据源
- watch侦听函数的数据源有两种类型:
- 一个getter函数:但是该getter函数必须引用可响应式的对象(比如reactive或者ref);
- 直接写入一个可响应式的对象,reactive或者ref(比较常用的是ref);
<template>
<div>
<h2 ref="title">{{info.name}}</h2>
<button @click="changeData">修改数据</button>
</div>
</template>
<script>
import { ref, reactive, watch } from 'vue';
export default {
setup() {
const info = reactive({name: "why", age: 18});
// 1.侦听watch时,传入一个getter函数
watch(() => info.name, (newValue, oldValue) => {
console.log("newValue:", newValue, "oldValue:", oldValue);
})
// 2.传入一个可响应式对象: reactive对象/ref对象
// 情况一: reactive对象获取到的newValue和oldValue本身都是reactive对象
// watch(info, (newValue, oldValue) => {
// console.log("newValue:", newValue, "oldValue:", oldValue);
// })
// 如果希望newValue和oldValue是一个普通的对象
watch(() => {
return {...info}
}, (newValue, oldValue) => {
console.log("newValue:", newValue, "oldValue:", oldValue);
})
// 情况二: ref对象获取newValue和oldValue是value值的本身
// const name = ref("why");
// watch(name, (newValue, oldValue) => {
// console.log("newValue:", newValue, "oldValue:", oldValue);
// })
const changeData = () => {
info.name = "kobe";
}
return {
changeData,
info
}
}
}
</script>
侦听多个数据源
- 侦听器还可以使用数组同时侦听多个源:
侦听响应式对象
- 如果我们希望侦听一个数组或者对象,那么可以使用一个getter函数,并且对可响应对象进行解构:
watch的选项
- 如果我们希望侦听一个深层的侦听,那么依然需要设置 deep 为true:
- 也可以传入 immediate 立即执行;
- 示例:
<template>
<div>
<h2 ref="title">{{info.name}}</h2>
<button @click="changeData">修改数据</button>
</div>
</template>
<script>
import { ref, reactive, watch } from 'vue';
export default {
setup() {
// 1.定义可响应式的对象
const info = reactive({
name: "why",
age: 18,
friend: {
name: "kobe"
}
});
// 2.侦听器watch
watch(() => ({...info}), (newInfo, oldInfo) => {
console.log(newInfo, oldInfo);
}, {
deep: true,
immediate: true
})
const changeData = () => {
info.friend.name = "james";
}
return {
changeData,
info
}
}
}
</script>
生命周期钩子
- 可以使用直接导入的 onX 函数注册生命周期钩子
- 示例:
<template>
<div>
<button @click="increment">{{counter}}</button>
</div>
</template>
<script>
import { onMounted, onUpdated, onUnmounted, ref } from 'vue';
export default {
setup() {
const counter = ref(0);
const increment = () => counter.value++
onMounted(() => {
console.log("App Mounted1");
})
onMounted(() => {
console.log("App Mounted2");
})
onUpdated(() => {
console.log("App onUpdated");
})
onUnmounted(() => {
console.log("App onUnmounted");
})
return {
counter,
increment
}
}
}
</script>
Provide函数、nject函数
- 示例:
- Home.vue
<template>
<div>
<h2>{{name}}</h2>
<h2>{{counter}}</h2>
</div>
</template>
<script>
import { inject } from 'vue';
export default {
setup() {
const name = inject("name");
const counter = inject("counter");
return {
name,
counter,
}
}
}
</script>
- App.vue
<template>
<div>
<home/>
<h2>App Counter: {{counter}}</h2>
<button @click="increment">App中的+1</button>
</div>
</template>
<script>
import { provide, ref, readonly } from 'vue';
import Home from './Home.vue';
export default {
components: {
Home
},
setup() {
const name = ref("coderwhy");
let counter = ref(100);
provide("name", readonly(name));
provide("counter", readonly(counter));
const increment = () => counter.value++;
return {
increment,
counter
}
}
}
</script>
Composition API 综合示例
- 示例目录结构:
- index.js
import useCounter from './useCounter';
import useTitle from './useTitle';
import useScrollPosition from './useScrollPosition';
import useMousePosition from './useMousePosition';
import useLocalStorage from './useLocalStorage';
export {
useCounter,
useTitle,
useScrollPosition,
useMousePosition,
useLocalStorage
}
- useCounter.js
import { ref, computed } from 'vue';
export default function() {
const counter = ref(0);
const doubleCounter = computed(() => counter.value * 2);
const increment = () => counter.value++;
const decrement = () => counter.value--;
return {
counter,
doubleCounter,
increment,
decrement
}
}
- useLocalStorage.js
import { ref, watch } from 'vue';
export default function(key, value) {
const data = ref(value);
if (value) {
window.localStorage.setItem(key, JSON.stringify(value));
} else {
data.value = JSON.parse(window.localStorage.getItem(key));
}
watch(data, (newValue) => {
window.localStorage.setItem(key, JSON.stringify(newValue));
})
return data;
}
- useMousePosition.js
import { ref } from 'vue';
export default function() {
const mouseX = ref(0);
const mouseY = ref(0);
window.addEventListener("mousemove", (event) => {
mouseX.value = event.pageX;
mouseY.value = event.pageY;
});
return {
mouseX,
mouseY
}
}
- useScrollPosition.js
import { ref } from 'vue';
export default function() {
const scrollX = ref(0);
const scrollY = ref(0);
document.addEventListener("scroll", () => {
scrollX.value = window.scrollX;
scrollY.value = window.scrollY;
});
return {
scrollX,
scrollY
}
}
- useTitle.js
import { ref, watch } from 'vue';
export default function(title = "默认的title") {
const titleRef = ref(title);
watch(titleRef, (newValue) => {
document.title = newValue
}, {
immediate: true
})
return titleRef
}
- App.vue
<template>
<div>
<h2>当前计数: {{counter}}</h2>
<h2>计数*2: {{doubleCounter}}</h2>
<button @click="increment">+1</button>
<button @click="decrement">-1</button>
<h2>{{data}}</h2>
<button @click="changeData">修改data</button>
<p class="content"></p>
<div class="scroll">
<div class="scroll-x">scrollX: {{scrollX}}</div>
<div class="scroll-y">scrollY: {{scrollY}}</div>
</div>
<div class="mouse">
<div class="mouse-x">mouseX: {{mouseX}}</div>
<div class="mouse-y">mouseY: {{mouseY}}</div>
</div>
</div>
</template>
<script>
import { ref, computed } from 'vue';
import {
useCounter,
useLocalStorage,
useMousePosition,
useScrollPosition,
useTitle
} from './hooks';
export default {
setup() {
// counter
const { counter, doubleCounter, increment, decrement } = useCounter();
// title
const titleRef = useTitle("coderwhy");
setTimeout(() => {
titleRef.value = "kobe"
}, 3000);
// 滚动位置
const { scrollX, scrollY } = useScrollPosition();
// 鼠标位置
const { mouseX, mouseY } = useMousePosition();
// localStorage
const data = useLocalStorage("info");
const changeData = () => data.value = "哈哈哈哈"
return {
counter,
doubleCounter,
increment,
decrement,
scrollX,
scrollY,
mouseX,
mouseY,
data,
changeData
}
}
}
</script>
<style scoped>
.content {
width: 3000px;
height: 5000px;
}
.scroll {
position: fixed;
right: 30px;
bottom: 30px;
}
.mouse {
position: fixed;
right: 30px;
bottom: 80px;
}
</style>
setup顶层编写方式
- 官方文档:单文件组件 <script setup> | Vue.js
- 示例:
- HelloWorld.vue
<template>
<div>
<h2>Hello World</h2>
<h2>{{message}}</h2>
<button @click="emitEvent">发射事件</button>
</div>
</template>
<script setup>
import { defineProps, defineEmit } from 'vue';
const props = defineProps({
message: {
type: String,
default: "哈哈哈"
}
})
const emit = defineEmit(["increment", "decrement"]);
const emitEvent = () => {
emit('increment', "100000")
}
</script>
- App.vue
<template>
<div>
<h2>当前计数: {{counter}}</h2>
<button @click="increment">+1</button>
<hello-world message="呵呵呵" @increment="getCounter"></hello-world>
</div>
</template>
<script setup>
import { ref } from 'vue';
import HelloWorld from './HelloWorld.vue';
const counter = ref(0);
const increment = () => counter.value++;
const getCounter = (payload) => {
console.log(payload);
}
</script>
Vue中使用jsx
- 如果不支持需要进行如下配置,现在Vue3支持jsx语法,不需要配置。
- 示例:
- 示例2:jsx组件的使用