该内容主要整理关于 Vue 的相关面试题,其他内容面试题请移步至 最新最全的前端面试题集锦 查看。
关于 Vue3.0 的相关面试题,请移步至 Vue3.0 篇 查看。
MVVM
?MVVM
是 Model-View-ViewModel
的缩写。MVVM
是一种设计思想。Model
层代表数据模型,也可以在 Model
中定义数据修改和操作的业务逻辑;View
代表UI 组件,它负责将数据模型转化成 UI 展现出来;ViewModel
是一个同步 View
和 Model
的对象。
在 MVVM
架构下,View
和 Model
之间并没有直接的联系,而是通过ViewModel
进行交互,Model
和 ViewModel
之间的交互是双向的, 因此View
数据的变化会同步到 Model
中,而 Model
数据的变化也会立即反应到View
上。
ViewModel
通过双向数据绑定把 View
层和 Model
层连接起来,而 View
和 Model
之间的同步工作完全是自动的,无需人为干涉,因此开发者只需关注业务逻辑,不需要手动操作 DOM
,不需要关注数据状态的同步问题,复杂的数据状态维护完全由 MVVM
来统一管理。
MVVM
和 MVC
区别?它和其它框架(jQuery
)的区别是什么?哪些场景适用?MVC
和 MVVM
其实区别并不大,都是一种设计思想。主要就是 MVC
中Controller
演变成 MVVM
中的 viewModel
。MVVM
主要解决了 MVC
中大量的 DOM
操作使页面渲染性能降低,加载速度变慢,影响用户体验。
和其它框架区别:Vue 数据驱动,通过数据来显示视图层而不是节点操作。
场景:数据操作比较多、频繁的场景,更加便捷。
Model
变化和修改,一个 ViewModel
可以绑定到不同的 View
上,当 View
变化的时候 Model
可以不变,当 Model
变化的时候 View
也可以不变。ViewModel
里面,让很多 View
重用这段视图逻辑。ViewModel
);设计人员可以专注于页面设计。ViewModel
来写。详细文档:Vue 之 父子组件通信与事件触发
:data = data
方式定义传值props
方法接受数据this.$emit
方法传递参数详细文档:Vue 之 路由跳转传参方式详解
声明式路由导航(
)
编程式路由导航(js的方式)
this.$router.push()
:跳转到指定 url 路径,并向 history 栈中添加一个记录,点击后退会返回到上一个页面 ==>> 队列的方式(先进先出)
this.$router.replace()
:跳转到指定 url 路径,但是 history 栈中不会有记录,点击返回会跳转到上上个页面 (就是直接替换了当前页面) ==>> 栈的方式(先进后出)
this.$router.back()
:请求(返回)上一个记录路由
this.$router.go(n)
:向前或者向后跳转n个页面,n可为正整数或负整数
vue.cli
中怎样使用自定义组件?遇到过哪些问题?components
目录新建组件文件(indexPage.vue
)import indexPage from 'src/components/indexPage.vue'
components
属性上面,components:{ indexPage }
template
视图 view
中使用该组件。webpack
设置?webpack
中提供了 require.ensure()
来实现按需加载。
以前引入路由是通过 import
这样的方式引入,现在改为 const
定义的方式进行引入。
import home from '../../common/home.vue'
const home = r => require.ensure( [], () => r (require('../../common/home.vue')))
keep-alive
组件有什么作用?作用:包裹动态组件时,会缓存不活动的组件实例,主要用于保留组件状态或避免重新渲染。
keep-alive
是 vue 的内置组件,而这个组件的作用就是能够缓存不活动的组件。一般情况下,组件进行切换的时候,默认是会进行销毁的,如果我们有需求,在某个组件切换后不进行销毁,而是保存之前的状态,那么就可以利用 keep-alive
来实现。
在 keep-alive
上有两个属性,可以对字符串或正则表达式进行匹配,匹配到的组件会被缓存。
include
值为字符串或者正则表达式匹配的组件 name
会被缓存。(缓存匹配到的组件)exclude
值为字符串或正则表达式匹配的组件 name
不会被缓存。(排除匹配到的组件)其拥有两个独立的生命周期钩子函数 actived
和 deactived
,使用 keep-alive
包裹的组件在切换时不会被销毁,而是缓存到内存中并执行 deactived
钩子函数,命中缓存渲染后会执行 actived
钩子函数。
每个vue实例在被创建时都要经过一系列的初始化过程。
所有的生命周期钩子自动绑定 this
上下文到实例中,因此可以在函数中访问数据,对属性和方法进行运算。这意味着不能使用箭头函数来定义一个生命周期方法【这是因为箭头函数绑定了父上下文,因此this与你期待的 Vue 实例不同】。
阶段一:Vue实例创建阶段
beforeCreate
Vue实例在内存中刚被创建,this
变量还不能使用,数据对象(data
)和方法(methods
)未初始化,watcher
中的事件都不能获得到;
created
实例已经在内存中创建好,数据和方法已经初始化完成,但是模板还未编译,页面还是没有内容,还不能对 dom
节点进行操作(此时访问 this.$el
和 this.$refs.xxx
都是 undefined
)
beforeMounte
找到对应的 template
模板,编译成 render
函数,转换成虚拟 dom,此时模板已经编译完成,数据未挂载到页面,也就是说在这个阶段你可以看到标签间的双花括号,数据还未渲染到页面中;
render : h=>h(App)
在 beforeMounte
之后和 mounted
之前,还有渲染 render
函数,它的作用是把模板渲染成虚拟 dom。
mounted
模板编译好了,虚拟 dom 渲染成真正的 dom
标签,数据渲染到页面,此时Vue实例已经创建完毕,如果没有其他操作的话,Vue实例会静静的躺在内存中,一动不动。
一般会在 mounted
中来渲染从后端获取的数据。(页面初始化时,如果有操作 dom
的事件一般也会放在 mounted
钩子函数中。当然,也可以放在 create
中,前提需使用 this.$nextTick(function(){})
,在回调函数中操作dom
。)
阶段二:Vue实例运行阶段
beforeUpdate
数据依赖改变或者用 $forceUpdata
强制刷新时,对象 data
中的数据已经更改(虚拟 dom 已经重新渲染),但是 页面中的值还是原来,未改变,因为此时还未开始渲染 dom
;
update
此时 data
中的数据和页面更新完毕,页面已经被重新渲染。
在实际开发中,一般会用监听器
watch
来代替上边2个方法,因为watch
会知道是哪一个数据变化。
阶段三:Vue实例销毁阶段
beforeDestroy
实例销毁前使用,在此刻实例还是可用的。
destroyed
Vue实例被销毁,观察者、子组件、事件监听被清除(页面数据不会消失,只不过是响应式无效了)。
v-show
和 v-if
指令的共同点和不同点?v-show
指令是通过修改元素的 display
的 CSS属性让其显示或者隐藏;v-if
指令是直接销毁和重建 DOM 达到让元素显示和隐藏的效果;v-show
会更加节省性能上的开销;当只需要一次显示或隐藏时,使用v-if
更加合理。将当前组件的 修改为
,添加
scoped
属性。
采用 ES6 的 import ... from ...
语法或 CommonJS 的 require()
方法引入组件;
对组件进行注册 components:{ my-component }
使用组件
v-el
的作用是什么?提供一个在页面上已存在的 DOM
元素作为 Vue
实例的挂载目标。可以是 CSS
选择器,也可以是一个 HTMLElement
实例。
import ... from ...
语法或 CommonJS 的 require()
方法引入插件。Vue.use( plugin )
使用插件,可以传入一个选项对象Vue.use(MyPlugin, { someOption: true })
。active-class
是哪个组件的属性?active-class
是 vue-router
模块的 router-link
组件中的属性,用来做选中样式的切换;
computed
与 watch
的区别?相同点:
computed
和监听器 watch
都可以观察属性的变化从而做出响应。不同点:
计算属性 computed
更多是作为缓存功能的观察者,它可以将一个或者多个 data
的属性进行复杂的计算生成一个新的值,提供给渲染函数使用,当依赖的属性变化时,computed
不会立即重新计算生成新的值,而是先标记为当前数据,当下次 computed
被获取时候,才会进行重新计算并返回。也就是说:computed
只有当依赖的数据变化时才会计算,会缓存数据。
监听器 watch
并不具备缓存性,监听器 watch
提供一个监听函数,当监听的属性发生变化时,会立即执行该函数。watch
更适用于数据变化时的异步操作。
data
必须是一个函数?一个组件可能在很多地方使用,也就是会创建很多个实例,如果 data
是一个对象的话,对象是引用类型,一个实例修改了 data
会影响到其他实例,所以 data
必须使用函数,为每一个实例创建一个属于自己的 data
,使其同一个组件的不同实例互不影响。
Vuex 官网的解释是:Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
使用:
vuex
依赖src
下新建 store
目录和 store.js
文件main.js
中引用 vuex
:import store from './store/store.js';
(就可以在组件使用 this.$store
来调用方法)场景:单页应用中,组件之间的状态,音乐播放、登录状态、加入购物车等。
Vuex 属性有五种,分别是 State
、 Getter
、Mutation
、Action
、 Module
。
State
Vuex 就是一个仓库,仓库里面放了很多对象。其中 state
就是数据源存放地,对应于一般 Vue 对象里面的 data
。
state
里面存放的数据是响应式的,Vue 组件从 store
中读取数据,若是store
中的数据发生改变,依赖这个数据的组件也会发生更新。
可以通过 mapState
把全局 state
和 getters
映射到当前组件的 computed
计算属性中使用。
Getter
getters
可以对 State
进行计算操作,它就是 Store
的计算属性。
虽然在组件内也可以做计算属性,但是 getters
可以在多组件之间复用,如果一个状态只在一个组件内使用,可以不用 getters
。
Mutation
更改 Vuex 的 store
中的状态的唯一方法是提交 mutation
。
Vuex 中的 mutation
非常类似于事件:每个 mutation
都有一个字符串的事件类型 (type
) 和 一个 回调函数 (handler
)。这个回调函数就是我们实际进行状态更改的地方,并且它会接受 state
作为第一个参数。
Mutation
就是提供存储设置 state
数据的方法。
Action
Action
类似于 mutation
。
不同在于:Action
提交的是 mutation
,而不是直接变更状态;Action
可以包含任意异步操作。
Module
当应用变得非常复杂时,store
对象就有可能变得相当臃肿。
为了解决这个问题,Vuex 允许我们将 store
分割成模块(module
)。每个模块拥有自己的 state
、mutation
、action
、getter
、甚至是嵌套子模块 module
——从上至下进行同样方式的分割。
且看官方的一个示例图:
从上图可以很好的看出这几个属性之间的调用关系(不过官方图没有画出getters的使用)
Vue Component
通过 dispatch
来调用 actions
提供的方法;actions
除了可以和 api
打交道外,还可以通过 commit
来调用 mutations
提供的方法;mutaions
将数据保存到 state
中;Vue Components
还可以通过 getters
提供的方法获取 state
中的数据。state
active
mution
);Component
本意就是为了减少耦合,现在这么用,和组件化的初衷相背。vue-router
动态路由以及如何获取传过来的动态参数?详细文档:Vue 之 路由跳转传参方式详解
vue-router
有哪几种导航钩子?详细文档:Vue-Router 之 路由导航守卫钩子详解
vue-router
中使用 active-class
的方法?在使用过程中碰到过什么问题?使用 active-class
有两种方法:
直接在路由js文件中配置 linkActiveClass
export default new Router({
linkActiveClass: 'active'
})
在
中写入 active-class
<router-link to="/home" class="menu-home" active-class="active">首页router-link>
如果使用第二种方法添加 active-class
,跳转到my页面后,两个router-link
始终都会有选中样式,代码如下:
<div class="menu-btn">
<router-link to="/" class="menu-home" active-class="active">
首页
router-link>
div>
<div class="menu-btn">
<router-link to="/my" class="menu-my" active-class="active">
我的
router-link>
div>
原因:可能是因为 to="/"
引起的,active-class
选择样式时根据路由中的路径去匹配,然后显示。例如在my页面中,路由为 localhost:8080/#/my
,那么to="/"
和 to="/my"
都可以匹配到,所以都会激活选中样式。
解决方案:要解决问题也有两种方式,都是通过加入一个 exact
属性
直接在路由js文件中配置 linkExactActiveClass
export default new Router({
linkExactActiveClass: 'active',
})
在
中写入 exact
<router-link to="/" class="menu-home" active-class="active" exact>首页router-link>
也可以在路由中加入重定向
<router-link to="/home" class="menu-home" active-class="active" exact>首页</router-link>
// 路由js中加入重定向
{
path: '/',
redirect: '/home'
}
Vue 为 MVVM
框架,当数据模型 data
变化时,页面视图会得到响应更新,其原理对 data
的 getter
/setter
方法进行拦截(Object.defineProperty
或者Proxy
),利用发布订阅的设计模式,在 getter
方法中进行订阅,在 setter
方法中发布通知,让所有订阅者完成响应。
在响应式系统中,Vue 会为数据模型 data 的每一个属性新建一个订阅中心作为发布者,而监听器 watch
、计算属性 computed
、视图渲染 template
/render
三个角色同时作为订阅者,对于监听器 watch
,会直接订阅观察监听的属性,对于计算属性 computed
和视图渲染 template
/render
,如果内部执行获取了 data
的某个属性,就会执行该属性的 getter
方法,然后自动完成对该属性的订阅,当属性被修改时,就会执行该属性的 setter
方法,从而完成该属性的发布通知,通知所有订阅者进行更新。
v-if
:判断是否隐藏;v-for
:数据循环遍历;v-bind:class
:绑定一个属性;v-model
:实现双向绑定;Vue如何创建自定义指令?
详细文档:Vue进阶 之 自定义指令详解
vue-loader
是什么?用途有哪些?vue-loader
是解析 .vue
文件的一个加载器。
用途:js 可以写 es6
、style 样式可以 scss
或 less
、template
可以加 jade
等。
scss
是什么?在 vue.cli
中的安装使用步骤是?有哪几大特性?scss
是 css
的预编译语言。css-loader
、node-loader
、sass-loader
等加载器模块;build
目录找到 webpack.base.config.js
,在 extends
属性中加一个拓展 .scss
;module
属性;style
标签加上 lang
属性 ,例如:lang=”scss”
;$变量名称 = 值
);key
?key
特性设置唯一的值,来标记以让 Vue
区分它们,否则 Vue
为了效率只会替换相同标签内部的内容。key
是给每一个 vnode
的唯一 id
,可以依靠 key
,更准确,更快的拿到 oldVnode
中对应的 vnode
节点。v-if
和 v-for
一起用?当 Vue
处理指令时,v-for
比 v-if
具有更高的优先级,通过 v-if
移动到容器元素,不会再重复遍历列表中的每个值。取而代之的是,只检查它一次,且不会在 v-if
为否的时候运算 v-for
。
VNode
是什么?虚拟 DOM
是什么?VNode
是 Vue
在页面上渲染的节点,及其子节点称为“虚拟节点 ( Virtual Node
)”,简写为“VNode
”。“虚拟 DOM
” 是由 Vue
组件树建立起来的整个 VNode
树的称呼。
Class
有几种方式?详细文档:Vue进阶 之 动态绑定Class 详解
slot
是什么?作用?原理?定义:slot
又名插槽,是 Vue 的内容分发机制,组件内部的模板引擎使用slot
元素作为承载分发内容的出口。插槽 slot
是子组件的一个模板标签元素,而这一个标签元素是否显示,以及怎么显示是由父组件决定的。
slot
又分三类:默认插槽,具名插槽和作用域插槽。
slot
没有指定 name
属性值的时候一个默认显示插槽,一个组件内只能有一个匿名插槽。name
属性的 slot
,一个组件可以出现多个具名插槽。slot-scope
:默认插槽、具名插槽的一个变体,可以是匿名插槽,也可以是具名插槽,该插槽的不同点是在子组件渲染作用域插槽时,可以将子组件内部的数据传递给父组件,让父组件根据子组件的传递过来的数据决定如何渲染该插槽。使用步骤:
;实现原理:
vm
实例化时,获取到父组件传入的 slot
标签的内容,存放在vm.$slot
中,默认插槽为 vm.$slot.default
,具名插槽为 vm.$slot.xxx
,xxx
为插槽名,当组件执行渲染函数时候,遇到 slot
标签,使用 $slot
中的内容进行替换,此时可以为插槽传递数据,若存在数据,则可称该插槽为作用域插槽。SSR
有了解吗?原理是什么?SSR
SSR
。DOM
节点映射到 Virtual DOM
即可,不需要重新创建DOM
节点,这个将数据和 HTML 同步的过程,又叫做客户端激活。SSR
的利弊:
优点:
SEO
优化:其实就是有利于爬虫来爬你的页面,因为部分页面爬虫是不支持执行 JavaScript 的,这种不支持执行 JavaScript 的爬虫抓取到的非 SSR
的页面会是一个空的 HTML 页面,而有了 SSR
以后,这些爬虫就可以获取到完整的 HTML 结构的数据,进而收录到搜索引擎中,有利于 SEO
优化。URL
之后已经得到了一个带有数据的 HTML 文本,浏览器只需要解析 HTML,直接构建 DOM
树就可以。而客户端渲染,需要先得到一个空的 HTML 页面,这个时候页面已经进入白屏,之后还需要经过加载并执行 JavaScript、请求后端服务器获取数据、JavaScript 渲染页面几个过程才可以看到最后的页面。特别是在复杂应用中,由于需要加载 JavaScript 脚本,越是复杂的应用,需要加载的 JavaScript 脚本就越多、越大,这会导致应用的首屏加载时间非常长,进而降低了体验感。弊端:
所以在使用服务端渲染 SSR
之前,需要开发者考虑投入产出比,比如大部分应用系统都不需要 SEO
,而且首屏时间并没有非常的慢,如果使用 SSR
反而小题大做了。
Vue 中通过 v-on
或其语法糖 @
指令来给元素绑定事件并且提供了事件修饰符,基本流程是进行模板编译生成 AST
(抽象语法树),生成 render
函数后并执行得到VNode
,VNode
生成真实 DOM
节点或者组件时候使用 addEventListener
方法进行事件绑定。
Vue 中的模板 template
无法被浏览器解析并渲染,因为这不属于浏览器的标准,不是正确的 HTML 语法,所以需要将 template
转化成一个 JavaScript 函数,这样浏览器就可以执行这一个函数并渲染出对应的 HTML 元素,就可以让视图跑起来了,这一个转化的过程,就成为模板编译。
模板编译又分三个阶段,解析parse
,优化 optimize
,生成 generate
,最终生成可执行函数 render
。
parse
阶段:使用大量的正则表达式对 template
字符串进行解析,将标签、指令、属性等转化为抽象语法树 AST
。optimize
阶段:遍历 AST
,找到其中的一些静态节点并进行标记,方便在页面重渲染的时候进行 diff
比较时,直接跳过这一些静态节点,优化 runtime
的性能。generate
阶段:将最终的 AST
转化为 render
函数字符串。template
预编译是什么?对于 Vue 组件来说,模板编译只会在组件实例化的时候编译一次,生成渲染函数之后在也不会进行编译。因此,编译对组件的 runtime
是一种性能损耗。
而模板编译的目的仅仅是将 template
转化为 render function
,这个过程,正好可以在项目构建的过程中完成,这样可以让实际组件在 runtime
时直接跳过模板渲染,进而提升性能,这个在项目构建的编译 template
的过程,就是预编译。
template
和 jsx
的有什么分别?对于 runtime
来说,只需要保证组件存在 render
函数即可,而我们有了预编译之后,我们只需要保证构建过程中生成 render
函数就可以。
在 webpack
中,我们使用 vue-loader
编译 .vue
文件,内部依赖的 vue-template-compiler
模块,在 webpack
构建过程中,将 template
预编译成 render
函数。
与 React 类似,在添加了 jsx
的语法糖解析器 babel-plugin-transform-vue-jsx
之后,就可以直接手写 render
函数。
所以,template
和 jsx
的都是 render
的一种表现形式,不同的是:
JSX
相对于 template
而言,具有更高的灵活性,在复杂的组件中,更具有优势,而 template
虽然显得有些呆滞,但是 template
在代码结构上更符合视图与逻辑分离的习惯,更简单、更直观、更好维护。
Virtual DOM
?Virtual DOM
是 DOM
节点在 JavaScript 中的一种抽象数据结构,之所以需要虚拟DOM
,是因为浏览器中操作 DOM
的代价比较昂贵,频繁操作 DOM
会产生性能问题。虚拟 DOM
的作用是在每一次响应式数据发生变化引起页面重渲染时,Vue 对比更新前后的虚拟 DOM
,匹配找出尽可能少的需要更新的真实 DOM
,从而达到提升性能的目的。
Diff
算法?我们使用了 Virtual DOM
来进行真实 DOM
的渲染,在页面更新的时候,也不能全量地将整棵 Virtual DOM
进行渲染,而是去渲染改变的部分,这时候就需要一个计算 Virtual DOM
树改变部分的算法了,这个算法就是 Diff
算法。
在对比新老虚拟 DOM
时:
patchVnode
,判断如何对该节点的子节点进行处理,先判断一方有子节点一方没有子节点的情况(如果新的 children
没有子节点,将旧的子节点移除)updateChildren
,判断如何对这些新老节点的子节点进行操作(diff
核心)。在 diff
中,只对同层的子节点进行比较,放弃跨级的节点比较,使得时间复杂从 O(n^3)
降低值 O(n)
,也就是说,只有当新旧 children
都为多个子节点时才需要用核心的 Diff
算法进行同层级比较。
nextTick
实现原理?参考文章:vue核心面试题(nextTick实现原理)
vue 实现按需加载有三种方式:
vue-router
配置 resolve
+require
加载 (vue异步组件技术)
这种方式就是 下一个组件生成一个js文件
路由配制如下:
{
path: '/demo',
name: 'Demo',
component: resolve => require(['../components/Demo'], resolve)
}
es6 提案的 import()
方法
官方文档 :路由懒加载
// 下面2行代码,没有指定 webpackChunkName,每个组件打包成一个js文件。
const ImportFuncDemo1 = () => import('../components/ImportFuncDemo1')
const ImportFuncDemo2 = () => import('../components/ImportFuncDemo2')
// 下面2行代码,指定了相同的 webpackChunkName,会合并打包成一个js文件。
const ImportFuncDemo1 = () => import(/* webpackChunkName: 'ImportFuncDemo' */ '../components/ImportFuncDemo1')
const ImportFuncDemo2 = () => import(/* webpackChunkName: 'ImportFuncDemo' */ '../components/ImportFuncDemo2')
export default new Router({
routes: [
{
path: '/importfuncdemo1',
name: 'ImportFuncDemo1',
component: ImportFuncDemo1
},
{
path: '/importfuncdemo2',
name: 'ImportFuncDemo2',
component: ImportFuncDemo2
}
]
})
webpack 提供的 resolve
+ require.ensure()
vue-router
配置路由,使用 webpack
的 require.ensure
技术,也可以实现按需加载。
这种情况下,多个路由指定相同的 chunkName
,会合并打包成一个js文件。
import home from '../../common/home.vue'
require.ensure()
引入方式:{
path: '/promisedemo',
name: 'PromiseDemo',
component: resolve => require.ensure([], () => resolve(require('../components/PromiseDemo')), 'demo')
},
{
path: '/hello',
name: 'Hello',
component: resolve => require.ensure([], () => resolve(require('../components/Hello')), 'demo')
}
// 这两个路由最终会打包成一个demo.js文件
关于 Vue 3.0 的更多面试题,我也进行了整理,请移步至:面试题 Vue3.0 篇
a. 重构响应式系统,使用 Proxy
替换 Object.defineProperty
,使用 Proxy
优势:
Object.defineProperty
一样遍历每个属性,有一定的性能提升;apply
、ownKeys
、has
等13种方法,而 Object.defineProperty
不行;b. 新增 Composition
API,更好的逻辑复用和代码组织。
c. 重构 Virtual DOM
slot
优化,将 slot
编译为 lazy
函数,将 slot
的渲染的决定权交给子组件;d. 代码结构调整,更便于 Tree shaking
,使得体积更小。
e. 使用 Typescript
替换 Flow
持续更新中。。。欢迎大家关注
12个vue高频原理面试题(附分析)
参考文章:
1、No Silver Bullet:https://blog.csdn.net/sunhuaqiang1/article/details/89450535
2、亲爱的阿乾:https://segmentfault.com/a/1190000038848131
感谢原作者辛苦付出