前端篇:朋友面试之Vue框架,故事未止(二)
前言
上回说到,牛富贵面试给自己挖坑,险些吃了挂面,近期他迎来第二场面试,预知后事如何,请等他娓娓道来。
开场
富贵上回差点吃了亏,他学乖了,预约之前找村口先生算了一卦,算得幸运时辰为巳时,面试时间也恰巧有此时间段,于是乎富贵预约了这个时间。很快就到了面试时间,富贵整理好衣裳,打开电脑准备面试。
稍等片刻,一个熟悉的面孔出现在屏幕,没错,还是那位面试官。富贵顿时背后一阵冷汗,支支吾吾说了句“面试官好”。
面试官露出了微笑
顿了顿,“你好,接下来我们就开始面试”。
正文
面试官:我们今天继续聊一下Vue框架吧
牛富贵:(开始慌了)好的
面试官:那先问一下Vue-Router的问题吧。
面试官:讲讲vue-router有多少种模式。
牛富贵:主要有以下几种
- hash模式:兼容所有浏览器,包括不支持 HTML5 History Api 的浏览器,例如http://www.baidu.com/#/index,hash值为#/index, hash的改变会触发hashchange事件,我们可以通过监听hashchange事件来完成操作实现前端路由。
// 监听hash变化,点击浏览器的前进后退会触发 window.addEventListener('hashchange', function(event){ let newURL = event.newURL; // hash 改变后的新 url let oldURL = event.oldURL; // hash 改变前的旧 url },false)
分析:当 URL 改变时,页面不会重新加载。 hash(#)是URL 的锚点,代表的是网页中的一个位置,单单改变#后的部分,浏览器只会滚动到相应位置,不会重新加载网页,也就是说 #是用来指导浏览器动作的,对服务器端完全无用,HTTP请求中也不会不包括#;同时每一次改变#后的部分,都会在浏览器的访问历史中增加一个记录,使用”后退”按钮,就可以回到上一个位置;所以说Hash模式通过锚点值的改变,根据不同的值,渲染指定DOM位置的不同数据
- history模式:能支持 HTML5 History Api 的浏览器,依赖HTML5 History API来实现前端路由。没有
#
的特殊符号,例如http://www.baidu.com/index,路由地址跟正常的url一样,不过第一次访问或者刷新页面都会向服务器请求,如果没有请求到对应的资源就会返回404,所以路由地址匹配不到任何静态资源,则应该返回同一个index.html 页面,需要在nginx中配置。
location / { try_files $uri $uri/ @router index index.html; } location @router { rewrite ^.*$ /index.html last; }
分析:利用了 HTML5 History Interface 中新增的 pushState() 和 replaceState() 方法。pushState()方法可以改变URL地址且不会发送请求,replaceState()方法可以读取历史记录栈,还可以对浏览器记录进行修改。 这两个方法应用于浏览器的历史记录栈,在当前已有的 back、forward、go 的基础之上,它们提供了对历史记录进行修改的功能。只是当它们执行修改时,虽然改变了当前的 URL,但浏览器不会立即向后端发送请求。
- abstract模式:支持所有 JavaScript 运行环境,如 Node.js 服务器端。如果发现没有浏览器的 API,路由会自动强制进入这个模式。
- hash模式和history模式实现vue-router跳转api的区别
api | hash | history |
---|---|---|
push | window.location.assign | window.history.pushState |
replace | window.location.replace | window.history.replaceState |
go | window.history.go | window.history.go |
back | window.history.go(-1) | window.history.go(-1) |
forward | window.history.go(1) | window.history.go(1) |
面试官:了解过动态路由吗?
牛富贵:
传参数 | 获取参数 | url中形式 | 参数问题 | |
---|---|---|---|---|
this.$route.query | this.$router.push({path: '/index',query:{id:id}}) | this.$route.query.id | http://127.0.0.1:8080/#/index?id=1 | 刷新路由跳转页面参数不消失 |
this.$route.params | this.$router.push({name: 'index',params:{id:id} }) | this.$route.params.id | http://127.0.0.1:8080/#/index | 刷新路由跳转页面参数消失 |
面试官:知道route和router的区别吗?
牛富贵:
1、router是VueRouter的一个对象,通过Vue.use(VueRouter)和Vue构造函数得到一个router的实例对象,这个对象中是一个全局的对象,他包含了所有的路由,包含了许多关键的对象和属性。
2、route是一个跳转的路由对象,每一个路由都会有一个$route对象,是一个局部的对象,可以获取对应的name,path,params,query等
从这两者不同的结构可以看出两者的区别,他们的一些属性是不同的
$route.path 字符串,等于当前路由对象的路径,会被解析为绝对路径,如/home/index
$route.params 对象,含路有种的动态片段和全匹配片段的键值对,不会拼接到路由的url后面
$route.query 对象,包含路由中查询参数的键值对。会拼接到路由url后面
$route.router 路由规则所属的路由器
$route.matchd 数组,包含当前匹配的路径中所包含的所有片段所对象的配置参数对象
$route.name 当前路由的名字,如果没有使用具体路径,则名字为空
面试官:那讲讲Vue-Router 的懒加载是如何实现的
牛富贵:
- 这是一个普通的路由配置
import Vue from 'vue' import Router from 'vue-router' import HelloWorld from '@/components/HelloWorld' Vue.use(Router) export default new Router({ routes: [ { path: '/', name: 'HelloWorld', component:HelloWorld } ] })
- vue异步组件实现懒加载
import Vue from 'vue' import Router from 'vue-router' /* 此处省去之前导入的HelloWorld模块 */ Vue.use(Router) export default new Router({ routes: [ { path: '/', name: 'HelloWorld', //此处对原代码进行修改 component: resolve=>(require(["@/components/HelloWorld"],resolve)) } ] })
- import方法
import Vue from 'vue' import Router from 'vue-router' Vue.use(Router) export default new Router({ routes: [ { path: '/', name: 'HelloWorld', //此处对原代码进行修改 component: ()=>import("@/components/HelloWorld") } ] })
组件懒加载
- 这是一个普通的组件
<template> <div> <One></One> </div> </template> <script> import One from './one' export default { components:{ "One":One }, data () { return { msg: 'This is a component' } } } </script>
- 异步方法
<template> <div> <One></One> </div> </template> <script> export default { components:{ //原代码修改 "One":resolve=>(['./one'],resolve) }, data () { return { msg: 'This is a component' } } } </script>
- import方法
<template> <div> <One></One> </div> </template> <script> export default { components:{ //原代码修改 "One": ()=>import("./one"); }, data () { return { msg: 'This is a component' } } } </script>
面试官:不错,那你能讲讲路由导航守卫有哪些?
牛富贵:主要有
- 全局守卫:beforeEach、beforeResolve、afterEach
- 路由独享守卫:beforeEnter
- 组件内的守卫:beforeRouteEnter、beforeRouteUpdate、beforeRouteLeave
面试官:你讲得有些快,能具体一点吗
牛富贵(不可以):不急,我画个表先~
路由守卫 | 名称 | 作用 |
---|---|---|
全局守卫 | beforeEach(to,from,next) | 路由跳转前触发,常用于登录验证。 |
beforeResolve(to,from,next) | 在 beforeEach 和 组件内 beforeRouteEnter 之后,afterEach 之前调用。 | |
afterEach(to,from) | 发生在 beforeEach 和 beforeResolve 之后,beforeRouteEnter 之前。路由在触发后执行。 | |
路由独享守卫 | beforeEnter | 在 beforeEach 之后执行,和它功能一样 ,不怎么常用 |
组件内的守卫 | beforeRouteEnter | 路由进入之前调用。不能获取组件 this 实例 ,因为路由在进入组件之前,组件实例还没有被创建。 |
beforeRouteUpdate | 在当前路由改变时,并且该组件被复用时调用,可以通过 this 访问实例。当前路由 query 变更时,该守卫会被调用。 | |
beforeRouteLeave | 导航离开该组件的对应路由时调用,可以访问组件实例 this。 |
导航守卫的三个参数
to:即将要进入的目标 路由对象。
from:当前导航正要离开的路由对象。
next:函数,必须调用,不然路由跳转不过去。
next()
:进入下一个路由。next(false)
:中断当前的导航。next('/')
或next({ path: '/' })
: 跳转到其他路由,当前导航被中断,进行新的一个导航。
触发钩子的完整顺序
- 路由导航、keep-alive、和组件生命周期钩子结合起来的,触发顺序,假设是从 a 组件离开,第一次进入 b 组件
- 触发进入其他路由。
- 调用要离开路由的组件守卫
beforeRouteLeave
- 调用局前置守卫:
beforeEach
- 在重用的组件里调用
beforeRouteUpdate
- 调用路由独享守卫
beforeEnter
。 - 解析异步路由组件。
- 在将要进入的路由组件中调用
beforeRouteEnter
- 调用全局解析守卫
beforeResolve
- 导航被确认。
- 调用全局后置钩子的
afterEach
钩子。 - 触发DOM更新(
mounted
)。 - 执行
beforeRouteEnter
守卫中传给 next 的回调函数
面试官:说说你对router-link的了解
牛富贵:<router-link>
是Vue-Router的内置组件,在具有路由功能的应用中作为声明式的导航使用。
面试官:你知道里面props全部有哪些吗?
牛富贵:
面试官:(喝了杯水):
面试官:那我告诉你
<router-link>
有8个props,其作用是:
props | 作用 |
---|---|
to | 必填,表示目标路由的链接。<router-link :to="{ name: 'user', params: { userId: 123 }}">User</router-link> |
replace | 默认值为false,若设置的话,当点击时,会调用router.replace() |
append | 设置 append 属性后,则在当前 (相对) 路径前添加基路径。 |
tag | 让<router-link>渲染成tag设置的标签,如tag:'li',渲染结果为<li>foo</li> |
active-class | 默认值为router-link-active,设置链接激活时使用的 CSS 类名。 |
exact-active-class | 默认值为router-link-exact-active,设置链接被精确匹配的时候应该激活的 class。 |
exact | 是否精确匹配,默认为false。 |
event | 声明可以用来触发导航的事件。 |
面试官:那我们来聊聊Vue里面的指令吧
面试官:平时有用过哪些指令?
牛富贵:主要有v-show
、v-if
、v-else-if
、v-else
、v-for
、v-on
、v-bind
、v-model
、v-once
、v-slot
、v-html
、v-text
。
面试官:那你讲讲v-if、v-show、v-html 的原理
牛富贵:
- v-if 会调用 addIfCondition 方法,生成 vnode 的时候会忽略对应节点,render 的时候就不会渲染;
- v-show 会生成 vnode,render 的时候也会渲染成真实节点,只是在 render 过程中会在节点的属性中修改 show 属性值,也就是常说的 display;
- v-html 会先移除节点下的所有节点,调用 html 方法,通过 addProp 添加 innerHTML 属性,归根结底还是设置 innerHTML 为 v-html 的值。
面试官:讲讲v-if 和 v-show 的区别
牛富贵:
牛富贵:一个表格就搞定
Vue指令 | v-if | v-show |
---|---|---|
共同点 | 动态显示DOM元素 | 动态设置DOM元素 |
手段 | 动态的向DOM树内添加或者删除DOM元素 | 设置DOM元素的display样式属性控制显示和隐藏 |
编译过程 | 有一个局部编译/卸载的过程,切换过程中合适地销毁和重建内部的事件监听和子组件 | 简单的基于CSS切换 |
编译条件 | 如果初始条件为假,则不进行操作;只有在条件第一次变为真时才开始局部编译 | 在任何条件下都会被编译,然后被缓存,而且DOM元素保留 |
性能消耗 | v-if有更高的切换消耗 | v-show有更高的初始渲染消耗 |
使用场景 | v-if适合条件不太可能改变,也就是不需要频繁切换条件的场景 | v-show适合频繁切换的场景 |
面试官:那你了解v-if和v-for中key的作用吗?
牛富贵:vue 中 key 值的作用可以分为两种情况来考虑,
- 第一种情况是 v-if 中使用 key。由于 Vue 会尽可能高效地渲染元素,通常会复用已有元素而不是从头开始渲染。因此当使用 v-if 来实现元素切换的时候,如果切换前后含有相同类型的元素,那么这个元素就会被复用。如果是相同的 input 元素,那么切换前后用户的输入不会被清除掉,这样是不符合需求的。因此可以通过使用 key 来唯一的标识一个元素,这个情况下,使用 key 的元素不会被复用。这个时候 key 的作用是用来标识一个独立的元素。
- 第二种情况是 v-for 中使用 key。用 v-for 更新已渲染过的元素列表时,它默认使用“就地复用”的策略。如果数据项的顺序发生了改变,Vue 不会移动 DOM 元素来匹配数据项的顺序,而是简单复用此处的每个元素。因此通过为每个列表项提供一个 key 值,来以便 Vue 跟踪元素的身份,从而高效的实现复用。这个时候 key 的作用是为了高效的更新渲染虚拟 DOM。
题外话:当然,在开发过程并非一定要使用key,如果只是为了简单展示数据,其实也可以index来标识,视情况而定就好啦。
面试官:v-on可以绑定多个方法吗?
牛富贵:(打了个哈欠)当然可以~
<p v-on="{click:one,mousemove:two}">v-on绑定多个方法</p>//这里绑定一个点击事件和鼠标移动事件
- vue 事件修饰符
.stop:
阻止事件冒泡,相当于调用了 event.stopPropagation()方法.prevent:
阻止默认行为,相当于调用了 event.preventDefault()方法,比如表单的提交、a 标签的跳转就是默认事件.self:
只有点击元素本身才会触发。比如一个 div 里面有个按钮,div 和按钮都有事件,我们点击按钮,div 绑定的方法也会触发,如果 div 的 click 加上 self,只有点击到 div 的时候才会触发,变相的算是阻止冒泡。.once:
事件只能触发一次,无论点击多少次,执行第一次之后就不执行了.captrue:
捕获冒泡,即有冒泡发生时,有该修饰符的 dom 元素会先执行,如果有多个,从外到内依次执行,然后再按自然顺序执行触发的事件。.passive:
执行默认方法
面试官:接下来聊一下Vue的一些属性吧
面试官:了解methods watch和compute的区别吗
牛富贵:(小问题小问题)
牛富贵:还是老套路,画个表
computed | watch | methods | |
---|---|---|---|
作用机制 | 自动调用,完成我们希望完成的作用 | 自动调用,完成我们希望完成的作用 | 主动调用 |
性质 | 计算属性,事实上和data对象里的数据属性是相同的 | 类似于监听机制跟事件机制 | 定义的是函数,使用时跟函数调用一样 |
缓存 | 支持缓存,只有依赖的数据发生了变化,才会重新计算 | 不支持缓存,数据变化时,它就会触发相应的操作 | 无 |
是否支持异步 | 不支持异步,当 Computed 中有异步操作时,无法监听数据的变化 | 支持异步监听 | 支持异步处理 |
场景 | 一个数据受多个数据影响 | 一个数据影响多个数据 | 提供可调用的函数 |
computed和methods关于缓存的一个小细节
- 在computed中定义一个计算属性,并且返回new Date(),可以发现多次返回的时间是相同的。这是因为new Date()不是依赖型数据(不是放在data对象下的实例数据),所以computed只提供了缓存的值,而没有重新计算。这也是整理这方面的知识过程发现的一个小细节。
watch和computed处理场景的对比
watch
computed
面试官:不错不错,讲一下Virtual DOM
牛富贵:好的
牛富贵:由于在浏览器中操作 DOM 是很昂贵的。频繁操作 DOM,会产生一定性能问题。这就是虚拟 Dom 的产生原因。Vue2 的 Virtual DOM 借鉴了开源库 snabbdom 的实现。Virtual DOM 本质就是用一个原生的 JS 对象去描述一个 DOM 节点,是对真实 DOM 的一层抽象。
优点:
- 保证性能下限:框架的虚拟 DOM 需要适配任何上层 API 可能产生的操作,他的一些 DOM 操作的实现必须是普适的,所以它的性能并不是最优的;但是比起粗暴的 DOM 操作性能要好很多,因此框架的虚拟 DOM 至少可以保证在你不需要手动优化的情况下,依然可以提供还不错的性能,既保证性能的下限。
- 无需手动操作 DOM:我们不需手动去操作 DOM,只需要写好 View-Model 的 代码逻辑,框架会根据虚拟 DOM 和数据双向绑定,帮我们以可预期的方式更新视图,极大提高我们的开发效率。
- 跨平台:虚拟 DOM 本质上是 JavaScript 对象,而 DOM 与平台强相关,相比之下虚拟 DOM 可以进行更方便地跨平台操作,例如服务器端渲染、weex 开发等等。
缺点:
- 无法进行极致优化:虽然虚拟 DOM + 合理的优化,足以应对大部分应用的性能需要,但在一些性能要求极高的应用中虚拟 DOM 无法进行针对性的极致优化。
- 首次渲染大量 DOM 时,由于多了一层 DOM 计算,会比 innerHTML 插入慢。
面试官:那你了解diff算法吗
牛富贵:
首先,对比节点本身,判断是否为同一节点,如果不为相同节点,则删除该节点重新创建节点进行替换
如果为相同节点,进行patchVnode,判断如何对该节点的子节点进行处理,先判断一方有子节点一方没有子节点的情况(如果新的children没有子节点,将旧的子节点移除)
比较如果都有子节点,则进行updateChildren,判断如何对这些新老节点的子节点进行操作(diff核心)。
匹配时,找到相同的子节点,递归比较子节点
- patchVnode函数做了哪些操作
- 找到对应的
真实DOM
,称为el
- 判断
newVnode
和oldVnode
是否指向同一个对象,如果是,那么直接return
- 如果他们都有文本节点并且不相等,那么将
el
的文本节点设置为newVnode
的文本节点。 - 如果
oldVnode
有子节点而newVnode
没有,则删除el
的子节点 - 如果
oldVnode
没有子节点而newVnode
有,则将newVnode
的子节点真实化之后添加到el
- 如果两者都有子节点,则执行
updateChildren
函数比较子节点,这一步很重要
- 找到对应的
- updateChildren方法
做了一个动图方便理解
另外,如果一开始oldL在newNode的指针找不到时,新列表的第一个节点b去旧列表进行遍历比较,这里会有两种情况,
找到相同节点
和没找到相同节点
找到的情况,在旧节点中找到相同节点b,将节点b移动到首位,然后重新开始进行双端的步骤对比。如果在旧节点找不到,则在头部直接添加新节点,并将newL指针指向下一位,再继续进行对比。
面试官:了解过Vue插槽吗,有几种?
牛富贵:slot 又名插槽,是 Vue 的内容分发机制,组件内部的模板引擎使用 slot 元素作为承载分发内容的出口。插槽 slot 是子组件的一个模板标签元素,而这一个标签元素是否显示,以及怎么显示是由父组件决定的。slot 又分三类,默认插槽,具名插槽和作用域插槽。
- 默认插槽:又名匿名插槽,当 slot 没有指定 name 属性值的时候一个默认显示插槽,一个组件内只有有一个匿名插槽,作为找不到匹配的内容片段时的备用插槽。
- 具名插槽:带有具体名字的插槽,也就是带有 name 属性的 slot,一个组件可以出现多个具名插槽。
- 作用域插槽:默认插槽、具名插槽的一个变体,可以是匿名插槽,也可以是具名插槽,该插槽的不同点是在子组件渲染作用域插槽时,可以将子组件内部的数据传递给父组件,让父组件根据子组件的传递过来的数据决定如何渲染该插槽。
实现原理:当子组件 vm 实例化时,获取到父组件传入的 slot 标签的内容,存放在
vm.slot
中,默认插槽为vm.slot.default
,具名插槽为vm.slot.xxx
,xxx 为插槽名,当组件执行渲染函数时候,遇到 slot 标签,使用$slot
中的内容进行替换,此时可以为插槽传递数据,若存在数据,则可称该插槽为作用域插槽。
面试官:有做过哪些性能优化?
牛富贵:
- 对象层级不要过深,否则性能就会差。
- 不需要响应式的数据不要放在 data 中(可以使用 Object.freeze() 冻结数据)
- v-if 和 v-show 区分使用场景
- computed 和 watch 区分场景使用
- 大数据列表和表格性能优化——虚拟列表 / 虚拟表格
- 防止内部泄露,组件销毁后把全局变量和时间销毁
- 服务端渲染
- 图片懒加载
- 路由懒加载
- 适当采用 keep-alive 缓存组件
- 开启 gzip 压缩
- 防抖、节流的运用
面试官:服务端渲染了解过吗?
牛富贵:
- 服务端渲染(SSR),
也就是将 Vue 在客户端把标签渲染成 HTML 的工作放在服务端完成,然后再把 html 直接返回给客户端
。 - SSR 有着更好的 SEO、并且首屏加载速度更快等优点。不过它也有一些缺点,比如我们的开发条件会受到限制,服务器端渲染只支持
beforeCreate
和created
两个钩子,当我们需要一些外部扩展库时需要特殊处理,服务端渲染应用程序也需要处于 Node.js 的运行环境。还有就是服务器会有更大的负载需求。
大致流程就是将 Source(源码)通过 webpack 打包出两个 bundle,其中 Server Bundle 是给服务端用的,服务端通过渲染器 bundleRenderer 将 bundle 生成 html 给浏览器用;另一个 Client Bundle 是给浏览器用的,别忘了服务端只是生成前期首屏页面所需的 html ,后期的交互和数据处理还是需要能支持浏览器脚本的 Client Bundle 来完成。
一个小栗子
// 第 1 步:创建一个 Vue 实例 const Vue = require('vue') const app = new Vue({ template: `<div>Hello World</div>` }) // 第 2 步:创建一个 renderer const renderer = require('vue-server-renderer').createRenderer() // 第 3 步:将 Vue 实例渲染为 HTML renderer.renderToString(app, (err, html) => { if (err) throw err console.log(html) // => <div data-server-rendered="true">Hello World</div> })
上面例子利用 vue-server-renderer
npm 包将一个vue示例最后渲染出了一段 html。将这段html发送给客户端就轻松的实现了服务器渲染了。
const server = require('express')() server.get('*', (req, res) => { // ... 生成 html res.end(html) }) server.listen(8080)
- 服务端渲染和客户端渲染的区别
客户端渲染 | 服务端渲染 | |
---|---|---|
html的生成原理 | 由js生成html | 由后台语言通过一些模板引擎生成 |
优点 | 前端做视图和交互 | 响应快,用户体验好 |
后端提供接口,数据 | 搜索引擎友好,有seo优化 | |
前后端分离 | nodejs层服务器渲染,前端性能优化更顺手 | |
前端做路由 | 可操作空间更大 | |
服务器计算压力变轻 | ||
缺点 | 用户等待时间变长,尤其是请求数多且有一定先后顺序的时候 | 增加服务器计算压力;如果不是增加node中间层,前后端分工不明,不能很好的并行并发 |
耗时比较 | 数据请求:客户端在不同网络环境进行数据请求,外网http请求开销大,导致时间差 | 数据请求:服务端在内网请求,数据响应速度快 |
步骤:客户端需要等待js代码下载,加载完成在请求数据,渲染 | 步骤:服务端是先请求数据再渲染可视化部分,即服务端不需要等待js代码下载,并会返回一个已经有内容的页面 | |
渲染内容:客户端渲染,是经历一个从无到有完整的渲染步骤 | 渲染内容:服务端先渲染可视化部分,客户端再做二次渲染 | |
适合场景 | 单页面应用,如Vue | 用户体验比较高的比如首屏加载,重复较多的公共页面可以使用服务器渲染,减少ajax请求,提高用户体验 |
面试官:讲讲图片懒加载
牛富贵:
- 图片懒加载大概思路,渲染时设置一个节点的自定义属性,比如说
data-src
,然后值为图片 url 地址,图片的 src 属性指向懒加载的封面,监听 scroll 事件,通过getClientBoundingRect
API 获得图片相对视口的位置,当图片距离视口底部一定时,替换 url 地址。达成目标; - 当浏览器支持
Intersection Observer
API 时,可以使用该构造函数创建一个观察者,观察所有待懒加载的图片资源; - 现在浏览器原生支持图片和 iframe 懒加载,使用
loading="lazying"
,不过不太可控,而且浏览器兼容性并不好。
<img v-lazy="/static/img/01.png"/>
面试官:刚刚你提到keep-alive,你能简单介绍一下吗
牛富贵:
keep-alive
可以实现组件缓存,当组件切换时不会对当前组件进行卸载。- 常用的两个属性
include/exclude
,允许组件有条件的进行缓存。 - 两个生命周期
activated/deactivated
,用来得知当前组件是否处于活跃状态。keep-alive 的中还运用了LRU(Least Recently Used)
算法。
keep-alive 原理
- 在具体实现上,keep-alive 在内部维护了一个 key 数组和一个缓存对象
//keep-alive 内部声明周期函数 created () { this.cache = Object.create(null) this.keys = [] }
key 数组记录目前缓存的组件 key 值,如果组件没有指定 key 值,会自动生成一个唯一的 key 值
cache 对象会以 key 值为键,vnode 为值,用于缓存组件对应的虚拟 DOM
在 keep-alive 的渲染函数中,其基本逻辑是判断当前渲染的 vnode 是否有对应的缓存,如果有,会从缓存中读取到对应的组件实例,如果没有就会把它缓存。当缓存的数量超过
max
设置的数值时,keep-alive
会移除 key 数组中的第一个元素
LRU缓存策略
从内存中找出最久未使用的数据并置换新的数据。 LRU(Least rencently used)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是 "如果数据最近被访问过,那么将来被访问的几率也更高"。
利用map实现一个LRU算法
//力扣解题思路 1、创建map来保存数据 2、get:访问某key,访问完要将其放在最后的。若key存在,先保存value值,删除key,再添加key,最后返回保存的value值。若key不存在,返回-1 3、put:新加一个key,要将其放在最后的。所以,若key已经存在,先删除,再添加。如果容量超出范围了,将map中的头部删除。 class LRUCache { constructor(capacity) { this.capacity = capacity; this.map = new Map(); } get(key) { if (this.map.has(key)) { // get表示访问该值 // 所以在访问的同时,要将其调整位置,放置在最后 const temp = this.map.get(key); // 先删除,再添加 this.map.delete(key); this.map.set(key, temp); // 返回访问的值 return temp; } else { // 不存在,返回-1 return -1; } } put(key, value) { // 要将其放在最后,所以若存在key,先删除 if (this.map.has(key)) this.map.delete(key); // 设置key、value this.map.set(key, value); if (this.map.size > this.capacity) { // 若超出范围,将map中头部的删除 // map.keys()返回一个迭代器 // 迭代器调用next()方法,返回包含迭代器返回的下一个值,在value中 this.map.delete(this.map.keys().next().value); } } }
面试官: gzip 压缩了解多少
牛富贵:gizp压缩是一种http请求优化方式,通过减少文件体积来提高加载速度。html、js、css文件甚至json数据都可以用它压缩,可以减小60%以上的体积。前端配置gzip压缩,并且服务端使用nginx开启gzip,用来减小网络传输的流量大小。
牛富贵:命令行执行:npm i compression-webpack-plugin -D
牛富贵:在webpack的dev开发配置文件中加入如下代码:
const CompressionWebpackPlugin = require('compression-webpack-plugin') plugins: [ new CompressionWebpackPlugin() ]
启用gzip压缩打包之后,会自动生成gz包。目前大部分主流浏览器客户端都是支持gzip的,不支持gzip格式文件的会默认访问源文件的,故不要配置清除源文件。配置好之后,打开浏览器访问线上,F12查看控制台,如果该文件资源的响应头里显示有Content-Encoding: gzip,表示浏览器支持并且启用了Gzip压缩的资源。
面试官:知道它是怎样实现吗
面试官:那讲一下防抖节流吧
牛富贵:
- 防抖,就是指触发事件后 n 秒后才执行函数,如果在 n 秒内又触发了事件,则会重新计算函数执行时间。
function debounce(fn, delay) { var timeout = null; return function (e) { clearTimeout(timeout); timeout = setTimeout(() => { fn.apply(this, arguments); }, delay) } } function handle() { console.log('防抖', Math.random()) } window.addEventListener('scroll', debounce(handle, 50))
- 节流,就是指连续触发事件但是在 n 秒中只执行一次函数。 节流会稀释函数的执行频率。
function throttle(fn, delay) { let canRun = true; return function () { if (!canRun) return; canRun = false; setTimeout(()=>{ fn.apply(this,arguments) canRun = true },delay) } } function sayHi(e) { console.log('节流:', e.target.innerWidth, e.target.innerHeight); } window.addEventListener('resize', throttle(sayHi,500));
- 应用场景
- 防抖:search 搜索联想,用户在不断输入值时,用防抖来节约请求资源;window 触发 resize 的时候,不断的调整浏览器窗口大小会不断的触发这个事件,用防抖来让其只触发一次。
- 节流:鼠标不断点击触发,mousedown(单位时间内只触发一次);监听滚动事件,比如是否滑到底部自动加载更多,用 throttle 来判断。
面试官:问你一个场景题吧
(牛富贵:女生喜欢我,如何高情商回复)
面试官:咳咳,如何实现一个 axios 拦截器
牛富贵:不好意思,走神了。主要如下步骤
- 新建 request.js 文件,导入 axios
import axios from 'axios'
- 创建一个 axios 的实例
const request = axios.create({ baseURL: xxx, // baseURL: '项目基地址' timeout: 5000 // 设置 5 秒延时关闭请求 })
- 设置请求拦截器
// request.interceptors.request.use() // 请求拦截器 request.interceptors.request.use(config => { config.headers.Authorization = `Bearer ${token}` // 设置请求头携带 token return config }, error => { console.log(error) // 发生错误打印 return error })
- 设置响应拦截器
// request.interceptors.response.use() // 响应拦截器 request.interceptors.response.use(config => { return config // 成功直接返回 }, error => { if (error.response.status === 401) { //如果发生错误,查看错误码是多少 401 为权限不够,token 过期 alert('token 请求超时!请重新登录!') // 进行操作,如删除 vuex 中过期用户数据等一系列操作 router.push('/login') // 强行返回到登录页 } return error })
- 导出 axios 实例
export default request
面试官:如何中断axios请求
牛富贵:官方提供了两种方法
- 使用
CancelToken.source
工厂方法创建 cancel token
const CancelToken = axios.CancelToken; const source = CancelToken.source(); axios.get('/user/12345', { cancelToken: source.token }).catch(function(thrown) { if (axios.isCancel(thrown)) { console.log('Request canceled', thrown.message); } else { // 处理错误 } }); axios.post('/user/12345', { name: 'new name' }, { cancelToken: source.token }) // 取消请求(message 参数是可选的) source.cancel('Operation canceled by the user.');
- 通过传递一个 executor 函数到
CancelToken
的构造函数来创建 cancel token
const CancelToken = axios.CancelToken; let cancel; axios.get('/user/12345', { cancelToken: new CancelToken(function executor(c) { // executor 函数接收一个 cancel 函数作为参数 cancel = c; }) }); // cancel the request cancel();
面试官:Vue渲染大量数据时应该怎么优化?说下你的思路
牛富贵:常见的有懒加载和分页,那我介绍一下长列表优化
牛富贵:对于长列表,通常分两种情况来优化。
- 一是静态列表:如果这个列表仅仅用于数据的展示,不会有任何数据变化,那么就不需要作响应式处理。但由于 vue中data 是响应式的,所以我们可以利用
Object.freeze
将其冻结起来。
export default { data: () => { return { users: [ /* a long static list */ ] }; }, async create() { const users = await axios.get("/users"); //数据冻结 this.users = Object.freeze(users); } };
Object.freeze() 方法可以冻结一个对象,冻结指的是不能向这个对象添加新的属性,不能修改其已有属性的值,不能删除已有属性,以及不能修改该对象已有属性的可枚举性、可配置性、可写性。该方法返回被冻结的对象。
//举个栗子 let arr = [0]; Object.freeze(a); // 数组中数据不能被修改了. arr[0] = 1; // fails silently arr.push(2); // fails silently
- 二是虚拟滚动:对于大数据的长列表,如果一次性全部渲染,显然是非常消耗性能的,所以可以采用虚拟滚动技术,只渲染被展示出来的部分
1、假设有 1 万条记录需要同时渲染,我们屏幕的可见区域
的高度为 500px
,而列表项的高度为 50px
,则此时我们在屏幕中最多只能看到 10 个列表项,那么在首次渲染的时候,我们只需加载 10 条即可。
2、当滚动发生时,我们可以通过计算当前滚动值得知此时在屏幕可见区域
应该显示的列表项。
3、假设滚动发生,滚动条距顶部的位置为 150px
,则我们可得知在可见区域内的列表项为第 4 项至第 13 项。
4、实现
虚拟列表的实现,实际上就是在首屏加载的时候,只加载可视区域
内需要的列表项,当滚动发生时,动态通过计算获得可视区域
内的列表项,并将非可视区域
内存在的列表项删除。
5、页面结构
//可视化区域容器 <div class="list-container"> //真实数据容器,以便生成滚动条 <div class="list-phantom"></div> //渲染区域 <div class="list"> <!-- item(1) --> <!-- item(2) --> <!-- ...... --> <!-- item(3) --> </div> </div>
6、接着,监听 list-container
的 scroll
事件,获取滚动位置 scrollTop
推算出:
- 列表总高度
listHeight
= listData.length * itemSize - 可显示的列表项数
visibleCount
= Math.ceil(screenHeight / itemSize) - 数据的起始索引
startIndex
= Math.floor(scrollTop / itemSize) - 数据的结束索引
endIndex
= startIndex + visibleCount - 列表显示数据为
visibleData
= listData.slice(startIndex,endIndex) - 偏移量
startOffset
= scrollTop - (scrollTop % itemSize);
7、完整版
<template> <div ref="list" class="list-container" @scroll="scrollEvent($event)"> <div class="list-phantom" :style="{ height: listHeight + 'px' }"></div> <div class="list" :style="{ transform: getTransform }"> <div ref="items" class="list-item" v-for="item in visibleData" :key="item.id" :style="{ height: itemSize + 'px',lineHeight: itemSize + 'px' }" >{{ item.value }}</div> </div> </div> </template>
export default { name:'VirtualList', props: { //所有列表数据 listData:{ type:Array, default:()=>[] }, //每项高度 itemSize: { type: Number, default:200 } }, computed:{ //列表总高度 listHeight(){ return this.listData.length * this.itemSize; }, //可显示的列表项数 visibleCount(){ return Math.ceil(this.screenHeight / this.itemSize) }, //偏移量对应的 style getTransform(){ return `translate3d(0,${this.startOffset}px,0)`; }, //获取真实显示列表数据 visibleData(){ return this.listData.slice(this.start, Math.min(this.end,this.listData.length)); } }, mounted() { this.screenHeight = this.$el.clientHeight; this.start = 0; this.end = this.start + this.visibleCount; }, data() { return { //可视区域高度 screenHeight:0, //偏移量 startOffset:0, //起始索引 start:0, //结束索引 end:null, }; }, methods: { scrollEvent() { //当前滚动位置 let scrollTop = this.$refs.list.scrollTop; //此时的开始索引 this.start = Math.floor(scrollTop / this.itemSize); //此时的结束索引 this.end = this.start + this.visibleCount; //此时的偏移量 this.startOffset = scrollTop - (scrollTop % this.itemSize); } } };
面试官:好了,今天面试就先到这吧,后续有通知会告诉你
两天后,富贵手机屏幕弹出一条对话框,富贵满心欢喜打开了手机,看到消息那刻,他的心突然变成一块石头不断往下坠,眼睛逐渐模糊到看不清手机上的字。是的,他收到了感谢信,原本觉得自己准备得很充分,心想着通过面试不成问题,但是结果却事与愿违。后来富贵跟我谈起此事时,我替他感到遗憾,想说几句安慰他的话,富贵却抢在我前头说“或许这才是我们真正的生活,总会遇到挫折的,但也不会总是如此,哪怕看不到希望,只要坚持,一定能守得云开见月明。“那一刻,我看到他身上散发的光。
后序
富贵的面试暂且告一段落了,牛富贵只不过是我虚构的人物,但他又像是真实的我们。原本想写他凭借自己能力拿下offer,可我不这样写。想来我们自己不就是这样吗,一开始信心满满参加每一场面试,不过老天爱开你玩笑,你可能初面就挂,亦或是终面挂,吃了各种闭门羹,似乎看不到一点点希望,然后开始胡思乱想,自己究竟是不是真的适合这一行,因此选择“躺平”,这听起来很消极,至少我觉得。看到游戏逆风的苗头于是你选择三分投,但是你看过那场韩信单枪匹马拆塔完成逆风的比赛吗?游戏尚且如此,生活呢?当你知道努力没有回报,你会不会放弃?思来想去,我也说不准,但至少不是现在,即便最后失败了,也不会因为不战而败或没尽全力而遗憾。牛富贵最后虽失败了,但他仍抱有希望,还在继续坚持。牛富贵的故事虽然落幕,但是生活还在继续,在一个看不到希望的道路上,总会彷徨和迷茫,你可以稍稍驻足,切莫深陷其中,继续坚持和努力就好,总会看到微光的。
像,牛一样刻苦学习,侠客一般洒脱生活,我想这才是牛客精神吧,希望每个迷茫的人都能够拥有!我们下期再见~(这图送给你,你会成为自己的英雄)