前言:
2020年是多灾多难的一年,疫情持续至今,到目前,全世界的经济都受到不同程序的影响,各大公司裁员,在这样一片严峻的形式下,找工作更是难上加难。
企业的门槛提高,第一,对于学历的要求,必须学信网可查的统招本科;第二,对于技术的掌握程序,更多的是底层原理,项目经验,等等。
下面是面试几周以来,总结的一些面试中常被问到的题目,还有吸取的一些前辈们分享的贴子,全部系统的罗列出来,希望能够帮到正在面试的人。
1. Vue原理
Vue是采用数据劫持配合发布者-订阅者模式,通过
Object.defineProperty()
来劫持各个属性的getter
和setter
。在数据发生变化的时候,发布消息给依赖收集器,去通知观察者,做出对应的回调函数去更新视图。
具体执行流程:
1.MVVM
作为绑定入口,整合Observe
,Compil
和Watcher
三者,通过Observe
来监听model
的变化。
2.通过Compil
来解析编译模版指令,最终利用Watcher
搭起Observe
和Compil
之前的通信桥梁。
3.从而达到数据变化 => 更新视图,视图交互变化(input) => 数据model变更的双向绑定效果。
2. Vue的生命周期
单个组件的生命周期
1.beforeCreated
2.created
3.beforeMounted
4.mounted
5.activated
6.beforeUpdated
7.updated
8.deactivated
9.beforeDestory
10.destoryed
父子组件的生命周期顺序
1.父组件先执行(beforeCreated
->created
->beforeMounted
)函数
2.父组件挂载前,子组件再执行(beforeCreated
->created
->beforeMounted
->mounted
)。
3.子组件挂载完成之后,最后执行父组件挂载函数mounted
。
4.接着是下面的三种情况:
(1)更新
只更新父或子: 局部更新,父或子beforeUpdate -> updated
同时更新父和子: 父beforeUpdate -> 子beforeUpdate -> 子updated -> 父updated
(2)销毁父组件
父beforeDestroy -> 子beforeDestroy -> 子destroyed -> 父destroyed
(3)激活父组件
子activated -> 父activated -> 停止 -> 子deactivated -> 父deactivated
3. Vue响应式原理
使用
defineReactive
函数将深度遍历一个对象(或循环遍历数组),将对象构建成响应式式对象。明显的标志就是ob
属性 实质是通过Object.defineProperty
对属性(深度遍历)进行setter
和getter
拦截。get
中主要做依赖收集dep.depend()
【子属性也收集该依赖】set
中主要做派发更新 (新的值才更新)dep.notify()
调用dep
数组中每个渲染watcher
的update
方法更新DOM
响应式对象使用应该注意哪些点
1.对象的新增属性,数组的新增元素,因为不是响应式的,所以不会触发视图渲染。此时应该使用$set
2.改变某一下标的元素,因为Vue
未实现监听,所以不会触发视图渲染。此时应该使用$set
3.删除对象的属性,数组下标的某一元素,确保删除属性能触发视图渲染。此时应该使用$delete
4. data为什么必须是函数而不是对象?
- 首先举个栗子
var option = {
data: {
a: 1
}
}
class component {
constructor(opt) {
this.data = opt.data;
Object.defineProperty(this.data, `a`, {
get: function () {
console.log('get val');
return this._a;
},
set: function (newVal) {
console.log('set val:' + newVal);
this._a = newVal;
}
});
}
}
var c1 = new component(option);
var c2 = new component(option);
c1.data.a = 3;
c2.data.a = 5;
console.log(`c1 : ` + c1.data.a);//c1 : 5
console.log(`c2 : ` + c2.data.a);//c2 : 5
示例代码中 Object.defineProperty
把传进来组件中的data
的 a
属性转化为 getter
和 setter
,可以实现 data.a
的数据监控。这个转化是Vue.js
响应式的基石。
这样就不难理解data
为什么不能是对象了,如果传进来是对象,new
出来的两个实例同时引用一个对象,那么当你修改其中一个属性的时候,另外一个实例也会跟着改。
总结:
1.对象是对于内存地址的引用。直接定义个对象的话,组件之间都会使用这个对象,这样会造成组件之间数据相互影响。
2.组件就是为了抽离开来提高复用的, 如果组件之间数据默认存在关系,就违背了组件的意义。
3.函数 return
一个新的对象,其实还是为 data
定义了一个对象, 只不过这个对象是内存地址的单独引用了,这样组件之间就不会存在数据干扰的问题。
5. v-model基本原理
首先在编译阶段,
v-model
被当成普通指令解析到el.directives
,然后在解析v-model
的时候,会根据节点的不同请求去执行不同的逻辑。
1.如果节点是select
、checkbox
,radio
,则监听的是change
事件
2.如果节点是input
,textarea
,则监听一般是input
事件,在.lazy
下的情况下是change
事件。
3.如果节点是组件,则是使用自定义的回调函数在运行的时候,通过相应事件的监听函数去更改数据
v-model
实质是一种语法糖,换成模板写法如下:
- 组件中使用
v-model
// 自定义属性名和事件名需要一致
export default {
model: {
prop: 'num', // 自定义属性名
event: 'addNum' // 自定义事件名
},
props: {
num: Number,
},
methods: {
add() {
this.$emit('addNum', this.num + 1)
}
}
}
6. vue2.0响应式的缺陷
Object.defineProperty
无法监控到数组下标的变化,导致通过数组下标添加元素,不能实时响应Object.defineProperty
本身是可以监控到数组下标的变化的,但是在Vue
中,从性能/体验的性价比考虑,弃用了这个特性。Object.defineProperty
只能劫持对象的属性,从而需要对每个对象,每个属性进行遍历,如果,属性值是对象,还需要深度遍历
7. Vue3.0为什么使用Proxy实现响应式
-
Proxy
可以劫持整个对象,并返回一个新的对象。 -
Proxy
不仅可以代理对象,还可以代理数组。还可以代理动态增加的属性。
8. Vue的通信方式
-
props
和$emit
-
$parent
和$children
-
vueBus
: 中央事务总线,一个发布订阅中心 -
vuex
:状态树管理(单一的) -
ref
和refs
-
$attr
和$listener: v-bind="$attrs" v-on="$listeners"
-
provide
和inject
: 实质就是递归父组件帮你寻找对应的provider -
provide
和inject
绑定并不是可响应的。这是刻意为之的。然而,如果你传入了一个可监听的对象,那么其对象的属性还是可响应的。
9. Vue.nextTick的原理
Vue.nextTick
是在执行render
渲染后运行的,即在render
渲染后的下一次tick
(event loop
最开始的时候执行)Vue.nextTikc
的降级顺序(优先使用)Promise.then(microtask) , MutationObserver(microtask) , setImmediate(task) , setTimeout(fn, 0)(task)
Vue
在修改数据后,视图不会立刻更新,而是等同一事件循环中的所有数据变化完成之后,再统一进行视图更新。应用场景
1.在Vue生命周期的created()
钩子函数进行DOM操作一定要放到Vue.nextTick()
的回调函数中。
2.在数据变化后要执行的某个操作,而这个操作需要使用随数据改变而改变的DOM结构的时候,这个操作都应该放进Vue.nextTick()
的回调函数中。-
10. new Vue会做什么操作
Vue.prototype._init = function (options) {
const vm = this
// ...忽略,从第45行看起
if (process.env.NODE_ENV !== 'production') {
initProxy(vm) // 作用域代理,拦截组件内访问其它组件的数据
} else {
vm._renderProxy = vm
}
// expose real self
vm._self = vm
initLifecycle(vm) // 建立父子组件关系,在当前实例上添加一些属性和生命周期标识。
initEvents(vm) // 用来存放除 @hook:生命周期钩子名称="绑定的函数"事件的对象。如:$on、 $emit等
initRender(vm) // 用于初始化 $slots、 $attrs、 $listeners
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props // 初始化 inject,一般用于更深层次的组件通信,相当于加强版子组件的 props。用于组件库开发较多
initState(vm) // 是很多选项初始化的汇总,包括:props、methods、data、computed和watch 等。
initProvide(vm) // resolve provide after data/props // 初始化 provide
callHook(vm, 'created')
// ...忽略
if (vm.$options.el) {
vm.$mount(vm.$options.el) // 挂载实例
}
}
11. Vue的diff原理
- 主要执行的是patch函数。主要流程如下:
function patch (oldVnode, vnode, hydrating, removeOnly)
1.如果oldVnode
不存在,即是新添加的节点,则创建vnode
的DOM
2.如果不是真实的节点且是相同类型的节点,则进入结点diff
,即patchVnode
函数。否则会用新的节点替换老的。这里的相同类型指的是具有相同的key
值和一些其他条件,例如标签相同等等。
3.如果oldVnode === vnode
,则认为没有变化, 如果oldVnode
的isAsyncPlaceholder
属性为true
时,跳过检查异步组件,return
;
4.如果oldVnode
跟vnode
都是静态节点(实例不会发生变化),且具有相同的key
,并且当vnode
是克隆节点或是v-once
指令控制的节点时,则把oldVnode.elm
和oldVnode.child
都复制到vnode
上;
5.如果vnode
不是文本节点或注释节点
(1)如果vnode
和oldVnode
都有子节点并且两者的子节点不一致时,就调用updateChildren
更新子节点
(2)如果只有vnode
有子节点,则调用addVnodes
创建子节点
(3)如果只有oldVnode
有子节点,则调用removeVnodes
把这些子节点都删除
(4)如果vnode
文本为undefined
,则清空vnode.elm
文本;
6.如果vnode
是文本节点但是vnode.text != oldVnode.text
时只需要更新vnode.elm
的文本内容就可以。
7.在updateChildren
主要是子节点数组对比,思路是通过首尾两端对比,如果是相同类型的节点也会使用patchVnode
函数。
- 在做对比中
key
的作用 主要是
1.决定节点是否可以复用
2.建立key-index
的索引,主要是替代遍历,提升性能
12. computed 和 watcher
computed
是计算属性,依赖其他属性计算,并且computed
的值有缓存,只有当计算值发生变化才会返回内容。所以,对于任何复杂逻辑,你都应当使用计算属性。watch
主要用于监控vue
实例的变化,它监控的变量当然必须在data
里面声明才可以,它可以监控一个变量,也可以是一个对象。比较适合的场景是一个数据影响多个数据。watch
支持异步。watcher的分类
1.内部-watcher
vue
组件上的每一条数据绑定指令(例如{{myData}}
)和computed
属性,通过compile
最后都会生成一个对应的watcher
对象。
2.user--watcher
在watch
属性中,由用户自己定义的,都属于这种类型,即只要监听的属性改变了,都会触发定义好的回调函数
3.render-watcher
每一个组件都会有一个render-watcher, function () {vm._update(vm._render(), hydrating);}
, 当data / computed
中的属性改变的时候,会调用该render-watcher
来更新组件的视图
watcher
也有固定的执行顺序,分别是:内部-watcher -> user-watcher -> render-watcher
13. Vue指令
// 全局
Vue.directive('my-click', config)
// 局部
new Vue({
directives:{
focus: config // v-focus
}
}})
- 配置参数
1.一个指令定义对象可以提供如下几个钩子函数 (均为可选):
(1)bind
:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
(2)inserted
:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。
(3)update
:所在组件的 VNode
更新时调用,但是可能发生在其子 VNode
更新之前。指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新。
(4)componentUpdated
:指令所在组件的 VNode
及其子 VNode
全部更新后调用。
(5)unbind
:只调用一次,指令与元素解绑时调用。
每个钩子函数都有四个参数el、binding、vnode 和 oldVnode
14. 混入 (mixin)
- 混入 (
mixin
) 提供了一种非常灵活的方式,来分发 Vue 组件中的可复用功能。 - 全局和局部
mixin
var mixin = {
data: function () {
return {
message: 'hello',
foo: 'abc'
}
}
}
Vue.mixin(mixin)
new Vue({
mixins: [mixin],
})
- 合并策略
1.钩子函数将合并成数组,且混入的函数先执行
2.其他的值为对象的将被合并为同一个对象。两个对象键名冲突时,取组件对象的键值对。
3.默认的合并策略可以使用下面的方面更改
Vue.config.optionMergeStrategies.myOption = function (toVal, fromVal) {
// 返回合并后的值
}
15. vue-router
Vue Router
是Vue.js
官方的路由管理器。它和Vue.js
的核心深度集成,让构建单页面应用变得易如反掌
和 和 路由模式
1.HashHistory
模式:实质是监听onhashchange
事件 (window.location API - location.hash
)
2.HTML5History
模式:实质是使用h5的window.history API
, 监听popstate
事件(pushState, replaceState
)。使用该模式,服务器和前端需要做好页面404的处理
3.AbstractHistory
模式:在不支持上面两种方式的环境下使用,如node
环境,实际是使用数组模拟路由历史栈导航守卫
// 全局守卫
// 在项目中,一般在beforeEach这个钩子函数中进行路由跳转的一些信息判断。
判断是否登录,是否拿到对应的路由权限等等。
router.beforeEach((to, from, next) => {
to: Route: // 即将要进入的目标 路由对象
from: Route: // 当前导航正要离开的路由
next: Function: // 一定要调用该方法来 resolve 这个钩子。
})
router.afterEach((to, from) => {})
router.beforeResolve((to, from) => {})
// 与afterEach类似, 区别是在导航被确认之前,同时在所有组件内守卫和异步路由组件被解析之后,解析守卫就被调用
// 路由独享守卫
const router = new VueRouter({
routes: [
{
path: '/foo',
component: Foo,
beforeEnter: (to, from, next) => {},
...
}
]
})
// 组件内守卫
const Foo = {
template: `...`,
beforeRouteEnter (to, from, next) {
// 在渲染该组件的对应路由被 confirm 前调用
// 不!能!获取组件实例 `this`
// 因为当守卫执行前,组件实例还没被创建
},
beforeRouteUpdate (to, from, next) {
// 在当前路由改变,但是该组件被复用时调用
// 举例来说,对于一个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候,
// 由于会渲染同样的 Foo 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
// 可以访问组件实例 `this`
},
beforeRouteLeave (to, from, next) {
// 导航离开该组件的对应路由时调用
// 可以访问组件实例 `this`
}
16. VueRouter
- 基于
vue
的插件机制,全局混入beforeCreated
和destroyed
的生命钩子 - 查找根实例上的
route
,注入到每个组件上,监听current
变化
Vue.util.defineReactive(this, '_route', this._router.history.current)
-
vue
原型上添加两个属性$router
和$route
, 拦截get
操作,限制set
操作
Object.defineProperty(Vue.prototype, '$router', {
get () { return this._routerRoot._router }
})
- 注册全局组件RouterView 和 RouterLink
17. Vuex
Vue.js
是一个专为Vue.js
应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。核心概念
1.state
:使用单一状态树,用一个对象就包含了全部的应用层级状态。
2.getter
:可看成数据的computed
计算属性
3.mutation
:唯一更改数据的方法 通过store.commit
使用相应的mutation
方法
4.Action
:支持异步的提交mutation
通过store.dispatch
使用相应的Action
方法
5.module
:数据分模块。如moduleA.xx
如何注入
在使用Vue.use(vuex)
的时候会执行install
方法在(vue
插件机制)。这个方法会混入一个minxin
Vue.mixin({
beforeCreate() {
const options = this.$options
// store injection
// 非根组件指向其父组件的$store,使得所有组件的实例,都指向根的store对象
if (options.store) {
this.$store = typeof options.store === 'function'
? options.store()
: options.store
} else if (options.parent && options.parent.$store) {
this.$store = options.parent.$store
}
}
})
- 如何实现响应式
通过添加到data中实现响应式
store._vm = new Vue({
data: {
$$state: state
},
computed // 这里是store的getter
})
18. 首屏加载慢的优化方案
-
webpack
来打包Vue
项目,vendor.js
过大问题解决
1.造成过大的原因是因为在main.js
导入第三库太多时,webpack
合并js
时生成了vendor.js
(我们习惯把第三方库放在vendor里面)造成的,js
文件过多,拖慢加载速度,所以:首先在index.html
中,使用CDN
的资源
2.在bulid/webpack.base.conf.js
文件中,添加externals
,导入index.html
下所需的资源模块:
module.exports = {
context: path.resolve(__dirname, '../'),
entry: {
app: ['babel-polyfill', 'lib-flexible', './src/main.js']
},
externals: { // <-添加
vue: 'Vue',
vuex: 'Vuex',
'vue-router': 'VueRouter',
VueAwesomeSwiper: 'VueAwesomeSwiper'
},
3.在main.js
里将以下 import
注释 替换 require
引入模块
// import Vue from 'vue'
// import VueAwesomeSwiper from 'vue-awesome-swiper'
const Vue = require('vue')
const VueAwesomeSwiper = require('VueAwesomeSwiper')
Vue.use(VueAwesomeSwiper)
4.当然可以在生产环境当中删除掉不必要的console.log
,打开build/webpack.prod.conf.js
在plugins
里添加以下代码
plugins: [
new webpack.optimize.UglifyJsPlugin({ //添加-删除console.log
compress: {
warnings: false,
drop_debugger: true,
drop_console: true
},
sourceMap: true
}),
5.执行npm run build
之后,会发现文件的体积明显小了很多,如果把一些Ui库也替换成CDN的方式,可能体积会更小,渲染解析更快。
-
Vue-cli
开启打包压缩 和后台配合gzip
访问开启打包压缩 和后台配合gzip
访问
1.首先打开config/index.js
,找到build
对象中的productionGzip
,改成true
2.打开productionGzip: true
之前,先要安装依赖compression-webpack-plugin
,官方推荐的命令是:
npm install --save-dev compression-webpack-plugin
//(此处有坑) 如果打包报错,应该是版本问题 ,先卸载之前安装的此插件 ,然后安装低版本
npm install --save-dev [email protected]
3.等安装好了,重新打包 npm run build
,此时打包的文件会 新增 .gz
文件。是不是比原来的js
文件小很多呢,之后项目访问的文件就是这个.gz
文件
4.后台nginx
开启gzip
模式访问,浏览器访问项目,自动会找到 .gz
的文件。加载速度明显提高。
http { //在 http中配置如下代码,
gzip on;
gzip_disable "msie6";
gzip_vary on;
gzip_proxied any;
gzip_comp_level 8; #压缩级别
gzip_buffers 16 8k;
#gzip_http_version 1.1;
gzip_min_length 100; #不压缩临界值
gzip_types text/plain application/javascript application/x-javascript text/css application/xml text/javascript application/x-httpd-php image/jpeg image/gif image/png;
}
19. Vue核心之虚拟DOM
- 真实DOM和其解析流程,浏览器渲染引擎工作流程都差不多,大致分为5步,创建
DOM
树-->创建StyleRules
-->创建Render
树-->布局Layout
-->绘制Painting
。
1.用HTML
分析器,创建DOM树。
2.用CSS
分析器,生成样式规则表。
3.关联DOM
树和规则表,生成渲染树。
4.通过渲染树计算节点属性。
5.通过计算好的节点属性,渲染页面
DOM
树的构建是文档加载完成开始的?构建DOM
数是一个渐进过程,为达到更好用户体验,渲染引擎会尽快将内容显示在屏幕上。它不必等到整个HTML
文档解析完毕之后才开始构建render
数和布局。
Render
树是DOM
树和CSSOM
树构建完毕才开始构建的吗?这三个过程在实际进行的时候又不是完全独立,而是会有交叉。会造成一边加载,一遍解析,一遍渲染的工作现象。
CSS
的解析是从右往左逆向解析的(从DOM
树的下-上解析比上-下解析效率高),嵌套标签越多,解析越慢。
-
JS
操作真实DOM
的代价
用我们传统的开发模式,原生JS
或JQ
操作DOM
时,浏览器会从构建DOM
树开始从头到尾执行一遍流程。在一次操作中,我需要更新10个DOM
节点,浏览器收到第一个DOM
请求后并不知道还有9次更新操作,因此会马上执行流程,最终执行10
次。例如,第一次计算完,紧接着下一个DOM
更新请求,这个节点的坐标值就变了,前一次计算为无用功。计算DOM
节点坐标值等都是白白浪费的性能。即使计算机硬件一直在迭代更新,操作DOM
的代价仍旧是昂贵的,频繁操作还是会出现页面卡顿,影响用户体验。
- 虚拟
DOM
有什么好处?虚拟DOM
,其实是一个大对象
1.Web
界面由DOM
树(树的意思是数据结构)来构建,当其中一部分发生变化时,其实就是对应某个DOM
节点发生了变化。
2.虚拟DOM
就是为了解决浏览器性能问题而被设计出来的。如前,若一次操作中有10次更新DOM
的动作,虚拟DOM
不会立即操作DOM
,而是将这10次更新的diff
内容保存到本地一个JS
对象中,最终将这个JS
对象一次性attch
到DOM
树上,再进行后续操作,避免大量无谓的计算量。所以,用JS
对象模拟DOM
节点的好处是,页面的更新可以先全部反映在JS
对象(虚拟DOM
)上,操作内存中的JS对象的速度显然要更快,等更新完成后,再将最终的JS
对象映射成真实的DOM
,交由浏览器去绘制。