前端篇:朋友面试之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位置的不同数据

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-showv-ifv-else-ifv-elsev-forv-onv-bindv-modelv-oncev-slotv-htmlv-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
    • 判断newVnodeoldVnode是否指向同一个对象,如果是,那么直接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、并且首屏加载速度更快等优点。不过它也有一些缺点,比如我们的开发条件会受到限制,服务器端渲染只支持 beforeCreatecreated两个钩子,当我们需要一些外部扩展库时需要特殊处理,服务端渲染应用程序也需要处于 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 事件,通过 getClientBoundingRectAPI 获得图片相对视口的位置,当图片距离视口底部一定时,替换 url 地址。达成目标;
  • 当浏览器支持 Intersection ObserverAPI 时,可以使用该构造函数创建一个观察者,观察所有待懒加载的图片资源;
  • 现在浏览器原生支持图片和 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-containerscroll事件,获取滚动位置 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,可我不这样写。想来我们自己不就是这样吗,一开始信心满满参加每一场面试,不过老天爱开你玩笑,你可能初面就挂,亦或是终面挂,吃了各种闭门羹,似乎看不到一点点希望,然后开始胡思乱想,自己究竟是不是真的适合这一行,因此选择“躺平”,这听起来很消极,至少我觉得。看到游戏逆风的苗头于是你选择三分投,但是你看过那场韩信单枪匹马拆塔完成逆风的比赛吗?游戏尚且如此,生活呢?当你知道努力没有回报,你会不会放弃?思来想去,我也说不准,但至少不是现在,即便最后失败了,也不会因为不战而败或没尽全力而遗憾。牛富贵最后虽失败了,但他仍抱有希望,还在继续坚持。牛富贵的故事虽然落幕,但是生活还在继续,在一个看不到希望的道路上,总会彷徨和迷茫,你可以稍稍驻足,切莫深陷其中,继续坚持和努力就好,总会看到微光的。

像,牛一样刻苦学习,侠客一般洒脱生活,我想这才是牛客精神吧,希望每个迷茫的人都能够拥有!我们下期再见~(这图送给你,你会成为自己的英雄)

图片说明

#高频知识点汇总##学习路径#
全部评论
1 回复
分享
发布于 2021-11-25 13:48
表情包可算是被你用明白了
1 回复
分享
发布于 2021-11-25 15:42
联想
校招火热招聘中
官网直投
给你顶顶
点赞 回复
分享
发布于 2021-11-25 13:53
🎉恭喜牛友成功参与 【创作激励计划】高频知识点汇总专场,并通过审核! ------------------- 创作激励计划5大主题专场等你来写,最高可领取500元京东卡和500元实物奖品! 👉快来参加吧:https://www.nowcoder.com/discuss/804743
点赞 回复
分享
发布于 2021-11-25 15:42
文章有几个表格的格式乱了,重新补充一下
点赞 回复
分享
发布于 2021-11-26 13:00
点赞 回复
分享
发布于 2021-11-26 16:21
富贵,我感觉你去sy有点屈才啊😯
点赞 回复
分享
发布于 2021-11-30 10:18
哇,我好喜欢你的表情包啊,不愧是三周霸榜王哇
点赞 回复
分享
发布于 2021-12-14 17:15
这些答案都是正确的吗,为什么牛富贵挂了😂
点赞 回复
分享
发布于 2021-12-19 16:31
内容已整理到牛客博客,且附PDF和markdown文档网盘链接供大家下载
点赞 回复
分享
发布于 2022-01-14 21:51
可以转载到公众号和掘金吗,会标明出处😊
点赞 回复
分享
发布于 2022-01-16 10:50

相关推荐

31 92 评论
分享
牛客网
牛客企业服务