前端Vue面试题总结(持续更新...)

328 阅读34分钟

1. 简述MVVM ,MVC模式

MVC模式

以往的MVC模式是单向绑定,即Model绑定到View,当我们用JavaScript代码更新Model时,View就会自动更新

MVVM模式

  • M - Model,Model 代表数据模型,也可以在 Model 中定义数据修改和操作的业务逻辑
  • V - View,View 代表 UI 组件,它负责将数据模型转化为 UI 展现出来
  • VM - ViewModel,ViewModel 监听模型数据的改变和控制视图行为、处理用户交互,简单理解就是一个同步 View 和 Model 的对象,连接 Model 和 View

MVVM模式就是Model–View–ViewModel模式。它实现了View的变动,自动反映在 ViewModel,反之亦然。对于双向绑定的理解,就是用户更新了View,Model的数据也自动被更新了,这种情况就是双向绑定。再说细点,就是在单向绑定的基础上给可输入元素input、textare等添加了change(input)事件,(change事件触发,View的状态就被更新了)来动态修改model

在MVVM框架下视图和模型是不能直接通信的,只能通过ViewModel进行交互,它能够监听到数据的变化,然后通知视图进行自动更新,而当用户操作视图时,VM也能监听到视图的变化,然后通知数据做相应改动,这实际上就实现了数据的双向绑定。并且V和VM可以进行通信。

MVVM模式的优点:

  1. 低耦合:View可以独立于Model变化和修改,一个ViewModel可以绑定到不同的View上,当View变化的时候Model可以不变,当Model变化的时候View也可以不变。
  2. 可重用性: 可以把一些视图逻辑放在一个ViewModel里面,让很多View重用这段视图逻辑。
  3. 独立开发: 开发人员可以专注于业务逻辑和数据的开发,设计人员可以专注于页面的设计。
  4. 可测试

MVC与MVVM的区别:

MVC和MVVM的区别并不是VM完全取代了C,ViewModel存在目的在于抽离Controller中展示的业务逻辑,而不是替代Controller,其它视图操作业务等还是应该放在Controller中实现。也就是说MVVM实现的是业务逻辑组件的重用。

  • MVC中Controller演变成MVVM中的ViewModel
  • MVVM通过数据来显示视图层而不是节点操作
  • MVVM主要解决了MVC中大量的dom操作使页面渲染性能降低,加载速度变慢,影响用户体验等问题。

2. Vue的优点及缺点

首先Vue最核心的两个特点,响应式组件化

响应式:这也就是vue.js最大的优点,通过MVVM思想实现数据的双向绑定,通过虚拟DOM让我们可以用数据来操作DOM,而不必去操作真实的DOM,提升了性能。且让开发者有更多的时间去思考业务逻辑。

组件化:把一个单页应用中的各个模块拆分到一个个组件当中,或者把一些公共的部分抽离出来做成一个可复用的组件。所以组件化带来的好处就是,提高了开发效率,方便重复使用,使项目的可维护性更强。

虚拟DOM,当然,这个不是vue中独有的。

缺点:基于对象配置文件的写法,也就是options写法,开发时不利于对一个属性的查找。另外一些缺点,在小项目中感觉不太出什么,vuex的魔法字符串,对ts的支持。兼容性上存在一些问题。

  • 不利于seo。
  • 导航不可用,如果一定要导航需要自行实现前进、后退。(由于是单页面不能用浏览器的前进后退功能,所以需要自己建立堆栈管理)。
  • 初次加载时耗时多。

3. 介绍一下虚拟DOM

面试官问: 如何理解Virtual DOM?

虚拟DOM本质就是用一个原生的JavaScript对象去描述一个DOM节点。是对真实DOM的一层抽象。

虚拟dom就是用js对象去模拟dom,通过算法对比去更新部分dom,节省性能。

由于在浏览器中操作DOM是很昂贵的。频繁的操作DOM,会产生一定的性能问题,因此我们需要这一层抽象,在patch过程中尽可能地一次性将差异更新到DOM中,这样保证了DOM不会出现性能很差的情况。

另外还有很重要的一点,也是它的设计初衷,为了更好的跨平台,比如Node.js就没有DOM,如果想实现SSR(服务端渲染),那么一个方式就是借助Virtual DOM,因为虚拟DOM本身是JS对象

虚拟 DOM 的实现原理主要包括以下 3 部分:

  • 用 JavaScript 对象模拟真实 DOM 树,对真实 DOM 进行抽象;
  • diff 算法 — 比较两棵虚拟 DOM 树的差异;
  • pach 算法 — 将两个虚拟 DOM 对象的差异应用到真正的 DOM 树。

虚拟DOM更加优秀的地方在于:

  1. 它打开了函数式的UI编程的大门,即UI = f(data)这种构建UI的方式。

    就是对DOM结构的抽象,让UI渲染变成 UI = f(data)的形式, 而f就应该是vDO

  2. 可以将JS对象渲染到浏览器DOM以外的环境中,也就是支持了跨平台开发,比如ReactNative。

优点:

  • 保证性能下限: 框架的虚拟 DOM 需要适配任何上层 API 可能产生的操作,它的一些 DOM 操作的实现必须是普适的,所以它的性能并不是最优的;但是比起粗暴的 DOM 操作性能要好很多,因此框架的虚拟 DOM 至少可以保证在你不需要手动优化的情况下,依然可以提供还不错的性能,即保证性能的下限;
  • 无需手动操作 DOM: 我们不再需要手动去操作 DOM,只需要写好 View-Model 的代码逻辑,框架会根据虚拟 DOM 和 数据双向绑定,帮我们以可预期的方式更新视图,极大提高我们的开发效率;
  • 跨平台: 虚拟 DOM 本质上是 JavaScript 对象,而 DOM 与平台强相关,相比之下虚拟 DOM 可以进行更方便地跨平台操作,例如服务器渲染、weex 开发等等。

缺点:

  • 无法进行极致优化: 虽然虚拟 DOM + 合理的优化,足以应对绝大部分应用的性能需求,但在一些性能要求极高的应用中虚拟 DOM 无法进行针对性的极致优化。

4. Diff算法

vue虚拟DOM与diff算法

diff算法,就是用来找出两段文本之间的差异的一种算法。

vdom为什么用diff算法

DOM操作是非常昂贵的,因此我们需要尽量地减少DOM操作。这就需要找出本次DOM必须更新的节点来更新,其他的不更新,这个找出的过程,就需要应用diff算法。

Vue优化的diff策略

跟react一样,只进行同层级比较,忽略跨级操作

react以及Vue在diff时,都是在对比虚拟dom节点,下文提到的节点都指虚拟节点。

DOM-diff比较两个虚拟DOM的区别,也就是在比较两个对象的区别。根据两个虚拟对象创建出补丁,描述改变的内容,将这个补丁用来更新DOM。

Vue是怎样描述一个节点的呢?

  • Vue虚拟节点

  • patch

    • diff时调用patch函数,patch接收两个参数vnode,oldVnode,分别代表新旧节点。
    • 根据key,判断这两个节点是否为同一类型节点。即便同一个节点元素比如div,他的className不同,Vue就认为是两个不同类型的节点,执行删除旧节点、插入新节点操作。
  • patchVnode 对于同类型节点调用patchVnode(oldVnode, vnode)进一步比较:

    • updateChildren 这是Vue diff实现的核心:

DOM-diff的过程/当数据发生变化时,vue是怎么更新节点的?

  1. 用JS对象模拟DOM(虚拟DOM)
  2. 把此虚拟DOM转成真实DOM并插入页面中(render)
  3. 如果有事件发生修改了虚拟DOM,比较两棵虚拟DOM树的差异,得到差异对象(diff)
  4. 把差异对象应用到真正的DOM树上(patch)

总结:

  • 尽量不要跨层级的修改dom
  • 设置key可以最大化的利用节点
  • 不要盲目相信diff的效率,在必要时可以手工优化

Vue2.x中的虚拟DOM主要是借鉴了snabbdom.jsVue3中借鉴inferno.js算法进行优化。

使用snabbdom实现vdom

这是一个简易的实现vdom功能的库,vdom里面有两个核心的api,一个是h函数,一个是patch函数,前者用来生成vdom对象,后者的功能在于做虚拟dom的比对和将vdom挂载到真实DOM上。

Vue2.X Diff——双端比较

双端比较就是新列表旧列表两个列表的头与尾互相对比,,在对比的过程中指针会逐渐向内靠拢,直到某一个列表的节点全部遍历过,对比停止。

Vue3 Diff —— 最长递增子序列

vue3diff借鉴于inferno,该算法其中有两个理念。第一个是相同的前置与后置元素的预处理;第二个则是最长递增子序列,此思想与Reactdiff类似又不尽相同。

5. 谈谈对vue生命周期的理解?

Vue 实例从创建到销毁的过程,就是生命周期。也就是从开始创建、初始化数据、编译模板、挂载Dom→渲染、更新→渲染、卸载等一系列过程,我们称这是 Vue 的生命周期

每个Vue实例在创建时都会经过一系列的初始化过程,vue的生命周期钩子,就是说在达到某一阶段或条件时去触发的函数,目的就是为了完成一些动作或者事件

触发的函数,目的就是为了完成一些动作或者事件

第一次页面加载时会触发 beforeCreate, created, beforeMount, mounted 这几个钩子

  • create阶段:vue实例被创建 beforeCreate: 创建前,此时data和methods中的数据都还没有初始化 created: 创建完毕,data中有值,未挂载
  • mount阶段: vue实例被挂载到真实DOM节点 beforeMount:可以发起服务端请求,去数据 mounted: 此时可以操作DOM
  • update阶段:当vue实例里面的data数据变化时,触发组件的重新渲染 beforeUpdate :更新前 updated:更新后
  • destroy阶段:vue实例被销毁 beforeDestroy:实例被销毁前,此时可以手动销毁一些方法 destroyed:销毁后

keep-alive下:activated deactivated

nextTick : 更新数据后立即操作dom

组件生命周期

生命周期(父子组件) 父beforeCreate --> 父created --> 父beforeMount --> 子beforeCreate --> 子created --> 子beforeMount --> 子mounted --> 父mounted -->父beforeUpdate -->子beforeDestroy--> 子destroyed --> 父updated

Vue 的父组件和子组件生命周期钩子函数执行顺序可以归类为以下 4 部分:

加载渲染过程 父beforeCreate->父created->父beforeMount->子beforeCreate->子created->子beforeMount->子mounted->父mounted

挂载阶段 父created->子created->子mounted->父mounted

父组件更新阶段 父beforeUpdate->父updated

子组件更新阶段 父beforeUpdate->子beforeUpdate->子updated->父updated

销毁阶段 父beforeDestroy->子beforeDestroy->子destroyed->父destroyed

父组件可以监听到子组件的生命周期吗?

比如有父组件 Parent 和子组件 Child,如果父组件监听到子组件挂载 mounted 就做一些逻辑处理,可以通过以下写法实现:

// Parent.vue
<Child @mounted="doSomething"/>
    
// Child.vue
mounted() {
  this.$emit("mounted");
}

以上需要手动通过 $emit 触发父组件的事件,更简单的方式可以在父组件引用子组件时通过 @hook 来监听即可,如下所示:

//  Parent.vue
<Child @hook:mounted="doSomething" ></Child>
​
doSomething() {
   console.log('父组件监听到 mounted 钩子函数 ...');
},
    
//  Child.vue
mounted(){
   console.log('子组件触发 mounted 钩子函数 ...');
},    
    
// 以上输出顺序为:
// 子组件触发 mounted 钩子函数 ...
// 父组件监听到 mounted 钩子函数 ...     

当然 @hook 方法不仅仅是可以监听 mounted,其它的生命周期事件,例如:created,updated 等都可以监听

在哪个生命周期内调用异步请求?

可以在钩子函数 created、beforeMount、mounted 中进行调用,因为在这三个钩子函数中,data 已经创建,可以将服务端端返回的数据进行赋值。

但是推荐在 created 钩子函数中调用异步请求,因为在 created 钩子函数中调用异步请求有以下优点:

  • 能更快获取到服务端数据,减少页面 loading 时间;
  • ssr 不支持 beforeMount 、mounted 钩子函数,所以放在 created 中有助于一致性;

6. vue的单向数据流

  • 状态,驱动应用的数据源;
  • 视图,以声明方式将状态映射到视图;
  • 操作,响应在视图上的用户输入导致的状态变化。

所有的 prop 都使得其父子 prop 之间形成了一个单向下行绑定:父级 prop 的更新会向下流动到子组件中,但是反过来则不行。这样会防止从子组件意外改变父级组件的状态,从而导致你的应用的数据流向难以理解。

额外的,每次父级组件发生更新时,子组件中所有的 prop 都将会刷新为最新的值。这意味着你不应该在一个子组件内部改变 prop。如果你这样做了,Vue 会在浏览器的控制台中发出警告。子组件想修改时,只能通过 $emit 派发一个自定义事件,父组件接收到后,由父组件修改

vue的init() 原理

beforeCreate init了events和生命周期

7. Vue底层实现原理(双向绑定原理)

Vue 数据双向绑定主要是指:数据变化更新视图,视图变化更新数据。

vue.js是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter和getter,在数据变动时发布消息给订阅者,触发相应的监听回调。

Vue是一个典型的MVVM框架,模型(Model)只是普通的javascript对象,修改它则视图(View)会自动更新。这种设计让状态管理变得非常简单而直观。

Vue 主要通过以下 4 个步骤来实现数据双向绑定的:

Observer(数据监听器) : 对数据对象进行遍历,包括子属性对象的属性。Observer的核心是通过Object.defineProprtty()来监听数据的变动,这个函数内部可以定义setter和getter,每当数据发生变化,就会触发setter。这时候Observer就要通知订阅者,订阅者就是Watcher

实现一个解析器 Compile:解析 Vue 模板指令,将模板中的变量都替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,调用更新函数进行数据更新。

Watcher(订阅者) : Watcher订阅者作为Observer和Compile之间通信的桥梁,主要的任务是订阅 Observer 中的属性值变化的消息,当收到属性值变化的消息时,触发解析器 Compile 中对应的更新函数。

  1. 在自身实例化时往属性订阅器(dep)里面添加自己
  2. 自身必须有一个update()方法
  3. 待属性变动dep.notice()通知时,能调用自身的update()方法,并触发Compile中绑定的回调

实现一个订阅器 Dep:订阅器采用 发布-订阅 设计模式,用来收集订阅者 Watcher,对监听器 Observer 和 订阅者 Watcher 进行统一管理。

defineProperty的属性值有哪些

  • configurable 是否可以删除属性和属性描述
  • enumerable 才能出现在对象枚举中
  • value 初始值
  • writable 是否能被赋值运算符改变
  • get
  • set

defineProperty和proxy的区别

  • defineProperty是劫持对象的数据
  • 而proxy是整个对象

Proxy 的优势如下:

  • Proxy 可以直接监听对象而非属性;
  • Proxy 可以直接监听数组的变化;
  • Proxy 有多达 13 种拦截方法,不限于 apply、ownKeys、deleteProperty、has 等等是 Object.defineProperty 不具备的;
  • Proxy 返回的是一个新对象,我们可以只操作新的对象达到目的,而 Object.defineProperty 只能遍历对象属性直接修改;
  • Proxy 作为新标准将受到浏览器厂商重点持续的性能优化,也就是传说中的新标准的性能红利;

Object.defineProperty 的优势如下:

  • 兼容性好,支持 IE9,而 Proxy 的存在浏览器兼容性问题,而且无法用 polyfill 磨平,因此 Vue 的作者才声明需要等到下个大版本( 3.0 )才能用 Proxy 重写。

Vue 框架怎么实现对象和数组的监听?

通过 Object.defineProperty() 对数据进行劫持,但是 Object.defineProperty() 只能对属性进行数据劫持,不能对整个对象进行劫持,同理无法对数组进行劫持。

Vue 框架是通过遍历数组 和递归遍历对象,从而达到利用 Object.defineProperty() 也能对对象和数组(部分方法的操作)进行监听。

Vue 怎么用 vm.$set() 解决对象新增属性不能响应的问题 ?

受现代 JavaScript 的限制 ,Vue 无法检测到对象属性的添加或删除。由于 Vue 会在初始化实例时对属性执行 getter/setter 转化,所以属性必须在 data 对象上存在才能让 Vue 将它转换为响应式的。

如果目标是数组,直接使用数组的 splice 方法触发响应式;

如果目标是对象,会先判读属性是否存在、对象是否是响应式,最终如果要对属性进行响应式处理,则是通过调用 defineReactive 方法进行响应式处理( defineReactive 方法就是 Vue 在初始化对象时,给对象属性采用 Object.defineProperty 动态添加 getter 和 setter 的功能所调用的方法)

8. vue3proxy 讲讲,和 2.0 区别(广度)

Vue2.0中,数据双向绑定就是通过Object.defineProperty去监听对象的每一个属性,然后在get,set方法中通过发布订阅者模式来实现的数据响应,但是存在一定的缺陷,比如只能监听已存在的属性,对于新增删除属性就无能为力了,同时无法监听数组的变化,所以在Vue3.0中将其换成了功能更强大的Proxy

ProxyES6新推出的一个特性,可以用它去拦截js操作的方法,从而对这些方法进行代理操作。Proxy代理的是整个对象,而不是对象的某个特定属性,不需要我们通过遍历来逐个进行数据绑定。

  • Object.defineProperty监听的是对象的每一个属性,而Proxy可以直接监听对象而非属性
  • Proxy可以直接监听数组的变化,Proxy对新增的属性也能监听到,但Object.defineProperty无法监听到。
  • Proxy返回的是一个新对象,我们可以只操作新的对象达到目的,而Object.defineProperty只能遍历对象属性直接修改。
  • Proxy的劣势就是兼容性问题,

对比Vue2.0

  • 在使用Vue2.0的时候,如果给对象添加新属性的时候,往往需要调用$set, 这是因为Object.defineProperty只能监听已存在的属性,而新增的属性无法监听,而通过$set相当于手动给对象新增了属性,然后再触发数据响应。但是对于Vue3.0来说,因为使用了Proxy, 在他的set钩子函数中是可以监听到新增属性的,所以就不再需要使用$set
  • Vue2.0是无法监听到属性被删除的,所以提供了$delete用于删除属性,但是对于Proxy,是可以监听删除操作的,所以就不需要再使用$delete

Vue3 Proxy

9. vue2和vue3的区别 (vue3快在哪)

  1. 响应式原理的改变:使用proxy直接获取属性而不是之前defineProperty劫持数据属性

    这消除了 Vue 2 当中基于 Object.defineProperty 的实现所存在的很多限制:

    • 只能监测属性,不能监测对象
    • 检测属性的添加和删除;
    • 检测数组索引和长度的变更;
    • 支持 Map、Set、WeakMap 和 WeakSet。
  2. diff算法增加patchFlag静态标识,只对比有静态标识的dom元素

  3. 渲染函数 文本dom元素提升 只声明一次 render的时候可以多次使用不像之前vue2需要每次都重新声明渲染

  4. 事件增加了缓存ssr渲染 以字符串方式渲染

  5. typeScript的支持

  6. 打包体积变化

    Vue 3中,我们通过将大多数全局API和内部帮助程序移动到Javascriptmodule.exports属性上实现这一点。

  • vite

  • 组件选项声明方式Vue3.x 使用Composition API setup 是Vue3.x新增的一个选项,是组件内使用Composition API 的入口

  • 模板语法变化slot具名插槽语法

    2.x 的机制导致作用域插槽变了,父组件会重新渲染,而 3.0 把作用域插槽改成了函数的方式,这样只会影响子组件的重新渲染,提升了渲染的性能。

  • 自定义指令 v-model 升级

10. vue3 hooks用过吗?

以函数式的思想编程

相关链接

Vue3 的 hooks 让我们可以在组件外部调用 Vue 的所有能力

vue3 之下,我们可以把相关代码维护在一起,代码空间距离更低,可读性高

11. vue3新增的属性

直接给一个数组项赋值,Vue 能检测到变化吗?

由于 JavaScript 的限制,Vue 不能检测到以下数组的变动:

  • 当你利用索引直接设置一个数组项时,例如:vm.items[indexOfItem] = newValue

    使用 vm.$set,Vue.set的一个别名

  • 当你修改数组的长度时,例如:vm.items.length = newLength

    使用 Array.prototype.splice

12. Vue 项目中 key 的作用

  • key的作用是为了在diff算法执行时更快的找到对应的节点,提高diff速度,更高效的更新虚拟DOM;

    vue和react都是采用diff算法来对比新旧虚拟节点,从而更新节点。在vue的diff函数中,会根据新节点的key去对比旧节点数组中的key,从而找到相应旧节点。如果没找到就认为是一个新增节点。而如果没有key,那么就会采用遍历查找的方式去找到对应的旧节点。一种一个map映射,另一种是遍历查找。相比而言。map映射的速度更快。

  • 为了在数据变化时强制更新组件,以避免就地复用带来的副作用

    当 Vue.js 用 v-for 更新已渲染过的元素列表时,它默认用“就地复用”策略。如果数据项的顺序被改变,Vue 将不会移动 DOM 元素来匹配数据项的顺序,而是简单复用此处每个元素,并且确保它在特定索引下显示已被渲染过的每个元素。重复的key会造成渲染错误。

为什么不要用index作为key

如果你的列表顺序会改变,别用 index 作为 key,和没写基本上没区别,因为不管你数组的顺序怎么颠倒,index 都是 0, 1, 2 这样排列,导致 Vue 会复用错误的旧子节点,做很多额外的工作。列表顺序不变也尽量别用,可能会误导新人。

为什么不要用随机数作为key?

使用随机数作为 key,更新后会生成全新的随机数,diff子节点的首尾对比如果都没有命中,就会进入key的详细比对过程。

就是利用旧节点的 key -> index 的关系建立一个 map 映射表,然后用新节点的 key 去匹配,如果没找到的话,就会调用 createElm 方法 重新建立 一个新节点。

旧节点会被全部删掉,新节点重新创建

为什么 Vue 中不要用 index 作为 key?

13. 组件中的data为什么是一个函数?new Vue实例里,data可以直接是一个对象

  • 组件时用来复用的,组件中的data写成一个函数,数据以函数返回值形式定义,函数有独立的作用域,这样每复用一次组件,就会返回一份新的data,类似于给每个组件实例创建一个私有的数据空间,让各个组件实力维护各自的数据。
  • 如果data是对象的话,对象属于引用类型,会影响到所有的实例。所以为了保证组件不同的实例之间data不冲突,data必须是一个函数
  • 而 new Vue 的实例,是不会被复用的,因此不存在引用对象的问题。

14. vue的component 全局注册和局部注册有什么区别

  • 全局组件:Vue.component()
  • 局部组件:components:

15. 开发中常用的指令有哪些

v-model :一般用在表单输入,很轻松的实现表单控件和数据的双向绑定

v-html: 更新元素的 innerHTML

v-show 与 v-if: 条件渲染, 注意二者区别

  • 使用了v-if的时候,如果值为false,那么页面将不会有这个html标签生成 v-show则是不管值为true还是false,html元素都会存在,只是CSS中的display显示或隐藏

v-on : click: 可以简写为@click,@绑定一个事件。如果事件触发了,就可以指定事件的处理函数

v-for:基于源数据多次渲染元素或模板块

v-bind: 当表达式的值改变时,将其产生的连带影响,响应式地作用于 DOM

16. vue中插值表达式、v-text、v-html的区别与用法

插值表达式

数据绑定最常见的形式就是使用“Mustache”语法{{}}(双大括号)的文本插值

msg的值会从data中传递,而这就造成了在某种情况下的弊端

当网络环境较差、网速较慢、刷新频繁的时候,插值表达式会出现{undefined{message}}的情况,(之后再用真实的数据替换掉)。因为页面加载是自上而下的,加载到表达式时,可能还没有解析到script标签中的data数据。

解决方法:

  • 使用v-clock解决屏幕闪动问题

    Vue提供了内置属性v-clock,这个指令保持在元素上直到关联实例结束编译。和 CSS 规则如 [v-cloak] { display: none } 一起用时,可以隐藏未编译的 Mustache 标签直到实例准备完毕。

  • 使用v-text或者v-html

v-text

<p v-text='msg'></p>

  • v-text的显示效果与插值表达式一致,并且不会出现上述的屏幕闪动现象。
  • 插值表达式相当于一个占位符,只会替换掉占位置的内容。
  • v-text只渲染Vue传递过来的数据,会替换掉节点内已有内容。 因此需要根据需求判断是否需要同时展示。

v-html

  • 插值表达式和v-text不能够解析html标签,v-html能够解析html标签

17. v-show与v-if区别

v-show是css切换,v-if是完整的销毁和重新创建

v-if=‘false’ v-if是条件渲染,当false的时候不会渲染,因为它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建;

v-if 适用于在运行时很少改变条件,不需要频繁切换条件的场景;v-show 则适用于需要非常频繁切换条件的场景。

18. 为什么v-for和v-if不建议用在一起

  • 当 v-for 和 v-if 处于同一个节点时,v-for 的优先级比 v-if 更高,这意味着 v-if 将分别重复运行于每个 v-for 循环中。如果要遍历的数组很大,而真正要展示的数据很少时,这将造成很大的性能浪费
  • 这种场景建议使用 computed,先对数据进行过滤

注意: 3.x 版本中 v-if 总是优先于 v-for 生效。由于语法上存在歧义,建议避免在同一元素上同时使用两者。比起在模板层面管理相关逻辑,更好的办法是通过创建计算属性筛选出列表,并以此创建可见元素。

19. Class 与 Style 如何动态绑定?

Class 可以通过对象语法和数组语法进行动态绑定

Style 也可以通过对象语法和数组语法进行动态绑定:

对象方法

<div v-bind:class="{ active: isActive, 'text-danger': hasError }"></div>

data: {
  isActive: true,
  hasError: false
}

数组方法

<div v-bind:class="[isActive ? activeClass : '', errorClass]"></div>
​
data: {
  activeClass: 'active',
  errorClass: 'text-danger'
}

行内 v-bind:style="{color: color, fontSize: fontSize+'px' }"

20. v-model 的原理

vue 项目中主要使用 v-model 指令在表单 input、textarea、select 等元素上创建双向数据绑定,我们知道 v-model 本质上不过是语法糖,v-model 在内部为不同的输入元素使用不同的属性并抛出不同的事件:

  • text 和 textarea 元素使用 value 属性和 input 事件;
  • checkbox 和 radio 使用 checked 属性和 change 事件;
  • select 字段将 value 作为 prop 并将 change 作为事件;

修饰符:

  • trim:去除首尾空格
  • lazy:只在输入框失去焦点或按回车键时更新内容,不是实时更新
  • number:将数据转换成number类型(原本是字符串类型)

21. computed 和 watch 区分使用场景

既能用 computed 实现又可以用 watch 监听来实现的功能,推荐用 computed, 重点在于 computed 的缓存功能,computed 计算属性是用来声明式的描述一个值依赖了其它的值,当所依赖的值或者变量改变时,计算属性也会跟着改变; watch 监听的是已经在 data 中定义的变量,当该变量变化时,会触发 watch 中的方法。

computed: 是计算属性,依赖其它属性值,并且 computed 的值有缓存,只有它依赖的属性值发生改变,下一次获取 computed 的值时才会重新计算 computed 的值;

watch: 是一个对象,键是需要观察的属性,值是对应回调函数,主要用来监听某些特定数据的变化,从而进行某些具体的业务逻辑操作,监听属性的变化,需要在数据变化时执行异步或开销较大的操作时使用。无缓存性,页面重新渲染时值不变化也会执行

运用场景:

  • 当我们需要进行数值计算,并且依赖于其它数据时,应该使用 computed,因为可以利用 computed 的缓存特性,避免每次获取值时,都要重新计算;例:购物车商品结算功能
  • 当我们需要在数据变化时执行异步或开销较大的操作时,应该使用 watch,使用 watch 选项允许我们执行异步操作 ( 访问一个 API ),限制我们执行该操作的频率,并在我们得到最终结果前,设置中间状态。这些都是计算属性无法做到的。例:搜索数据

22. vue组件的通信方式

Vue3的8种和Vue2的12种组件通信

  1. 父组件向子组件传递数据

    • 父传子,使用props
    • 子传父,在子组件中使用$emit派发事件,父组件中使用$on
    • 监听事件;缺点:组件嵌套层次多的话,传递数据比较麻烦。
  2. 通过$attrs$listeners 适用于 隔代组件通信

    • $attrs 属于组件中的一个属性,可以获取父组件中传递过来的props数据v-bind="$attrs",父组件不需要用v-bind。如果子组件通过props接收的属性,$attrs获取不到
    • $listeners组件实例中的一个属性,获取父组件的自定义事件。子组件使用v-on="$listeners",
  3. 通过属性$root / $parent / $children /ref,访问根组件、父级组件、子组件中的数据 适用 父子组件通信

    缺点:要求组件之间要有传递性。

    • ref 如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件实例
    • $children 获取到当前组件全部的子组件
    • $parent 获取到某一个组件父组件,可以操作父组件的数据与方法
  4. 祖先组件 适用于 隔代组件通信

    -   祖先组件中通过` provider` 来提供变量,然后在子孙组件中通过`inject` 来注入变量。
    
    • 缺点:无法监听数据修改的来源,不支持响应式。
  5. 自定义事件,事件总线(event bus)的方式,可以实现任意两个组件间进行数据传递,

    Vue.prototype.$bus = new Vue()

    缺点:不支持响应式,这个概念是vue1.0版本中的,现在已经废弃。

    vue3可以引入mitt.js

  6. 通过 Vuex,实现多个组件进行数据共享,推荐使用这种方式进行项目中各组件间的数据传递。

  7. sync,父子通信 vue2

    :money.sync,代表父组件给字符串传递props[money]
    给当前子组件绑定一个自定义事件 update:money
    
  8. 通过v-model父子通信

  9. 插槽

    • 父子组件通信(一般结构)

23. nextTick的实现

在下次DOM更新循环结束之后执行延迟回调。在修改数据之后,立即使用的这个回调函数,获取更新后的DOM

实现原理: DOM 更新循环结束之后执行延迟回调,在修改数据之后立即使用 nextTick 来获取更新后的 DOM。 nextTick主要使用了宏任务和微任务。 根据执行环境分别尝试采用Promise、MutationObserver、setImmediate,如果以上都不行则采用setTimeout定义了一个异步方法,多次调用nextTick会将方法存入队列中,通过这个异步方法清空当前队列。

在Vue生命周期的created()钩子函数进行的DOM操作一定要放在Vue.nextTick()的回调函数中。

原因是在created()钩子函数执行的时候DOM 其实并未进行任何渲染,而此时进行DOM操作无异于徒劳,所以此处一定要将DOM操作的js代码放进Vue.nextTick()的回调函数中。与之对应的就是mounted钩子函数,因为该钩子函数执行时所有的DOM挂载和渲染都已完成,此时在该钩子函数中进行任何DOM操作都不会有问题 。

在数据变化后要执行的某个操作,而这个操作需要使用随数据改变而改变的DOM结构的时候,这个操作都应该放进Vue.nextTick()的回调函数中。

原因是,Vue是异步执行dom更新的,一旦观察到数据变化,Vue就会开启一个队列,然后把在同一个事件循环 (event loop) 当中观察到数据变化的 watcher 推送进这个队列。如果这个watcher被触发多次,只会被推送到队列一次。这种缓冲行为可以有效的去掉重复数据造成的不必要的计算和DOm操作。而在下一个事件循环时,Vue会清空队列,并进行必要的DOM更新。

当你设置 vm.someData = ‘new value’,DOM 并不会马上更新,而是在异步队列被清除,也就是下一个事件循环开始时执行更新时才会进行必要的DOM更新。如果此时你想要根据更新的 DOM 状态去做某些事情,就会出现问题。。为了在数据变化之后等待 Vue 完成更新 DOM ,可以在数据变化之后立即使用 Vue.nextTick(callback) 。这样回调函数在 DOM 更新完成后就会调用。

24. 常用的事件修饰符和按键修饰符

事件修饰符:

  1. .stop:阻止冒泡

    对于嵌套的两级,如果子父级元素都存在click事件,点击子级元素会触发父级元素的事件;如果子级元素设置@click.stop的话就不会触发父级的click事件

  2. .prevent: 阻止默认行为 对于如<a href=“www.baidu.com” @click.prevent=“linkMethod”>百度自带事件的,添加prevent事件后,href跳转路径将不会触发

  3. .self:仅绑定元素自身触发,防止事件冒泡 对于嵌套的两级,如果子父级元素都存在click事件,点击子级元素会触发父级元素的事件;如果父级元素设置@click.self的话就不会被子级元素的click事件影响

  4. .once: 事件只触发一次(常用表单提交)

  5. .capture: 对于冒泡事件,且存在多个冒泡事件时,存在该修饰符的会优先执行,如果有多个,则从外到内执行

  6. .native: 将vue组件转换为一个普通的HTML标签,如果该修饰符用在普通html标签上是不起任何作用的

  7. .passive: 滚动事件的默认行为 (即滚动行为) 将会立即触发,不能和.prevent 一起使用,浏览器内核线程在每个事件执行时查询prevent,造成卡顿,使用passive将会跳过内核线程查询,进而提升流畅度

按键修饰符:

.tab .enter .esc .space .delete .up .down .left .right

25. 使用过插槽么?用的是具名插槽还是匿名插槽或作用域插槽

vue中的插槽是一个非常好用的东西slot说白了就是一个占位的

单个插槽

当子组件模板只有一个没有属性的插槽时,
父组件传入的整个内容片段将插入到插槽所在的 DOM 位置,
并替换掉插槽标签本身

命名插槽

slot元素可以用一个特殊的特性name来进一步配置如何分发内容。
多个插槽可以有不同的名字。 这样可以将父组件模板中 slot 位置,
和子组件 slot 元素产生关联,便于插槽内容对应传递

作用域插槽

可以访问组件内部数据的可复用插槽(reusable slot)
在父级中,具有特殊特性 slot-scope<template> 元素必须存在,
表示它是作用域插槽的模板。slot-scope 的值将被用作一个临时变量名,
此变量接收从子组件传递过来的 prop 对象

26. keep-alive的实现

作用:实现组件缓存,保持这些组件的状态,以避免反复渲染导致的性能问题。 需要缓存组件 频繁切换,不需要重复渲染

场景:tabs标签页 后台导航,vue性能优化

  • 一般结合路由和动态组件一起使用,用于缓存组件;
  • 提供 include 和 exclude 属性,两者都支持字符串或正则表达式, include 表示只有名称匹配的组件会被缓存,exclude 表示任何名称匹配的组件都不会被缓存 ,其中 exclude 的优先级比 include 高;
  • 对应两个钩子函数 activated 和 deactivated ,当组件被激活时,触发钩子函数 activated,当组件被移除时,触发钩子函数 deactivated。

原理:Vue.js内部将DOM节点抽象成了一个个的VNode节点,keep-alive组件的缓存也是基于VNode节点的而不是直接存储DOM结构。它将满足条件(pruneCache与pruneCache)的组件在cache对象中缓存起来,在需要重新渲染的时候再将vnode节点从cache对象中取出并渲染。

27. mixins

mixin 项目变得复杂的时候,多个组件间有重复的逻辑就会用到mixin,将多个组件有相同的逻辑,抽离出来。

mixin并不是完美的解决方案,会有一些问题

vue3提出的Composition API旨在解决这些问题【追求完美是要消耗一定的成本的,如开发成本】

场景: PC端新闻列表和详情页一样的右侧栏目,可以使用mixin进行混合

劣势:

  1. 变量来源不明确,不利于阅读
  2. 多mixin可能会造成命名冲突
  3. mixin和组件可能出现多对多的关系,使得项目复杂度变高

28. extends和vue.use mixins混入

extends 允许一个组件扩展到另一个组件,且继承该组件选项

mixins 选项主要用来组合功能,而 extends 主要用来考虑继承性。

插件要有install才可以use

区分extends:...和vue.extend

  • extend只能导入一个对象而mixins可以一个数组多对象
  • vue.extend()方法其实是vue的一个构造器,继承自vue
  • 可以通过extends拓展全局组件

29. vue2的filter

在 2.x 中,开发者可以使用过滤器来处理通用文本格式。

在 3.x 中,过滤器已移除,且不再支持。取而代之的是,我们建议用方法调用或计算属性来替换它们。

可以通过全局属性以让它能够被所有组件使用到:

// main.js
const app = createApp(App)

app.config.globalProperties.$filters = {
  currencyUSD(value) {
    return '$' + value
  }
}

<p>{{ $filters.currencyUSD(accountBalance) }}</p>

30. vue-router

路由(Router)这个概念最先是后端出现的,是用来跟后端服务器进行交互的一种方式,通过不同的路径,来请求不同的资源,请求不同的页面是路由的其中一种功能。

前端随着 ajax 的流行,数据请求可以在不刷新浏览器的情况下进行。异步交互体验中最盛行的就是 SPA —— 单页应用。单页应用不仅仅是在页面交互时无刷新的,连页面跳转都是无刷新的,为了实现单页应用,所以就有了前端路由。

1. 前端Router基本功能

  1. 前端Router可以控制浏览器的 history,使的浏览器不会在 URL 发生改变时刷新整个页面。
  2. 前端Router需要维护一个 URL 历史栈,通过这个栈可以返回之前页面,进入下一个页面

前端路由实现原理就是匹配不同的 url 路径,进行解析,然后动态的渲染出区域 html 内容。但是这样存在一个问题,就是 url 每次变化的时候,都会造成页面的刷新。那解决问题的思路便是在改变 url 的情况下,保证页面的不刷新。目前 Router有两种实现方式 History 和 hash

vue-router路由种类

3 种路由模式的说明如下:

  • hash: 使用 URL hash 值来作路由。支持所有浏览器,包括不支持 HTML5 History Api 的浏览器;
  • history : 依赖 HTML5 History API 和服务器配置。具体可以查看 HTML5 History 模式;
  • abstract : 支持所有 JavaScript 运行环境,如 Node.js 服务器端。如果发现没有浏览器的 API,路由会自动强制进入这个模式.

2. History 和 Hash 对比

  1. hash 使用 # 后面的内容模拟一个完整路径,不太美观。

  2. Vue底层对它们的实现方式不同。

    • hash模式是依靠onhashchange事件(监听location.hash的改变),来监听 hash 的改变,借此实现无刷新跳转的功能。不利于 SEO 优化。
    • history模式是主要是依靠的HTML5 history中新增的两个方法,pushState()可以改变url地址且不会发送请求,replaceState()可以读取历史记录栈,还可以对浏览器记录进行修改。实现无刷新跳转的功能。
  3. hash 在请求时不会发送给服务器,用户手动刷新页面,后端接受到了也是同一个地址。

  4. History 直接修改浏览器 URL,用户手动刷新页面,后端接受到是不同的地址,需要后端做处理跳转到统一的html页面

3.vue-router有哪几种导航钩子

  • 全局前置守卫:

    router.beforeEach(to,from,next),作用:跳转前进行判断拦截

  • 全局解析守卫

    router.beforeResolve 是获取数据或执行任何其他操作的理想位置

  • 全局后置钩子:

    router.afterEach 对于分析、更改页面标题、声明页面等辅助功能以及许多其他事情都很有用。

  • 路由独享守卫:

    beforeEnter 只有在 从一个不同的 路由导航时,才会被触发。

  • 组件内的守卫

    • beforeRouteEnter 在渲染该组件的对应路由被验证前调用,不能获取组件实例 this,因为当守卫执行时,组件实例还没被创建!
    • beforeRouteUpdate 在当前路由改变,但是该组件被复用时调用
    • beforeRouteLeave 在导航离开渲染该组件的对应路由时调用

4. Vue路由传参的两种方式,params和query方式与区别

$routerVueRouter 实例,想要导航到不同 URL,则使用 $router.push 方法

$route当前 router 跳转对象,里面可以获取 name、path、query、params 等

  1. 声明式 使用router-link

  2. 编程式导航跳转:

    this.$router.push({  name:'router1',params: { id: status ,id2: status3},query: { queryId:  status2 }});
    //编程跳转写在一个函数里面,通过click等方法来触发
    

路由设置这里,当你使用params方法传参的时候,要在路由后面加参数名, 并且传参的时候,参数名要跟路由后面设置的参数名对应。使用query方法,就没有这种限制,直接在跳转里面用就可以。

  1. params传参,必须使用命名路由的方式传参;
  2. params传参,不会显示在地址栏上,会保存在内存中,刷新会丢失,可以配合本地存储进行使用;
  3. query的参数会显示在地址栏上,不会丢失;

5. router守卫和系统权限

  • 路由封装、懒加载等等
  • 系统权限

路由懒加载

// 将
// import UserDetails from './views/UserDetails'
// 替换成
const UserDetails = () => import('./views/UserDetails')

component (和 components) 配置接收一个返回 Promise 组件的函数,Vue Router 只会在第一次进入页面时才会获取这个函数,然后使用缓存数据。这意味着你也可以使用更复杂的函数,只要它们返回一个 Promise 。

注意:不要在路由中使用异步组件。异步组件仍然可以在路由组件中使用,但路由组件本身就是动态导入的。

6. 导航守卫和axios拦截器的区别

  • 导航守卫就是路由守卫,想进入一个页面时,判断是否有权限访问(有token,就有权限,没有就返回),但并不能判断是否失效。
  • axios拦截器是发送请求判断token的有效性,如果有就将token放在请求头里。
  • 导航守卫和axios拦截器一起使用,进而来确保登录的状态

7. 怎么定义vue-router的动态路由?怎么获取传过来的动态参数

路径参数 用冒号 : 表示。当一个路由被匹配时,它的 params 的值将在每个组件中以 this.$route.params 的形式暴露出来。

可以在同一个路由中设置有多个 路径参数,它们会映射到 $route.params 上的相应字段。

使用带有参数的路由时需要注意的是,当用户从 /users/johnny 导航到 /users/jolyne 时,相同的组件实例将被重复使用。因为两个路由都渲染同个组件,比起销毁再创建,复用则显得更加高效。不过,这也意味着组件的生命周期钩子不会被调用

要对同一个组件中参数的变化做出响应的话,你可以简单地 watch $route 对象上的任意属性,在这个场景中,就是 $route.params :或者,使用 beforeRouteUpdate 导航守卫,它也可以取消导航:

8. 怎么拦截路由的非法跳转,哪些情况需要拦截

31. Vuex的理解及使用场景

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式

当我们的应用遇到多个组件共享状态时,单向数据流的简洁性很容易被破坏:

  • 多个视图依赖于同一状态。 传参的方法对于多层嵌套的组件将会非常繁琐,并且对于兄弟组件间的状态传递无能为力。
  • 来自不同视图的行为需要变更同一状态。 我们经常会采用父子组件直接引用或者通过事件来变更和同步状态的多份拷贝。

以上的这些模式非常脆弱,通常会导致无法维护的代码。

把组件的共享状态抽取出来,以一个全局单例模式管理。在这种模式下,我们的组件树构成了一个巨大的“视图”,不管在树的哪个位置,任何组件都能获取状态或者触发行为!这就是“状态管理模式”。

应用场景有:单页应用中,组件之间的数据状态。

应用实例: 1、购物车功能; 2、下单页面有选择优惠券按钮,点击进入优惠券页面,选择后返回到下单页,数据会绑定回来,显示已选择的优惠券; 3、登录状态等等

1. Vuex有哪几种属性?

有五种,分别是 State、 Getter、Mutation 、Action、 Module

State: 数据源存放地。

  • state里面存放的数据是响应式的,Vue组件从store中读取数据,若是store中的数据发生改变,依赖这个数据的组件也会发生更新
  • 它通过mapState把全局的 state 和 getters 映射到当前组件的 computed 计算属性中

Mutation: 是唯一更改 store 中状态的方法,且必须是同步函数

Action : 用于提交 mutation,而不是直接变更状态,可以包含任意异步操作

Getter:

  • getters 可以对State进行计算操作,它就是Store的计算属性
  • 虽然在组件内也可以做计算属性,但是getters 可以在多组件之间复用
  • 如果一个状态只在一个组件内使用,是可以不用getters

Module: Module 可以让每一个模块拥有自己的state、mutation、action、getters,使得结构非常清晰,方便管理。

2. 使用Vuex的好处

  • 多层嵌套的组件、兄弟组件间的状态会更好管理维护。
  • 缓存一些当前要使用请求远程或本地的数据集(刷新后会自己销毁)
  • 有了第二条,就可以减少向服务器的请求,节省资源。如果你的用户足够多,那么每多出一个请求,对公司来说,都是一大笔钱。
  • 对开发者来说,如果你的项目足够复杂,团队的规模也不仅是一个人,数据集中处理更利于程序的稳定和维护

32. 自定义指令你是怎么用的

33. vue如何实现组件封装

  • props:数据父组件传入子组件
  • 子组件触发父组件事件 emit on
  • 预留slot
  • 合理运用 scoped 控制样式作用域
  • 动态组件
import Toast from './Toast'const obj = {}
​
obj.install = function (Vue) {
​
  // document.body.appendChild(Toast.$el)
  // 1.创建组件构造器
  const toastContrustor = Vue.extend(Toast)
​
  // 2.new的方式,根据组件构造器,可以创建出来一个组件对象
  const toast = new toastContrustor()
​
  // 3.将组件对象,手动挂载到某一个元素上
  toast.$mount(document.createElement('div'))
​
  // 4.toast.$el对应的就是div
  document.body.appendChild(toast.$el)
​
  // 5.将toast挂载到Vue的原型上
  Vue.prototype.$toast = toast
​
}
​
export default obj

34. Vue 中 axios 的封装

聊聊 Vue 中 axios 的封装

Axios 是一个基于 promise 的 HTTP 库,可以用在浏览器和 node.js 中。

  • 根据环境设置 baseURL

    在开发环境下和生产模式下有着不同的 baseURL

  • 统一设置请求头

  • 跨域、超时、响应码处理

  • 请求、响应处理

  • 拦截器

    分为请求拦截器以及响应拦截器,请求或响应被 then 或 catch 处理前拦截它们

import axios from "axios"
// 引入进度条
import nprogress from "nprogress"
import "nprogress/nprogress.css"import store from '@/store'// 创建axios实例
const instance = axios.create({
​
  // 配置对象
  baseURL: "/api",
  timeout: 5000
})
​
// 请求拦截器,在发请求之前,请求拦截器可以检测到
instance.interceptors.request.use((config) => {
  // config:配置对象:对象里面有一个属性很重要,headers请求头
  if(store.state.detail.uuid_token) {
    // 请求头添加一个字段(userTempId),和后台商量好的
    config.headers.userTempId = store.state.detail.uuid_token;
  }
  // 需要携带token
  if(store.state.user.token) {
    config.headers.token = store.state.user.token;
  }
​
  // 进度条开始动
  nprogress.start()
  return config
}) 
​
// 响应拦截器
instance.interceptors.response.use((res) => {
  // 成功的回调
  // 进度条结束
  nprogress.done()
  return res.data
}), (err) => {
  // 失败的回调
  return Promise.reject(err)
​
}
​
export default instance

1. 系统权限的实现和控制 (可以考虑一下项目登陆相关问题)

  • 结合后端
  • 纯前端
  • 如何封装
  • 不同角色

vue中如何实现后台管理系统的权限控制

2. 说一下前端登录的流程?

  • 初次登录的时候,前端调后调的登录接口,发送用户名和密码,后端收到请求,验证用户名和密码,验证成功,就给前端返回一个token,和一个用户信息的值
  • 前端拿到token,将token储存到Vuex中,然后从Vuex中把token的值存入浏览器Cookies中。把用户信息存到Vuex然后再存储到LocalStroage中,然后跳转到下一个页面,
  • 根据后端接口的要求,只要不登录就不能访问的页面需要在前端每次跳转页面师判断Cookies中是否有token,没有就跳转到登录页,有就跳转到相应的页面,
  • 我们应该再每次发送post/get请求的时候应该加入token,常用方法再项目utils/service.js中添加全局拦截器,将token的值放入请求头中 后端判断请求头中有无token,有token,就拿到token并验证token是否过期,在这里过期会返回无效的token然后有个跳回登录页面重新登录并且清除本地用户的信息

3. 怎么判断登录用户

是否有该页面权限 token过期 等等知识

35. 使用过 Vue SSR 吗?说说 SSR?

Vue.js 是构建客户端应用程序的框架。默认情况下,可以在浏览器中输出 Vue 组件,进行生成 DOM 和操作 DOM。然而,也可以将同一个组件渲染为服务端的 HTML 字符串,将它们直接发送到浏览器,最后将这些静态标记"激活"为客户端上完全可交互的应用程序。

即:SSR大致的意思就是vue在客户端将标签渲染成的整个 html 片段的工作在服务端完成,服务端形成的html 片段直接返回给客户端这个过程就叫做服务端渲染。

SSR原理

我们需要通过Webpack打包生成两份bundle文件:

  • Client Bundle,给浏览器用。和纯Vue前端项目Bundle类似
  • Server Bundle,供服务端SSR使用,一个json文件

服务端渲染的优点:

  • 更好的 SEO: 因为 SPA 页面的内容是通过 Ajax 获取,而搜索引擎爬取工具并不会等待 Ajax 异步完成后再抓取页面内容,所以在 SPA 中是抓取不到页面通过 Ajax 获取到的内容;而 SSR 是直接由服务端返回已经渲染好的页面(数据已经包含在页面中),所以搜索引擎爬取工具可以抓取渲染好的页面;
  • 更快的内容到达时间(首屏加载更快): SPA 会等待所有 Vue 编译后的 js 文件都下载完成后,才开始进行页面的渲染,文件下载等需要一定的时间等,所以首屏渲染需要一定的时间;SSR 直接由服务端渲染好页面直接返回显示,无需等待下载 js 文件及再去渲染等,所以 SSR 有更快的内容到达时间;

服务端渲染的缺点:

  • 更多的开发条件限制: 例如服务端渲染只支持 beforCreate 和 created 两个钩子函数,这会导致一些外部扩展库需要特殊处理,才能在服务端渲染应用程序中运行;并且与可以部署在任何静态文件服务器上的完全静态单页面应用程序 SPA 不同,服务端渲染应用程序,需要处于 Node.js server 运行环境;
  • 更多的服务器负载:在 Node.js 中渲染完整的应用程序,显然会比仅仅提供静态文件的 server 更加大量占用CPU 资源 (CPU-intensive - CPU 密集),因此如果你预料在高流量环境 ( high traffic ) 下使用,请准备相应的服务器负载,并明智地采用缓存策略。

36. 说一下SPA单页面的理解,有什么优缺点?

SPA( single-page application )仅在 Web 页面初始化时加载相应的 HTML、JavaScript 和 CSS。一旦页面加载完成,SPA 不会因为用户的操作而进行页面的重新加载或跳转;取而代之的是利用路由机制实现 HTML 内容的变换,UI 与用户的交互,避免页面的重新加载。

优点:

  1. 用户体验好、快,内容的改变不需要重新加载整个页面,避免了不必要的跳转和重复渲染;
  2. 减轻服务端压力
  3. 前后端职责分离,架构清晰,前端进行交互逻辑,后端负责数据处理;

缺点:

  1. 首屏加载过慢;为实现单页 Web 应用功能及显示效果,需要在加载页面的时候将 JavaScript、CSS 统一加载,部分页面按需加载;
  2. 前进后退路由管理:由于单页应用在一个页面中显示所有的内容,所以不能使用浏览器的前进后退功能,所有的页面切换需要自己建立堆栈管理;
  3. SEO 不利于搜索引擎抓取,所有的内容都在一个页面中动态替换显示

37. 首屏(白屏)性能优化你是怎么做的

  • ssr服务端渲染
  • 骨架屏
  • 配置gizp,启用代码压缩
  • 关闭productionSourceMap 减少打包资源包大小,加密源码
  • 资源拆分引用CDN
  • 路由懒加载
  • 预加载 prerender-spa-plugin

38. 你有对 Vue 项目进行哪些优化?

代码 网络 资源加载 打包部署 等等层面去简单阐述

1.前端性能优化 24 条建议(2020)

2.写给中高级前端关于性能优化的9大策略和6大指标 | 网易四年实践

3.聊一聊前端性能优化

4.Vue 项目性能优化 — 实践指南(网上最全 / 详细)

思考:用户体验优化 比如白屏加载问题(骨架屏)

首页白屏优化实践

代码层面的优化

  • v-if 和 v-show 区分使用场景
  • computed 和 watch 区分使用场景
  • v-for 遍历必须为 item 添加 key,且避免同时使用 v-if
  • 长列表性能优化
  • 事件的销毁
  • 图片资源懒加载vue-lazyload
  • 路由懒加载
  • 第三方插件的按需引入
  • 优化无限列表性能
  • 服务端渲染 SSR or 预渲染

Webpack 层面的优化

  • Webpack 对图片进行压缩
  • 减少 ES6 转为 ES5 的冗余代码
  • 提取公共代码
  • 模板预编译
  • 提取组件的 CSS
  • 优化 SourceMap
  • 构建结果输出分析
  • Vue 项目的编译优化

基础的 Web 技术的优化

  • 开启 gzip 压缩

    gizp 压缩是一种 http 请求优化方式,通过减少文件体积来提高加载速度,对于用户量多的网站,开启 gizp 压缩会大大降低服务器压力,提高加载速度,降低服务器流量成本有两种开启方式: 通过 webpack plugin 插件 & Nginx 配置方式 第二种方式纯 Nginx 配置

  • 浏览器缓存

  • CDN 的使用

  • 使用 Chrome Performance 查找性能瓶颈

39. 说说你自己项目碰到的难点

结合自己项目 说几个 可以往自己擅长方向引 比如性能优化 数据埋点 或者工程化实践等等

音乐项目vue3

问题

  • 歌曲播放

    在歌曲没有加载成功的情况下,连续切换歌曲会导致播放混乱,解决方法是将songRead作为歌曲缓冲标识,在watch监听歌曲播放与歌曲切换时加入对songReady的判断,当歌曲加载成功,即songReady为true时,才能继续执行下一步逻辑。

  • 由于最近播放和我的喜欢是在本地存储的,歌曲的URL每天都会变化,所以在入口文件main.js会重新加载音乐URL

  • 做歌手详情页的时候切换歌手,路由参数发送改变了,但是页面还是上一个歌手的信息。原因是没有给router-view 设置key,导致vue去复用相同的组件,将不在执行created, mounted之类的钩子。

    设置 router-view 的 key 属性值为 $route.fullPath,通过绑定一个fullPath,可以识别当前页面路由的完整地址,当地址发生改变(包括参数改变)则重新渲染页面(例如动态路由参数的变化)。

    组件加载顺序为:beforeRouteUpdate => created => mounted

    也可以在路由组件中, 添加beforeRouteUpdate钩子来执行相关方法拉去数据。

  • 移动端HTML5 audio autoplay失效问题

    由于自动播放网页中的音频或视频会给用户带来困扰或不必要的流量消耗,所以苹果系统和安卓系统通常都会禁止自动播放和使用JS的触发播放,必须由用户来触发才播放;解决方法思路:先通过用户touchstart触碰触发播放并暂停(让音频开始加载),后面用JS再操作就没问题了;解决代码:

    document.addEventListener('touchstart', function () {
        document.getElementsByTagName('audio')[0].play();
        document.getElementsByTagName('audio')[0].pause();
    });
    

业务

  • 播放器的cd的位移及缩放

先计算出小播放器图片离最终大播放器cd的x,y轴上的距离

使用 create-keyframe-animation 进行一个css3动画状态的注册

再利用transition的动画方法钩子

enterrun动画,afterEnter时清除动画 leave同理

  • 播放器的旋转

定义一个旋转的css动画,在一个class中进行调用,在play的状态下给它addClss,pause时加上animation-play-state: paused

布局

  • 绝大多数使用了flex webpack中配置低版本安卓,ios加前缀
  • 考虑到fixed元素的移动端问题,在这种场景下,使用100%高度+absolute方案更适合
  • 使用媒体查询,兼容一下某些样式在768px以上的样式变形
  • 使用rem 在vue实例的mounted的钩子里注册resizeonload监听,进行最外层rem基准的计算
  • 使用骨架屏进行加载资源白屏时填充,待优化至完全的主页面服务端渲染

商城项目vue2

  • 在进行修改商品属性的时候,需要拷贝一份数据,当时用的是浅拷贝,出现问题,数据拷贝失败,发现原因是当前接口返回的数据结构复杂,对象里面套数组,数字里面套对象,因此需要用到深拷贝。

    这个地方的业务需要切换输入框的查看模式和修改模式,在相应的属性值元素上加上flag标记,因为Vue2底层的原理的缺陷,新添加的数据不是响应式的,所以利用$set添加这个新属性,使其成为响应式的。

    在这里实现自动聚焦的时候,会出现有时候聚焦失败,后来发现是DOM还没有挂载完成,将聚焦逻辑写在nextTick的回调中,在DOM加载完后立刻执行回调,后续没有出现问题。

  • vue在用v-if v-else渲染两个相同的按钮,一个绑定了事件,另外一个没有绑定事件。当渲染状态切换的时候,会导致未绑定事件的按钮也绑定上了事件。原因是有的vue版本在没给条件渲染的元素加上key标识时候会默认复用元素提升渲染能力,导致事件被错误的绑定上另一个按钮。解决方案:更换高版本vue,加上key标识两个按钮。

40. 怎么进行权限管理

不同的用户(角色),登录的时候会向服务器发请求,服务器会把用户相应的菜单的权限的信息,返回给我们 我们可以根据服务器返回的数据(信息),可以动态的设置路由,可以根据不同的用户展示不同的菜单。

菜单权限:当用户获取用户信息的时候,服务器会把相应的用户拥有菜单的权限信息返回,需要根据用户身份对比出,当前这个用户需要展示哪些菜单。

  • 在Vuex保存用户信息

  • 拆分路由

    常量路由(不分角色):登录、首页

    任意路由:当路径出现错误的时候重定向404

    异步路由:不同的用户(角色),需要过滤筛选出的路由

    • 项目中已有的异步路由asyncRoutes,与服务器返回的标记信息data.routes进行对比最终需要展示的路由

    • 在vue的state中定义:resultAsyncRoutes: [],存放对比之后的路由

    • 合并常量、异步、任意路由,确认当前用户最终需要展示的全部路由

    • 给路由器添加新的路由,(遍历菜单不能只遍历常量路由,需要遍历的是仓库计算完毕的全部路由)

      //定义一个函数:两个数组进行对比,对比出当前用户到底显示哪些异步路由
       const computedAsyncRoutes = (asyncRoutes,routes)=>{
           //过滤出当前用户【超级管理|普通员工】需要展示的异步路由
          return asyncRoutes.filter(item=>{
               //数组当中没有这个元素返回索引值-1,如果有这个元素返回的索引值一定不是-1 
              if(routes.indexOf(item.name)!=-1){
                //递归:别忘记还有2、3、4、5、6级路由
                if(item.children&&item.children.length){
                    item.children = computedAsyncRoutes(item.children,routes);
                }
                return true;
              }
           })
       }
       
       // state
       
           //对比之后【项目中已有的异步路由,与服务器返回的标记信息进行对比最终需要展示的理由】
          resultAsyncRoutes:[],
          //用户最终需要展示全部路由
          resultAllRputes:[]
      ​
      // mutations 
        //最终计算出的异步路由
        SET_RESULTASYNCROUTES:(state,asyncRoutes)=>{
           //vuex保存当前用户的异步路由,注意,一个用户需要展示完成路由:常量、异步、任意路由
           state.resultAsyncRoutes = asyncRoutes;
           //计算出当前用户需要展示所有路由
           state.resultAllRputes = constantRoutes.concat(state.resultAsyncRoutes,anyRoutes);
           //给路由器添加新的路由
            router.addRoutes(state.resultAllRputes)
        }
        
        // actions
        commit('SET_RESULTASYNCROUTES',computedAsyncRoutes(cloneDeep(asyncRoutes),data.routes));
      

菜单权限:不同的用户(角色),能操作、能观看的菜单是不同的。

按钮的权限:不同的用户(角色),有的用户的是可见按钮、当然有的用户不可见。

角色权限:超级管理员分配

41. 说一下购物车的逻辑?

//vue中购物车逻辑的实现
1. 购物车信息用一个数组来存储,数组中保存对象,对象中有id和count属性
​
2. 在vuex中state中添加一个数据 cartList 用来保存这个数组
​
3. 由于商品详情页需要用到加入购物车功能,所以我们需要提供一个mutation, 用来将购物车信息加入 cartList中
​
4. 加入购物车信息的时候,遵照如下规则: 如果购物车中已经有了该商品信息,则数量累加,如果没有该商品信息,则新增一个对象
​
5. 在商品详情页,点击加入购物车按钮的时候,调用vuex提供的addToCart这个mutation将当前的商品信息 (id count)传给addTocart  this.$store.commit("addToCart", {id:  , count:})
​
// js中购物车逻辑的实现
1.商品页点击“加入购物车”按钮,触发事件
​
2.事件调用购物车“增加商品”的Js程序(函数、对象方法)
​
3.向Js程序传递传递“商品id”、“商品数量”等数据
​
4.存储“商品id”、“商品数量”到浏览器的localStorage中
​
**展示购物车中的商品******
​
1.打开购物车页面
​
2.从localStorage中取出“商品Id”、“商品数量”等信息。
​
3.调用服务器端“获得商品详情”的接口得到购物车中的商品信息(参数为商品Id)
​
4.将获得的商品信息显示在购物车页面。
​
**完成购物车中商品的购买******
​
1.用户对购物车中的商品完成购买流程,产生购物订单
​
2.清除localStorage中存储的已经购买的商品信息
​
备注1:购物车中商品存储的数据除了“商品id”、“商品数量”之外,根据产品要求还可以有其他的信息,例如完整的商品详情(这样就不用掉服务器接口获得详情了)、购物车商品的过期时间,超过时间的购物车商品在下次打开网站或者购物车页面时被清除。
​
备注2:购物车商品除了存储在localStorage中,根据产品的需求不同,也可以存储在sessionStorage、cookie、session中,或者直接向服务器接口发起请求存储在服务器上。何种情况使用哪种方式存储、有啥区别请自己分析。
​

参考文章

有些并没标记出来,希望大家看到指出来源

30 道 Vue 面试题,内含详细讲解

面试官问: 如何理解Virtual DOM?

vue虚拟DOM与diff算法

Vue3 Proxy

为什么 Vue 中不要用 index 作为 key?

Vue3的8种和Vue2的12种组件通信

聊聊 Vue 中 axios 的封装

vue中如何实现后台管理系统的权限控制