目录
实验介绍
Vue 作为最受国内开发者喜爱的前端框架,同时也是面试中的重要考点。特别是 Vue3 版本的发布,一度让 Vue3 成为面试必考点,可见掌握好 Vue 框架是十分必要的。那面试中面试官都爱问哪些 Vue 相关的问题呢?下面我们来共同学习。
知识点
- Vue 生命周期
- v-for 中 key 的作用
- Vue 组件通讯
- Vue 中计算属性和监听器
- Vue 新增内容
Vue 生命周期
Vue 的生命周期都有哪些?你通常在哪个生命周期下进行网络请求?这基本上是面试过程中最常遇见的问题。如果面试官没有明确说 Vue 的哪个版本,那就意味着需要分版本作答,如下所示,
Vue2 生命周期:
- beforeCreate 数据为初始化前执行 created 数据和方法已初始化
- beforeMount 模板已经在内存中编译完成但是尚未渲染到页面上
- mounted 页面已渲染
- beforeUpdate 数据已经被更新,但是页面没被更新
- updated 页面和 data 都已更新
- beforeDestroy Vue 实例销毁之前执行
- destroyed 实例销毁执行
可以看到 Vue2 中的生命周期钩子函数还是比较多的。有的面试官会这样问:请你说说 Vue 在初始化时都触发哪些钩子函数?
这道题的关键点就是问的是在初始化时触发的哪些钩子,在 Vue 初始化的时候触发的钩子只有四个,如下所示:
- beforeCreate 数据为初始化前执行
- created 数据和方法已初始化
- beforeMount 模板已经在内存中编译完成但是尚未渲染到页面上
- mounted 页面已渲染
其他的钩子都不是在初始化的时候触发的。
Vue3 生命周期:
- setup 对应 Vue2 中的 beforeCreate 和 created
- onBeforeMount 对应 Vue2 中的 beforeMount
- onMounted 对应 Vue2 中的 mounted
- onBeforeUpdate 对应 Vue2 中的 beforeUpdate
- onUpdated 对应 Vue2 中的 updated
- onBeforeUnmount 对应 Vue2 中的 beforeDestroy
- onUnmounted 对应 Vue2 中的 destroyed
这些生命周期钩子注册函数只能在 setup() 期间同步使用, 因为它们依赖于内部的全局状态来定位当前组件实例(正在调用 setup() 的组件实例)如下所示:
import { onMounted, onUpdated, onUnmounted } from "vue";
const MyComponent = {
setup() {
onMounted(() => {
console.log("mounted!");
});
onUpdated(() => {
console.log("updated!");
});
onUnmounted(() => {
console.log("unmounted!");
});
},
};
v-for 中 key 的作用
在实际面试中,面试官会这样问:v-for 中 key 的作用是什么?如果不添加 key 程序会不会报错? 下面我们就带着问题具体学习。
为了搞清楚这个问题,我们需要先了解什么是虚拟 DOM 以及 diff 算法。
- 虚拟 DOM
虚拟 DOM 简称 VNode 其实是一棵以 JavaScript 对象作为基础的树, 是对真实 DOM 的抽象。虚拟 DOM 经过一系列转换可以变成真实 DOM 并渲染到页面上。
我们可以用虚拟 DOM 来描述一个简单的 vue 组件,如下所示:
<template>
<span class="demo" v-show="isShow"> This is a span. </span>
</template>
对应的 VNode
{
tag: 'span',
data: {
/* 指令集合数组 */
directives: [
{
/* v-show指令 */
rawName: 'v-show',
expression: 'isShow',
name: 'show',
value: true
}
],
/* 静态class */
staticClass: 'demo'
},
text: undefined,
children: [
/* 子节点是一个文本VNode节点 */
{
tag: undefined,
data: undefined,
text: 'This is a span.',
children: undefined
}
]
}
- diff 算法
假设现在我们已经有一棵 VNode 树,现在又有一棵 VNode 新树,现在我们需要把最新的这棵树更新上去要怎么操作呢?
最简单粗暴的方式就是直接拿最新的树把之前的树替换掉,然后页面重新渲染即可,这样确实可以解决问题,但是肯定不是最佳解决方案。我们肯定是想可不可以只更新那些变化的地方,那些没有变化的元素不更新,这样不就更高效吗?
要想实现这种高效的更新,就需要我们在两棵树中找出变化的地方从而进行更新。而 diff 算法就是专门来干这件事情的一种算法。
接下来我们再来说说 v-for
中的 key。首先我们先来看下不使用 key 的一段代码:
<template>
<ul>
<li v-for="v in arr">{{v}}</li>
</ul>
</template>
<script>
export default {
data() {
arr: ["A", "B", "C"];
},
create() {
setTimeout(() => {
this.arr.splice(1, 0, "D"); // [A,D,B,C]
}, 2000);
},
};
</script>
上面的代码很简单就是把一个数组 [A,B,C]
变成 [A,D,B,C]
同时页面也更新,diff 过程如下所示:
一共做了两次更新一次插入操作。下面我们再来看下有 key 的情况,如下所示:
<template>
<ul>
<li v-for="v in arr" :key="v">{{v}}</li>
</ul>
</template>
<script>
export default {
data() {
arr: ["A", "B", "C"];
},
create() {
setTimeout(() => {
this.arr.splice(1, 0, "D"); // [A,D,B,C]
}, 2000);
},
};
</script>
在有 key 的情况下,只是执行了一次插入操作。
所以我们可以知道,v-for
中 key
的作用就是让每个被循环元素有一个唯一的身份标识,这样 Vue 就可以更加精准的追踪到每个元素,从而更加高效的更新页面。当然如果没有 key
程序也不会报错,只不过此时的程序变得非常的“笨”。
Vue 组件通讯
这应该是 Vue 中必考的一道题,其重要性不言而喻。下面我们就来具体学习下 Vue 的通讯方式都有哪些。
Vue2 中组件通讯方式
- 父组件向子组件传递数
通过 props 传递即可
<!-- 父组件 -->
<template>
<!-- 传递数据 -->
<child-page :childData="childData"></child-page>
</template>
<script>
import childPage from "childPage.vue";
export default {
data() {
return {
childData: [1, 2, 3],
};
},
components: {
childPage,
},
};
</script>
<!-- 子组件 -->
<script>
export default {
props: {
// 接收数据
childData: {
type: Array,
default: () => [],
},
},
};
</script>
- 父组件调用子组件方法
- 设置子组件的
ref
属性 - 通过
this.$refs
进行触发
<template>
<div>
<!-- 设置ref -->
<child-page ref="child"></child-page>
<button @click="childFn">调用子组件事件</button>
</div>
</template>
<script>
import childPage from "./childPage.vue";
export default {
components: {
childPage,
},
methods: {
childFn() {
//调用子组件
this.$refs.child.sendMsg();
},
},
};
</script>
<!-- 子组件 -->
<template>
<div></div>
</template>
<script>
export default {
methods: {
sendMsg() {
console.log("调用子组件方法成功");
},
},
};
</script>
- 子组件向父组件传递数据
子组件向父组件传递参数只能通过触发父组件方法的形式去传递,如下所示:
<!--子组件 -->
<template>
<div>
<button @click="parentFn">向父组件传递数据</button>
</div>
</template>
<script>
export default {
methods: {
parentFn() {
this.$emit("parent", "is son");
},
},
};
</script>
<!--父组件 -->
<template>
<div>
<child-page @parent="parentchange"></child-page>
</div>
</template>
<script>
import childPage from "./childPage.vue";
export default {
components: {
childPage,
},
methods: {
parentchange(val) {
console.log("这是子组件传递过来的值:", val);
},
},
};
</script>
- 子组件调用父组件的方法
和子组件向父组件传递数据方式一样。
- 兄弟及跨组件传参
- provide / inject
这对选项需要一起使用,以允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在其上下游关系成立的时间里始终生效。 -vue 官方文档
这组 API 主要解决的是组件的跨级别通讯问题,同时也可以用于兄弟组件的通讯,数据流如下所示:
使用方式如下所示:
// 在父组件中定义
export default {
name: "App",
provide: {
userInfo: {
name: "蓝桥",
},
},
components: {
HelloWorld,
},
};
<!-- 在任意一个子组件(跨级组件)中使用 -->
<template>
<div>{{ userInfo.name }}</div>
</template>
<script>
export default {
inject: ["userInfo"],
};
</script>
- 使用
Provider/inject
只能是父组件派发数据,子组件接受数据。如果要想父子组件都可以派发数据那就需要使用 EventBus(事件总线)。原理就是在 Vue 的 prototype 上在 new 出来一个 vue,通过$emit
和$son
来进行数据通讯,如下所示:
// main.js
import Vue from "vue";
import App from "./App.vue";
Vue.config.productionTip = false;
// 设置EventBus
Vue.prototype.EventBus = new Vue();
new Vue({
render: (h) => h(App),
}).$mount("#app");
<!-- 兄弟组件1 -->
<template>
<div>
<button @click="siblingsFn">传递参数到兄弟组件</button>
</div>
</template>
<script>
export default {
methods: {
siblingsFn() {
this.EventBus.$emit("test", "ChildItem");
},
},
};
</script>
<!-- 兄弟组件2 -->
<template> </template>
<script>
export default {
created() {
// 数据监听
this.EventBus.$on("test", (val) => {
console.log(val);
});
},
};
</script>
3. Vuex
如果项目中有大量的数据需要跨组件进行通讯,那么我们最好使用专门的数据管理的库 Vuex 进行数据的管理,具体的使用方式我们可以在vuex 官网中进行学习。
Vue3 中组件通讯方式
Vue3 的通讯方式和 Vue2 类似,因为 component-api
没有了 this,所以在写法上有所区别。
- 父组件向子组件传递数据
<!-- 父组件 -->
<template>
<div>
<child-page :childData="childData"> </child-page>
</div>
</template>
<script>
import { ref, defineComponent } from "vue";
import childPage from "./childPage";
export default defineComponent({
components: {
childPage,
},
setup() {
const childData = ref([1, 2, 3]);
return {
childData,
};
},
});
</script>
<!-- 子组件 -->
<template>
<div v-for="v in childData" :key="v">{{ v }}</div>
</template>
<script>
import { defineComponent } from "vue";
export default defineComponent({
props: {
childData: {
type: Array,
default: () => [],
},
},
setup(props) {
console.log(props);
},
});
</script>
- 父组件调用子组件方法
还是通过 ref
来完成,具体代码如下所示:
<template>
<div>
<child-page ref="chilData"> </child-page>
<button @click="childFn">调用子组件里面的方法</button>
</div>
</template>
<script>
import { ref, defineComponent } from "vue";
import childPage from "./childPage";
export default defineComponent({
components: {
childPage,
},
setup() {
// 在使用ref定义变量的时候,把变量名称和上面的ret=“xxx”对应起来就可以获取到子组件的实例
const chilData = ref(null);
const childFn = () => {
chilData.value.myFn();
};
return {
chilData,
childFn,
};
},
});
</script>
<!-- 子组件 -->
<template><div></div> </template>
<script>
import { defineComponent } from "vue";
export default defineComponent({
setup() {
const myFn = () => {
console.log("子组件方法");
};
return {
myFn,
};
},
});
</script>
- 子组件向父组件传递数据
同样也是通过 emit
派发事件的方式进行传递
<!-- 子组件 -->
<template>
<div>
<button @click="parentFn">调用父组件方法</button>
</div>
</template>
<script>
import { defineComponent } from "vue";
export default defineComponent({
setup(props, { emit }) {
const parentFn = () => {
// 触发父组件中的方法
emit("par", "from parent ");
};
return {
parentFn,
};
},
});
</script>
- 子组件调用父组件的方法
同子组件向父组件传递数据方法一样。
- 兄弟及跨组件传参
- provide/inject
用于跨组件传递,使用上和 Vue2 一致。
- 事件总线(mitt)
作用和 Vue2 的 EventBus 一样。只不过现在需要借助于 mitt 插件来完成这件事,使用上如下所示:
安装 mitt
npm install mitt -S | yarn add miss -S
使用 mitt
需要配合 provide/inject 一块使用
<!-- app.vue -->
<template>
<img alt="Vue logo" src="./assets/logo.png" />
<HelloWorld msg="Welcome to Your Vue.js App" />
</template>
<script>
import HelloWorld from "./components/HelloWorld.vue";
import Mitt from "mitt";
const mitt = Mitt();
export default {
name: "App",
provide: {
mitt,
},
components: {
HelloWorld,
},
setup() {
mitt.emit("text", "蓝桥");
},
};
</script>
在任意一个子组件中
<template>
<div>childPage</div>
</template>
<script>
import { inject } from "vue";
export default {
setup() {
const mitt = inject("mitt");
mitt.on("text", (val) => {
console.log(val);
});
},
};
</script>
- vuex-next
vuex-next 是 Vue3 版本的 vuex 具体的使用方式可以看官方文档
- Pinia
Pinia 也是 Vue3 的状态管理框架,和 vuex-next 作用类似,但在使用上比 vuex-next 更加简洁,且 Pinia 对 TypeScript 项目更加友好,我们可以通过 Pinia 官网进行学习。
计算属性和监听器
也就是我们常用的 computed
和 watch
,主要有以下区别:
computed
- 支持缓存,只有依赖数据发生改变,才会重新进行计算
- 不支持异步,当
computed
内有异步操作时无效,无法监听数据的变化 - 如果一个属性是由其他属性计算而来的,这个属性依赖其他属性,是一个多对一或者一对一,一般用
computed
watch
- 不支持缓存,数据变,直接会触发相应的操作
- watch 支持异步
- 监听数据必须是 data 中声明过或者父组件传递过来的 props 中的数据,当数据变化时,触发其他操作
在 Vue2 和 Vue3 中的使用方式有所不同,如下所示:
Vue2 中使用 computed 和 watch
<template>
<div>
<input type="text" v-model="name" />
<div>{{ myName }}</div>
</div>
</template>
<script>
export default {
data() {
return {
name: "",
};
},
computed: {
myName() {
return `我的名字是${this.name}`;
},
},
watch: {
name(val) {
if (val.length > 4) alert("您输入的名字过长");
},
},
};
</script>
Vue3 中使用 computed 和 watch
<template>
<div>
<input type="text" v-model="name" />
<div>{{ myName }}</div>
</div>
</template>
<script>
import { ref, watch } from "vue";
export default {
setup() {
const name = ref("");
const myName = computed(() => `我的名字是${name.value}`);
watch(
() => name,
() => {
if (val.length > 3) alert("您输入的名字过长");
}
);
return {
name,
myName,
};
},
};
</script>
Vue3 新特性
Vue3 势必会成为 Vue 开发的主流版本,在实际开发过程中也有很多项目都在向 Vue3 进行过渡,所以尽早的去了解及使用 Vue3 在面试中就越有优势。接下来我们就来具体学习下 Vue3 中都有哪些值得学习的新特性。
- component-api
component-api
主要是为了解决组件逻辑之间的复用问题,也是 Vue3 最重要的新特性。 在 Vue2 中处理逻辑之间复用关系最常用的就是混入(Mixin),但是使用 Mixin
有以下弊端:
- 命名空间冲突;
- 如果一个组件使用多个 mixin 的模板时,很难看出某个数据是从哪一个 mixin 中注入的。
而使用 component-api
语法去组织代码就会让逻辑的复用变得简单高效,比如现在封装一段登陆逻辑,使用 component-api
我们可以这样组织。
// useLogin.js
import { ref } from "vue";
import axios from "axios";
const isLogin = ref(false);
const token = ref("");
async function UseisLoading() {
const res = axios.get("/api/login");
const { code, token } = res.data;
if (code === 200) {
isLogin.value = true;
token.value = tokens;
}
return {
isLogin,
token,
};
}
export default UseisLoading;
在业务中这样使用
import useLogin from "./useLogin.vue";
const { isLogin, token } = useLogin();
if (isLogin) {
console.log(`登陆成功,登陆token为${token}`);
}
这样我们就封装了一个简单的业务逻辑。
- 生命周期的变化
在 Vue2 中生命周期如下所示:
Vue3 中对这些生命周期钩子函数做了如下修改,细节如下:
值得注意的是 beforeCreate
和 created
这两个钩子函数在 Vue3 中被合并成了 setup
- Teleport
Teleport 是一种能够将我们的模板移动到 DOM 中 Vue app 之外的其他位置。比如我们现在嵌套了很多层组件,这个时候需要最里面的组件弹出一个全局的弹框,这个时候可能就会出现弹窗被遮挡的问题,而 Teleport 可以让我们的弹框挂载到任意的 DOM 元素上,这样就从根本上解决了这个问题。使用方式如下所示:
<teleport to="#targetId">你的弹窗组件</teleport>
- 根结点
在 2.x 中,不支持多根组件,当用户意外创建多根组件时会发出警告,因此,为了修复此错误,许多组件被包装在一个 <div>
中,如下所示:
<template>
<div>
<header>...</header>
<main>...</main>
<footer>...</footer>
</div>
</template>
在 3.x 中,组件可以有多个根节点,如下所示:
<template>
<header>...</header>
<main v-bind="$attrs">...</main>
<footer>...</footer>
</template>
<script setup>
及<style> v-bind
在 Vue3.2 版本中 setup
可以直接写在 script
标签上面,而且在 style
中可以用 v-bind
方法去使用 script
中的变量, 如下所示:
<script setup>
import { ref } from "vue";
const color = ref("red");
</script>
<template>
<button @click="color = color === 'red' ? 'green' : 'red'">
Color is: {{ color }}
</button>
</template>
<style scoped>
button {
color: v-bind(color);
}
</style>
这样以来很大程度上减少了我们组件编写的代码量。
以上是 Vue3 新增部分,更多细节上的改变就不再这里赘述了,可以看官方文档进行更加细致的学习
实验总结
本节实验我们学习了 vue 的常考点,当然 vue 作为一个框架而言里面的知识点也非常多,并不是说在本节实验中没有提到的就不考,在平时的工作和学习中我们要注意知识的积累,逐渐去完善自己关于 vue 方面的知识体系。