本片文章将会每周不定期更新不定数量面试题,毕竟质量才是关键。
  实践是检验真理的唯一标准!看题不记题,看题要做题!
  面试是一个学习交流的过程,抱着虚心求教的心,人无完人,祝你变得更优秀!

1. Vue 中 Key 的理解

作用

  • key 是虚拟 DOM 中的唯一标识
    • 使 Diff 算法更快的找到对应的节点,从而提高 Diff 的运行效率
      • Vue 存在就地更新策略,默认是高效的,当该策略不产生副作用时,建议不要使用 key,这样可以取得更好的性能【只适用于不依赖子组件状态或临时 DOM 状态 (例如:表单输入值) 的列表渲染输出。】
      • 有相同父元素的子元素必须有独特的 key
    • 保证相同标签名元素的过度切换(使 Vue 可以区分,否则只会替换其内部属性而不会触发过渡效果)

场景

  • 完整地触发组件的生命周期钩子

    • 1
      2
      3
      4
      <!-- text 变化就会重新渲染,diff 依靠 key 识别是否为新元素 -->
      <transition>
      <span :key="text">{{ text }}</span>
      </transition>
  • 触发过渡效果

原理

  • 采用 Diff 算法
    • 通过 patchVnode 方法对比新旧 vnode(深度优先,同层比较)
    • 通过 updateChildren 方法更新(首尾交叉对比)

分析方法

  • 下载源码
  • 写个小例子打断点找到源码位置(比如写个列表)
  • 源码位置:src/core/vdompatch.js
    • patchVnode 函数(Diff 发生的地方)
    • updateChildren 函数

2. Vue 中 Diff 算法的理解

作用

  • Diff 算法是为虚拟 DOM 的框架而生的,可以精确定位所改变的元素使组件更新更加准确与快速
  • 源码 mountComponent 函数中,可以看出每挂载一个组件就会产生一个新 Watcher,为了保证 Vue 组件更新的准确性,采用 Diff 也就可以理解了
  • Diff 算法比较方式:深度优先,同层比较
  • 触发时机:数据响应式触发 setter => setter 会触发通知 => 通知的方式就是将 watcher 添加到异步更新队列 => 事件循环结束则清空队列(watcher 执行他们的更新函数) => 更新函数执行时调用组件渲染函数和组件的更新函数 => 重新渲染最新的 VDom => 组件实例执行更新函数
  • 打补丁:新旧 VDom 对比就叫做 patch

分析方法

  • 源码位置:src/instance/lifecycle.js
    • mountComponent 函数
      • 每个组件都有一个 watcher,为了精确的更新,Diff 算法就很有必要了
  • 源码位置:src/core/vdompatch.js
    • patchVnode 函数(深度优先,同层比较,Diff 发生的地方,源码见1)
    • updateChildren 函数(首位交叉对比,算法核心,源码见1)

3. MVC、MVP、MVVM 的理解

共同点:解决代码杂糅问题。
区别:MVP 是 MVC2.0,MVVM 是 MVC3.0。

  • Web 1.0 时代
    • 早期开发,前后端未分离。虽然开发快,但代码较混乱(jsp,php,.net)。业务逻辑越复杂,单文件代码量越大,项目越难维护;
    • MVC 的诞生就是解决这一些问题,将项目拆分成 Model、View 和 Controller 三大部分。
      • Model:保存应用数据,与后端数据进行同步;
      • View:将 Model 中的数据可视化,用户可在此进行操作;
      • Controler:业务逻辑,根据用户操作修改 Model 中的数据。
      • MVC 刚诞生时仅用于后端,比如 spring、Structs 等框架,前端只是 View 部分(前端输出静态页面交给后端,后端人员使用模板语法做整合)。
      • 优点
        • 低耦合
        • 可复用
        • 易维护
      • 缺点
        • 增加系统结构,实现较复杂,需要考虑代码功能划分,最好用于大点的项目,否则得不偿失;
        • View 与 Controller 联系过于紧密。拆分不够完全,很难独立复用;
        • View 对 Model 的低效率访问。Model 中接口不同,View 可能需要多次调用才能拿全数据;
  • Web 2.0 时代
    • Google 的 Gmail 出现,伴生的 ajax 可使前后台进行交互;
    • 前后端分离,前端专注 HTML、CSS、JS,后端专注数据服务,协同开发,效率提高;
    • 前端通过 ajax 拥有了使页面局部刷新的能力,减轻了后端的负载和流量消耗,用户体验更佳;
    • 专职前端出现,页面内容越来越多,前端代码量变大,项目难维护。
  • MVP 模式
    • Model:提供数据;
    • View:负责显示;
    • Presenter:负责 Model 与 View 之间的通信;
    • MVC 衍变而来。
    • 优点
      • View 与 Model 完全隔离;
      • Presenter 与 View 具体实现技术无关。
    • 缺点
      • 增加了代码复杂度
      • Presenter 中除了有 Controller 代码,还有大量 Model 与 View 的手动同步逻辑代码,难维护;
      • Presenter 与 View 的耦合度较高。
  • Web 3.0 时代
    • 前端 MVC 模式出现(将后端 MVC 中的 V 拆为前端的 MVC);
    • 前端 MVVM 模式出现(Angular、React、Vue)。
      • Model-View-ViewModel
      • 优点
        • 低耦合
        • 可重用
        • 独立开发
        • 可测试(测试 viewModel)

4. Vue 设计原则(理念)

  • 渐进式 JavaScript 框架;
    • 自底向上逐层应用(有着足够庞大的生态去应对多种需求,无论是简单的静态页,还是需要路由跳转,更或者全局状态管理都可以满足);
    • 核心库(DeclarativeRendering、ComponsentSystem)只关注视图层。
  • 易用:没有学习过 Vue,只要你是前端,看着开发文档就可以上手,学习曲线很平滑,模板语法太友善了;
  • 灵活:渐进式,可以是构建用户界面的 JavaScript 库,也可以是一套完整的框架;
  • 高效:Vue2 引入了虚拟 DOM 和 Diff 算法,Vue3 引入 Proxy 对数据响应式改进。

5. Vue 组件化的理解

定义

  • 将复杂系统解耦,拆分成多个功能模块,并根据业务进行重组复用;
  • 组件化其实就是一个做积木搭积木的过程;
  • Vue 的核心特性之一。

使用场景
页面元素多样,业务逻辑复杂,功能模块可拆分。

优点

  • 功能复用,协同开发,提高开发效率;
  • 单元测试;
  • 可针对指定组件进行更新,提高性能(lifecycle.js - mountComponent(),一个组件一个 watcher);
  • 扩展性高。

注意事项

  • 组件应该是高内聚、低耦合的(任性复用);
  • 合理的拆分组件才能将组件化的优点最大化;
  • 数据传递时尽量遵循单向数据流原则,避免关系混乱。

Vue 组件化特点

  • 声明组件:Vue.component();
  • 单文件组件(vue-loader 将 template 编译成 render 函数);
  • 组件配置 => VDOM => DOM;(通过 patch.js - createElm() 实例化及挂载)
  • 属性 prop;
  • 自定义事件;
  • 插槽;
  • 双向数据绑定。

6. Vue 组件之间的通讯方式

父子组件

  • 父组件向子组件传值
    • props
    • ref(直接访问 DOM 元素)
    • $children(做封装后使用,避免依赖 DOM)
  • 子组件向父组件传值
    • $emit / $on
    • $parent(做封装后使用,避免依赖 DOM)

兄弟组件

  • $parent
  • $root
  • eventbus
  • vuex

跨层级关系

  • eventbus
  • $attrs / $listeners(在子组件中处理非属性值的传递)
  • provide/inject(单向,祖辈向后代传值)
  • vuex

7. Vue 组件模板只能有一个根元素

  • 实例化 Vue 创建组件时,el 选项只能定一个根元素(入口);
  • 单文件组件为一个 Vue 实例,一个 Vue 实例只能定一个根元素;
  • Diff 算法比较时需要有相同的根。

8. Vue 组件中 data 选项必须为函数而根实例无此限制

  • Vue 组件
    • 官网有说明当在组件中使用 data property 的时候 (除了 new Vue 外的任何地方),它的值必须是返回一个对象的函数
    • Vue 组件中的 data 不是函数时无法通过 Vue 检测,提示需要将 data 改为函数;
    • Vue 组件可能存在多个实例;
      • 以对象形式定义 data 会导致它们公用一个 data 从而产生数据污染;
      • 以函数形式定义 data,由于函数的闭包特性,不会产生数据污染;
  • Vue 根实例
    • 根实例只有一个,不会产生数据污染,所以没限制,也不需要做这种限制。

9. v-if 和 v-for 优先级

  • 官网有说明v-forv-if具有更高的优先级;
  • 写个v-forv-if同级的情况,打印下app.$options.render,看下渲染函数;
  • 源码src/compiler/codegen/index.js genElement() 函数中判断条件for是高于if的;
  • 如果两个条件需要同时作用,可以先将v-if提到v-for的上层,减少每次循环都做判断导致的性能消耗;
  • 如果v-forv-if必须作用在一个标签上,那就应该考虑列表数组中数据的筛选了,v-for作用的应该是渲染数组(这么理解的话,就不会产生v-forv-if必须作用在一个标签上的情况)。

10. vue-router 中保护指定路由的安全

通过导航守卫(钩子函数)在路由跳转时先判断条件(是否登录,是否有权限)再决定是否跳转

  • 全局守卫
    • 触发时机:全局有导航发生时
    • 无法获取组件实例
    • 全局前置守卫(beforeEach(to, from, next))
      • to:目标路由
      • from:当前路由
      • next:高阶函数,处理传过来的参数,返回一个设置的 hook 来决定导航后续该怎么做
        • 不传参:正常放行
        • 传参
          • next(false):未登录、无权限时阻止导航
          • next(path):传递 path 字符串可以重定向到一个新地址
    • 全局解析守卫(beforeResolve(to, from, next))
    • 全局后置钩子(afterEach(to, from))
  • 路由独享守卫(beforeEnter(to, from, next))
    • 触发时机:发生与当前路由相关的导航时
    • 无法获取组件实例
  • 组件内的守卫
    • 触发时机:用到当前组件时
    • 可以获取组件实例
    • beforeRouteEnter(to, from, next)
      • 导航确认前调用
      • 不能获取组件实例 this
      • 组件实例还没被创建
    • beforeRouteUpdate(to, from, next)
      • 导航更新时调用(例如:复用组件时,路径由test/01跳转至test/02时,就会调用此函数)
    • beforeRouteLeave(to, from, next)
      • 导航离开前调用(例如:填完表单还未保存就离开,执行此函数让用户进行确认是否离开)

导航解析流程:

  1. 导航被触发。
  2. 在失活的组件里调用 beforeRouteLeave 守卫。
  3. 调用全局的 beforeEach 守卫。
  4. 在重用的组件里调用 beforeRouteUpdate 守卫 (2.2+)。
  5. 在路由配置里调用 beforeEnter。
  6. 解析异步路由组件。
  7. 在被激活的组件里调用 beforeRouteEnter。
  8. 调用全局的 beforeResolve 守卫 (2.5+)。
  9. 导航被确认。
  10. 调用全局的 afterEach 钩子。
  11. 触发 DOM 更新。
  12. 用创建好的实例调用 beforeRouteEnter 守卫中传给 next 的回调函数。

11. nextTick 的理解

定义
nextTick 是 Vue 的一个全局 API,由于 Vue 的异步更新策略导致我们对数据的直接修改不会立刻体现在 DOM 中,在这种情况下使用 nextTick 方法即可解决。

作用
由于 Vue 在更新 DOM 时是异步执行。只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到队列一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。nextTick 方法会在队列中加入一个回调函数,确保函数在前面的 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

<div id="example">{{message}}</div>
var vm = new Vue({
el: '#example',
data: {
message: '123'
}
})
vm.message = 'new message' // 更改数据
vm.$el.textContent === 'new message' // DOM 未更新
Vue.nextTick(function () {
vm.$el.textContent === 'new message' // DOM 更新
})

/*********************************************************/

// 组件内使用
Vue.component('example', {
template: '<span>{{ message }}</span>',
data: function () {
return {
message: '未更新'
}
},
methods: {
updateMessage: function () {
this.message = '已更新'
console.log(this.$el.textContent) // 未更新
this.$nextTick(function () {
console.log(this.$el.textContent) // 已更新
})
}
}
})

/********************************************************/

// 作为一个 Promise 使用 (2.1.0 起新增)
Vue.nextTick()
.then(function () {
// DOM 更新了
})

原理
将我们传入 nextTick 中的函数加入到 callbacks 里,然后使用 timerFunc 函数异步调用,Vue 在内部对异步队列尝试使用原生的 Promise.then、MutationObserver 和 setImmediate,如果执行环境不支持,则会采用 setTimeout(fn, 0) 代替。

12. Vue 的响应式

定义
监听数据变化(更改数据) 并 作出响应(更新 DOM)。

作用
MVVM 框架中解决的一个核心问题就是连接数据层和视图层,通过数据驱动应用,数据变化,视图更新,数据响应式处理就是为了解决这个问题。

原理

优缺点

  • 优点
    • 只操作数据,不用频繁的操作 DOM,既降低了开发难度,又提升了开发效率(Diff 算法)
  • 缺点
    • 初始化时递归遍历会造成性能损失

Vue3 中响应式的新变化

13. Vue 性能优化

路由懒加载

1
2
3
4
5
const router = new VueRouter({
routes: [
{path:'/foo', component: () => import('.Foo.vue')}
]
})

keep-alive缓存页面

1
2
3
4
5
6
7
<template>
<div id="app">
<keep-alive>
<router-view />
</keep-live>
</div>
</template>

合理的利用v-show替换v-if
v-show 的原理是使用 display 属性实现的隐藏显示,v-if 的原理是删除创建,所以你懂得~

v-for与v-if同级时
官方文档建议在同时需要使用 v-for 和 v-if 时,把 v-if 提到外层,避免每次循环都进行条件判断,但是有时业务逻辑就需要你在循环项做判断,我们可以使用 computed 对需遍历数组先过滤一遍。

长列表做虚拟滚动
使用 vue-virtual-scroller 对长列表进行区域渲染(DOM 复用,只对内容进行更新)。

1
2
3
4
5
6
7
8
9
10
11
12
<recycle-scroller
class="items"
:items="items"
:item-size="24"
>
<template v-slot="{items}">
<FetchItemView
:item="item"
@vote="voteItem(item)"
/>
</template>
</recycle-scroller>

订阅后要在组件销毁时取消订阅
Vue 组件销毁时,会自动解绑它的全部指令及事件监听器,但是仅限于组件本身的事件。

1
2
3
4
5
6
7
created() {
this.timer = setInterval(this.refresh, 2000)
},
beforeDestroy() {
// 取消订阅防止内存泄漏
clearInterval(this.timer)
}

图片懒加载
图片过多时,使用 vue-lazyload 仅加载当前可视区域的图片。

1
<img v-lazy="/static/img/1.png" />

第三方库做按需引入策略
项目开发中仅依赖第三方库中的部分组件或方法,按需引入避免体积过大。

1
import { Button, Select } from 'element-ui'

无状态组件标记为函数时组件

1
2
3
4
5
6
7
8
9
10
11
12
<template functional>
<div class="cell">
<div v-if="props.value" class="on"></div>
<section v-else class="off"></section>
</div>
</template>

<script>
export default {
props: ['value']
}
</script>

子组件分割
对组件内部渲染频繁的部分进行拆分,使其单独管理自身渲染,减少不必要的渲染。

变量本地化
频繁调用的数据存储为变量,减少引用次数。

1
2
3
4
5
6
7
8
const a = this.a;
let b;

for (let i=0; i<1000; i++) {
b = b + a
}

return b;

14. Vue3 新特性

  • 更快
    • 虚拟 DOM 重写
      • 组件快速路径(Component fast path)
      • 单个调用(Monomorphic calls)
      • 子节点类型检测(Children type detection)
    • 优化 slots 生成
    • 静态树提升
    • 静态属性提升
    • 基于 Proxy 的响应式系统
  • 更小
    • 通过摇树优化核心库体积
  • 更易维护
    • TypeScript + 模块化
  • 更加友好
    • 跨平台:编译器核心和运行时核心与平台无关,使 Vue 更容易在多平台使用
  • 更易使用
    • 改进对 TypeScript 的支持,编辑器可以提供更明确地类型检查和错误警告
    • 更好的调试支持
    • 独立的响应化模块
    • Composition API

理解才能更好的使用!