前端面试-Vue

2021/5/10 InterviewVue

# 说说你对vue的理解

参考文章:面试官:说说你对vue的理解? (opens new window)

Vue三要素

  • 响应式:例如如何监听数据变化,其中的实现方法就是我们提到的双向绑定

  • 模板引擎:如何解析模板

  • 渲染:Vue如何将监听到的数据变化和解析后的HTML进行渲染

  • 数据驱动(MVVM

    MVVM表示的是 Model-View-ViewModel

    • Model:模型层,负责处理业务逻辑以及和服务器端进行交互
    • View:视图层:负责将数据模型转化为UI展示出来,可以简单的理解为HTML页面
    • ViewModel:视图模型层,用来连接 ModelView ,是 ModelView 之间的通信桥梁,Vue里的data部分就是ViewModel
  • 组件化

  • 指令系统,当表达式的值改变时,将其产生的连带影响,响应式地作用于 DOM,v-if, v-for, v-show, v-bind, v-on, v-model

# 说说你对SPA(单页应用)的理解

参考文章:面试官:说说你对SPA(单页应用)的理解? (opens new window)

单页面应用(SPA) 多页面应用(MPA)
组成 一个主页面和多个页面片段 多个主页面
刷新方式 局部刷新 整页刷新
url模式 哈希模式 历史模式
SEO搜索引擎优化 难实现,可使用SSR方式改善 容易实现
数据传递 容易 通过url、cookie、localStorage等传递
页面切换 速度快,用户体验良好 切换加载资源,速度慢,用户体验差
维护成本 相对容易 相对复杂

单页应用优缺点

优点:

  • 具有桌面应用的即时性、网站的可移植性和可访问性
  • 用户体验好、快,内容的改变不需要重新加载整个页面,避免了不必要的跳转和重复渲染
  • 基于上面一点,SPA相对对服务器压力小
  • 良好的前后端分离,分工更明确,前端进行交互逻辑,后端负责数据处理

缺点:

  • 不利于搜索引擎的抓取
  • 首次渲染速度相对较慢,初次加载耗时多:为实现单页Web应用功能及显示效果,需要在加载页面的时候将JavaScript、CSS统一加载,部分页面按需加载
  • 前进后退路由管理:由于单页应用在一个页面中显示所有的内容,所以不能使用浏览器的前进后退功能,所有的页面切换需要自己建立堆栈管理

Vue做SEO的三种方式:

  1. SSR服务端渲染

    将组件或页面通过服务器生成html,再返回给浏览器,如 nuxt.js

  2. 静态化

    目前主流的静态化主要有两种:

    • 一种是通过程序将动态页面抓取并保存为静态页面,这样的页面的实际存在于服务器的硬盘中
    • 另外一种是通过WEB服务器的 URL Rewrite 的方式,它的原理是通过web服务器内部模块按一定规则将外部的URL请求转化为内部的文件地址,一句话来说就是把外部请求的静态地址转化为实际的动态页面地址,而静态页面实际是不存在的。这两种方法都达到了实现URL静态化的效果
  3. 使用 Phantomjs 针对爬虫处理

    原理是通过 Nginx 配置,判断访问来源是否为爬虫,如果是则搜索引擎的爬虫请求会转发到一个 node server ,再通过 PhantomJS 来解析完整的HTML,返回给爬虫。

# 说说你对双向绑定的理解

参考文章:面试官:说说你对双向绑定的理解? (opens new window)

  1. new Vue()首先执行初始化,对data执行响应化处理,这个过程发生Observer
  2. 同时对模板执行编译,找到其中动态绑定的数据,从data中获取并初始化视图,这个过程发生在Compile
  3. 同时定义一个更新函数和Watcher,将来对应数据变化时Watcher会调用更新函数
  4. 由于data的某个key在一个视图中可能出现多次,所以每个key都需要一个管家Dep来管理多个Watcher
  5. 将来data中数据一旦发生变化,会首先找到对应的Dep,通知所有Watcher执行更新函数

数据双向绑定的详细步骤:

  1. 实现一个监听器Observer:对数据对象进行遍历,包括子属性对象的属性,利用Object.defineProperty()对属性都加上settergetter。这样的话,给这个对象的某个值赋值,就会触发setter,那么就能监听到了数据变化。
  2. 实现一个解析器Compile:解析Vue模板指令,将模板中的变量都替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,调用更新函数进行数据更新。
  3. 实现一个订阅者WatcherWatcher订阅者是ObserverCompile之间通信的桥梁,主要的任务是订阅Observer中的属性值变化的消息,当收到属性值变化的消息时,触发解析器Compile中对应的更新函数。
  4. 实现一个订阅器Dep:订阅器采用发布-订阅设计模式,用来收集订阅者Watcher,对监听器Observer和订阅者Watcher进行统一管理。
image-default

# 说说你对Vue生命周期的理解

参考文章:对Vue生命周期的理解?

# Vue组件间通信方式都有哪些

参考文章:面试官:Vue组件间通信方式都有哪些? (opens new window)

组件间通信的分类可以分成以下

  • 父子组件之间的通信
  • 兄弟组件之间的通信
  • 祖孙与后代组件之间的通信
  • 非关系组件间之间的通信

Vue 中8种常规的通信方案

  1. 通过 props 传递
  2. 通过 $emit 触发自定义事件
  3. 使用 ref + $refs
  4. EventBus:一个无DOM的空组件,通过$emit$on处理事件
  5. $parent$root$children
  6. $attrs$listeners
  7. provideinject
  8. Vuex

注意事项:

  1. 使用vm.$parentvm.$children直接去修改父级或者子级的数据是可以的,但是会有一个问题,修改子级数据,但是子级的数组顺序是不能保证的,有可能造成操作失败。如果是使用vm.$parent去修改父级数据,那么只要这个组件引用的地方都会去修改父级,有的组件不需要修改但是被修改了,甚至有的修改会造成程序出错。

$root$parent$children$refs的使用:

  • vm.$root:当前组件树的根Vue实例。如果当前实例没有父实例,此实例将会是其自己。一般就是App.vue组件。

  • vm.$parent:当前组件的父实例,如果当前实例有的话。

  • vm.$children:当前实例的直接子组件。需要注意$children并不保证顺序,也不是响应式的。如果你发现自己正在尝试使用$children来进行数据绑定,考虑使用一个数组配合v-for来生成子组件,并且使用Array作为真正的来源。

  • vm.$refs:一个对象,持有注册过ref attribute 的所有DOM元素和组件实例。它不是响应式的。这仅作为一个用于直接操作子组件的“逃生舱”——你应该避免在模板进行数据绑定或计算属性中访问$refs

    • 如果有多个相同的ref,如果这些相同的ref指向的组件或者DOM是嵌套的层级关系,this.$refs[ref名称]指向的是最外层那个。如果不是嵌套关系,指向的是文档流中位置靠下的那个。
    • 如果ref使用在v-for中,当循环内的ref值不同时,需提供this.$refs[ref名称][0]来获取该DOM节点或组件实例;当ref值相同时,this.$refs[ref名称]获取到的是该DOM节点或组件的数组。

# 为什么不用$root而要使用vuex呢

vue框架中组件可以通过this.$root.data的方式访问根组件的data对象,为何不直接把全局状态放在这里,反而使用vuex呢?

  1. 追踪变化,主要是方便开发调试
  2. 避免污染,或随意修改(类似angularjs里面的$rootscope)
  3. vuex体现了代码分层的思想:this.$root更像是一个数据点,所有公共数据都流向了这个点,即与根组件耦合,又违背了接口隔离原则。而有了vuex这一层,我们就可以专注于这类数据的操作了,例如:可以针对不同的组件群创建不同的公共数据容器,这样明显合理多了
  4. vuex“通过定义和隔离状态管理中的各种概念并通过强制规则维持视图和状态间的独立性,我们的代码将会变得更结构化且易维护”,典型的如:要修改数据,必须通过提交mutation的方式,而不是随意改变这种多方依赖的公共数据
  5. 稍微大型的项目会遇到问题,无法回溯,无法利用中间件记录变更日志、调试等,而数据回溯、监听中间件等功能是必须要有的。
  6. vuex背后反映的一些单向数据流,分层比如module,如果需要还是得去root里面去自己实现。

# Vue中的v-show和v-if怎么理解

参考文章:面试官:Vue中的v-show和v-if怎么理解? (opens new window)

  • 控制手段:v-show隐藏则是为该元素添加css--display:nonedom元素依旧还在。v-if显示隐藏是将dom元素整个添加或删除

  • 编译过程:v-if切换有一个局部编译/卸载的过程,切换过程中合适地销毁和重建内部的事件监听和子组件;v-show只是简单的基于css切换

  • 编译条件:v-if是真正的条件渲染,它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建。只有渲染条件为假时,并不做操作,直到为真才渲染

    • v-showfalse变为true的时候不会触发组件的生命周期
    • v-iffalse变为true的时候,触发组件的beforeCreatecreatebeforeMountmounted钩子,由true变为false的时候触发组件的beforeDestroydestroyed方法
  • 性能消耗:v-if有更高的切换消耗;v-show有更高的初始渲染消耗

# 为什么Vue中的v-if和v-for不建议一起用

参考文章:面试官:为什么Vue中的v-if和v-for不建议一起用? (opens new window)

v-for 优先级比 v-if

  • 永远不要把 v-ifv-for 同时用在同一个元素上,带来性能方面的浪费(每次渲染都会先循环再进行条件判断)
  • 如果避免出现这种情况,则在外层嵌套 template (页面渲染不生成dom节点),在这一层进行v-if判断,然后在内部进行v-for循环
  • 如果条件出现在循环内部,可通过计算属性 computed 提前过滤掉那些不需要显示的项

# SPA首屏加载速度慢的怎么解决

参考文章:面试官:SPA(单页应用)首屏加载速度慢怎么解决? (opens new window)

性能指标

  • FP(首次绘制)
  • FCP(首次内容绘制 First contentful paint)
  • LCP(最大内容绘制时间 Largest contentful paint)
  • FPS(每秒传输帧数)
  • TTI(页面可交互时间 Time to Interactive)
// 通过DOMContentLoad或者performance来计算出首屏时间
// 方案一:
document.addEventListener('DOMContentLoaded', (event) => {
  console.log('first contentful painting');
});
// 方案二:
performance.getEntriesByName('first-contentful-paint')[0].startTime;
// performance.getEntriesByName("first-contentful-paint")[0]
// 会返回一个 PerformancePaintTiming 的实例,结构如下:
// {
//   name: "first-contentful-paint",
//   entryType: "paint",
//   startTime: 507.80000002123415,
//   duration: 0,
// };
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

加载慢的原因

  • 网络延时问题
  • 资源文件体积是否过大
  • 资源是否重复发送请求去加载了
  • 加载脚本的时候,渲染内容堵塞了

常见的几种SPA首屏优化方式

  • 减小入口文件积
  • 静态资源本地缓存
  • UI框架按需加载
  • 图片资源的压缩
  • 组件重复打包
  • 开启GZip压缩
  • 使用SSR
image-default

# Vue中组件和插件有什么区别

参考文章:面试官:Vue中组件和插件有什么区别? (opens new window)

对组件的定义:

组件就是把图形、非图形的各种逻辑均抽象为一个统一的概念(组件)来实现开发的模式,在 Vue 中每一个 .vue 文件都可以视为一个组件

组件的优势

  • 降低整个系统的耦合度,在保持接口不变的情况下,我们可以替换不同的组件快速完成需求,例如输入框,可以替换为日历、时间、范围等组件作具体的实现
  • 调试方便,由于整个系统是通过组件组合起来的,在出现问题的时候,可以用排除法直接移除组件,或者根据报错的组件快速定位问题,之所以能够快速定位,是因为每个组件之间低耦合,职责单一,所以逻辑会比分析整个系统要简单
  • 提高可维护性,由于每个组件的职责单一,并且组件在系统中是被复用的,所以对代码进行优化可获得系统的整体升级

插件是什么

插件通常用来为 Vue 添加全局功能。插件的功能范围没有严格的限制,一般有下面几种:

  • 添加全局方法或者属性。如: vue-custom-element
  • 添加全局资源:指令/过滤器/过渡等。如 vue-touch
  • 通过全局混入来添加一些组件选项。如 vue-router
  • 添加 Vue 实例方法,通过把它们添加到 Vue.prototype 上实现。
  • 一个库,提供自己的 API,同时提供上面提到的一个或多个功能。如 vue-router

# 为什么data属性是一个函数而不是一个对象

参考文章:面试官:为什么data属性是一个函数而不是一个对象? (opens new window)

  • 根实例对象 data 可以是对象也可以是函数(根实例是单例),不会产生数据污染情况
  • 组件实例对象 data 必须为函数,目的是为了防止多个组件实例对象之间共用一个 data ,产生数据污染。采用函数的形式, initData 时会将其作为工厂函数都会返回全新 data 对象

# Vue中给对象添加新属性界面不刷新

参考文章:面试官:Vue中给对象添加新属性界面不刷新? (opens new window)

Vue 不允许在已经创建的实例上动态添加新的响应式属性

若想实现数据与视图同步更新,可采取下面三种解决方案:

  • Vue.set(target, propertyName/index, value)

    别名:vm.$set(),参数一样

    • {Object | Array} target
    • {string | number} propertyName/index
    • {any} value
  • Object.assign()

    直接使用 Object.assign() 添加到对象的新属性不会触发更新,应创建一个新的对象,合并原对象和混入对象的属性

    this.someObject = Object.assign({}, this.someObject, {newProperty1:1, newProperty2:2 ...})

  • vm.$forcecUpdated()

    迫使 Vue 实例重新渲染。注意它仅仅影响实例本身和插入插槽内容的子组件,而不是所有子组件。

P.S.: vue3 是用过 proxy 实现数据响应式的,直接动态添加新属性仍可以实现数据响应式

# Vue实例挂载的过程中发生了什么

参考文章:面试官:Vue实例挂载的过程中发生了什么? (opens new window)

  • new Vue 的时候调用会调用 _init 方法

    • 定义 $set$get$delete$watch 等方法
    • 定义 $on$off$emit$off 等事件
    • 定义 _update$forceUpdate$destroy 生命周期
    • 事件监听(initEvents(vm)) -> 渲染方法(initRender(vm))-> initInjections(vm) -> initState(vm) (props -> methods -> data -> computed -> watch) -> initProvide(vm),这些处理完成才会触发 created 方法
  • 调用 $mount 进行页面的挂载

  • 挂载的时候主要是通过 mountComponent 方法

  • 定义 updateComponent 更新函数

  • 执行 render 生成虚拟 DOM

  • _update 将虚拟 DOM 生成真实 DOM 结构,并且渲染到页面中

# Vue中的$nextTick有什么作用

参考文章:面试官:Vue中的$nextTick怎么理解? (opens new window)

nextTick 是什么

在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM

  • Vue.nextTick( [callback, context] )
  • vm.$nextTick( [callback] )

vm.$nextTick() 跟全局方法 Vue.nextTick 一样,不同的是回调的 this 自动绑定到调用它的实例上。

Vue 在更新 DOM 时是异步执行的。当数据发生变化, Vue 将开启一个异步更新队列,视图需要等队列中所有数据变化完成之后,再统一进行更新

2.1.0 起新增:如果没有提供回调且在支持 Promise 的环境中,则返回一个 Promise

原理

  • 把回调函数放入 callbacks (异步操作队列)等待执行
  • 将执行函数放到微任务或者宏任务中( Promise.thenMutationObserversetImmediatesetTimeout )
  • 事件循环到了微任务或者宏任务,执行函数依次执行 callbacks 中的回调

# 说说你对vue的mixin的理解

参考文章:Vue的mixin的理解

mixin(混入),提供了一种非常灵活的方式,来分发 Vue 组件中的可复用功能。

本质其实就是一个 js 对象,它可以包含我们组件中任意功能选项,如 datacomponentsmethodscreatedcomputed 等等

我们只要将共用的功能以对象的方式传入 mixins 选项中,当组件使用 mixins 对象时所有 mixins 对象的选项都将被混入该组件本身的选项中来

在 Vue 中我们可以局部混入跟全局混入

使用全局混入需要特别注意,因为它会影响到每一个组件实例(包括第三方组件)

当组件存在与 mixin 对象相同的选项的时候,进行递归合并的时候组件的选项会覆盖 mixin 的选项

但是如果相同选项为生命周期钩子的时候,会合并成一个数组,先执行 mixin 的钩子,再执行组件的钩子

# 说说你对slot的理解slot使用场景有哪些

参考文章:面试官:说说你对slot的理解?slot使用场景有哪些? (opens new window)

slot可以分来以下三种:

  • 默认插槽

  • 具名插槽

  • 作用域插槽

  • v-slot 属性只能在 <template> 上使用,但在只有默认插槽时可以在组件标签上使用

  • 默认插槽名为 default ,可以省略 default 直接写 v-slot

  • 缩写为 # 时不能不写参数,写成 #default

  • 可以通过解构获取 v-slot={user} ,还可以重命名 v-slot="{user: newName}" 和定义默认值 v-slot="{user = '默认值'}"

# Vue.observable你有了解过吗说说看

参考文章:面试官:说说对observable的理解 (opens new window)

Observable 翻译过来我们可以理解成可观察的

Vue.observable,让一个对象变成响应式数据。Vue 内部会用它来处理 data 函数返回的对象

返回的对象可以直接用于渲染函数和计算属性内,并且会在发生变更时触发相应的更新。也可以作为最小化的跨组件状态存储器

Vue 2.x 中,被传入的对象会直接被 Vue.observable 变更,它和被返回的对象是同一个对象

Vue 3.x 中,则会返回一个可响应的代理,而对源对象直接进行变更仍然是不可响应的

import Vue from 'vue';
// 创建state对象,使用observable让state对象可响应
export const state = Vue.observable({
  name: '张三',
  age: 38,
});
// 创建对应的方法
export const mutations = {
  changeName(name) {
    state.name = name;
  },
  setAge(age) {
    state.age = age;
  },
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<template>
  <div>
    姓名:{{ name }}
    年龄:{{ age }}
    <button @click="changeName('李四')">
      改变姓名
    </button>
    <button @click="setAge(18)">
      改变年龄
    </button>
  </div>
</template>

<script>
import { state, mutations } from './store';

export default {
  // 在计算属性中拿到值
  computed: {
    name() {
      return state.name;
    },
    age() {
      return state.age;
    },
  },
  // 调用mutations里面的方法,更新数据
  methods: {
    changeName: mutations.changeName,
    setAge: mutations.setAge,
  },
};
</script>
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

# 你知道vue中key的原理吗

参考文章:面试官:说说为什么要在列表组件中写 key,其作用是什么? (opens new window)

两个实际工作场景

  1. 当我们在使用v-for时,需要给单元加上key

    <ul>
      <li v-for="item in items" :key="item.id">...</li>
    </ul>
    
    1
    2
    3
  2. +new Date()生成的时间戳作为key,手动强制触发重新渲染

    <Comp :key="+new Date()" />
    
    1

key是给每一个vnode的唯一id,也是diff的一种优化策略,可以根据key,更准确,更快的找到对应的vnode节点

源码地址:vue/src/core/vdom/patch.js

Vue中key的作用是:key是为Vue中vnode的唯一标记,通过这个key,我们的diff操作可以更准确、更快速。

  • 更准确:因为带key就不是就地复用了,在sameNode函数a.key === b.key对比中可以避免就地复用的情况。所以会更加准确。
  • 更快速:利用key的唯一性生成map对象来获取对应节点,比遍历方式更快。

当我们在使用v-for时,需要给单元加上key

  • 如果不用key,Vue会采用就地复地原则:最小化element的移动,并且会尝试尽最大程度在同适当的地方对相同类型的element,做patch或者reuse
  • 如果使用了key,Vue会根据keys的顺序记录element,曾经拥有了keyelement如果不再出现的话,会被直接remove或者destoryed

+new Date()生成的时间戳作为key,手动强制触发重新渲染

  • 当拥有新值的rerender作为key时,拥有了新keyComp出现了,那么旧keyComp会被移除,新keyComp触发渲染。

设置key值不一定能提高diff效率:

Vue.jsv-for正在更新已渲染过的元素列表时,它默认用“就地复用”策略。如果数据项的顺序被改变,Vue将不会移动DOM元素来匹配数据项的顺序,而是简单复用此处每个元素,并且确保它在特定索引下显示已被渲染过的每个元素

这个默认的模式是高效的,但是只适用于不依赖子组件状态或临时DOM状态(例如:表单输入值)的列表渲染输出。建议尽可能在使用v-for时提供key,除非遍历输出的DOM内容非常简单,或者是刻意依赖默认行为以获取性能上的提升。

# 说说你对keep-alive的理解

参考文章:面试官:说说你对keep-alive的理解是什么?怎么缓存当前的组件?缓存后怎么更新? (opens new window)

  • keep-alive 是 Vue 中的内置组件,能在组件切换过程中将状态保留在内存中,使其不被销毁,防止重复渲染 DOM

  • keep-alive 其拥有两个独立的生命周期钩子函数activeddeactived,使用keep-alive包裹的组件在切换时不会被销毁,而是缓存到内存中并执行deactived钩子函数,命中缓存渲染后会执行actived钩子函数。

  • <keep-alive> 是一个抽象组件:它自身不会渲染一个 DOM 元素,也不会出现在组件的父组件链中。

  • keep-alive 可以设置以下 props 属性:

    • include - 字符串或正则表达式或数组。只有名称匹配的组件会被缓存
    • exclude - 字符串或正则表达式或数组。任何名称匹配的组件都不会被缓存
    • max - 字符串或数字。最多可以缓存多少组件实例
<keep-alive>
  <component :is="view"></component>
</keep-alive>

<keep-alive include="a,b">
  <component :is="view"></component>
</keep-alive>

<!-- 正则表达式 (使用 `v-bind`) -->
<keep-alive :include="/a|b/">
  <component :is="view"></component>
</keep-alive>

<!-- 数组 (使用 `v-bind`) -->
<keep-alive :include="['a', 'b']">
  <component :is="view"></component>
</keep-alive>

<div id="app" class='wrapper'>
  <keep-alive>
    <!-- 需要缓存的视图组件 -->
    <router-view v-if="$route.meta.keepAlive"></router-view>
  </keep-alive>
  <!-- 不需要缓存的视图组件 -->
  <router-view v-if="!$route.meta.keepAlive"></router-view>
</div>
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

匹配首先检查组件自身的 name 选项,如果 name 选项不可用,则匹配它的局部注册名称 (父组件 components 选项的键值),匿名组件不能被匹配。所以 <keep-alive> 要求被切换到的组件都有自己的名字,不论是通过组件的 name 选项还是局部/全局注册(但是在源码中如果没有提供name,它会去匹配tag)。

设置了 keep-alive 缓存的组件,会多出两个生命周期钩子( activateddeactivated ):

  • 首次进入组件时: beforeRouteEnter > beforeCreate > created> mounted > activated > ... ... > beforeRouteLeave > deactivated
  • 再次进入组件时: beforeRouteEnter >activated > ... ... > beforeRouteLeave > deactivated
  1. 缓存后如何获取数据

    每次组件渲染的时候,都会执行 beforeRouteEnter

    beforeRouteEnter(to, from, next) {
      next(vm=>{
          console.log(vm)
          // 每次进入路由执行
          vm.getData()  // 获取数据
      })
    },
    
    1
    2
    3
    4
    5
    6
    7

    keep-alive 缓存的组件被激活的时候,都会执行 actived 钩子

    activated() {
      this.getData() // 获取数据
    },
    
    1
    2
    3

    注意:服务器端渲染期间 avtived 不被调用

  2. 为什么不会生成真正的DOM节点?

    Vue在初始化生命周期的时候,为组件实例建立父子关系会根据abstract属性决定是否忽略某个组件。在keep-alive中,设置了abstract:true,那Vue就会跳过该组件实例。最后构建的组件树中就不会包含keep-alive组件,那么由组件树渲染成的DOM树自然也不会有keep-alive相关的节点了。

  3. keep-alive包裹的组件是如何使用缓存的?

    • 源码:vue/src/core/components/keep-alive.js

    • 参考:通俗易懂了解Vue内置组件keep-alive内部原理 (opens new window)

    • 在首次加载被包裹组建时,由keep-alive.js中的render函数可知,首先获取组件名字,如果设置的缓存规则无法匹配当前组件,直接返回这个组件。

    • 如果能匹配上,获取当前的缓存和缓存的keys,如果当前组件的key不存在则生成一个,判断这个key在缓存cache中是否有值,如果有就直接取缓存,并且根据LRU算法把节点的key移动到最后,如果没有缓存,则把当前节点添加到缓存中,以备下次使用,如果新加入缓存组件之后,缓存组件的数量大于了设定的最大值,那么就需要删除第一个缓存。

    • 再次访问被包裹组件时,cache中已经有组件的key对应的缓存了,直接取用,并更新缓存的顺序。

    LRU(Least recently used,最近最少使用)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。

# Vue常用的修饰符

参考文章:面试官:Vue常用的修饰符有哪些?有什么应用场景? (opens new window)

vue中修饰符分为以下五种:

  • 表单修饰符
  • 事件修饰符
  • 鼠标按键修饰符
  • 键值修饰符
  • v-bind 修饰符

v-model 指令的表单修饰符

  • lazy : 在我们填完信息,光标离开标签的时候,才会将值赋予给 value ,也就是在 change 事件之后再进行信息同步
  • trim : 自动过滤用户输入的首尾空格字符,而中间的空格不会过滤
  • number : 自动将用户的输入值转为数值类型,但如果这个值无法被 parseFloat() 解析,则会返回原来的值

v-on 指令(@)的事件修饰符

  • stop: 阻止了事件冒泡,相当于调用了 event.stopPropagation 方法
  • prevent: 阻止了事件的默认行为,相当于调用了 event.preventDefault 方法
  • capture: 添加事件监听器时使用事件捕获模式,即内部元素触发的事件先在此处理,然后才交由内部元素进行处理,使事件触发从包含这个元素的顶层开始往下触发
  • self: 只当在 event.target 是当前元素自身时触发处理函数,即事件不是从内部元素触发的
  • once: 点击事件将只会触发一次
  • passive: 在移动端,当我们在监听元素滚动事件的时候,会一直触发 onscroll 事件会让我们的网页变卡,因此我们使用这个修饰符的时候,相当于给 onscroll 事件整了一个 .lazy 修饰符
  • native: 让组件变成像 html 内置标签那样监听根元素的原生事件,否则组件上使用 v-on 只会监听自定义事件,使用 .native 修饰符来操作普通 HTML 标签是会令事件失效的
  • .exact : 修饰符允许你控制由精确的系统修饰符组合触发的事件。

使用修饰符时,顺序很重要;相应的代码会以同样的顺序产生。因此,用 v-on:click.prevent.self 会阻止所有的点击,而 v-on:click.self.prevent 只会阻止对元素自身的点击。

鼠标按钮修饰符

  • left : 左键点击 @click.left="shout(1)"
  • right : 右键点击 @click.right="shout(1)"
  • middle : 中键点击 @click.middle="shout(1)"

键盘修饰符

键盘修饰符是用来修饰键盘事件( onkeyuponkeydown )的,有如下:

keyCode 存在很多,但 Vue 为我们提供了别名,分为以下两种:

普通键( entertabdeletespaceescupdownleftright ...) 系统修饰键( ctrlaltmetashift ...)

<input type="text" @keyup.enter="shout()">
1

自定义按键修饰符别名: Vue.config.keyCodes.f2 = 113

v-bind修饰符

  • async : 能对 props 进行一个双向绑定 this.$emit('update:xxx', params);
  • prop : 作为一个 DOM property 绑定而不是作为 attribute 绑定
  • camel : 将 kebab-case attribute 名转换为 camelCase

# Vue自定义指令

参考文章:面试官:你有写过自定义指令吗?自定义指令的应用场景有哪些? (opens new window)

全局注册注册主要是用过 Vue.directive 方法进行注册

Vue.directive 第一个参数是指令的名字(不需要写上 v- 前缀),第二个参数可以是对象数据,也可以是一个指令函数

// 注册一个全局自定义指令 v-focus
Vue.directive('focus', {
  // 当被绑定的元素插入到 DOM 中时……
  inserted(el) {
    // 聚焦元素
    el.focus(); // 页面加载完成之后自动让输入框获取到焦点的小功能
  },
});
1
2
3
4
5
6
7
8

局部注册通过在组件 options 选项中设置 directive 属性

directives: {
  focus: {
    // 指令的定义
    inserted: function (el) {
      el.focus() // 页面加载完成之后自动让输入框获取到焦点的小功能
    }
  }
}
1
2
3
4
5
6
7
8

自定义指令也像组件那样存在钩子函数:

  • bind :只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置
  • inserted :被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)
  • update :所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新
  • componentUpdated :指令所在组件的 VNode 及其子 VNode 全部更新后调用
  • unbind :只调用一次,指令与元素解绑时调用

所有的钩子函数的参数都有以下:

  • el :指令所绑定的元素,可以用来直接操作 DOM

  • binding :一个对象,包含以下 property

    • name :指令名,不包括 v- 前缀。
    • value :指令的绑定值,例如:v-my-directive="1 + 1" 中,绑定值为 2
    • oldValue :指令绑定的前一个值,仅在 updatecomponentUpdated 钩子中可用。无论值是否改变都可用
    • expression :字符串形式的指令表达式。例如 v-my-directive="1 + 1" 中,表达式为 "1 + 1"
    • arg :传给指令的参数,可选。例如 v-my-directive:foo 中,参数为 "foo"
    • modifiers :一个包含修饰符的对象。例如:v-my-directive.foo.bar 中,修饰符对象为 { foo: true, bar: true }
  • vnode : Vue 编译生成的虚拟节点

  • oldVnode :上一个虚拟节点,仅在 updatecomponentUpdated 钩子中可用

除了 el 之外,其它参数都应该是只读的,切勿进行修改。如果需要在钩子之间共享数据,建议通过元素的 dataset 来进行

# Vue中的过滤器

参考文章:面试官:Vue中的过滤器了解吗?过滤器的应用场景有哪些? (opens new window)

Vue 中的过滤器可以用在两个地方:双花括号插值和 v-bind 表达式,过滤器应该被添加在 JavaScript 表达式的尾部,由“管道”符号指示:

<!-- 在双花括号中 -->
<!-- {{ message | capitalize }} -->
<!-- 在 `v-bind` 中 -->
<div v-bind:id="rawId | formatId"></div>
1
2
3
4
// 在组件的选项中定义本地的过滤器
filters: {
  capitalize: function (value) {
    if (!value) return ''
    value = value.toString()
    return value.charAt(0).toUpperCase() + value.slice(1)
  }
}

// 定义全局过滤器:
Vue.filter('capitalize', function (value) {
  if (!value) return ''
  value = value.toString()
  return value.charAt(0).toUpperCase() + value.slice(1)
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

注意:当全局过滤器和局部过滤器重名时,会采用局部过滤器

过滤器函数总接收表达式的值 (之前的操作链的结果) 作为第一个参数。

# 如何实现一个虚拟DOM

参考文章:面试官:什么是虚拟DOM?如何实现一个虚拟DOM? (opens new window)

虚拟DOM的实现原理主要包括以下3部分:

  1. 用JavaScript对象模拟真实DOM树,对真实DOM进行抽象,{ tag: 'div', props: { class: 'item' }, children: [] }
  2. diff算法 — 比较两棵虚拟DOM树的差异;
  3. patch算法 — 将两个虚拟DOM对象的差异应用到真正的DOM树。

虚拟DOM的优缺点:

优点

  • 保证性能下限:框架的虚拟DOM需要适配任何上层API可能产生的操作,它的一些DOM操作的实现必须是普适的,所以它的性能并不是最优的;但是比起粗暴的DOM操作性能要好很多,因此框架的虚拟DOM至少可以保证在你不需要手动优化的情况下,依然可以提供还不错的性能,即保证性能的下限;
  • 无需手动操作DOM:我们不再需要手动去操作DOM,只需要写好View-Model的代码逻辑,框架会根据虚拟DOM和数据双向绑定,帮我们以可预期的方式更新视图,极大提高我们的开发效率;
  • 跨平台:虚拟DOM本质上是JavaScript对象,而DOM与平台强相关,相比之下虚拟DOM可以进行更方便地跨平台操作,例如服务器渲染、weex开发等等。

缺点

  • 无法进行极致优化:虽然虚拟DOM+合理的优化,足以应对绝大部分应用的性能需求,但在一些性能要求极高的应用中虚拟DOM无法进行针对性的极致优化。
  • 需要额外的创建函数,如createElement或h,但可以通过JSX或者vue-loader来简化成XML写法。但是这么做会依赖打包工具。

# Vue项目中有封装过axios吗

参考文章:前端数据请求(axios, jQuery, 原生)

  • 请求拦截

    • 添加请求头,如token
    • 拼接公共请求地址
    • 请求数据格式Content-Type
    • 处理IE请求缓存
    • timeout
  • 响应拦截

    • 状态码拦截
    • 权限及登录状态校验
    • 请求失败响应公共处理

# 有看过axios的源码吗

参考文章:面试官:你了解Axios的原理吗?有看过它的源码吗? (opens new window)

# SSR解决了什么问题有做过SSR吗

参考文章:面试官:SSR解决了什么问题?有做过SSR吗?你是怎么做的? (opens new window)

Server-Side Rendering 我们称其为 SSR ,意为服务端渲染

Vue SSR将包含两部分:服务端渲染的首屏,包含交互的SPA

SSR主要解决了以下两种问题:

  • seo:搜索引擎优先爬取页面HTML结构,使用SSR时,服务端已经生成了和业务想关联的HTML,有利于seo
  • 首屏呈现渲染:用户无需等待页面所有js加载完成就可以看到页面视图(压力来到了服务器,所以需要权衡哪些用服务端渲染,哪些交给客户端)

但是使用SSR同样存在以下的缺点:

  • 复杂度:整个项目的复杂度

  • 库的支持性,代码兼容,服务端渲染只支持beforCreatecreated两个钩子函数

  • 性能问题

    • 每个请求都是n个实例的创建,不然会污染,消耗会变得很大
    • 缓存 node serve、 nginx判断当前用户有没有过期,如果没过期的话就缓存,用刚刚的结果。
    • 降级:监控cpu、内存占用过多,就spa,返回单个的壳
  • 服务器负载变大,相对于前后端分离务器只需要提供静态资源来说,服务器负载更大,所以要慎重使用

所以在我们选择是否使用SSR前,我们需要慎重问问自己这些问题:

  • 需要SEO的页面是否只是少数几个,这些是否可以使用预渲染(Prerender SPA Plugin)实现
  • 首屏的请求响应逻辑是否复杂,数据返回是否大量且缓慢

小结

  • 使用SSR不存在单例模式,每次用户请求都会创建一个新的vue实例
  • 实现SSR需要实现服务端首屏渲染和客户端激活
  • 服务端异步获取数据asyncData可以分为首屏异步获取和切换组件获取
  • 首屏异步获取数据,在服务端预渲染的时候就应该已经完成
  • 切换组件通过mixin混入,在beforeMount钩子完成数据获取

# vue开发过程你是怎么做接口管理的

  • Axios请求封装
  • 公用请求以及各个模块请求拆分

# 说下你的vue项目的目录结构

参考文章:面试官:说下你的vue项目的目录结构,如果是大型项目你该怎么划分结构和划分组件呢? (opens new window)

在划分项目结构的时候,需要遵循一些基本的原则:

  • 文件夹和文件夹内部文件的语义一致性
  • 单一入口/出口
  • 就近原则,紧耦合的文件应该放到一起,且应以相对路径引用
  • 公共的文件应该以绝对路径的方式从根目录引用
  • /src 外的文件不应该被引入

# vue要做权限管理该怎么做

参考文章:面试官:Vue要做权限管理该怎么做?控制到按钮级别的权限怎么做? (opens new window)

  • 路由方面,用户登录后只能看到自己有权访问的导航菜单,也只能访问自己有权访问的路由地址,否则将跳转 4xx 提示页

  • 视图方面,用户只能看到自己有权浏览的内容和有权操作的控件

  • 最后再加上请求控制作为最后一道防线,路由可能配置失误,按钮可能忘了加权限,这种时候请求控制可以用来兜底,越权请求将在前端被拦截

  • 接口权限

    接口权限目前一般采用jwt的形式来验证,没有通过的话一般返回401,跳转到登录页面重新进行登录

  • 路由权限

    方案一:初始化即挂载全部路由,并且在路由上标记相应的权限信息,每次路由跳转前做校验

    这种方式存在以下四种缺点:

    • 加载所有的路由,如果路由很多,而用户并不是所有的路由都有权限访问,对性能会有影响。
    • 全局路由守卫里,每次路由跳转都要做权限判断。
    • 菜单信息写死在前端,要改个显示文字或权限信息,需要重新编译
    • 菜单跟路由耦合在一起,定义路由的时候还有添加菜单显示标题,图标之类的信息,而且路由不一定作为菜单显示,还要多加字段进行标识

    方案二:初始化的时候先挂载不需要权限控制的路由,比如登录页,404等错误页。如果用户通过URL进行强制访问,则会直接进入404,相当于从源头上做了控制,登录后,获取用户的权限信息,然后筛选有权限访问的路由,在全局路由守卫里进行调用addRoutes添加路由

    这种方式也存在了以下的缺点:

    • 全局路由守卫里,每次路由跳转都要做判断
    • 菜单信息写死在前端,要改个显示文字或权限信息,需要重新编译
    • 菜单跟路由耦合在一起,定义路由的时候还有添加菜单显示标题,图标之类的信息,而且路由不一定作为菜单显示,还要多加字段进行标识
  • 菜单权限

    菜单权限可以理解成将页面与理由进行解耦

  • 按钮权限

    按钮权限也可以用v-if判断或者通过自定义指令进行按钮权限的判断

# vue项目部署后报404是什么

参考文章:面试官:vue项目如何部署?有遇到布署服务器后刷新404问题吗? (opens new window)

history模式下有问题,因为切换路由会重载页面

hash模式下没有问题,hash 虽然出现在 URL 中,但不会被包括在 HTTP 请求中,对服务端完全没有影响,因此改变 hash 不会重新加载页面

产生问题的本质是因为我们的路由是通过JS来执行视图切换的,当我们进入到子路由时刷新页面,web容器没有相对应的页面此时会出现404,所以我们只需要配置将任意页面都重定向到 index.html,把路由交由前端处理,对nginx配置文件.conf修改,添加try_files $uri $uri/ /index.html;

# 你是怎么处理vue项目中的错误的

参考文章:面试官:你是怎么处理vue项目中的错误的? (opens new window)

主要的错误来源包括:

  • 后端接口错误

    通过axios的interceptor实现网络请求的response先进行一层拦截

  • 代码中本身逻辑错误

    设置全局错误处理函数,errorHandler指定组件的渲染和观察期间未捕获错误的处理函数。这个处理函数被调用时,可获取错误信息和 Vue 实例

    Vue.config.errorHandler = function (err, vm, info) {
      // `info` 是 Vue 特定的错误信息,比如错误所在的生命周期钩子
      // 只在 2.2.0+ 可用
    
      // 从 2.2.0 起,这个钩子也会捕获组件生命周期钩子里的错误。
      // 同样的,当这个钩子是 undefined 时,被捕获的错误会通过
      // console.error 输出而避免应用崩
    
      // 从 2.4.0 起,这个钩子也会捕获 Vue 自定义事件处理函数内部的错误了
    
      // 从 2.6.0 起,这个钩子也会捕获 v-on DOM 监听器内部抛出的错误。
      // 另外,如果任何被覆盖的钩子或处理函数返回一个 Promise 链 (例如 async 函数),
      // 则来自其 Promise 链的错误也会被处理
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14

    生命周期钩子,errorCaptured是 2.5.0 新增的一个生命钩子函数,当捕获到一个来自子孙组件的错误时被调用

    (err: Error, vm: Component, info: string) => ?boolean
    
    1

    此钩子会收到三个参数:错误对象、发生错误的组件实例以及一个包含错误来源信息的字符串。此钩子可以返回 false 以阻止该错误继续向上传播

tips:使用其他插件如 sentry

# Vue3跟Vue2的区别吗

参考文章:Vue3与Vue2的区别

# Object.defineProperty和Proxy比较

链接:Object.defineProperty和Proxy比较

# vue项目中的性能方面优化

  • 代码层面

    • v-ifv-show选择调用
    • computedwatch区分使用场景
    • v-for为item设置唯一key
    • 细分vuejs组件
    • 内容类系统的图片资源按需加载
    • 图片资源懒加载
    • 事件用完销毁
    • 第三方插件的按需引入
    • 优化无限列表性能
    • 对路由组件进行懒加载
    • SSR(服务端渲染)
    • 公共组件、css、js方法的复用
    • vue组件keep-alive
  • webpack层面

    • webpack对图片进行压缩
    • 减少ES6转为ES5的冗余代码,Babel插件会在将ES6代码转换成ES5代码时会注入一些辅助函数,babel-plugin-transform-runtime可以解决
    • 本地打包的gzip:productionGzip: false
    • 添加externals来告诉webpack我们这些第三方库不需要打包
  • 其他

    • nginx上开启gzip压缩
    • 服务器缓存,资源请求时匹配文件后缀获取文件类型,指定缓存时间(如expires 1h;),cssjs这些可以缓存时间长点甚至永久缓存,文件带有hash值,html文件则不要缓存太长时间
    • 浏览器缓存,http-equiv,语法格式是:<meta http-equiv="参数" content="参数变量值">,例如:<meta http-equiv="expires" content="Wed, 20 Jun 2007 22:33:00 GMT">,或者设置其他关于缓存的请求头字段,Cache-Control,Expires,Last-Modified/If-Modified-Since,If-None-Match/Etags
    • CDN
    • 预加载,prefetch,域名预解析 <link rel='dns-prefetch' href='http://www.baidu.com'>

# 自定义组件的双向绑定怎么写

v-model默认接收一个名为valueprops值,触发事件为input,自定义组件中的触发this.$emit('input', value)

<input v-model="inputValue">
<!-- 等同于 -->
<input v-bind:value="inputValue" v-on:input="inputValue = $event.target.value">
1
2
3

可以通过mode字段自定义v-model传递的数据名和触发的方法

export default {
  model: {
    prop: "currentValue",
    event: "change"
  },
  methods() {
    changeData(val) {
      this.$emit('change', val)
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11

# vue双向绑定的sync

.sync 修饰符 (opens new window)

<text-document
  v-bind:title="doc.title"
  v-on:update:title="doc.title = $event"
></text-document>
1
2
3
4

简写成 .sync 修饰符

<text-document v-bind:title.sync="doc.title"></text-document>
1

.sync修饰符就是一个语法糖:v-bind:title.sync="doc.title" === @update:title="val => doc.title = val"

触发

this.$emit('update:title', newTitle)
1

注意带有 .sync 修饰符的 v-bind 不能和表达式一起使用 (例如 v-bind:title.sync="doc.title + '!'" 是无效的)。取而代之的是,你只能提供你想要绑定的 property 名,类似 v-model

当我们用一个对象同时设置多个 prop 的时候,也可以将这个 .sync 修饰符和 v-bind 配合使用:

<text-document v-bind.sync="doc"></text-document>
1

这样会把 doc 对象中的每一个 property (如 title) 都作为一个独立的 prop 传进去,然后各自添加用于更新的 v-on 监听器。

v-bind.sync 用在一个字面量的对象上,例如 v-bind.sync="{ title: doc.title }",是无法正常工作的,因为在解析一个像这样的复杂表达式的时候,有很多边缘情况需要考虑。

# 数组有哪些API会改变原数组

  • copyWithin
  • fill
  • push
  • pop
  • unshift
  • shift
  • reverse
  • sort
  • splice

# MVVM和MVC区别

  • MVC

    MVC全名是Model View Controller,是模型(model)-视图(view)-控制器(controller)的缩写,一种软件设计典范,用一种业务逻辑、数据、界面显示分离的方法组织代码,将业务逻辑聚集到一个部件里面,在改进和个性化定制界面及用户交互的同时,不需要重新编写业务逻辑。MVC被独特的发展起来用于映射传统的输入、处理和输出功能在一个逻辑的图形化用户界面的结构中。

    MVC的思想:一句话描述就是Controller负责将Model的数据用View显示出来,换句话说就是在Controller里面把Model的数据赋值给View。

  • MVVM

    Model、View、ViewModel(ViewModel只是使得Controller弱化了,并非使它消亡)

    ViewModel:是 MVVM 的核心,是连接 View 与 Model 的桥梁。通过数据双向绑定实现。

    • 方向1:通过数据绑定将模型转化成视图
    • 方向2:通过DOM事件监听,将视图转换为模型(即需要传给后端的数据)

它和其它框架(如 jQuery)的区别是什么?

  • vue:通过对数据的操作就可以完成对页面视图的渲染
  • jquery:直接操作DOM,对其进行赋值、取值、事件绑定等 操作

适合哪些场景?

  • vue:复杂数据操作的后台页面,表单填写页面;数据操作比较多的场景,更加便捷
  • jquery:一些html5的动画页面,一些需要js来操作页面样式的页面。

# Vue的父组件和子组件生命周期钩子函数执行顺序

Vue 的父组件和子组件生命周期钩子函数执行顺序可以归类为以下 4 部分:

  • 加载渲染过程

    beforeCreate -> 父 created -> 父 beforeMount -> 子 beforeCreate -> 子 created -> 子 beforeMount -> 子 mounted -> 父 mounted

    注意:mounted 不会保证所有的子组件也都一起被挂载。如果你希望等到整个视图都渲染完毕,可以在 mounted 内部使用 vm.$nextTick

  • 子组件更新过程

    beforeUpdate -> 子 beforeUpdate -> 子 updated -> 父 updated

  • 父组件更新过程

    beforeUpdate -> 父 updated

  • 销毁过程

    beforeDestroy -> 子 beforeDestroy -> 子 destroyed -> 父 destroyed

# VueRouter有哪些钩子函数

  • 全局前置守卫router.beforeEach

    const router = new VueRouter({ ... })
    router.beforeEach((to, from, next) => {
      // ...
    })
    
    1
    2
    3
    4
  • 全局解析守卫router.beforeResolve

    在 2.5.0+ 你可以用 router.beforeResolve 注册一个全局守卫。这和 router.beforeEach 类似,区别是在导航被确认之前,同时在所有组件内守卫和异步路由组件被解析之后,解析守卫就被调用。

  • 全局后置钩子router.afterEach

    和守卫不同的是,这些钩子不会接受 next 函数也不会改变导航本身

    router.afterEach((to, from) => {
      // ...
    })
    
    1
    2
    3
  • 路由独享的守卫beforeEnter

    const router = new VueRouter({
      routes: [
        {
          path: '/foo',
          component: Foo,
          beforeEnter: (to, from, next) => {
            // ...
          }
        }
      ]
    })
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
  • 组件内的守卫 beforeRouteEnter(to, from, next) beforeRouteUpdate(to, from, next) beforeRouteLeave(to, from, next)

    beforeRouteEnter 守卫 不能 访问 this,因为守卫在导航确认前被调用,因此即将登场的新组件还没被创建。可以通过传一个回调给 next 来访问组件实例(其他两个都不支持回调,因为可以访问this了)

    beforeRouteEnter (to, from, next) {
      next(vm => {
        // 通过 `vm` 访问组件实例
      })
    }
    
    1
    2
    3
    4
    5

确保 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 的回调函数,创建好的组件实例会作为回调函数的参数传入

# VueRouter路由模式hash和history的实现原理

  • hash

使用 URLhash 来模拟一个完整的 URL,于是当 URL 改变时,页面不会重新加载。 hash#)是URL 的锚点,代表的是网页中的一个位置,单单改变#后的部分,不会重新加载网页,也就是说hash 出现在 URL 中,但不会被包含在 http 请求中,对后端完全没有影响,因此改变 hash 不会重新加载页面;同时每一次改变#后的部分,都会在浏览器的访问历史中增加一个记录,使用”后退”按钮,就可以回到上一个位置;所以说Hash模式通过锚点值的改变,根据不同的值,对页面进行跳转(渲染)。

  • history

利用了html5 history interface 中新增的 pushState()replaceState() 方法。这两个方法应用于浏览器记录栈,在当前已有的 backforwardgo 基础之上,它们提供了对历史记录修改的功能。只是当它们执行修改时,虽然改变了当前的 URL ,但浏览器不会立即向后端发送请求。不过这种模式要玩好,还需要后台配置支持。因为我们的应用是个单页客户端应用,如果后台没有正确的配置,可能就会返回 404,这就不好看了。所以呢,你要在服务端增加一个覆盖所有情况的候选资源:如果 URL 匹配不到任何静态资源,则应该返回同一个 index.html 页面,这个页面就是你 app 依赖的页面。

vue-router的mode除了hashhistory之外还有一个模式:

abstract: 支持所有 JavaScript 运行环境,如 Node.js 服务器端。如果发现没有浏览器的 API,路由会自动强制进入这个模式。(Node.js 环境)

# VueRouter跳转的方式

vm.$routervm.$route的区别

  • $router是VueRouter创建的路由实例,上面包含了beforeEach,beforeResolve,afterEach,push,replace,go等等实例方法
  • $route返回的是一个路由对象 (route object) 表示当前激活的路由的状态信息,包含了当前 URL 解析得到的信息,还有 URL 匹配到的路由记录 (route records)。路由对象是不可变 (immutable) 的,每次成功的导航后都会产生一个新的对象。里面包含path,fullPath,params,query,hash,name,matched(一个数组,包含当前路由的所有嵌套路径片段的路由记录 。路由记录就是 routes 配置数组中的对象副本 (还有在 children 数组)。),redirectedFrom(如果存在重定向,即为重定向来源的路由的名字)

跳转实现

  1. router-link

    <router-link to="home">Home</router-link>
    <router-link :to="{ path: 'home' }">Home</router-link>
    <router-link :to="{ name: 'user', params: { userId: 123 }}">User</router-link>
    <router-link :to="{ path: 'register', query: { plan: 'private' }}">Register</router-link>
    
    1
    2
    3
    4

    当被点击后,内部会立刻把 to 的值传到 router.push()

    标签上其他的props

    • replace:更换为router.replace()<router-link :to="{ path: '/abc'}" replace></router-link>
    • append:设置 append 属性后,则在当前 (相对) 路径前添加基路径。例如,我们从 /a 导航到一个相对路径 b,如果没有配置 append,则路径为 /b,如果配了,则为 /a/b
    • tag:tag prop 类指定渲染成何种标签,默认 a
    • active-class:设置链接激活时使用的 CSS 类名,默认router-link-active
    • exact:“是否激活”默认类名的依据是包含匹配(即/这个路径任何时候都会激活),加上这个精确匹配模式。
    • event:声明可以用来触发导航的事件。默认click
    • exact-active-class:配置当链接被精确匹配的时候应该激活的 class,默认router-link-exact-active
    • aria-current-value:当链接根据精确匹配规则激活时配置的 aria-current 的值。默认page
  2. vm.$router.push

    // 字符串
    router.push('home')
    // 对象
    router.push({ path: 'home' })
    // 命名的路由
    router.push({ name: 'user', params: { userId: '123' }})
    // 带查询参数,变成 /register?plan=private 会在地址栏显示参数 刷新不消失
    router.push({ path: 'register', query: { plan: 'private' }})
    
    1
    2
    3
    4
    5
    6
    7
    8

    如果提供了 pathparams 会被忽略,上述例子中的 query 并不属于这种情况,所以在使用时要name+paramspath+query配合使用

    还有一种传递参数的方式就是动态路由

    const router = new VueRouter({
      routes: [
        // 动态路径参数 以冒号开头
        { path: '/user/:id', component: User }
      ]
    })
    
    router.push({ path: `/user/${userId}` }) // -> /user/123
    
    1
    2
    3
    4
    5
    6
    7
    8

    path+query获取参数:this.$route.query,之外的都是this.$route.params

  3. vm.$router.replace

    vm.$router.push用法一样,跟 router.push 很像,唯一的不同就是,它不会向 history 添加新记录,而是跟它的方法名一样 —— 替换掉当前的 history 记录。

# 不同路由使用相同组件缓存问题

多个路由解析到同一个 Vue 组件,使用相同组件的路由之间切换,则不会有任何改变

const routes = [
  {
    path: '/a',
    component: MyComponent,
  },
  {
    path: '/b',
    component: MyComponent,
  },
];
1
2
3
4
5
6
7
8
9
10

要解决此问题,你需要在 <router-view> 元素上添加 :key 属性

<router-view :key='$route.path' />
1

# props的验证

官方文档:props (opens new window)

props可以是数组或对象,用于接收来自父组件的数据。props可以是简单的数组,或者使用对象作为替代,对象允许配置高级选项,如类型检测、自定义验证和设置默认值。

你可以基于对象的语法使用以下选项:

  • type:可以是下列原生构造函数中的一种:StringNumberBooleanArrayObjectDateFunctionSymbol任何自定义构造函数、或上述内容组成的数组。会检查一个prop是否是给定的类型,否则抛出警告。
  • defaultany - 为该prop指定一个默认值。如果该prop没有被传入,则换做用这个值。对象或数组的默认值必须从一个工厂函数返回。
  • requiredBoolean - 定义该prop是否是必填项。在非生产环境中,如果这个值为truthy且该prop没有被传入的,则一个控制台警告将会被抛出。
  • validatorFunction - 自定义验证函数会将该prop的值作为唯一的参数代入。在非生产环境下,如果该函数返回一个falsy的值(也就是验证失败),一个控制台警告将会被抛出。
// 简单语法
Vue.component('PropsDemoSimple', {
  props: ['size', 'myMessage'],
});

// 对象语法,提供验证
Vue.component('PropsDemoAdvanced', {
  props: {
    // 检测类型
    height: Number,
    // 多个类型
    width: [String, Number], // 并没有 String | Number 这种写法
    // 检测类型 + 其他验证
    age: {
      type: Number,
      default: 0,
      required: true,
      validator(value) {
        return value >= 0;
      },
    },
  },
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

WARNING

注意那些prop会在一个组件实例创建之前进行验证,所以实例的property(如datacomputed等)在defaultvalidator函数中是不可用的。

P.S.:$props可以获取所有props,可以通过v-bind="$props"全部传给子组件

# watch的使用

官方文档:watch (opens new window)

类型:{ [key: string]: string | Function | Object | Array }

详细:一个对象,键是需要观察的表达式,值是对应回调函数。值也可以是方法名,或者包含选项的对象。Vue 实例将会在实例化时调用$watch(),遍历watch对象的每一个property。

const vm = new Vue({
  data: {
    a: 1,
    b: 2,
    c: 3,
    d: 4,
    e: {
      f: {
        g: 5,
      },
    },
  },
  watch: {
    a(val, oldVal) {
      console.log('new: %s, old: %s', val, oldVal);
    },
    // 方法名
    b: 'someMethod',
    // 该回调会在任何被侦听的对象的property改变时被调用,不论其被嵌套多深
    c: {
      handler(val, oldVal) { /* ... */ },
      deep: true,
      // 打印oldVal和newVal值一样的原因是它们索引同一个对象/数组,深度监听
      // 虽然可以监听到对象的变化,但是无法监听到具体对象里面那个属性的变化。
      // Vue不会保留修改之前值的副本
    },
    // 该回调将会在侦听开始之后被立即调用
    d: {
      handler: 'someMethod',
      immediate: true,
    },
    // 你可以传入回调数组,它们会被逐一调用
    e: [
      'handle1',
      function handle2(val, oldVal) { /* ... */ },
      {
        handler: function handle3(val, oldVal) { /* ... */ },
        /* ... */
      },
    ],
    // watch vm.e.f's value: {g: 5} 通过字符串监听内部结构
    'e.f': function (val, oldVal) { /* ... */ },
  },
});
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

WARNING

注意,不应该使用箭头函数来定义watcher函数 (例如searchQuery: newValue => this.updateAutocomplete(newValue))。理由是箭头函数绑定了父级作用域的上下文,所以this将不会按照期望指向Vue实例,this.updateAutocomplete将是undefined

methods里面的函数同理

# vuex中为什么异步放在action,同步在mutation

来自尤雨溪的回答: (opens new window)

区分actionsmutations并不是为了解决竞态问题,而是为了能用devtools追踪状态变化。

事实上在vuex里面actions只是一个架构性的概念,并不是必须的,说到底只是一个函数,你在里面想干嘛都可以,只要最后触发mutation就行。异步竞态怎么处理那是用户自己的事情。vuex真正限制你的只有mutation必须是同步的这一点(在redux里面就好像reducer必须同步返回下一个状态一样)。同步的意义在于这样每一个mutation执行完成后都可以对应到一个新的状态(和reducer一样),这样devtools就可以打个snapshot存下来,然后就可以随便time-travel了。如果你开着devtool调用一个异步的action,你可以清楚地看到它所调用的mutation是何时被记录下来的,并且可以立刻查看它们对应的状态。其实我有个点子一直没时间做,那就是把记录下来的mutations做成类似rx-marble那样的时间线图,对于理解应用的异步状态变化很有帮助。

其实就是做了代码隔离,不非受控的代码集中到actionmutation只做纯函数的状态改变,mvvm一般强调的就是直接面对view的那层不要做复杂的逻辑

# v-model绑定vuex中的数据报错如何处理

官方文档:表单处理 (opens new window)

<input v-model="obj.message">
1

v-model的数据是双向的,但是vuex中的数据流是单向的,当输入值修改时,v-model是去修改obj.message,由于这个值在vuex中不是通过mutation提交的,所以会报错

解决方法:

  • 不使用v-model,使用computed计算值,通过value属性传给input,并且监听输入提交mutation
  • 使用computed属性的gettersetterget方法用来获取值,set方法用来提交mutation修改状态

所以v-model的双向绑定与vuex的单向数据流并不冲突,两个所需要解决的场景不同,其实没有可比性

# 如何处理输入框中文输入校验问题

为了解决中文输入法输入内容时还没将中文插入到输入框就验证的问题,我们希望中文输入完成以后才验证

<input
  ref="input"
  @compositionstart="handleComposition"
  @compositionupdate="handleComposition"
  @compositionend="handleComposition"
>
1
2
3
4
5
6

compositionstart事件触发于一段文字的输入之前(类似于keydown事件,但是该事件仅在若干可见字符的输入之前,而这些可见字符的输入可能需要一连串的键盘操作、语音识别或者点击输入法的备选词)

简单来说就是切换中文输入法时在打拼音时(此时input内还没有填入真正的内容),会首先触发compositionstart,然后每打一个拼音字母,触发compositionupdate,最后将输入好的中文填入input中时触发compositionend。触发compositionstart时,文本框会填入“虚拟文本”(待确认文本),同时触发input事件;在触发compositionend时,就是填入实际内容后(已确认文本),所以这里如果不想触发input事件的话就得设置一个bool变量来控制。

# v-for是否需要事件委托

首先我们需要知道事件委托主要有什么作用?

  • 事件委托能够避免我们逐个的去给元素新增和删除事件
  • 事件委托比每一个元素都绑定一个事件性能要更好

事件委托作用主要是2个

  • 将事件处理程序代理到父节点,减少内存占用率
  • 动态生成子节点时能自动绑定事件处理程序到父节点

结论

  • v-for中,我们直接用一个for循环就能在模板中将每个元素都绑定上事件,并且当组件销毁时,vue也会自动给我们将所有的事件处理器都移除掉。所以事件委托能做到的第一点vue已经给我们做到了
  • v-for中,给元素绑定的都是相同的事件,所以除非上千行的元素需要加上事件,其实和使用事件委托的性能差别不大,所以也没必要用事件委托,只需要在没有委托的情况下真正发现任何性能问题时才使用它。

vue源码中并没有处理事件委托 (opens new window)

# react,vue的diff从O(n^3)到O(n)是怎么算出来

  • O(n^3):具体不太清楚,大概是根据两棵树的编辑距离(tree edit distance),同时对比两棵树以及更新,时间复杂度大概在O(n3),也有说树的最小距离编辑算法的时间复杂度是O(n2m(1+logmn)),m和n是两棵树的节点数,如果m和n同阶,就可以看做O(n^3)
  • O(n):代表如果有n节点需要更新,只需要操作dom n次就能完成。但是这里有个前提是这n个节点更新后和原来dom要在同层,如果跨层更新节点,肯定比O(n)复杂。

# vm.$set()的原理

全局方法Vue.set()的别名

Vue无法检测到对象属性的添加或删除。由于Vue会在初始化实例时对属性执行getter/setter转化,所以属性必须在data对象上存在才能让Vue将它转换为响应式的。但是Vue提供了Vue.set(object, propertyName, value)/vm.$set(object, propertyName, value)来实现为对象添加响应式属性

源码位置:vue/src/core/observer/index.js

  • 如果目标是数组,直接使用数组的splice方法触发相应式;
  • 如果目标是对象,会先判断属性是否存在,如果存在直接修改该属性,判断对象是否是响应式,如果不是也是直接赋值,最终如果要对属性进行响应式处理,则是通过调用defineReactive方法进行响应式处理(defineReactive方法就是Vue在初始化对象时,给对象属性采用Object.defineProperty动态添加gettersetter的功能所调用的方法),并且调用设置对象下的dep对象上的notify通知

直接给一个数组项赋值,Vue不能检测到变化

  1. 当你利用索引直接设置一个数组项时,例如:vm.items[indexOfItem] = newValue
  2. 当你修改数组的长度时,例如:vm.items.length = newLength

为了解决第一个问题,Vue提供了以下操作方法:

  • vm.$set(vm.items, indexOfItem, newValue)
  • vm.items.splice(indexOfItem, 1, newValue)

为了解决第二个问题,Vue提供了以下操作方法:

  • vm.items.splice(newLength)

# 自动导入文件

  • 手动一个个引入
  • webpack提供的api require.context

require.context函数接受三个参数

  • directory {String} -读取文件的路径
  • useSubdirectories {Boolean} -是否遍历文件的子目录
  • regExp {RegExp} -匹配文件的正则

语法: require.context(directory, useSubdirectories = false, regExp = /^.//);

const files = require.context('.', false, /.js$/);
const obj = {};
files.keys().forEach((key) => {
  if (key === './index.js') return;
  Object.assign(obj, { ...files(key).default });
});
export default obj;
1
2
3
4
5
6
7

# Vue无法渲染以_和$开始的变量

官方文档 (opens new window):实例创建之后,可以通过vm.$data访问原始数据对象。Vue实例也代理了data对象上所有的property,因此访问vm.a等价于访问vm.$data.a。以_$开头的property不会被Vue实例代理,因为它们可能和Vue内置的property、API方法冲突。你可以使用例如vm.$data._property的方式访问这些property。

注意:是不是响应式的跟有没有代理是两码事,如果是_或者$开头的变量没有被代理,但是在$data里面仍然是响应式的

<template>
  <div>
    <ol>
      <!-- 内容为空 -->
      <li>{{ _aaa }}</li>
      <li>{{ $bbb }}</li>
      <!-- 有内容 -->
      <li>{{ $data._aaa }}</li>
      <li>{{ $data.$bbb }}</li>
    </ol>
  </div>
</template>

<script>
export default {
  data() {
    return {
      _aaa: '_aaa',
      $bbb: '$bbb',
    };
  },
  mounted() {
    console.log(this._aaa, this.$bbb); // undefined undefined
    console.log(this.$data._aaa, this.$data.$bbb); // _aaa $bbb
    setTimeout(() => {
      this._aaa = '1111';
      this.$bbb = '2222';
      this.$data._aaa = '3333';
      this.$data.$bbb = '4444';
      // 定时器执行后,页面上四个值都显示出来了
      // 如果只有前面两行,没有通过$data去修改,那么不会触发视图渲染
    }, 2000);
  },
};
</script>
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

源码解析:vue/src/core/instance/state.js

  • initData函数,通过isReserved方法,判断是否是$或者_开头

    // 源码地址:vue/src/core/util/lang.js
    
    /**
     * Check if a string starts with $ or _
     */
    export function isReserved (str: string): boolean {
      const c = (str + '').charCodeAt(0)
      return c === 0x24 || c === 0x5F
      // ascii 36表示$ 95表示_
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
  • 然后通过proxy去代理这个属性,成功代理的属性会直接以当前组件实例的属性出现

  • vue/src/core/instance/proxy.js定义访问和获取是检测是否代理的方法

  • stateMixin方法中将$data_data关联起来,Object.defineProperty(Vue.prototype, '$data', dataDef),所以vm.avm.$data.a是等价的

最近更新: 2025年03月06日 15:35:02