大厂面试题分享 面试题库
前后端面试题库 (面试必备) 推荐:★★★★★
地址:前端面试题库 web前端面试题库 VS java后端面试题库大全
一、MVVM
原理
在Vue2
官方文档中没有找到Vue
是MVVM
的直接证据,但文档有提到:虽然没有完全遵循MVVM模型
,但是 Vue 的设计也受到了它的启发,因此在文档中经常会使用vm
(ViewModel 的缩写) 这个变量名表示 Vue 实例。
为了感受MVVM模型
的启发,我简单列举下其概念。
MVVM是Model-View-ViewModel的简写,由三部分构成:
- Model: 模型持有所有的数据、状态和程序逻辑
- View: 负责界面的布局和显示
- ViewModel:负责模型和界面之间的交互,是Model和View的桥梁
二、SPA
单页面应用
单页Web应用(single page web application,SPA),就是只有一张Web页面的应用,是加载单个HTML页面并在用户与应用程序交互时动态更新该页面的Web应用程序。我们开发的Vue
项目大多是借助个官方的CLI
脚手架,快速搭建项目,直接通过new Vue
构建一个实例,并将el:'#app'
挂载参数传入,最后通过npm run build
的方式打包后生成一个index.html
,称这种只有一个HTML
的页面为单页面应用。
当然,vue
也可以像jq
一样引入,作为多页面应用的基础框架。
三、Vue
的特点
- 清晰的官方文档和好用的
api
,比较容易上手。 - 是一套用于构建用户界面的渐进式框架,将注意力集中保持在核心库,而将其他功能如路由和全局状态管理交给相关的库。
- 使用 Virtual DOM。
- 提供了响应式 (Reactive) 和组件化 (Composable) 的视图组件。
四、Vue
的构建入口
vue使用过程中可以采用以下两种方式:
- 在vue脚手架中直接使用,参考文档:
https://cn.vuejs.org/v2/guide/installation.html
- 或者在html文件的头部通过静态文件的方式引入:
那么问题来了,使用的或者引入的到底是什么?
答:引入的是已经打包好的vue.js文件,通过rollup构建打包所得。
构建入口在哪里?
答:在vue
源码的package.json文件中:
"scripts": { // ... "build": "node scripts/build.js", "build:ssr": "npm run build -- web-runtime-cjs,web-server-renderer", "build:weex": "npm run build -- weex", // ... },
通过执行npm run build的时候,会进行scripts/build.js文件的执行,npm run build:ssr和npm run build:weex的时候,将ssr和weex作为参数传入,按照参数构建出不一样的vue.js打包文件。
所以说,vue
中的package.json
文件就是构建的入口,具体构建流程可以参考vue2入口:构建入口。
五、对import Vue from "vue"
的理解
在使用脚手架开发项目时,会有一行代码import Vue from "vue"
,那么这个Vue
指的是什么。
答:一个构造函数。
function Vue (options) { if (process.env.NODE_ENV !== 'production' && !(this instanceof Vue) ) { warn('Vue is a constructor and should be called with the `new` keyword') } this._init(options) } initMixin(Vue) stateMixin(Vue) eventsMixin(Vue) lifecycleMixin(Vue) renderMixin(Vue)
我们开发中引入的Vue
其实就是这个构造函数,而且这个构造函数只能通过new Vue
的方式进行使用,否则会在控制台打印警告信息。定义完后,还会通过initMixin(Vue)
、stateMixin(Vue)
、eventsMixin(Vue)
、lifecycleMixin(Vue)
和renderMixin(Vue)
的方式为Vue
原型中混入方法。我们通过import Vue from "Vue"
引入的本质上就是一个原型上挂在了好多方法的构造函数。
六、对new Vue
的理解
// main.js文件 import Vue from "vue"; var app = new Vue({ el: '#app', data() { return { msg: 'hello Vue~' } }, template: `<div>{{msg}}</div>`, }) console.log(app);
new Vue
就是对构造函数Vue
进行实例化,执行结果如下:
可以看出实例化后的实例中包含了很多属性,用来对当前app
进行描述,当然复杂的Vue
项目这个app
将会是一个树结构,通过$parent
和$children
维护父子关系。
new Vue
的过程中还会执行this._init
方法进行初始化处理。
七、编译
虚拟DOM
的生成必须通过render
函数实现,render
函数的产生是在编译阶段完成,核心代码如下:
export const createCompiler = createCompilerCreator(function baseCompile ( template: string, options: CompilerOptions ): CompiledResult { const ast = parse(template.trim(), options) if (options.optimize !== false) { optimize(ast, options) } const code = generate(ast, options) return { ast, render: code.render, staticRenderFns: code.staticRenderFns } })
主要完成的功能是:
- 通过
const ast = parse(template.trim(), options)
将template
转换成ast
树 - 通过
optimize(ast, options)
对ast
进行优化 - 通过
const code = generate(ast, options)
将优化后的ast
转换成包含render
字符串的code
对象,最终render
字符串通过new Function
转换为可执行的render
函数
八、虚拟DOM
先看浏览器对HTML
的理解:
<div> <h1>My title</h1> Some text content <!-- TODO: Add tagline --> </div>
当浏览器读到这些代码时,它会建立一个DOM树来保持追踪所有内容,如同你会画一张家谱树来追踪家庭成员的发展一样。 上述 HTML 对应的 DOM 节点树如下图所示:
每个元素都是一个节点。每段文字也是一个节点。甚至注释也都是节点。一个节点就是页面的一个部分。就像家谱树一样,每个节点都可以有孩子节点 (也就是说每个部分可以包含其它的一些部分)。
再看Vue
对HTML template
的理解
Vue 通过建立一个虚拟 DOM 来追踪自己要如何改变真实 DOM。因为它所包含的信息会告诉 Vue 页面上需要渲染什么样的节点,包括及其子节点的描述信息。我们把这样的节点描述为“虚拟节点 (virtual node)”,也常简写它为“VNode”。“虚拟 DOM”是我们对由 Vue 组件树建立起来的整个 VNode 树的称呼。
简言之,浏览器对HTML的理解是DOM树,Vue对HTML
的理解是虚拟DOM,最后在patch
阶段通过DOM操作的api将其渲染成真实的DOM节点。
九、模板或者组件渲染
Vue
中的编译会执行到逻辑vm._update(vm._render(), hydrating)
,其中的vm._render
执行会获取到vNode
,vm._update
就会对vNode
进行patch
的处理,又分为模板渲染和组件渲染。
十、数据响应式处理
Vue
的数据响应式处理的核心是Object.defineProperty
,在递归响应式处理对象的过程中,为每一个属性定义了一个发布者dep
,当进行_render
函数执行时会访问到当前值,在get
中通过dep.depend
进行当前Watcher
的收集,当数据发生变化时会在set
中通过dep.notify
进行Watcher
的更新。
数据响应式处理以及发布订阅者模式的关系请参考vue2从数据变化到视图变化:发布订阅模式
十一、this.$set
const app = new Vue({ el: "#app", data() { return { obj: { name: "name-1" } }; }, template: `<div @click="change">{{obj.name}}的年龄是{{obj.age}}</div>`, methods: { change() { this.obj.name = 'name-2'; this.obj.age = 30; } } });
以上例子执行的结果是:
name-1的年龄是
当点击后依然是:
name-2的年龄是
可以看出点击后,obj
的name
属性变化得到了视图更新,而age
属性并未进行变化。
name
属性响应式的过程中锁定了一个发布者dep
,在当前视图渲染时在发布者dep
的subs
中做了记录,一旦其发生改变,就会触发set
方法中的dep.notify
,继而执行视图的重新渲染。然而,age
属性并未进行响应式的处理,当其改变时就不能进行视图渲染。
十二、组件注册
组件的使用是先注册后使用,又分为:
- 全局注册:可以直接在页面中使用
- 局部注册:使用时需要通过
import xxx from xxx
的方式引入,并且在当前组件的选项components
中增加局部组件的名称。
十三、异步组件
Vue单页面应用中一个页面只有一个
承载所有节点,因此复杂项目可能会出现首屏加载白屏等问题,Vue异步组件就很好的处理了这问题。
十四、this.$nextTick
因为通过new
实例化构造函数Vue
的时候会执行初始化方法this._init
,其中涉及到的方法大多都是同步执行。nextTick
在vue中是一个很重要的方法,在new Vue
实例化的同步过程中将一些需要异步处理的函数推到异步队列中去,可以等new Vue
所有的同步任务执行完后,再执行异步队列中的函数。
十五、keep-alive
内置组件
vue
中支持组件化,并且也有用于缓存的内置组件keep-alive
可直接使用,使用场景为路由组件
和动态组件
。
activated
表示进入组件的生命周期,deactivated
表示离开组件的生命周期include
表示匹配到的才缓存,exclude
表示匹配到的都不缓存max
表示最多可以缓存多少组件
十六、生命周期
vue
中的生命周期有哪些?
答案:11
个,分别为beforeCreate
、created
、beforeMount
、mounted
、beforeUpdate
、updated
、activated
、deactivated
、beforeDestroy
、destroyed
和errorCaptured
。
十七、v-show
和v-if
的区别
先看v-if
和v-show
的使用场景:
(1)v-if
更多的使用在需要考虑白屏时间或者切换次数很少的场景
(2)v-show
更多使用在transition
控制的动画或者需要非常频繁地切换的场景
再从底层实现思路上分析:
(1)v-if
条件为false
时,会生成空的占位注释节点,那么在考虑首页白屏时间时,选用v-if
比较合适。条件从false
变化为true
的话会从空的注释节点变成真实节点,条件再变为false
时真实节点又会变成注释节点,如果切换次数比较多,那么开销会比较大,频繁切换场景不建议使用v-if
。
(2)v-show
条件为false
时,会生成真实的节点,只是为当前节点增加了display:none
来控制其隐藏,相比v-if
生成空的注释节点其首次渲染开销是比较大的,所以不建议用在考虑首屏白屏时间的场景。如果我们频繁切换v-show
的值,从display:none
到display:block
之间的切换比起空的注释节点和真实节点的开销要小很多,这种场景就建议使用v-show
。
十八、v-for
中key
的作用
在v-for
进行循环展示过程中,当数据发生变化进行渲染的过程中,会进行新旧节点列表的比对。首先新旧vnode
列表首先通过首首
、尾尾
、首尾
和尾首
的方式进行比对,如果key
相同则采取原地复用的策略进行节点的移动。
如果首尾两两比对的方式找不到对应关系,继续通过key
和vnode
的对应关系进行寻找。
如果key
和vnode
对应关系中找不到,继续通过sameVnode
的方式在未比对的节点中进行寻找。
如果都找不到,则将其按照新vnode
进行createElm
的方式进行创建,这种方式是比节点移动的方式计算量更大。
最后将旧的vnode
列表中没有进行匹配的vnode
中的vnode.elm
在父节点中移除。
简单总结就是,新的vnode
列表在旧的vnode
列表中去寻找具有相同的key
的节点进行原地复用,如果找不到则通过创建的方式createElm
去创建一个,如果旧的vnode
列表中没有进行匹配则在父节点中移除其vnode.elm
。这就是原地复用逻辑的大体实现。
十九、v-for
和v-if
能同时使用吗
答案是:用了也能出来预期的效果,但是会有性能浪费。
同时包含v-for
和v-if
的template
模板在编辑阶段会执行v-for
比v-if
优先级更高的编译流程;在生成vnode
的阶段,会包含属性isComment
为true
的空白占位vnode
;在patch
阶段,会生成真实的占位节点。虽然一个空的占位节点无妨,但是如果数据量比较大的话,也是一个性能问题。
当然,可以在获取到数据(一般是在beforeCreate
或者created
阶段)时进行过滤处理,也可以通过计算属性对其进行处理。
二十、vue
中的data
为什么是函数
答案是:是不是一定是函数,得看场景。并且,也无需担心什么时候该将data
写为函数还是对象,因为vue
内部已经做了处理,并在控制台输出错误信息。
场景一:new Vue({data: ...})
这种场景主要为项目入口或者多个html
页面各实例化一个Vue
时,这里的data
即可用对象的形式,也可用工厂函数返回对象的形式。因为,这里的data
只会出现一次,不存在重复引用而引起的数据污染问题。
场景二:组件场景中的选项
在生成组件vnode
的过程中,组件会在生成构造函数的过程中执行合并策略:
// data合并策略 strats.data = function ( parentVal, childVal, vm ) { if (!vm) { if (childVal && typeof childVal !== 'function') { process.env.NODE_ENV !== 'production' && warn( 'The "data" option should be a function ' + 'that returns a per-instance value in component ' + 'definitions.', vm ); return parentVal } return mergeDataOrFn(parentVal, childVal) } return mergeDataOrFn(parentVal, childVal, vm) };
如果合并过程中发现子组件的数据不是函数,即typeof childVal !== 'function'
成立,进而在开发环境会在控制台输出警告并且直接返回parentVal
,说明这里压根就没有把childVal
中的任何data
信息合并到options
中去。
二十一、this.$watch
使用场景:用来监听数据的变化,当数据发生变化的时候,可以做一些业务逻辑的处理。
配置参数:
deep
:监听数据的深层变化immediate
:立即触发回调函数
实现思路: Vue
构造函数定义完成以后,在执行stateMixin(Vue)
时为Vue.prototype
上定义$watch
。该方法通过const watcher = new Watcher(vm, expOrFn, cb, options)
进行Watcher
的实例化,将options
中的user
属性设置为true
。并且,$watch
逻辑结束的会返回函数function unwatchFn () { watcher.teardown() }
,用来取消侦听的函数。
二十二、计算属性和侦听属性的区别
相同点: 两者都是Watcher
实例化过程中的产物
计算属性:
- 使用场景:模板内的表达式主要用于简单运算,对于复杂的计算逻辑可以用计算属性
- 计算属性是基于它们的响应式依赖进行缓存的,当依赖的数据未发生变化时,多次调用无需重复执行函数
- 计算属性计算结果依赖于
data
中的值 - 同步操作,不支持异步
侦听属性:
- 使用场景:当需要在数据变化时执行异步或开销较大的操作时,可以用侦听属性
- 可配置参数:可以通过配置
immediate
和deep
来控制立即执行和深度监听的行为 - 侦听属性侦听的是
data
中定义的
二十三、v-model
// main.js new Vue({ el: "#app", data() { return { msg: "" }; }, template: `<div> <input v-model="msg" placeholder="edit me"> <p>msg is: {{ msg }}</p> </div>` });
普通input:input
中的v-model
,最终通过target.addEventListener
处理成在节点上监听input
事件function($event){msg=$event.target.value}}
的形式,当input
值变化时msg
也跟着改变。
// main.js const inputBox = { template: `<input @input="$emit('input', $event.target.value)">`, }; new Vue({ el: "#app", template: `<div> <input-box v-model="msg"></input-box> <p>{{msg}}</p> </div>`, components: { inputBox }, data() { return { msg: 'hello world!' }; }, });
组件:v-model
在组件中则通过给点击事件绑定原生事件,当触发到$emit
的时候,再进行回调函数ƒunction input($$v) {msg=$$v}
的执行,进而达到子组件修改父组件中数据msg
的目的。
二十四、v-slot
v-slot
产生的主要目的是,在组件的使用过程中可以让父组件有修改子组件内容的能力,就像在子组件里面放了个插槽,让父组件往插槽内塞入父组件中的楔子;并且,父组件在子组件中嵌入的楔子也可以访问子组件中的数据。v-slot
的产生让组件的应用更加灵活。
1、具名插槽
let baseLayout = { template: `<div class="container"> <header> <slot name="header"></slot> </header> <main> <slot></slot> </main> <footer> <slot name="footer"></slot> </footer> </div>`, data() { return { url: "" }; } }; new Vue({ el: "#app", template: `<base-layout> <template v-slot:header> <h1>title-txt</h1> </template> <p>paragraph-1-txt</p> <p>paragraph-2-txt</p> <template v-slot:footer> <p>foot-txt</p> </template> </base-layout>`, components: { baseLayout } });
引入的组件baseLayout
中的template
被添加了属性v-slot:header
和v-slot:footer
,子组件中定义了对应的插槽被添加了属性name="header"
和name="footer"
,未被进行插槽标识的内容被插入到了匿名的中。
2、作用域插槽
let currentUser = { template: `<span> <slot name="user" v-bind:userData="childData">{{childData.firstName}}</slot> </span>`, data() { return { childData: { firstName: "first", lastName: "last" } }; } }; new Vue({ el: "#app", template: `<current-user> <template v-slot:user="slotProps">{{slotProps.userData.lastName}}</template> </current-user>`, components: { currentUser } });
当前例子中作用域插槽通过v-bind:userData="childData"
的方式,将childData
作为参数,父组件中通过v-slot:user="slotProps"
的方式进行接收,为父组件使用子组件中的数据提供了可能。
二十五、Vue.filters
filters
类似于管道流可以将上一个过滤函数的结果作为下一个过滤函数的第一个参数,又可以在其中传递参数让过滤器更灵活。
// main.js文件 import Vue from "vue"; Vue.filter("filterEmpty", function(val) { return val || ""; }); Vue.filter("filterA", function(val) { return val + "平时周末的"; }); Vue.filter("filterB", function(val, info, fn) { return val + info + fn; }); new Vue({ el: "#app", template: `<div>{{msg | filterEmpty | filterA | filterB('爱好是', transformHobby('chess'))}}</div>`, data() { return { msg: "张三" }; }, methods: { transformHobby(type) { const map = { bike: "骑行", chess: "象棋", game: "游戏", swimming: "游泳" }; return map[type] || "未知"; } } });
其中我们对msg
通过filterEmpty
、filterA
和filterB('爱好是', transformHobby('chess'))}
进行三层过滤。
二十六、Vue.use
- 作用:
Vue.use
被用来安装Vue.js插件,例如vue-router
、vuex
、element-ui
。 install
方法:如果插件是一个对象,必须提供install
方法。如果插件是一个函数,它会被作为install
方法。install
方法调用时,会将Vue
作为参数传入。- 调用时机:该方法需要在调用
new Vue()
之前被调用。 - 特点:当 install 方法被同一个插件多次调用,插件将只会被安装一次。
二十七、Vue.extend
和选项extends
1、Vue.extend
Vue.extend
使用基础Vue
构造器创建一个“子类”,参数是一个包含组件选项的对象,实例化的过程中可以修改其中的选项,为实现功能的继承提供了思路。
new Vue({ el: "#app", template: `<div><div id="person1"></div><div id="person2"></div></div>`, mounted() { // 定义子类构造函数 var Profile = Vue.extend({ template: '<p @click="showInfo">{{name}} 喜欢 {{fruit}}</p>', data: function () { return { name: '张三', fruit: '苹果' } }, methods: { showInfo() { console.log(`${this.name}喜欢${this.fruit}`) } } }) // 实例化1,挂载到`#person1`上 new Profile().$mount('#person1') // 实例化2,并修改其`data`选项,挂载到`#person2`上 new Profile({ data: function () { return { name: '李四', fruit: '香蕉' } }, }).$mount('#person2') }, });
在当前例子中,通过Vue.extend
构建了子类构造函数Profile
,可以通过new Profile
的方式实例化无数个vm
实例。我们定义初始的template
、data
和methods
供vm
进行使用,如果有变化,在实例的过程中传入新的选项参数即可,比如例子中实例化第二个vm
的时候就对data
进行了调整。
2、选项extends
extends
允许声明扩展另一个组件 (可以是一个简单的选项对象或构造函数),而无需使用 Vue.extend
。这主要是为了便于扩展单文件组件,以实现组件继承的目的。
const common = { template: `<div>{{name}}</div>`, data() { return { name: '表单' } } } const create = { extends: common, data() { return { name: '新增表单' } } } const edit = { extends: common, data() { return { name: '编辑表单' } } } new Vue({ el: "#app", template: `<div> <create></create> <edit></edit> </div>`, components: { create, edit, } });
当前极简demo中定义了公共的表单common
,然后又在新增表单组件create
和编辑表单组件edit
中扩展了common
。
二十八、Vue.mixin
和选项mixins
全局混入和局部混入视情况而定,主要区别在全局混入是通过Vue.mixin
的方式将选项混入到了Vue.options
中,在所有获取子组件构建函数的时候都将其进行了合并,是一种影响全部组件的混入策略。
而局部混入是将选项通过配置mixins
选项的方式合并到当前的子组件中,只有配置了mixins
选项的组件才会受到混入影响,是一种局部的混入策略。
二十九、Vue.directive
和directives
1、使用场景
主要用于对于DOM的操作,比如:文本框聚焦,节点位置控制、防抖节流、权限管理、复制操作等功能
2、钩子函数
bind
:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。inserted
:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。update
:所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新。componentUpdated
:指令所在组件的 VNode 及其子 VNode 全部更新后调用。unbind
:只调用一次,指令与元素解绑时调用。
3、钩子函数参数
el
:指令所绑定的元素,可以用来直接操作 DOM。binding
:一个对象,包含以下 property:
name
:指令名,不包括v-
前缀。value
:指令的绑定值,例如:v-my-directive="1 + 1"
中,绑定值为2
。oldValue
:指令绑定的前一个值,仅在update
和componentUpdated
钩子中可用。无论值是否改变都可用。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
:上一个虚拟节点,仅在update
和componentUpdated
钩子中可用。
4、动态指令参数
指令的参数可以是动态的。例如,在 v-mydirective:[argument]="value"
中,argument
参数可以根据组件实例数据进行更新!这使得自定义指令可以在应用中被灵活使用。
【面试题】6月 vue核心面试题汇总(二):https://developer.aliyun.com/article/1415037