最近面试被问到的 vue 题
v-for 为什么要加 key
如果不使用 key,Vue 会使用一种最大限度减少动态元素并且尽可能的尝试就地修改/复用相同类型元素的算法。key 是为 Vue 中 vnode 的唯一标记,通过这个 key,我们的 diff 操作可以更准确、更快速
更准确:因为带 key 就不是就地复用了,在 sameNode 函数 a.key === b.key 对比中可以避免就地复用的情况。所以会更加准确。
更快速:利用 key 的唯一性生成 map 对象来获取对应节点,比遍历方式更快
如果让你从零开始写一个 vuex,说说你的思路
思路分析
这个题目很有难度,首先思考vuex
解决的问题:存储用户全局状态并提供管理状态 API。
vuex
需求分析如何实现这些需求
回答范例
官方说
vuex
是一个状态管理模式和库,并确保这些状态以可预期的方式变更。可见要实现一个vuex
要实现一个
Store
存储全局状态要提供修改状态所需 API:
commit(type, payload), dispatch(type, payload)
实现
Store
时,可以定义Store
类,构造函数接收选项options
,设置属性state
对外暴露状态,提供commit
和dispatch
修改属性state
。这里需要设置state
为响应式对象,同时将Store
定义为一个Vue
插件commit(type, payload)
方法中可以获取用户传入mutations
并执行它,这样可以按用户提供的方法修改状态。dispatch(type, payload)
类似,但需要注意它可能是异步的,需要返回一个Promise
给用户以处理异步结果
实践
Store
的实现:
vuex 简易版
验证方式
Vue 的性能优化有哪些
(1)编码阶段
尽量减少 data 中的数据,data 中的数据都会增加 getter 和 setter,会收集对应的 watcher
v-if 和 v-for 不能连用
如果需要使用 v-for 给每项元素绑定事件时使用事件代理
SPA 页面采用 keep-alive 缓存组件
在更多的情况下,使用 v-if 替代 v-show
key 保证唯一
使用路由懒加载、异步组件
防抖、节流
第三方模块按需导入
长列表滚动到可视区域动态加载
图片懒加载
(2)SEO 优化
预渲染
服务端渲染 SSR
(3)打包优化
压缩代码
Tree Shaking/Scope Hoisting
使用 cdn 加载第三方模块
多线程打包 happypack
splitChunks 抽离公共文件
sourceMap 优化
(4)用户体验
骨架屏
PWA
还可以使用缓存(客户端缓存、服务端缓存)优化、服务端开启 gzip 压缩等。
谈谈对 keep-alive 的了解
keep-alive 可以实现组件的缓存,当组件切换时不会对当前组件进行卸载。常用的2个属性
include/exclude ,2个生命周期
activated ,
deactivated
参考:前端vue面试题详细解答
created 和 mounted 的区别
created:在模板渲染成 html 前调用,即通常初始化某些属性值,然后再渲染成视图。
mounted:在模板渲染成 html 后调用,通常是初始化页面完成后,再对 html 的 dom 节点进行一些需要的操作。
Vue3.x 响应式数据原理
Vue3.x
改用Proxy
替代Object.defineProperty
。因为Proxy
可以直接监听对象和数组的变化,并且有多达13
种拦截方法。并且作为新标准将受到浏览器厂商重点持续的性能优化。
proxy 基本用法
data 为什么是一个函数而不是对象
JavaScript 中的对象是引用类型的数据,当多个实例引用同一个对象时,只要一个实例对这个对象进行操作,其他实例中的数据也会发生变化。
而在 Vue 中,更多的是想要复用组件,那就需要每个组件都有自己的数据,这样组件之间才不会相互干扰。
所以组件的数据不能写成对象的形式,而是要写成函数的形式。数据以函数返回值的形式定义,这样当每次复用组件的时候,就会返回一个新的 data,也就是说每个组件都有自己的私有数据空间,它们各自维护自己的数据,不会干扰其他组件的正常运行。
nextTick 在哪里使用?原理是?
nextTick
中的回调是在下次DOM
更新循环结束之后执行延迟回调,用于获得更新后的DOM
在修改数据之后立即使用这个方法,获取更新后的
DOM
主要思路就是采用
微任务优先
的方式调用异步方法去执行nextTick
包装的方法
nextTick
方法主要是使用了宏任务和微任务,定义了一个异步方法.多次调用nextTick
会将方法存入队列中,通过这个异步方法清空当前队列。所以这个nextTick
方法就是异步方法
根据执行环境分别尝试采用
先采用
Promise
Promise
不支持,再采用MutationObserver
MutationObserver
不支持,再采用setImmediate
如果以上都不行则采用
setTimeout
最后执行
flushCallbacks
,把callbacks
里面的数据依次执行
回答范例
nextTick
中的回调是在下次DOM
更新循环结束之后执行延迟回调,用于获得更新后的DOM
Vue
有个异步更新策略,意思是如果数据变化,Vue
不会立刻更新 DOM,而是开启一个队列,把组件更新函数保存在队列中,在同一事件循环中发生的所有数据变更会异步的批量更新。这一策略导致我们对数据的修改不会立刻体现在 DOM 上,此时如果想要获取更新后的 DOM 状态,就需要使用nextTick
开发时,有两个场景我们会用到
nextTick
created
中想要获取DOM
时响应式数据变化后获取
DOM
更新后的状态,比如希望获取列表更新后的高度
nextTick
签名如下:function nextTick(callback?: () => void): Promise<void>
所以我们只需要在传入的回调函数中访问最新 DOM 状态即可,或者我们可以await nextTick()
方法返回的Promise
之后做这件事
在
Vue
内部,nextTick
之所以能够让我们看到 DOM 更新后的结果,是因为我们传入的callback
会被添加到队列刷新函数(flushSchedulerQueue
)的后面,这样等队列内部的更新函数都执行完毕,所有 DOM 操作也就结束了,callback
自然能够获取到最新的 DOM 值
基本使用
相关代码如下
数据更新的时候内部会调用nextTick
Vue 性能优化
编码优化:
事件代理
keep-alive
拆分组件
key
保证唯一性路由懒加载、异步组件
防抖节流
Vue 加载性能优化
第三方模块按需导入(
babel-plugin-component
)图片懒加载
用户体验
app-skeleton
骨架屏shellap
p 壳pwa
SEO 优化
预渲染
描述下 Vue 自定义指令
在 Vue2.0 中,代码复用和抽象的主要形式是组件。然而,有的情况下,你仍然需要对普通 DOM 元素进行底层操作,这时候就会用到自定义指令。一般需要对 DOM 元素进行底层操作时使用,尽量只用来操作 DOM 展示,不修改内部的值。当使用自定义指令直接修改 value 值时绑定 v-model 的值也不会同步更新;如必须修改可以在自定义指令中使用 keydown 事件,在 vue 组件中使用 change 事件,回调中修改 vue 数据;
(1)自定义指令基本内容
全局定义:
Vue.directive("focus",{})
局部定义:
directives:{focus:{}}
钩子函数:指令定义对象提供钩子函数
o bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
o inSerted:被绑定元素插入父节点时调用(仅保证父节点存在,但不一定已被插入文档中)。
o update:所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前调用。指令的值可能发生了改变,也可能没有。但是可以通过比较更新前后的值来忽略不必要的模板更新。
o ComponentUpdate:指令所在组件的 VNode 及其子 VNode 全部更新后调用。
o unbind:只调用一次,指令与元素解绑时调用。
钩子函数参数 o el:绑定元素
o bing: 指令核心对象,描述指令全部信息属性
o name
o value
o oldValue
o expression
o arg
o modifers
o vnode 虚拟节点
o oldVnode:上一个虚拟节点(更新钩子函数中才有用)
(2)使用场景
普通 DOM 元素进行底层操作的时候,可以使用自定义指令
自定义指令是用来操作 DOM 的。尽管 Vue 推崇数据驱动视图的理念,但并非所有情况都适合数据驱动。自定义指令就是一种有效的补充和扩展,不仅可用于定义任何的 DOM 操作,并且是可复用的。
(3)使用案例
初级应用:
鼠标聚焦
下拉菜单
相对时间转换
滚动动画
高级应用:
自定义指令实现图片懒加载
自定义指令集成第三方插件
常见的事件修饰符及其作用
.stop
:等同于 JavaScript 中的event.stopPropagation()
,防止事件冒泡;.prevent
:等同于 JavaScript 中的event.preventDefault()
,防止执行预设的行为(如果事件可取消,则取消该事件,而不停止事件的进一步传播);.capture
:与事件冒泡的方向相反,事件捕获由外到内;.self
:只会触发自己范围内的事件,不包含子元素;.once
:只会触发一次。
Computed 和 Watch 的区别
对于 Computed:
它支持缓存,只有依赖的数据发生了变化,才会重新计算
不支持异步,当 Computed 中有异步操作时,无法监听数据的变化
computed 的值会默认走缓存,计算属性是基于它们的响应式依赖进行缓存的,也就是基于 data 声明过,或者父组件传递过来的 props 中的数据进行计算的。
如果一个属性是由其他属性计算而来的,这个属性依赖其他的属性,一般会使用 computed
如果 computed 属性的属性值是函数,那么默认使用 get 方法,函数的返回值就是属性的属性值;在 computed 中,属性有一个 get 方法和一个 set 方法,当数据发生变化时,会调用 set 方法。
对于 Watch:
它不支持缓存,数据变化时,它就会触发相应的操作
支持异步监听
监听的函数接收两个参数,第一个参数是最新的值,第二个是变化之前的值
当一个属性发生变化时,就需要执行相应的操作
监听数据必须是 data 中声明的或者父组件传递过来的 props 中的数据,当发生变化时,会触发其他操作,函数有两个的参数:
immediate:组件加载立即触发回调函数
deep:深度监听,发现数据内部的变化,在复杂数据类型中使用,例如数组中的对象发生变化。需要注意的是,deep 无法监听到数组和对象内部的变化。
当想要执行异步或者昂贵的操作以响应不断的变化时,就需要使用 watch。
总结:
computed 计算属性 : 依赖其它属性值,并且 computed 的值有缓存,只有它依赖的属性值发生改变,下一次获取 computed 的值时才会重新计算 computed 的值。
watch 侦听器 : 更多的是观察的作用,无缓存性,类似于某些数据的监听回调,每当监听的数据变化时都会执行回调进行后续操作。
运用场景:
当需要进行数值计算,并且依赖于其它数据时,应该使用 computed,因为可以利用 computed 的缓存特性,避免每次获取值时都要重新计算。
当需要在数据变化时执行异步或开销较大的操作时,应该使用 watch,使用 watch 选项允许执行异步操作 ( 访问一个 API ),限制执行该操作的频率,并在得到最终结果前,设置中间状态。这些都是计算属性无法做到的。
slot 是什么?有什么作用?原理是什么?
slot 又名插槽,是 Vue 的内容分发机制,组件内部的模板引擎使用 slot 元素作为承载分发内容的出口。插槽 slot 是子组件的一个模板标签元素,而这一个标签元素是否显示,以及怎么显示是由父组件决定的。slot 又分三类,默认插槽,具名插槽和作用域插槽。
默认插槽:又名匿名查抄,当 slot 没有指定 name 属性值的时候一个默认显示插槽,一个组件内只有有一个匿名插槽。
具名插槽:带有具体名字的插槽,也就是带有 name 属性的 slot,一个组件可以出现多个具名插槽。
作用域插槽:默认插槽、具名插槽的一个变体,可以是匿名插槽,也可以是具名插槽,该插槽的不同点是在子组件渲染作用域插槽时,可以将子组件内部的数据传递给父组件,让父组件根据子组件的传递过来的数据决定如何渲染该插槽。
实现原理:当子组件 vm 实例化时,获取到父组件传入的 slot 标签的内容,存放在vm.$slot
中,默认插槽为vm.$slot.default
,具名插槽为vm.$slot.xxx
,xxx 为插槽名,当组件执行渲染函数时候,遇到 slot 标签,使用$slot
中的内容进行替换,此时可以为插槽传递数据,若存在数据,则可称该插槽为作用域插槽。
谈谈你对 SPA 单页面的理解
SPA
( single-page application )仅在Web
页面初始化时加载相应的HTML
、JavaScript
和CSS
。一旦页面加载完成,SPA
不会因为用户的操作而进行页面的重新加载或跳转;取而代之的是利用路由机制实现HTML
内容的变换,UI
与用户的交互,避免页面的重新加载
优点:
用户体验好、快,内容的改变不需要重新加载整个页面,避免了不必要的跳转和重复渲染;
基于上面一点,
SPA
相对对服务器压力小;前后端职责分离,架构清晰,前端进行交互逻辑,后端负责数据处理
缺点:
初次加载耗时多:为实现单页
Web
应用功能及显示效果,需要在加载页面的时候将JavaScript
、CSS
统一加载,部分页面按需加载;前进后退路由管理:由于单页应用在一个页面中显示所有的内容,所以不能使用浏览器的前进后退功能,所有的页面切换需要自己建立堆栈管理;
SEO
难度较大:由于所有的内容都在一个页面中动态替换显示,所以在SEO
上其有着天然的弱势
单页应用与多页应用的区别
实现一个 SPA
监听地址栏中
hash
变化驱动界面变化用
pushsate
记录浏览器的历史,驱动界面发送变化
hash 模式 :核心通过监听
url
中的hash
来进行路由跳转
history 模式 :
history
模式核心借用HTML5 history api
,api
提供了丰富的router
相关属性先了解一个几个相关的 api
history.pushState
浏览器历史纪录添加记录history.replaceState
修改浏览器历史纪录中当前纪录history.popState
当history
发生变化时触发
题外话:如何给 SPA 做 SEO
SSR 服务端渲染
将组件或页面通过服务器生成html
,再返回给浏览器,如nuxt.js
静态化
目前主流的静态化主要有两种:
一种是通过程序将动态页面抓取并保存为静态页面,这样的页面的实际存在于服务器的硬盘中
另外一种是通过 WEB 服务器的
URL Rewrite
的方式,它的原理是通过 web 服务器内部模块按一定规则将外部的 URL 请求转化为内部的文件地址,一句话来说就是把外部请求的静态地址转化为实际的动态页面地址,而静态页面实际是不存在的。这两种方法都达到了实现 URL 静态化的效果
使用
Phantomjs
针对爬虫处理
原理是通过Nginx
配置,判断访问来源是否为爬虫,如果是则搜索引擎的爬虫请求会转发到一个node server
,再通过PhantomJS
来解析完整的HTML
,返回给爬虫。下面是大致流程图
虚拟 DOM 的优劣如何?
优点:
保证性能下限: 虚拟 DOM 可以经过 diff 找出最小差异,然后批量进行 patch,这种操作虽然比不上手动优化,但是比起粗暴的 DOM 操作性能要好很多,因此虚拟 DOM 可以保证性能下限
无需手动操作 DOM: 虚拟 DOM 的 diff 和 patch 都是在一次更新中自动进行的,我们无需手动操作 DOM,极大提高开发效率
跨平台: 虚拟 DOM 本质上是 JavaScript 对象,而 DOM 与平台强相关,相比之下虚拟 DOM 可以进行更方便地跨平台操作,例如服务器渲染、移动端开发等等
缺点:
无法进行极致优化: 在一些性能要求极高的应用中虚拟 DOM 无法进行针对性的极致优化,比如 VScode 采用直接手动操作 DOM 的方式进行极端的性能优化
说说 Vue 的生命周期吧
什么时候被调用?
beforeCreate :实例初始化之后,数据观测之前调用
created:实例创建万之后调用。实例完成:数据观测、属性和方法的运算、
watch/event
事件回调。无$el
.beforeMount:在挂载之前调用,相关
render
函数首次被调用mounted:了被新创建的
vm.$el
替换,并挂载到实例上去之后调用改钩子。beforeUpdate:数据更新前调用,发生在虚拟 DOM 重新渲染和打补丁,在这之后会调用改钩子。
updated:由于数据更改导致的虚拟 DOM 重新渲染和打补丁,在这之后会调用改钩子。
beforeDestroy:实例销毁前调用,实例仍然可用。
destroyed:实例销毁之后调用,调用后,Vue 实例指示的所有东西都会解绑,所有事件监听器和所有子实例都会被移除
每个生命周期内部可以做什么?
created:实例已经创建完成,因为他是最早触发的,所以可以进行一些数据、资源的请求。
mounted:实例已经挂载完成,可以进行一些 DOM 操作。
beforeUpdate:可以在这个钩子中进一步的更改状态,不会触发重渲染。
updated:可以执行依赖于 DOM 的操作,但是要避免更改状态,可能会导致更新无线循环。
destroyed:可以执行一些优化操作,清空计时器,解除绑定事件。
ajax 放在哪个生命周期?:一般放在 mounted
中,保证逻辑统一性,因为生命周期是同步执行的, ajax
是异步执行的。单数服务端渲染 ssr
同一放在 created
中,因为服务端渲染不支持 mounted
方法。 什么时候使用 beforeDestroy?:当前页面使用 $on
,需要解绑事件。清楚定时器。解除事件绑定, scroll mousemove
。
vue 是如何实现响应式数据的呢?(响应式数据原理)
Vue2: Object.defineProperty
重新定义 data
中所有的属性, Object.defineProperty
可以使数据的获取与设置增加一个拦截的功能,拦截属性的获取,进行依赖收集。拦截属性的更新操作,进行通知。
具体的过程:首先 Vue 使用 initData
初始化用户传入的参数,然后使用 new Observer
对数据进行观测,如果数据是一个对象类型就会调用 this.walk(value)
对对象进行处理,内部使用 defineeReactive
循环对象属性定义响应式变化,核心就是使用 Object.defineProperty
重新定义数据。
虚拟 DOM 实现原理?
虚拟 DOM 的实现原理主要包括以下 3 部分:
用 JavaScript 对象模拟真实 DOM 树,对真实 DOM 进行抽象;
diff 算法 — 比较两棵虚拟 DOM 树的差异;
pach 算法 — 将两个虚拟 DOM 对象的差异应用到真正的 DOM 树。
Vue2.x 响应式数据原理
整体思路是数据劫持+观察者模式
对象内部通过 defineReactive
方法,使用 Object.defineProperty
来劫持各个属性的 setter
、getter
(只会劫持已经存在的属性),数组则是通过重写数组7个方法
来实现。当页面使用对应属性时,每个属性都拥有自己的 dep
属性,存放他所依赖的 watcher
(依赖收集),当属性变化后会通知自己对应的 watcher
去更新(派发更新)
Object.defineProperty 基本使用
源码分析
说一说你对 vue 响应式理解回答范例
所谓数据响应式就是能够使数据变化可以被检测并对这种变化做出响应的机制
MVVM
框架中要解决的一个核心问题是连接数据层和视图层,通过数据驱动应用,数据变化,视图更新,要做到这点的就需要对数据做响应式处理,这样一旦数据发生变化就可以立即做出更新处理以
vue
为例说明,通过数据响应式加上虚拟DOM
和patch
算法,开发人员只需要操作数据,关心业务,完全不用接触繁琐的 DOM 操作,从而大大提升开发效率,降低开发难度vue2
中的数据响应式会根据数据类型来做不同处理,如果是 对象则采用Object.defineProperty()
的方式定义数据拦截,当数据被访问或发生变化时,我们感知并作出响应;如果是数组则通过覆盖数组对象原型的 7 个变更方法 ,使这些方法可以额外的做更新通知,从而作出响应。这种机制很好的解决了数据响应化的问题,但在实际使用中也存在一些缺点:比如初始化时的递归遍历会造成性能损失;新增或删除属性时需要用户使用Vue.set/delete
这样特殊的api
才能生效;对于es6
中新产生的Map
、Set
这些数据结构不支持等问题为了解决这些问题,
vue3
重新编写了这一部分的实现:利用ES6
的Proxy
代理要响应化的数据,它有很多好处,编程体验是一致的,不需要使用特殊api
,初始化性能和内存消耗都得到了大幅改善;另外由于响应化的实现代码抽取为独立的reactivity
包,使得我们可以更灵活的使用它,第三方的扩展开发起来更加灵活了
MVVM 的优缺点?
优点:
分离视图(View)和模型(Model),降低代码耦合,提⾼视图或者逻辑的重⽤性: ⽐如视图(View)可以独⽴于 Model 变化和修改,⼀个 ViewModel 可以绑定不同的"View"上,当 View 变化的时候 Model 不可以不变,当 Model 变化的时候 View 也可以不变。你可以把⼀些视图逻辑放在⼀个 ViewModel⾥⾯,让很多 view 重⽤这段视图逻辑
提⾼可测试性: ViewModel 的存在可以帮助开发者更好地编写测试代码
⾃动更新 dom: 利⽤双向绑定,数据更新后视图⾃动更新,让开发者从繁琐的⼿动 dom 中解放
缺点:
Bug 很难被调试: 因为使⽤双向绑定的模式,当你看到界⾯异常了,有可能是你 View 的代码有 Bug,也可能是 Model 的代码有问题。数据绑定使得⼀个位置的 Bug 被快速传递到别的位置,要定位原始出问题的地⽅就变得不那么容易了。另外,数据绑定的声明是指令式地写在 View 的模版当中的,这些内容是没办法去打断点 debug 的
⼀个⼤的模块中 model 也会很⼤,虽然使⽤⽅便了也很容易保证了数据的⼀致性,当时⻓期持有,不释放内存就造成了花费更多的内存
对于⼤型的图形应⽤程序,视图状态较多,ViewModel 的构建和维护的成本都会⽐较⾼。
评论