前言

本文整理了高频出现的 Vue 相关面试题并且附带详解答案 难度分为简单 中等 困难 三种类型;

简单

1. 为什么 data 是一个函数

组件中的 data 写成一个函数,数据以函数返回值形式定义,这样每复用一次组件,就会返回一份新的 data,类似于给每个组件实例创建一个私有的数据空间,让各个组件实例维护各自的数据。而单纯的写成对象形式,就使得所有组件实例共用了一份 data,就会造成一个变了全都会变的结果

2. Vue 组件通讯有哪几种方式

  • props 和$emit 父组件向子组件传递数据是通过 prop 传递的,子组件传递数据给父组件是通过$emit 触发事件来做到的

  • $parent,$children 获取当前组件的父组件和当前组件的子组件

  • $attrs 和$listeners A->B->C。Vue 2.4 开始提供了$attrs 和$listeners 来解决这个问题

  • 父组件中通过 provide 来提供变量,然后在子组件中通过 inject 来注入变量。(官方不推荐在实际业务中使用,但是写组件库时很常用)

  • $refs 获取组件实例

  • eventBus 兄弟组件数据传递 这种情况下可以使用事件总线的方式

  • vuex 状态管理

3. 谈谈对vue生命周期的理解?

每个Vue实例在创建时都会经过一系列的初始化过程,vue的生命周期钩子,就是说在达到某一阶段或条件时去触发的函数,目的就是为了完成一些动作或者事件

  • beforeCreate 在实例初始化之后,数据观测(data observer) 和 event/watcher 事件配置之前被调用。在当前阶段 data、methods、computed 以及 watch 上的数据和方法都不能被访问
  • created 实例已经创建完成之后被调用。在这一步,实例已完成以下的配置:数据观测(data observer),属性和方法的运算, watch/event 事件回调。这里没有$el,如果非要想与 Dom 进行交互,可以通过 vm.$nextTick 来访问 Dom
  • beforeMount 在挂载开始之前被调用:相关的 render 函数首次被调用。
  • mounted 在挂载完成后发生,在当前阶段,真实的 Dom 挂载完毕,数据完成双向绑定,可以访问到 Dom 节点
  • beforeUpdate 数据更新时调用,发生在虚拟 DOM 重新渲染和打补丁(patch)之前。可以在这个钩子中进一步地更改状态,这不会触发附加的重渲染过程
  • updated 发生在更新完成之后,当前阶段组件 Dom 已完成更新。要注意的是避免在此期间更改数据,因为这可能会导致无限循环的更新,该钩子在服务器端渲染期间不被调用。
  • beforeDestroy 实例销毁之前调用。在这一步,实例仍然完全可用。我们可以在这时进行善后收尾工作,比如清除计时器。
  • destroyed Vue 实例销毁后调用。调用后,Vue 实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。 该钩子在服务器端渲染期间不被调用。
  • activated keep-alive 专属,组件被激活时调用
  • deactivated keep-alive 专属,组件被销毁时调用

常见的数据请求在哪一步发起?

可以在钩子函数 created、beforeMount、mounted 中进行异步请求,因为在这三个钩子函数中,data 已经创建,可以将服务端端返回的数据进行赋值。
如果异步请求不需要依赖 Dom 推荐在 created 钩子函数中调用异步请求,因为在 created 钩子函数中调用异步请求有以下优点:
能更快获取到服务端数据,减少页面 loading 时间;
SSR 不支持 beforeMount 、mounted 钩子函数,所以放在 created 中有助于一致性;

4. v-if 和 v-show 的区别?

v-if 在编译过程中会被转化成三元表达式,条件不满足时不渲染此节点。
v-show 会被编译成指令,条件不满足时控制样式将对应节点隐藏 (display:none)

扩展补充:display:none、visibility: hidden 和 opacity: 0 之间的区别?
三者都是隐藏元素,从四个角度说明不同点:

  • 是否占据空间

display: none; 隐藏之后不占位置;visibility: hidden 和 opacity: 0 隐藏后会占据位置;

  • 子元素是否继承

display: none 不会被子元素继承,父元素隐藏后是从页面视图消失,所以子元素也跟着隐藏了;
visibility: hidden 会被继承,可以通过子元素 visibility: visible 来显示资源数;
opacity: 0 会被继承,但不能设置子元素来重新显示;

  • 事件绑定

display: none; 和 visibility:hidden 隐藏之后 不能触发它绑定的事件;
opacity: 0 元素上的事件是可以被触发的;

  • 过滤动画

display: none; 和 visibility:hidden 使用transition 是无效的;
opacity:0 使用transition 是有效的,类似过度效果;

5. computed 和 watch 的区别和运用的场景

watch 属性监听
是一个对象,键是需要观察的属性,值是对应回调函数,主要用来监听某些特定数据的变化,从而进行某些具体的业务逻辑操作,监听属性的变化,需要在数据变化时执行异步或开销较大的操作时使用
computed 计算属性
属性的结果会被缓存,当computed中的函数所依赖的属性没有发生改变的时候,那么调用当前函数的时候结果会从缓存中读取。除非依赖的响应式属性变化时才会重新计算,主要当做属性来使用computed中的函数必须用return返回最终的结果computed更高效,优先使用

使用场景

  • computed:当一个属性受多个属性影响的时候使用,例:购物车商品结算功能
  • watch:当一条数据影响多条数据的时候使用,例:搜索数据

6. v-if 与 v-for 为什么不建议一起使用

  • key的作用主要是为了更高效的对比虚拟DOM中每个节点是否是相同节点;
  • Vue在patch过程中判断两个节点是否是相同节点,key是一个必要条件,渲染一组列表时,key往往是唯一标识,所以如果不定义key的话,Vue只能认为比较的两个节点是同一个,哪怕它们实际上不是,这导致了频繁更新元素,使得整个patch过程比较低效,影响性能;
  • 从源码中可以知道,Vue判断两个节点是否相同时主要判断两者的key和元素类型等,因此如果不设置key,它的值就是undefined,则可能永 远认为这是两个相同的节点,只能去做更新操作,这造成了大量的dom更新操作,明显是不可取的。

中等

7. Vue2.0 双向绑定实现原理

当一个Vue实例创建时,Vue会遍历data选项的属性,用 Object.defineProperty 将它们转为 getter/setter并且在内部追踪相关依赖,在属性被访问和修改时通知变化。每个组件实例都有相应的 watcher 程序实例,它会在组件渲染的过程中把属性记录为依赖,之后当依赖项的 setter 被调用时,会通知 watcher重新计算,从而致使它关联的组件得以更新。

相关代码如下

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
40
41
42
43
44
class Observer {
// 观测值
constructor(value) {
this.walk(value);
}
walk(data) {
// 对象上的所有属性依次进行观测
let keys = Object.keys(data);
for (let i = 0; i < keys.length; i++) {
let key = keys[i];
let value = data[key];
defineReactive(data, key, value);
}
}
}
// Object.defineProperty数据劫持核心 兼容性在ie9以及以上
function defineReactive(data, key, value) {
observe(value); // 递归关键
// --如果value还是一个对象会继续走一遍odefineReactive 层层遍历一直到value不是对象才停止
// 思考?如果Vue数据嵌套层级过深 >>性能会受影响
Object.defineProperty(data, key, {
get() {
console.log("获取值");

//需要做依赖收集过程 这里代码没写出来
return value;
},
set(newValue) {
if (newValue === value) return;
console.log("设置值");
//需要做派发更新过程 这里代码没写出来
value = newValue;
},
});
}
export function observe(value) {
// 如果传过来的是对象或者数组 进行属性劫持
if (
Object.prototype.toString.call(value) === "[object Object]" ||
Array.isArray(value)
) {
return new Observer(value);
}
}

8. Vue3.0 和 2.0 的响应式原理区别

Vue3.x 改用 Proxy 替代 Object.defineProperty。因为 Proxy 可以直接监听对象和数组的变化,并且有多达 13 种拦截方法。

相关代码如下

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
40
41
42
43
44
45
46
47
import { mutableHandlers } from "./baseHandlers"; // 代理相关逻辑
import { isObject } from "./util"; // 工具方法

export function reactive(target) {
// 根据不同参数创建不同响应式对象
return createReactiveObject(target, mutableHandlers);
}
function createReactiveObject(target, baseHandler) {
if (!isObject(target)) {
return target;
}
const observed = new Proxy(target, baseHandler);
return observed;
}

const get = createGetter();
const set = createSetter();

function createGetter() {
return function get(target, key, receiver) {
// 对获取的值进行放射
const res = Reflect.get(target, key, receiver);
console.log("属性获取", key);
if (isObject(res)) {
// 如果获取的值是对象类型,则返回当前对象的代理对象
return reactive(res);
}
return res;
};
}
function createSetter() {
return function set(target, key, value, receiver) {
const oldValue = target[key];
const hadKey = hasOwn(target, key);
const result = Reflect.set(target, key, value, receiver);
if (!hadKey) {
console.log("属性新增", key, value);
} else if (hasChanged(value, oldValue)) {
console.log("属性值被修改", key, value);
}
return result;
};
}
export const mutableHandlers = {
get, // 当获取属性时调用此方法
set, // 当修改属性时调用此方法
};

9. v-model的实现以及它的实现原理吗?

  • vue中双向绑定是一个指令v-model,可以绑定一个动态值到视图,同时视图中变化能改变该值。v-model是语法糖,默认情况下相于:value和@input。
  • 使用v-model可以减少大量繁琐的事件处理代码,提高开发效率,代码可读性也更好
  • 通常在表单项上使用v-model
  • 原生的表单项可以直接使用v-model,自定义组件上如果要使用它需要在组件内绑定value并处理输入事件
  • 输出包含v-model模板的组件渲染函数,发现它会被转换为value属性的绑定以及一个事件监听,事件回调函数中会做相应变量更新操作,这说明神奇魔法实际上是vue的编译器完成的。

10.虚拟 DOM 是什么 有什么优缺点

由于在浏览器中操作 DOM 是很昂贵的。频繁的操作 DOM,会产生一定的性能问题。这就是虚拟 Dom 的产生原因。Vue2 的 Virtual DOM 借鉴了开源库 snabbdom 的实现。Virtual DOM 本质就是用一个原生的 JS 对象去描述一个 DOM 节点,是对真实 DOM 的一层抽象。

  • 优点:

    1. 保证性能下限: 框架的虚拟 DOM 需要适配任何上层 API 可能产生的操作,它的一些 DOM 操作的实现必须是普适的,所以它的性能并不是最优的;但是比起粗暴的 DOM 操作性能要好很多,因此框架的虚拟 DOM 至少可以保证在你不需要手动优化的情况下,依然可以提供还不错的性能,即保证性能的下限;

    2. 无需手动操作 DOM: 我们不再需要手动去操作 DOM,只需要写好 View-Model 的代码逻辑,框架会根据虚拟 DOM 和 数据双向绑定,帮我们以可预期的方式更新视图,极大提高我们的开发效率;

    3. 跨平台: 虚拟 DOM 本质上是 JavaScript 对象,而 DOM 与平台强相关,相比之下虚拟 DOM 可以进行更方便地跨平台操作,例如服务器渲染、weex 开发等等。

  • 缺点:

    1. 无法进行极致优化: 虽然虚拟 DOM + 合理的优化,足以应对绝大部分应用的性能需求,但在一些性能要求极高的应用中虚拟 DOM 无法进行针对性的极致优化。

    2. 首次渲染大量 DOM 时,由于多了一层虚拟 DOM 的计算,会比 innerHTML 插入慢。

11. 谈一下对 vuex 的个人理解

vuex 是专门为 vue 提供的全局状态管理系统,用于多个组件中数据共享、数据缓存等。(无法持久化、内部核心原理是通过创造一个全局实例 new Vue)
主要包括以下几个模块:

  • State:定义了应用状态的数据结构,可以在这里设置默认的初始状态。
  • Getter:允许组件从 Store 中获取数据,mapGetters 辅助函数仅仅是将 store 中的 getter 映射到局部计算属性。
  • Mutation:是唯一更改 store 中状态的方法,且必须是同步函数。
  • Action:用于提交 mutation,而不是直接变更状态,可以包含任意异步操作。
  • Module:允许将单一的 Store 拆分为多个 store 且同时保存在单一的状态树中。

12. Vuex 页面刷新数据丢失怎么解决

需要做 vuex 数据持久化 一般使用本地存储的方案来保存数据 可以自己设计存储方案 也直接将状态保存至 cookie 或者 localStorage 中

13. Vuex 为什么要分模块并且加命名空间

模块: 由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿。为了解决以上问题,Vuex 允许我们将 store 分割成模块(module)。每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块。

命名空间:默认情况下,模块内部的 action、mutation 和 getter 是注册在全局命名空间的——这样使得多个模块能够对同一 mutation 或 action 作出响应。如果希望你的模块具有更高的封装度和复用性,你可以通过添加 namespaced: true 的方式使其成为带命名空间的模块。当模块被注册后,它的所有 getter、action 及 mutation 都会自动根据模块注册的路径调整命名。

14. 路由原理 history 和 hash 两种路由方式的特点

hash 模式:

  • location.hash 的值实际就是 URL 中#后面的东西 它的特点在于:hash 虽然出现 URL 中,但不会被包含在 HTTP 请求中,对后端完全没有影响,因此改变 hash 不会重新加载页面。

  • 可以为 hash 的改变添加监听事件

    1
    window.addEventListener("hashchange", funcRef, false);

每一次改变 hash(window.location.hash),都会在浏览器的访问历史中增加一个记录利用 hash 的以上特点,就可以来实现前端路由“更新视图但不重新请求页面”的功能了

history 模式:
利用了 HTML5 History Interface 中新增的 pushState() 和 replaceState() 方法,来实现无需刷新页面就可以改变URL并且能够在浏览器历史记录中进行前进和后退操作, 在当前已有的 back、forward、go 的基础之上,它们提供了对历史记录进行修改的功能。这两个方法有个共同的特点:当调用他们修改浏览器历史记录栈后,虽然当前 URL 改变了,但浏览器不会刷新页面,这就为单页应用前端路由“更新视图但不重新请求页面”提供了基础, 但是history也是有缺点的,不怕前进后退跳转,就怕刷新(如果后端没有准备的话),因为刷新是实实在在地去请求服务器了, 需要正确的配置以避免出现404错误。。

abstract 模式
abstract 是vue路由中的第三种模式,本身是用来在不支持浏览器API的环境中,而不论是hash还是history模式都会对浏览器上的url产生作用,本文要实现的功能就是在已存在的路由页面中内嵌其他的路由页面,而保持在浏览器当中依旧显示当前页面的路由path,这就利用到了abstract这种与浏览器分离的路由模式。

15. Vue 修饰符有哪些 ?

事件修饰符:用于控制事件的行为和触发条件

  • .stop 阻止事件继续传播
  • .prevent 阻止标签默认行为
  • .capture 使用事件捕获模式,即元素自身触发的事件先在此处处理,然后才交由内部元素进行处理
  • .self 只当在 event.target 是当前元素自身时触发处理函数
  • .once 事件将只会触发一次
  • .passive 告诉浏览器你不想阻止事件的默认行为

v-model 的修饰符: 用于增强其功能和控制其行为

  • .lazy:将输入事件转换为 change 事件,即在元素失去焦点或按下回车键时更新绑定值。
  • .number:自动将用户的输入转换为数字类型。
  • .trim:自动过滤用户输入的首尾空白字符。

键盘事件的修饰符:键盘事件的修饰符用于监听特定的键盘事件

  • .enter:回车键。
  • .tab:Tab 键。
  • .delete:删除或后退键。
  • .esc:Esc 键。
  • .space:空格键。
  • .up:上箭头键。
  • .down:下箭头键。
  • .left:左箭头键。
  • .right:右箭头键。

系统修饰键:用于监听按下特定的系统键盘组合键事件

  • .ctrl:Ctrl 键。
  • .alt:Alt 键。
  • .shift:Shift 键。
  • .meta:Meta 键(Windows 上是 Windows 键,Mac 上是 Command 键)。

鼠标按钮修饰符: 鼠标按钮修饰符用于监听特定的鼠标按钮事件

  • .left:左键(主键)。
  • .right:右键。
  • .middle:中键。

困难

16. Vue.nextTick()原理

在 Vue.js 中,Vue.nextTick() 方法用于在 DOM 更新之后执行回调函数。通常情况下,当更新DOM时,Vue.js 会将DOM操作异步化处理,并且会在下一次事件循环中批量处理,以提高性能和效率。这意味着,在更新DOM后立即访问DOM节点可能会失败或返回不正确的结果。为了解决这个问题,可以使用 Vue.nextTick() 方法来确保DOM已经更新并准备好操作。

实际上,Vue.nextTick() 方法内部是通过创建一个异步任务队列来实现的。每当需要更新DOM时,Vue.js 会将更新操作插入到异步任务队列中,并且会在下一次事件循环中批量处理这些更新操作。而 Vue.nextTick() 方法就是等待所有异步任务完成后才执行回调函数的方法。

更具体地说,Vue.nextTick() 方法会检查当前是否存在微任务(Promise 或 MutationObserver),如果存在,则使用微任务来延迟回调函数执行;否则,会创建一个新的 Promise 对象,并使用它来延迟回调函数执行。由于 Promise 的 then() 方法会在下一次微任务中执行,因此在 Promise 执行完毕后,回调函数就会在下一个事件循环中被调用。

总之,Vue.nextTick() 方法是Vue.js用于在DOM更新后执行回调函数的一种机制,其内部使用异步任务队列和微任务来实现。通过调用 Vue.nextTick() 方法,可以确保DOM已经更新并准备好操作,从而避免了访问DOM节点时出现的问题。

相关代码:

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
40
41
42
43
44
45
46
47
48
49
50
51
// src/util/next-tick.js

let callbacks = [];
let pending = false;
function flushCallbacks() {
pending = false; //把标志还原为false
// 依次执行回调
for (let i = 0; i < callbacks.length; i++) {
callbacks[i]();
}
}
let timerFunc; //定义异步方法 采用优雅降级
if (typeof Promise !== "undefined") {
// 如果支持promise
const p = Promise.resolve();
timerFunc = () => {
p.then(flushCallbacks);
};
} else if (typeof MutationObserver !== "undefined") {
// MutationObserver 主要是监听dom变化 也是一个异步方法
let counter = 1;
const observer = new MutationObserver(flushCallbacks);
const textNode = document.createTextNode(String(counter));
observer.observe(textNode, {
characterData: true,
});
timerFunc = () => {
counter = (counter + 1) % 2;
textNode.data = String(counter);
};
} else if (typeof setImmediate !== "undefined") {
// 如果前面都不支持 判断setImmediate
timerFunc = () => {
setImmediate(flushCallbacks);
};
} else {
// 最后降级采用setTimeout
timerFunc = () => {
setTimeout(flushCallbacks, 0);
};
}

export function nextTick(cb) {
// 除了渲染watcher 还有用户自己手动调用的nextTick 一起被收集到数组
callbacks.push(cb);
if (!pending) {
// 如果多次调用nextTick 只会执行一次异步 等异步队列清空之后再把标志变为false
pending = true;
timerFunc();
}
}

17. Vue.set 方法原理

在 Vue.js 中,Vue.set() 方法用于向响应式对象中添加新的属性或更新现有属性。这个方法可以确保响应式对象中新增或修改的属性也是响应式的,从而能够触发视图的更新。

实际上,Vue.set() 方法内部会检测目标对象是否为响应式对象,并且会使用 Vue.js 内置的响应式系统来添加或更新属性。如果目标对象不是响应式对象,则会将其转换为响应式对象,然后再进行添加或更新操作。

接下来,Vue.js 会在目标对象上调用 Object.defineProperty() 方法,以设置新的属性或更新现有属性。在这个过程中,Vue.js 会利用 Object.defineProperty() 方法提供的属性描述符,来确保新的属性也是响应式的。

总之,Vue.set() 方法是Vue.js用于向响应式对象中添加新的属性或更新现有属性的一种机制。通过检测目标对象是否为响应式对象,并利用 Vue.js 内置的响应式系统来添加或更新属性,Vue.set() 方法确保了新的属性也是响应式的,从而能够触发视图的更新。

相关代码:

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
export function set(target: Array | Object, key: any, val: any): any {
// 如果是数组 调用我们重写的splice方法 (这样可以更新视图)
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.length = Math.max(target.length, key);
target.splice(key, 1, val);
return val;
}
// 如果是对象本身的属性,则直接添加即可
if (key in target && !(key in Object.prototype)) {
target[key] = val;
return val;
}
const ob = (target: any).__ob__;

// 如果不是响应式的也不需要将其定义成响应式属性
if (!ob) {
target[key] = val;
return val;
}
// 将属性定义成响应式的
defineReactive(ob.value, key, val);
// 通知视图更新
ob.dep.notify();
return val;
}

18. Vue 模板编译原理

Vue 的编译过程就是将 template 转化为 render 函数的过程 分为以下三步

  • 1.将模板解析成 AST(抽象语法树)。AST 是一个树形结构,用于表示模板的语法结构和逻辑关系。

  • 2.将 AST 转换成可执行的渲染函数。在这个过程中,会使用一些优化技巧来提高渲染函数的性能和效率。

  • 3.将渲染函数和响应式数据关联起来,使得当响应式数据发生变化时,Vue.js 可以自动更新视图。

具体来说,模板编译的原理如下:

解析模板:Vue.js 会使用正则表达式等方法来解析模板,并将其转换成 AST。在这个过程中,会识别出各种语法结构,例如指令、插值、属性绑定等。

静态分析:Vue.js 会对 AST 进行静态分析,以确定每个节点的依赖关系和属性。同时,Vue.js 会检查模板是否存在语法错误或潜在的运行时错误,并生成相应的警告或错误信息。

代码生成:根据 AST 生成可执行的渲染函数。在这个过程中,会使用一些优化技巧来提高渲染函数的性能和效率,例如缓存中间结果、避免不必要的计算等。

关联数据:将渲染函数和响应式数据关联起来。在这个过程中,Vue.js 会使用响应式系统来监听数据的变化,并根据需要更新视图。