VueVue 3.0 组合式 API
WaterBoat1. 为什么学习 Vue3
目标: 了解学习 Vue3 的必要性。
Vue3 运行性能大幅提升,速度是 Vue2 的 1.5 倍左右
Vue3 支持 tree shaking,可以进行按需编译,编译后的文件体积比 Vue2 更小
Vue3 组合式 API 使应用中的功能代码更聚合,使组件间公共逻辑的抽取更容易
Vue3 对 TypeScript 的支持更加友好,对大型前端应用的支持更加游刃有余
Vue3 中提供了更加先进的功能,比如 teleport,suspense 等
Vue 是目前国内前端使用者最多的框架,Vue3 是将来的必然趋势
Vue3 官方中文文档
2. 使用 Vite 创建项目
目标: 能够使用 vite 构建工具创建 Vue 应用。Vite 官方
Vite 是一款新型的前端构建工具,核心卖点就是快,启用速度快,编译速度快。
- 创建应用:
npm init vite-app vue-tutorial
或者 npx create-vite-app vue-tutorial
- 切换至应用根目录:
cd vue-tutorial
- 下载应用依赖:
npm install
- 启动应用:
npm run dev
- 访问应用:
localhost:3000
Vite 虽然构建速度非常快,但目前它默认安装的插件非常少,随着开发过程依赖增多,需要自己额外配置,所以做项目时仍然使用 vue-cli
3. 安装编辑器插件
目标:安装 VSCode 编辑器插件
- Volar: Vue3 语法支持
- Vue.js AutoImport: 引用组件
- Vue3 Snippets: Vue3 代码片段
- Prettier-Code formatter: 代码格式化
- ESLint: 代码质量检查
- EditorConfig for VS Code: 覆盖编辑器编码风格配置
- Material Icon Theme: 编辑器主题
- Chinese (Simplified) Language Pack for Visual Studio Code: 中文语言支持
4. 组合式 API 的优势
目标:掌握组合式 API 相比较选项式 API,它的优势是什么。
在选项式 API 中,它将数据和逻辑进行了分离,所有不相关的数据被放置在了一起,所以不相关的逻辑被放置在了一起,随着应用规模的增加,项目将会变得越来越难以维护。
在组合式 API 中,它将同一个功能的逻辑和数据放置在了一起,使同一个的功能代码更加聚合。
同一个功能的代码可以被抽取到单独的文件中,使应用代码更加维护。
5. 组合式 API 入口
目标:掌握 setup 函数的基本使用。
setup
函数是一个新的组件选项,作为在组件中使用组合式 API 的入口
setup
函数在任何生命周期函数之前执行,且函数内部 this
为 undefined
,它不绑定组件实例对象
1 2 3 4 5 6 7 8
| export default { setup() { console.log(this); }, beforeCreate() { console.log("before create"); }, };
|
setup
函数的返回值为对象类型,对象中的属性可以在其他选项和模板中使用, 因为对象中的属性会被添加到组件实例对象中
1 2 3 4 5 6 7 8 9 10
| export default { setup() { let name = "张三"; let age = 20; return { name, age }; }, beforeCreate() { console.log(this.name); }, };
|
1
| <template>{{ name }} | {{ age }}</template>
|
注意:在 setup 方法中声明的变量虽然可以在模板中显示,但它不是响应式数据,就是说当数据更改后界面不会发生变化。
1 2 3 4 5 6 7 8 9 10 11
| export default { setup() { let name = "张三"; let age = 20; const onClickHandler = () => { name = "李四"; age = 30; }; return { name, age, onClickHandler }; }, };
|
1 2 3 4
| <template> {{ name }} | {{ age }} <button @click="onClickHandler">button</button> </template>
|
6. 响应式组件状态 ref
目标:掌握使用 ref 方法创建、修改响应式数据的方式。
ref 函数用于创建响应式数据,即数据变化视图更新。
使用 ref 函数创建基本数据类型的响应式数据。
1 2 3 4 5 6 7 8
| import { ref } from "vue"; export default { setup() { const name = ref("张三"); const age = ref(20); return { name, age }; }, };
|
使用 ref 创建的数据在模板中可以直接使用。
1
| <template>{{ name }} | {{ age }}</template>
|
在 JavaScript 中通过 value 属性修改数据。
1 2 3 4 5 6 7 8 9 10 11
| export default { setup() { const name = ref("张三"); const age = ref(20); const onClickHandler = () => { name.value = "李四"; age.value = 30; }; return { name, age, onClickHandler }; }, };
|
1 2 3 4
| <template> {{ name }} | {{ age }} <button @click="onClickHandler">button</button> </template>
|
使用 ref 函数创建引用数据类型的响应式数据。
1 2 3 4 5 6 7 8 9 10 11 12
| export default { setup() { const person = ref({ name: "张三", age: 30 }); const onClickHandler = () => { person.value.name = "王五"; person.value.age = 50; }; return { person, onClickHandler }; }, };
|
1 2 3 4
| <template> {{ person.name }} | {{ person.age }} <button @click="onClickHandler">button</button> </template>
|
7. 响应式组件状态 reactive
目标: 掌握使用 reactive 函数创建响应式数据的方式, 掌握 reactive 函数和 ref 函数的区别
reactive 函数也可以用来创建响应式数据。
使用 reactive 函数创建基于引用数据类型的响应式数据。
1 2 3 4 5 6 7 8 9 10 11
| import { reactive } from "vue"; export default { setup() { const person = reactive({ name: "张三", age: 20 }); const onClickHandler = () => { person.name = "李四"; person.age = 50; }; return { person, onClickHandler }; }, };
|
1 2 3 4
| <template> {{ person.name }} | {{ person.age }} <button @click="onClickHandler">button</button> </template>
|
reactive 函数只能基于引用数据类型创建响应式数据,对于基本数据类型它是不起作用的。
1 2 3 4 5 6 7 8 9 10 11
| export default { setup() { let name = reactive("张三"); const onClickHandler = () => { name = reactive("李四"); }; return { name, onClickHandler }; }, };
|
需求: 在点击按钮后将 newPerson
中的值赋值给 person
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| export default { name: "App", setup() { let person = reactive({ name: "张三", age: 30 }); const newPerson = { name: "李四", age: 50 }; const onClickHandler = () => { for (const attr in newPerson) { person[attr] = newPerson[attr]; } }; return { person, onClickHandler }; }, };
|
ref 既可以创建基于基本数据类型的响应式数据也可以创建基于引用数据类型的响应式数据,reactive 只用于创建基于引用数据类型的响应式数据
ref 在 JS 中使用时需要点上 value, 而 reactive 在 JS 中使用时不需要点上 value,在模板中使用时都不需要加 value
ref 创建的响应式数据可以被直接整体赋值,而 reactive 创建的响应式数据不可以,若要整体赋值需要使用遍历的方式
为什么使用 ref 方法创建的响应式数据在修改时需要使用 value 属性,而使用 reactive 方法创建的响应式数据不需要?
ref 既可以创建基于基本数据类型的响应式数据, 也可以创建基于引用数据类型的响应式数据, 基本数据类型的响应式是通过类的属性访问器实现的, 引用数据类型的响应式是通过代理对象实现的, 虽然内部实现不同, 但是为了更好的 API 使用体验, 内部封装了统一的调用入口, 即 value 属性, 具体通过哪种方式创建响应式数据由内部统一处理.
reactive 只用于创建基于引用数据类型的响应式数据, 不需要供统一的调用入口, 所以没有必要使用 value 属性.
8. 计算属性 computed
目标:掌握使用 computed 函数创建计算属性的方式
接收回调函数作为参数,基于回调函数中使用的响应式数据进行计算属性的创建. 回调函数的返回值就是计算结果.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| import { ref, computed } from "vue"; export default { setup() { const names = ref([ "林俊杰", "孙燕姿", "周杰伦", "张惠妹", "刘若英", "林宥嘉", "刘德华", "张韶涵", "周笔畅", "孙楠", ]); const search = ref(""); const filterNames = computed(() => names.value.filter((name) => name.includes(search.value)), ); return { search, filterNames }; }, };
|
1 2 3 4 5 6
| <template> <input type="text" v-model="search" /> <ul> <li v-for="name in filterNames">{{ name }}</li> </ul> </template>
|
9. 监听状态 watch
目标:掌握 watch 函数监听数据的方式
watch 函数用于监听响应式数据的变化。
使用 watch 函数监听基于 ref 创建的响应式数据 (基本数据类型)。
1 2 3 4 5 6 7 8 9 10 11
| import { ref, watch } from "vue"; export default { setup() { const text = ref(""); watch(text, (current, previous) => { console.log("current", current); console.log("previous", previous); }); return { text }; }, };
|
1 2 3
| <template> <input type="text" v-model="text" /> </template>
|
使用 watch 监听基于 ref 创建的响应式数据 (引用数据类型)。
1 2 3 4 5 6 7 8 9 10 11
| import { ref, watch } from "vue";
export default { name: "App", setup() { const person = ref({ name: "张三" }); watch(person.value, (current) => { console.log(current); }); }, };
|
1 2 3
| <template> <button @click="onClickHandler">{{ person.name }}</button> </template>
|
使用 watch 监听响应式数据内部的具体属性 (基本数据类型)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| import { ref, watch } from "vue";
export default { name: "App", setup() { const person = ref({ name: "张三" }); watch( () => person.value.name, (current) => { console.log(current); }, ); return { person }; }, };
|
使用 watch 监听响应式数据内部的具体属性 (引用数据类型)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| <template> <p>{{ person.brand.title }} {{ person.name }}</p> <button @click="changeBrandTitle">title</button> <button @click="changeName">name</button> </template>
<script> import { ref, watch } from "vue"; export default { name: "App", setup() { const person = ref({ brand: { title: "宝马" }, name: "张三" }); const changeBrandTitle = () => { person.value.brand.title = "奔驰"; }; const changeName = () => { person.value.name = "李四"; }; watch(person.value.brand, (current) => { console.log(current); }); return { person, changeBrandTitle, changeName }; }, }; </script>
|
使用 watch 监听基于 reactive 创建的响应式数据。
1 2 3 4 5 6 7 8 9 10 11 12 13
| import { reactive, watch } from "vue"; export default { setup() { const person = reactive({ name: "张三" }); const onClickHandler = () => { person.name = "李四"; }; watch(person, (current, previous) => { console.log(current); }); return { person, onClickHandler }; }, };
|
1 2 3 4
| <template> {{ person.name }} <button @click="onClickHandler">button</button> </template>
|
使用 watch 监听多个值的变化
1 2 3 4 5 6 7 8 9 10 11
| import { ref, watch } from "vue"; export default { setup() { const firstName = ref(""); const lastName = ref(""); watch([firstName, lastName], (current) => { console.log(current); }); return { firstName, lastName }; }, };
|
1 2 3 4
| <template> <input type="text" v-model="firstName" /> <input type="text" v-model="lastName" /> </template>
|
使 watch 监听数据在初始时执行一次
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| import { ref, watch } from "vue"; export default { setup() { const firstName = ref("hello"); const lastName = ref("world"); watch( [firstName, lastName], (current) => { console.log(current); }, { immediate: true, }, ); return { firstName, lastName }; }, };
|
10. 监听状态 watchEffect
目标:掌握使用 watchEffect 监听数据的方式
watchEffect 和 watch 一样,都是用于监听响应式数据的变化。
watchEffect 只关心数据的最新值,不关心旧值是什么,而且 watchEffect 默认会在初始时执行一次。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| import { ref, watchEffect } from "vue";
export default { name: "App", setup() { const firstName = ref(""); const lastName = ref(""); watchEffect(() => { console.log(firstName.value); console.log(lastName.value); }); return { firstName, lastName }; }, };
|
1 2 3 4
| <template> <input type="text" v-model="firstName" /> <input type="text" v-model="lastName" /> </template>
|
11. toRef 函数
目标:掌握 toRef 函数的使用方式及应用场景
说出以下代码的输出结果是什么?
1 2 3 4 5
| let person = { name: "张三" }; let name = person.name; person.name = "李四"; console.log(person.name); console.log(name);
|
当按钮被点击时模板中的数据会发生更新吗?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| <template> <p>{{ name }}</p> <p>{{ person }}</p> <button @click="onClickHandler">button</button> </template> <script> import { ref } from "vue"; export default { name: "App", setup() { const person = ref({ name: "张三" }); const onClickHandler = () => { person.value.name = "李四"; }; return { name: person.value.name, person, onClickHandler, }; }, }; </script>
|
toRef 方法用于将响应式数据内部的普通数据转换为响应式数据,并且转换后的数据和原始数据存在引用关系,存在引用关系意味着当原始数据发生变化后,toRef 转换后的数据也会跟着变化。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| <template> <p>{{ name }}</p> <p>{{ person }}</p> <button @click="onClickHandler">button</button> </template>
<script> import { ref, toRef } from "vue"; export default { name: "App", setup() { const person = ref({ name: "张三" }); const onClickHandler = () => { person.value.name = "李四"; }; return { name: toRef(person.value, "name"), person, onClickHandler, }; }, }; </script>
|
需求: 当响应式数据的结构层级比较深时,在模板中使用起来也比较繁琐,能不能在模板中使用时简化结构层级呢?
1 2 3 4 5 6
| export default { setup() { const person = ref({ brand: { name: "宝马" } }); return { person }; }, };
|
1
| <template>{{ person.brand.name }}</template>
|
如果能够将模板中的 person.brand.name
简化成 brandName
的话,模板代码会更加简洁,所以按照想法代码很自然的就写成了下面这样。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| <template> <p>{{ person }}</p> <p>{{ brandName }}</p> <button @click="onClickHandler">button</button> </template>
<script> import { ref } from "vue"; export default { name: "App", setup() { const person = ref({ brand: { name: "宝马" } }); const onClickHandler = () => { person.value.brand.name = "奔驰"; }; return { person, brandName: person.value.brand.name, onClickHandler, }; }, }; </script>
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| <template> <p>{{ person }}</p> <p>{{ brandName }}</p> <button @click="onClickHandler">button</button> </template>
<script> import { ref, toRef } from "vue"; export default { name: "App", setup() { const person = ref({ brand: { name: "宝马" } }); const onClickHandler = () => { person.value.brand.name = "奔驰"; }; return { person, brandName: toRef(person.value.brand, "name"), onClickHandler, }; }, }; </script>
|
12. toRefs 函数
目标:掌握 toRefs 方法批量转换响应式数据的方式。
通过 toRef
方法一次只能转换一个数据,通过 toRefs
方法可以实现批量数据转换。
toRefs 方法接收引用数据类型的响应式数据,它可以将数据中的第一层属性全部转换为响应式数据, 返回值是一个对象, 对象中存储了所有转换之后的响应式数据。
1 2 3 4 5 6 7 8 9 10 11 12 13
| import { reactive, toRefs } from "vue";
export default { name: "App", setup() { const person = reactive({ name: "张三", age: 20, brand: { title: "宝马", year: 1 }, }); return { ...toRefs(person) }; }, };
|
1
| <template>{{ name }} {{ age }} {{ brand.title }} {{ brand.year }}</template>
|
对引用数据类型内部的数据进行转换
1 2 3 4 5 6 7 8 9 10 11 12 13
| import { reactive, toRefs } from "vue";
export default { name: "App", setup() { const person = reactive({ name: "张三", age: 20, brand: { title: "宝马", year: 1 }, }); return { ...toRefs(person), ...toRefs(person.brand) }; }, };
|
1
| <template>{{ name }} {{ age }} {{ title }} {{ year }}</template>
|
13. 组件通讯
目标:掌握组合式 API 中父子组件通信的方式
父组件通过 props 向子组件传递数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| <template> <div>I am parent component</div> <hr /> <ChildComp :msg="msg"></ChildComp> </template>
<script> import ChildComp from "./components/ChildComp.vue"; import { ref } from "vue"; export default { components: { ChildComp }, setup() { const msg = ref("a message from parent"); return { msg }; }, }; </script>
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| <template> <div> {{ childMsg }} <hr /> {{ msg }} </div> </template> <script> import { computed } from "vue"; export default { name: "ChildComponent", props: ["msg"], setup(props) { // 当父组件更新 props 时 setup 函数是不会重新执行的 // 所以在 setup 函数中使用 props 时需要用到 computed 或者 watch 来响应 props 的变化 // 注意: 直接在模板中使用 props 数据是没有这个问题的 const childMsg = computed(() => props.msg + "😀😀"); return { childMsg }; }, }; </script>
|
子组件通过自定义事件向父组件传递数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| <template> <div> {{ childMsg }} <hr /> {{ msg }} <hr /> <button @click="onMsgChanged">change msg</button> </div> </template> <script> import { computed } from "vue";
export default { name: "ChildComponent", props: ["msg"], setup(props, { emit }) { const onMsgChanged = () => { emit("onMsgChanged", "changed msg from children"); }; return { onMsgChanged }; }, }; </script>
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| <template> <ChildComponent :msg="msg" @onMsgChanged="onMsgChanged" /> </template>
<script> import { ref } from "vue"; import ChildComponent from "./components/child-component.vue"; export default { components: { ChildComponent }, name: "App", setup() { const msg = ref("i am a message"); const onMsgChanged = (data) => { msg.value = data; }; return { msg, onMsgChanged }; }, }; </script>
|
注意事项:在 Vue2 中,模板需要被一个根元素包裹,但是在 Vue3 中是不需要的,Vue3 支持在模板中编写代码片段。
1 2 3 4
| <template> <div>{{ childMsg }}</div> <button @click="onClickHandler">change msg</button> </template>
|
如果在模板中使用代码片段, 自定义事件需要被显式的声明在 emits 选项中.
1
| emits: ["onMsgChanged"],
|
14. 组件生命周期
目标:掌握组件生命周期函数的使用方式 VUE3 生命周期函数
setup
: Vue3 中组合式 API 的入口, 它会在创建组件实例对象前执行, 会在每次组件重新挂载时执行。
创建组件实例对象前执行
1 2 3 4 5 6 7 8
| export default { setup() { console.log("setup"); }, beforeCreate() { console.log("before create"); }, };
|
每次组件重新挂载时执行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| <!-- App组件 父组件 --> <template> <button @click="show = !show">toggle</button> <ChildComponent v-if="show"></ChildComponent> </template>
<script> import { ref } from "vue"; import ChildComponent from "./components/child-component.vue"; export default { components: { ChildComponent }, name: "App", setup() { const show = ref(true); return { show }; }, }; </script>
|
1 2 3 4 5 6 7 8 9 10 11
| <!-- ChildComponent 组件 子组件 --> <template>child component</template> <script> export default { name: "ChildComponent", setup() { // setup 函数会在组件每次重新渲染时执行 console.log("setup"); }, }; </script>
|
onMounted
组件挂载完成后执行
onUpdated
组件数据更新后执行
onUnmounted
组件卸载后执行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| <!-- child-component --> <template>{{ count }} <button @click="onClickHandler">button</button></template> <script> import { onMounted, onUnmounted, onUpdated, ref } from "vue";
export default { name: "ChildComponent", setup() { let timer = null; // 组件挂载完成之后开启定时器 onMounted(() => { timer = setInterval(() => { console.log("timer..."); }, 1000); }); // 组件卸载完成之后清除定时器 onUnmounted(() => { clearInterval(timer); }); const count = ref(0); const onClickHandler = () => { count.value = count.value + 1; }; // 组件更新之后在控制台中输出 onUpdated onUpdated(() => { console.log("onUpdated"); }); return { count, onClickHandler }; }, }; </script>
|
15. 与服务端通信
目标:掌握在组合式 API 中实现与服务器端通讯的方式
向服务器端发送请求获取列表数据渲染列表数据, 没有数据要显示暂无数据, 如果请求报错展示错误信息, 加载过程显示 loading.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| <script> import { ref } from "vue"; import axios from "axios";
export default { name: "App", setup() { // 用于存储列表数据 const data = ref(null); // 用于标识加载状态 const loading = ref(false); // 用于存储错误信息 const error = ref(null); // 用于发送请求的方法 async function getPosts() { // 更新加载状态 loading.value = true; try { // 发送请求 let response = await axios.get( "https://jsonplaceholder.typicode.com/posts", ); // 存储列表数据 data.value = response.data; } catch (err) { // 存储错误信息 error.value = err.message; } // 更新加载状态 loading.value = false; } // 调用方法 发送请求 getPosts(); // 返回模板所需数据 return { data, loading, error }; }, }; </script>
|
1 2 3 4 5 6 7 8 9 10
| <template> <div v-if="loading">loading...</div> <div v-else-if="error">{{ error }}</div> <div v-else-if="data && data.length > 0"> <ul> <li v-for="item in data">{{ item.title }}</li> </ul> </div> <div v-else>暂无数据</div> </template>
|
注意: 如果在导入 axios 时报错,重新启动应用程序即可。
将获取 Posts 数据的逻辑抽取单独文件中,使其可以在多个组件中被重复使用。
1 2 3 4 5 6 7 8
| export default { name: "App", setup() { const { data, loading, error, getPosts } = usePosts(); getPosts(); return { data, loading, error }; }, };
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| import { ref } from "vue"; import axios from "axios";
function usePosts() { const data = ref(null); const loading = ref(false); const error = ref(null); async function getPosts() { loading.value = true; try { let response = await axios.get( "https://jsonplaceholder.typicode.com/posts", ); data.value = response.data; } catch (err) { error.value = err.message; } loading.value = false; } return { data, loading, error, getPosts }; }
|
16. 获取 DOM 对象
目标:掌握在组合式 API 中获取 DOM 对象的方式
获取单个 DOM 对象
1 2 3 4 5 6 7 8 9 10
| import { ref, onMounted } from "vue"; export default { setup() { const divRef = ref(null); onMounted(() => { console.log(divRef.value); }); return { divRef }; }, };
|
1 2 3
| <template> <div ref="divRef">Hello Ref</div> </template>
|
获取一组 DOM 对象
1 2 3 4 5 6 7 8 9 10 11
| import { ref, onMounted, onUpdated } from "vue"; export default { setup() { const list = ref(["a", "b", "c"]); const elms = ref([]); const onClickHandler = () => list.value.push("d"); onMounted(() => console.log(elms.value)); onUpdated(() => console.log(elms.value)); return { list, elms, onClickHandler }; }, };
|
1 2 3 4 5 6 7 8 9 10 11 12
| <template> <ul> <li v-for="(item, index) in list" :key="index" :ref="(el) => (elms[index] = el)" > {{ item }} </li> </ul> <button @click="onClickHandler">button</button> </template>
|
17. provide、inject 函数-跨组件层级传递数据的方式
目标:掌握跨组件层级传递数据的方式
通过 provide、inject 函数的配合使用,可以实现跨组件传递数据(组件与组件存在嵌套关系)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| <!-- 父组件 App --> <template> <ChildComponent /> </template>
<script> import { ref, provide } from "vue"; import ChildComponent from "./components/ChildComponent.vue";
export default { components: { ChildComponent }, name: "App", setup() { const person = ref({ name: "张三" }); const changePerson = () => { person.value.name = "李四"; }; provide("person", person); provide("changePerson", changePerson); }, }; </script>
|
1 2 3 4 5 6 7 8 9 10 11
| <template> <LastComponent /> </template> <script> import LastComponent from "./LastComponent.vue"; export default { components: { LastComponent }, name: "ChildComponent", }; </script>
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| <template> {{ person.name }} <button @click="changePerson">button</button> </template> <script> import { inject } from "vue"; export default { name: "LastComponent", setup() { const person = inject("person"); const changePerson = inject("changePerson"); return { person, changePerson }; }, }; </script>
|
18. teleport 传送门组件
目标:掌握 teleport 组件的使用方式
teleport 组件可以将指定组件渲染到应用外部的其他位置。
比如弹框组件,它可能在任意组件中使用,但它不属于任意组件,所以不能在使用它的组件中渲染它,我们需要将它渲染到指定位置。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| <!-- Modal.vue --> <template> <div class="wrapper"> <div class="content"> <a class="close" href="javascript:">关闭</a> </div> </div> </template> <script> export default { name: "Modal", }; </script> <style scoped> .wrapper { position: absolute; left: 0; top: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.4); } .content { width: 660px; height: 400px; background: white; position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); } .close { position: absolute; right: 10px; top: 10px; color: #999; text-decoration: none; } </style>
|
1 2 3 4 5 6 7 8 9 10 11 12 13
| <template> <teleport to="#modal"> <Modal /> </teleport> </template> <script> import Modal from "./components/Modal.vue"; export default { components: { Modal }, name: "App", }; </script>
|
19. Suspense 组件-确保组件中的 setup 函数调用和模板渲染之间的执行顺序
目标:掌握 Suspense 组件的使用方式
Suspense 用于确保组件中的 setup 函数调用和模板渲染之间的执行顺序。先执行 setup 后渲染模板。
当组件中的 setup 被写成异步函数的形式, 代码执行的顺序就变成了先渲染模板后执行 setup 函数了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| <!-- Posts.vue --> <template> <pre>{{ data }}</pre> </template> <script> import axios from "axios";
export default { name: "Posts", async setup() { let response = await axios.get( "https://jsonplaceholder.typicode.com/posts", ); return { data: response.data }; }, }; </script>
|
1 2 3 4 5 6 7 8 9 10 11 12 13
| <!-- App.vue --> <template> <Suspense> <Posts /> </Suspense> </template> <script> import Posts from "./components/Posts.vue"; export default { components: { Posts }, name: "App", }; </script>
|
通过 suspense 组件还可以为异步操作添加等待提示效果。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| <template> <Suspense> <template v-slot:default> <Posts /> </template> <template v-slot:fallback> loading... </template> </Suspense> </template> <script> import Posts from "./components/Posts.vue"; export default { components: { Posts }, name: "App", }; </script>
|
20. 过渡动画
目标:掌握 transition 组件的使用方式
20.1 概述
Vue 提供了 transition 组件供我们执行过渡动画, 我们只需要使用 transition 组件包裹你要执行动画的元素即可。
执行过渡动画的前提条件是元素具有创建与销毁的操作。
1 2 3
| <transition> <h1>hello world</h1> </transition>
|
当创建元素时, transiton 组件会为执行动画的元素添加三个类名, 我们可以通过这三个类名为元素添加入场动画。
1 2 3 4 5 6
| .enter-from { } // 元素执行动画的初始样式 (动画起点样式) .enter-to { } // 元素执行动画的目标样式 (动画终点样式) .enter-active { } // 可以用于指定元素指定动画的类型
|
1 2 3 4 5 6 7 8 9
| .enter-from { opacity: 0; } .enter-to { opacity: 1; } .enter-active { transition: opacity 2s ease-in; } // ease-in 先慢后快
|
当销毁元素时, transition 组件会为执行动画的元素添加三个类名, 我们可以通过这个三个类名为元素添加离场动画样式。
1 2 3 4 5 6
| .leave-from { } // 元素执行动画的初始样式 (动画起点样式) .leave-to { } // 元素执行动画的目标样式 (动画终点样式) .leave-active { } // 可以用于指定元素指定动画的类型
|
1 2 3 4 5 6 7 8 9
| .leave-from { opacity: 1; } .leave-to { opacity: 0; } .leave-active { transition: opacity 2s ease-out; } // ease-out 先快后慢
|
如果在页面中有多个元素要执行动画, 而多个元素要执行的动画不同时, 为了对多个元素的动画样式进行区分, 在调用 transiton 组件时需要为它添加 name 属性以区分样式类名。
1 2 3
| <transition name="fade"> <h1>hello world</h1> </transition>
|
1 2 3 4 5 6 7 8 9 10 11 12 13
| .fade-enter-from { } .fade-enter-to { } .fade-enter-active { }
.fade-leave-from { } .fade-leave-to { } .fade-leave-active { }
|
20.2 示例
需求: 点击按钮让元素显示隐藏 (执行动画)
1 2 3 4
| <transition name="fade"> <h2 v-if="show">hello world</h2> </transition> <button @click="show = !show">button</button>
|
1
| const show = ref(false);
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| .fade-enter-from { opacity: 0; } .fade-enter-to { opacity: 1; } .fade-enter-active { transition: opacity 2s ease-in; }
.fade-leave-from { opacity: 1; } .fade-leave-to { opacity: 0; } .fade-leave-active { transition: opacity 2s ease-out; }
|
21. 状态管理 Vuex
掌握 Vuex 实现全局状态管理的方式
21.1 问题
在不使用全局状态管理库时, 应用状态由组件管理, 当多个组件需要共享使用同一个应用状态时, 应用状态需要通过 props 或自定义事件在组件之间进行传递, 在组件与组件之间的关系比较疏远时, 手递手的这种传递方式显得特别混乱, 使得应用的维护变得困难.
在使用了全局状态管理库后, 需要共享的应用状态被单独存储在一个独立于组件的 Store 对象中, 所有组件可以直接从这个对象中获取状态, 省去了繁琐的组件状态传递过程. 而且当 Store 中的状态发生变化后,组件也会自动更新。
21.2 Vuex 工作流程
State: 用于存储应用状态 (store.state)
Action: 用于执行异步操作 (dispatch)
Mutation: 用于修改 state 中的应用状态 (commit)
Getter: vuex 中的计算属性 (store.getters)
Module: 模块, 用于对状态进行拆分
在组件中开发者可以调用 dispatch 方法触发 Action 执行异步操作, 当异步操作执行完成后, 在 Action 中可以继续调用 commit 方法触发 mutation 修改状态, 当状态被修改以后, 视图更新.
21.3 下载
Vuex 目前有两个版本, 一个是 3.6.2
, 另一个是 4.0.2
, 3.x 的版本是供 Vue2 使用的, 4.x 版本是供 Vue3 使用的.
在下载 Vuex 的时候如果不加版本号,默认下载的是 3.x 版本, 而我们要使用的是 4.x 的版本, 所以在下载时千万记得加版本号.
npm install vuex@4.0.2
21.4 创建 Store
1 2 3
| import { createStore } from "vuex"; export default createStore({});
|
1 2 3 4
| import store from "./store"; const app = createApp(App); app.use(store);
|
21.5 state
在应用状态对象中存储 username
状态.
1 2 3 4 5
| export default createStore({ state: { username: "张三", }, });
|
在组件中获取 username
状态
1
| <template> {{$store.state.username}} </template>
|
1 2 3 4 5 6 7 8 9
| <script> import { useStore } from "vuex"; export default { setup() { const store = useStore(); console.log(store.state.username); }, }; </script>
|
21.6 getters
getters 是 vuex 中的计算属性, 基于现有状态计算出新的状态。
1 2 3 4 5 6 7
| export default createStore({ getters: { newUsername(state) { return state.username + "😀😀😀😀"; }, }, });
|
1 2 3
| <template> {{ $store.getters.newUsername }} </template>
|
1 2 3 4 5 6 7
| <script> export default { setup() { console.log(store.getters.newUsername); }, }; </script>
|
21.7 mutations
mutations 是 vuex 中用于修改状态的方法。
1 2 3 4 5 6 7
| export default createStore({ mutations: { updateUsername(state, username) { state.username = username; }, }, });
|
1 2 3 4 5
| <template> <button @click="$store.commit('updateUsername', '李四')"> change username </button> </template>
|
21.8 actions
actions 在 Vuex 中用于执行异步操作, 当异步操作执行完成以后可以调用 commit 方法触发 mutation 来修改应用状态
1 2 3 4 5 6 7 8 9
| export default createStore({ actions: { updateName(ctx) { setTimeout(() => { ctx.commit("updateName", "李四"); }, 1000); }, }, });
|
1 2 3 4 5 6 7 8 9 10 11 12 13
| <template> <button @click="onClickHandler">button</button> </template> <script> export default { setup() { const onClickHandler = () => { store.dispatch("updateName"); }; return { onClickHandler }; }, }; </script>
|
21.9 module
21.9.1 概述
Vuex 允许开发者通过模块对状态进行拆分,允许开发者将不同功能的状态代码拆分到不同的模块中。
模块分为两种,一种是不具备命名空间的模块,另一种是具备命名空间的模块,推荐使用命名空间,命名空间使模块更加独立。
21.9.2 非命名空间模块
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| import { createStore } from "vuex";
const moduleA = { state() { return { name: "模块A", }; }, }; const moduleB = { state() { return { name: "模块B", }; }, };
export default createStore({ modules: { a: moduleA, b: moduleB, }, });
|
1 2 3 4 5 6 7 8 9 10 11 12
| <template> {{$store.state['a'].name}} {{$store.state['b'].name}} </template> <script> import { useStore } from "vuex"; export default { name: "App", setup() { const store = useStore(); console.log(store.state.a.name); console.log(store.state.b.name); }, }; </script>
|
非命名空间模块中的 mutation 方法, 当 updateName
方法被触发后,所有定义了此方法的模块都会调用该方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| import { createStore } from "vuex";
const moduleA = { mutations: { updateName(state) { state.name = "😀模块A😀"; }, }, }; const moduleB = { mutations: { updateName(state) { state.name = "😝模块B😝"; }, }, };
export default createStore({ modules: { a: moduleA, b: moduleB, }, });
|
1 2 3 4
| <template> {{$store.state['a'].name}} {{$store.state['b'].name}} <button @click="$store.commit('updateName')">updateName</button> </template>
|
非命名空间模块中的 getter,不能在两个模块中定义相同的 getter
以避免程序报错。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| import { createStore } from "vuex";
const moduleA = { getters: { newName(state) { return state.name + "😀"; }, }, }; const moduleB = { getters: { newName(state) { return state.name + "😝"; }, }, };
export default createStore({ modules: { a: moduleA, b: moduleB, }, });
|
1
| <template> {{$store.getters.newName}} </template>
|
21.9.3 命名空间模块
命名空间模块需要在模块对象中添加 namespaced: true
选项。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| import { createStore } from "vuex";
const moduleA = { namespaced: true, state() { return { name: "模块A" }; }, }; const moduleB = { namespaced: true, state() { return { name: "模块B" }; }, };
export default createStore({ modules: { a: moduleA, b: moduleB, }, });
|
1
| <template> {{$store.state['a'].name}} {{$store.state['b'].name}} </template>
|
具有命名空间的模块状态更加独立,比如可以在不同的命令空间中定义相同的 getter
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| import { createStore } from "vuex";
const moduleA = { namespaced: true, getters: { newName(state) { return state.name + "😀"; }, }, }; const moduleB = { namespaced: true, getters: { newName(state) { return state.name + "😀"; }, }, };
export default createStore({ modules: { a: moduleA, b: moduleB, }, });
|
1 2 3
| <template> {{$store.getters['a/newName']}} {{$store.getters['b/newName']}} </template>
|
在不同的命名空间模块中定义相同的变异方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| import { createStore } from "vuex";
const moduleA = { namespaced: true, mutations: { updateName(state) { state.name = "我是模块A"; }, }, }; const moduleB = { namespaced: true, mutations: { updateName(state) { state.name = "我是模块B"; }, }, };
export default createStore({ modules: { a: moduleA, b: moduleB, }, });
|
1 2 3 4 5 6
| <template> {{ $store.getters["a/newName"] }} {{ $store.getters["b/newName"] }} <button @click="$store.commit('a/updateName')">update moduleA</button> <button @click="$store.commit('b/updateName')">update moduleb</button> </template>
|
22. 代理对象
目标: 了解代理对象的使用方式
什么是数据响应式?
数据驱动视图, 即数据和视图进行绑定, 当数据发生变化后, 视图自动更新.
如何实现数据响应式?
实现数据响应式的核心在于监听数据的变化, 当数据发生变化后, 执行视图更新操作.
Vue3 使用代理对象监听数据变化.
创建对象的代理对象, 从而实现对对象操作的拦截和自定义.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| const person = { name: "张三", age: 20 };
const p = new Proxy(person, {});
console.log(p);
p.name = "李四";
console.log(person.name);
delete p.age;
console.log(person.age);
p.sex = "男";
console.log(person.sex);
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| const person = { name: "张三", age: 20, brand: { group: { title: "宝马" } }, };
const p = new Proxy(person, { get(target, property) { console.log("拦击到了获取操作"); return target[property]; }, set(target, property, value) { console.log("拦截到了设置或者新增操作"); target[property] = value; }, deleteProperty(target, property) { console.log("拦截到了删除操作"); return delete target[property]; }, });
console.log(p.brand.group.title);
console.log(person);
|
23. 双向数据绑定
23.1 实现表单双向数据绑定
1. 什么是双向数据绑定?
双向指的是视图(template)和逻辑(script), 双向数据绑定是指视图更新数据后自动同步到逻辑, 逻辑更新数据后自动同步到视图。
2. 如何实现双向数据绑定?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| <template> <input type="text" v-model="firstName" /> <button @click="onClickHandler">button</button> </template> <script> import { ref } from "vue"; export default { setup() { const firstName = ref("张三"); const onClickHandler = () => { firstName.value = "李四"; }; return { firstName, onClickHandler }; }, }; </script>
|
3. 如何监听双向数据绑定中数据的变化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| <template> <input type="text" v-model="firstName" @update:modelValue="onFirstNameChanged($event)" /> </template> <script> import { ref } from "vue"; export default { setup() { const firstName = ref("张三"); const onFirstNameChanged = (event) => { console.log(event); }; return { firstName, onFirstNameChanged }; }, }; </script>
|
23.2 实现组件双向数据绑定
1. 普通版
App.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| <template> <Test :firstName="firstName" @onFirstNameChanged="onFirstNameChanged($event)" /> <button @click="onClickHandler">我是App组件中的 button</button> </template> <script> import Test from "./components/Test.vue"; import { ref } from "vue"; export default { components: { Test }, name: "App", setup() { const firstName = ref("张三"); const onClickHandler = () => { firstName.value = "李四"; }; const onFirstNameChanged = (event) => { firstName.value = event; }; return { firstName, onClickHandler, onFirstNameChanged }; }, }; </script>
|
Test.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| <template> <div> {{ firstName }} <button @click="onClickHandler">我是Test组件中的button</button> </div> </template> <script> export default { props: ["firstName"], setup(props, { emit }) { const onClickHandler = () => { emit("onFirstNameChanged", "王五"); }; return { onClickHandler }; }, }; </script>
|
2. 升级版
App.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| <template> <Test v-model="firstName" /> <button @click="onClickHandler">我是App组件中的 button</button> </template> <script> import Test from "./components/Test.vue"; import { ref } from "vue"; export default { components: { Test }, name: "App", setup() { const firstName = ref("张三"); const onClickHandler = () => { firstName.value = "李四"; }; return { firstName, onClickHandler }; }, }; </script>
|
Test.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| <template> <div> {{ modelValue }} <button @click="onClickHandler">我是Test组件中的button</button> </div> </template> <script> export default { props: ["modelValue"], setup(props, { emit }) { const onClickHandler = () => { emit("update:modelValue", "王五"); }; return { onClickHandler }; }, }; </script>
|
3. 终极版
App.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| <template> <Test v-model:firstName="firstName" v-model:lastName="lastName" /> <button @click="onClickHandler">我是App组件中的 button</button> </template> <script> import Test from "./components/Test.vue"; import { ref } from "vue"; export default { components: { Test }, name: "App", setup() { const firstName = ref("张三"); const lastName = ref("李四"); const onClickHandler = () => { firstName.value = "孙悟空"; lastName.value = "猪八戒"; }; return { firstName, lastName, onClickHandler }; }, }; </script>
|
Test.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| <template> <div> {{ firstName }} {{ lastName }} <button @click="onClickHandler">我是Test组件中的button</button> </div> </template> <script> export default { props: ["firstName", "lastName"], setup(props, { emit }) { const onClickHandler = () => { emit("update:firstName", "刘备"); emit("update:lastName", "诸葛亮"); }; return { onClickHandler }; }, }; </script>
|
24. customRef
创建具有自定义行为的响应式数据, 通过拦截响应式数据的读取和设置实现。
防抖: 监听用户的连续操作, 最终只响应连续操作中的最后一次操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| <template> <input type="text" v-model="keyword" /> {{ keyword }} </template> <script> import { customRef } from "vue"; export default { name: "App", setup() { const keyword = useDebounceRef("Hello", 400); return { keyword }; }, };
function useDebounceRef(initialValue, delay) { let timer = null; return customRef((track, trigger) => { return { get() { // 跟踪 initialValue 值的变量 track(); return initialValue; }, set(newValue) { clearTimeout(timer); timer = setTimeout(() => { initialValue = newValue; // 触发视图更新 trigger(); }, delay); }, }; }); } </script>
|