写点什么

熬夜整理的 vue 面试题

作者:bb_xiaxia1998
  • 2022-11-16
    浙江
  • 本文字数:16431 字

    阅读完需:约 54 分钟

Vue-router 路由模式有几种

vue-router3 种路由模式:hashhistoryabstract,对应的源码如下所示


switch (mode) {    case 'history':    this.history = new HTML5History(this, options.base)    break    case 'hash':    this.history = new HashHistory(this, options.base, this.fallback)      break    case 'abstract':    this.history = new AbstractHistory(this, options.base)      break  default:    if (process.env.NODE_ENV !== 'production') {      assert(false, `invalid mode: ${mode}`)    }}
复制代码


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


  • hash: 使用 URL hash 值来作路由,支持所有浏览器

  • history : 依赖 HTML5 History API 和服务器配置

  • abstract : 支持所有 JavaScript 运行环境,如 Node.js 服务器端。如果发现没有浏览器的 API,路由会自动强制进入这个模式.

mixin 和 mixins 区别

mixin 用于全局混入,会影响到每个组件实例,通常插件都是这样做初始化的。


Vue.mixin({  beforeCreate() {    // ...逻辑        // 这种方式会影响到每个组件的 beforeCreate 钩子函数  },});
复制代码


虽然文档不建议在应用中直接使用 mixin,但是如果不滥用的话也是很有帮助的,比如可以全局混入封装好的 ajax 或者一些工具函数等等。


mixins 应该是最常使用的扩展组件的方式了。如果多个组件中有相同的业务逻辑,就可以将这些逻辑剥离出来,通过 mixins 混入代码,比如上拉下拉加载数据这种逻辑等等。另外需要注意的是 mixins 混入的钩子函数会先于组件内的钩子函数执行,并且在遇到同名选项的时候也会有选择性的进行合并。

虚拟 DOM 的优缺点?

优点:


  • 保证性能下限: 框架的虚拟 DOM 需要适配任何上层 API 可能产生的操作,它的一些 DOM 操作的实现必须是普适的,所以它的性能并不是最优的;但是比起粗暴的 DOM 操作性能要好很多,因此框架的虚拟 DOM 至少可以保证在你不需要手动优化的情况下,依然可以提供还不错的性能,即保证性能的下限;

  • 无需手动操作 DOM: 我们不再需要手动去操作 DOM,只需要写好 View-Model 的代码逻辑,框架会根据虚拟 DOM 和 数据双向绑定,帮我们以可预期的方式更新视图,极大提高我们的开发效率;

  • 跨平台: 虚拟 DOM 本质上是 JavaScript 对象,而 DOM 与平台强相关,相比之下虚拟 DOM 可以进行更方便地跨平台操作,例如服务器渲染、weex 开发等等。


缺点:


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

Vue 中的过滤器了解吗?过滤器的应用场景有哪些?

过滤器实质不改变原始数据,只是对数据进行加工处理后返回过滤后的数据再进行调用处理,我们也可以理解其为一个纯函数


Vue 允许你自定义过滤器,可被用于一些常见的文本格式化


ps: Vue3中已废弃filter

如何用

vue 中的过滤器可以用在两个地方:双花括号插值和 v-bind 表达式,过滤器应该被添加在 JavaScript 表达式的尾部,由“管道”符号指示:


<!-- 在双花括号中 -->{ message | capitalize }
<!-- 在 `v-bind` 中 --><div v-bind:id="rawId | formatId"></div>
复制代码

定义 filter

在组件的选项中定义本地的过滤器


filters: {  capitalize: function (value) {    if (!value) return ''    value = value.toString()    return value.charAt(0).toUpperCase() + value.slice(1)  }}
复制代码


定义全局过滤器:


Vue.filter('capitalize', function (value) {  if (!value) return ''  value = value.toString()  return value.charAt(0).toUpperCase() + value.slice(1)})
new Vue({ // ...})
复制代码


注意:当全局过滤器和局部过滤器重名时,会采用局部过滤器


过滤器函数总接收表达式的值 (之前的操作链的结果) 作为第一个参数。在上述例子中,capitalize 过滤器函数将会收到 message 的值作为第一个参数


过滤器可以串联:


{ message | filterA | filterB }
复制代码


在这个例子中,filterA 被定义为接收单个参数的过滤器函数,表达式 message 的值将作为参数传入到函数中。然后继续调用同样被定义为接收单个参数的过滤器函数 filterB,将 filterA 的结果传递到 filterB 中。


过滤器是 JavaScript函数,因此可以接收参数:


{{ message | filterA('arg1', arg2) }}
复制代码


这里,filterA 被定义为接收三个参数的过滤器函数。


其中 message 的值作为第一个参数,普通字符串 'arg1' 作为第二个参数,表达式 arg2 的值作为第三个参数


举个例子:


<div id="app">  <p>{{ msg | msgFormat('疯狂','--')}}</p></div>
<script> // 定义一个 Vue 全局的过滤器,名字叫做 msgFormat Vue.filter('msgFormat', function(msg, arg, arg2) { // 字符串的 replace 方法,第一个参数,除了可写一个 字符串之外,还可以定义一个正则 return msg.replace(/单纯/g, arg+arg2) })</script>
复制代码


小结:


  • 部过滤器优先于全局过滤器被调用

  • 一个表达式可以使用多个过滤器。过滤器之间需要用管道符“|”隔开。其执行顺序从左往右

应用场景

平时开发中,需要用到过滤器的地方有很多,比如单位转换数字打点文本格式化时间格式化之类的等


比如我们要实现将30000 => 30,000,这时候我们就需要使用过滤器


Vue.filter('toThousandFilter', function (value) {  if (!value) return ''  value = value.toString()  return .replace(str.indexOf('.') > -1 ? /(\d)(?=(\d{3})+\.)/g : /(\d)(?=(?:\d{3})+$)/g, '$1,')})
复制代码

原理分析

使用过滤器


{{ message | capitalize }}
复制代码


在模板编译阶段过滤器表达式将会被编译为过滤器函数,主要是用过parseFilters,我们放到最后讲


_s(_f('filterFormat')(message))
复制代码


首先分析一下_f


_f 函数全名是:resolveFilter,这个函数的作用是从this.$options.filters中找出注册的过滤器并返回


// 变为this.$options.filters['filterFormat'](message) // message为参数
复制代码


关于resolveFilter


import { indentity,resolveAsset } from 'core/util/index' 
export function resolveFilter(id){ return resolveAsset(this.$options,'filters',id,true) || identity}
复制代码


内部直接调用resolveAsset,将option对象,类型,过滤器id,以及一个触发警告的标志作为参数传递,如果找到,则返回过滤器;


resolveAsset的代码如下:


export function resolveAsset(options,type,id,warnMissing){ // 因为我们找的是过滤器,所以在 resolveFilter函数中调用时 type 的值直接给的 'filters',实际这个函数还可以拿到其他很多东西  if(typeof id !== 'string'){ // 判断传递的过滤器id 是不是字符串,不是则直接返回      return   }  const assets = options[type]  // 将我们注册的所有过滤器保存在变量中  // 接下来的逻辑便是判断id是否在assets中存在,即进行匹配  if(hasOwn(assets,id)) return assets[id] // 如找到,直接返回过滤器  // 没有找到,代码继续执行  const camelizedId  = camelize(id) // 万一你是驼峰的呢  if(hasOwn(assets,camelizedId)) return assets[camelizedId]  // 没找到,继续执行  const PascalCaseId = capitalize(camelizedId) // 万一你是首字母大写的驼峰呢  if(hasOwn(assets,PascalCaseId)) return assets[PascalCaseId]  // 如果还是没找到,则检查原型链(即访问属性)  const result = assets[id] || assets[camelizedId] || assets[PascalCaseId]  // 如果依然没找到,则在非生产环境的控制台打印警告  if(process.env.NODE_ENV !== 'production' && warnMissing && !result){    warn('Failed to resolve ' + type.slice(0,-1) + ': ' + id, options)  }  // 无论是否找到,都返回查找结果  return result}
复制代码


下面再来分析一下_s


_s 函数的全称是 toString,过滤器处理后的结果会当作参数传递给 toString函数,最终 toString函数执行后的结果会保存到Vnode中的 text 属性中,渲染到视图中


function toString(value){  return value == null  ? ''  : typeof value === 'object'    ? JSON.stringify(value,null,2)// JSON.stringify()第三个参数可用来控制字符串里面的间距    : String(value)}
复制代码


最后,在分析下parseFilters,在模板编译阶段使用该函数阶段将模板过滤器解析为过滤器函数调用表达式


function parseFilters (filter) {    let filters = filter.split('|')    let expression = filters.shift().trim() // shift()删除数组第一个元素并将其返回,该方法会更改原数组    let i    if (filters) {        for(i = 0;i < filters.length;i++){            experssion = warpFilter(expression,filters[i].trim()) // 这里传进去的expression实际上是管道符号前面的字符串,即过滤器的第一个参数        }    }    return expression}// warpFilter函数实现function warpFilter(exp,filter){    // 首先判断过滤器是否有其他参数    const i = filter.indexof('(')    if(i<0){ // 不含其他参数,直接进行过滤器表达式字符串的拼接        return `_f("${filter}")(${exp})`    }else{        const name = filter.slice(0,i) // 过滤器名称        const args = filter.slice(i+1) // 参数,但还多了 ‘)’        return `_f('${name}')(${exp},${args}` // 注意这一步少给了一个 ')'    }}
复制代码


小结:


  • 在编译阶段通过parseFilters将过滤器编译成函数调用(串联过滤器则是一个嵌套的函数调用,前一个过滤器执行的结果是后一个过滤器函数的参数)

  • 编译后通过调用resolveFilter函数找到对应过滤器并返回结果

  • 执行结果作为参数传递给toString函数,而toString执行后,其结果会保存在Vnodetext属性中,渲染到视图


参考:前端vue面试题详细解答

如何理解 Vue 中模板编译原理

Vue 的编译过程就是将 template 转化为 render 函数的过程


  • 解析生成 AST 树template模板转化成AST语法树,使用大量的正则表达式对模板进行解析,遇到标签、文本的时候都会执行对应的钩子进行相关处理

  • 标记优化 对静态语法做静态标记 markup(静态节点如div下有p标签内容不会变化) diff来做优化 静态节点跳过diff操作

  • Vue的数据是响应式的,但其实模板中并不是所有的数据都是响应式的。有一些数据首次渲染后就不会再变化,对应的DOM也不会变化。那么优化过程就是深度遍历AST树,按照相关条件对树节点进行标记。这些被标记的节点(静态节点)我们就可以跳过对它们的比对,对运行时的模板起到很大的优化作用

  • 等待后续节点更新,如果是静态的,不会在比较children

  • 代码生成 编译的最后一步是将优化后的AST树转换为可执行的代码


回答范例


思路


  • 引入vue编译器概念

  • 说明编译器的必要性

  • 阐述编译器工作流程


回答范例


  1. Vue中有个独特的编译器模块,称为compiler,它的主要作用是将用户编写的template编译为js中可执行的render函数。

  2. 之所以需要这个编译过程是为了便于前端能高效的编写视图模板。相比而言,我们还是更愿意用HTML来编写视图,直观且高效。手写render函数不仅效率底下,而且失去了编译期的优化能力。

  3. Vue中编译器会先对template进行解析,这一步称为parse,结束之后会得到一个JS对象,我们称为 抽象语法树 AST ,然后是对AST进行深加工的转换过程,这一步成为transform,最后将前面得到的AST生成为JS代码,也就是render函数


可能的追问


  1. Vue中编译器何时执行?



new Vue()之后。 Vue 会调用 _init 函数进行初始化,也就是这里的 init 过程,它会初始化生命周期、事件、 propsmethodsdatacomputedwatch等。其中最重要的是通过 Object.defineProperty 设置 settergetter 函数,用来实现「响应式」以及「依赖收集」


  • 初始化之后调用 $mount 会挂载组件,如果是运行时编译,即不存在 render function 但是存在 template 的情况,需要进行「编译」步骤

  • compile编译可以分成 parseoptimizegenerate 三个阶段,最终需要得到 render function


  1. React有没有编译器?


react 使用babelJSX语法解析


<div id="app"></div><script>    let vm = new Vue({        el: '#app',        template: `<div>            // <span>hello world</span> 是静态节点            <span>hello world</span>                // <p>{{name}}</p> 是动态节点            <p>{{name}}</p>        </div>`,        data() {          return { name: 'test' }        }    });</script>
复制代码


源码分析


export function compileToFunctions(template) {  // 我们需要把html字符串变成render函数  // 1.把html代码转成ast语法树  ast用来描述代码本身形成树结构 不仅可以描述html 也能描述css以及js语法  // 很多库都运用到了ast 比如 webpack babel eslint等等  let ast = parse(template);  // 2.优化静态节点:对ast树进行标记,标记静态节点    if (options.optimize !== false) {      optimize(ast, options);    }
// 3.通过ast 重新生成代码 // 我们最后生成的代码需要和render函数一样 // 类似_c('div',{id:"app"},_c('div',undefined,_v("hello"+_s(name)),_c('span',undefined,_v("world")))) // _c代表创建元素 _v代表创建文本 _s代表文Json.stringify--把对象解析成文本 let code = generate(ast); // 使用with语法改变作用域为this 之后调用render函数可以使用call改变this 方便code里面的变量取值 let renderFn = new Function(`with(this){return ${code}}`); return renderFn;}
复制代码

Vue 中的 key 到底有什么用?

key 是给每一个 vnode 的唯一 id,依靠 key,我们的 diff 操作可以更准确、更快速 (对于简单列表页渲染来说 diff 节点也更快,但会产生一些隐藏的副作用,比如可能不会产生过渡效果,或者在某些节点有绑定数据(表单)状态,会出现状态错位。)


diff 算法的过程中,先会进行新旧节点的首尾交叉对比,当无法匹配的时候会用新节点的 key 与旧节点进行比对,从而找到相应旧节点.


更准确 : 因为带 key 就不是就地复用了,在 sameNode 函数 a.key === b.key 对比中可以避免就地复用的情况。所以会更加准确,如果不加 key,会导致之前节点的状态被保留下来,会产生一系列的 bug。


更快速 : key 的唯一性可以被 Map 数据结构充分利用,相比于遍历查找的时间复杂度 O(n),Map 的时间复杂度仅仅为 O(1),源码如下:


function createKeyToOldIdx(children, beginIdx, endIdx) {  let i, key;  const map = {};  for (i = beginIdx; i <= endIdx; ++i) {    key = children[i].key;    if (isDef(key)) map[key] = i;  }  return map;}
复制代码

v-model 实现原理

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


  • texttextarea 元素使用 value 属性和 input 事件

  • checkboxradio 使用 checked 属性和 change 事件

  • select 字段将 value 作为 prop 并将 change 作为事件


所以我们可以 v-model 进行如下改写:


<input v-model="sth" /><!-- 等同于 --><input :value="sth" @input="sth = $event.target.value" />
复制代码


当在input元素中使用v-model实现双数据绑定,其实就是在输入的时候触发元素的input事件,通过这个语法糖,实现了数据的双向绑定


  • 这个语法糖必须是固定的,也就是说属性必须为value,方法名必须为:input

  • 知道了v-model的原理,我们可以在自定义组件上实现v-model


//Parent<template>  {{num}}  <Child v-model="num"></template>export default {  data(){    return {      num: 0    }  }}
//Child<template> <div @click="add">Add</div></template>export default { props: ['value'], // 属性必须为value methods:{ add(){ // 方法名为input this.$emit('input', this.value + 1) } }}
复制代码


原理


会将组件的 v-model 默认转化成value+input


const VueTemplateCompiler = require('vue-template-compiler'); const ele = VueTemplateCompiler.compile('<el-checkbox v-model="check"></el- checkbox>'); 
// 观察输出的渲染函数:// with(this) { // return _c('el-checkbox', { // model: { // value: (check), // callback: function ($$v) { check = $$v }, // expression: "check" // } // }) // }
复制代码


// 源码位置 core/vdom/create-component.js line:155
function transformModel (options, data: any) { const prop = (options.model && options.model.prop) || 'value' const event = (options.model && options.model.event) || 'input' ;(data.attrs || (data.attrs = {}))[prop] = data.model.value const on = data.on || (data.on = {}) const existing = on[event] const callback = data.model.callback if (isDef(existing)) { if (Array.isArray(existing) ? existing.indexOf(callback) === -1 : existing !== callback ) { on[event] = [callback].concat(existing) } } else { on[event] = callback } }
复制代码


原生的 v-model,会根据标签的不同生成不同的事件和属性


const VueTemplateCompiler = require('vue-template-compiler'); const ele = VueTemplateCompiler.compile('<input v-model="value"/>');
// with(this) { // return _c('input', { // directives: [{ name: "model", rawName: "v-model", value: (value), expression: "value" }], // domProps: { "value": (value) },// on: {"input": function ($event) { // if ($event.target.composing) return;// value = $event.target.value// }// }// })// }
复制代码


编译时:不同的标签解析出的内容不一样 platforms/web/compiler/directives/model.js


if (el.component) {     genComponentModel(el, value, modifiers) // component v-model doesn't need extra runtime     return false } else if (tag === 'select') {     genSelect(el, value, modifiers) } else if (tag === 'input' && type === 'checkbox') {     genCheckboxModel(el, value, modifiers) } else if (tag === 'input' && type === 'radio') {     genRadioModel(el, value, modifiers) } else if (tag === 'input' || tag === 'textarea') {     genDefaultModel(el, value, modifiers) } else if (!config.isReservedTag(tag)) {     genComponentModel(el, value, modifiers) // component v-model doesn't need extra runtime     return false }
复制代码


运行时:会对元素处理一些关于输入法的问题 platforms/web/runtime/directives/model.js


inserted (el, binding, vnode, oldVnode) {     if (vnode.tag === 'select') { // #6903     if (oldVnode.elm && !oldVnode.elm._vOptions) {         mergeVNodeHook(vnode, 'postpatch', () => {             directive.componentUpdated(el, binding, vnode)         })     } else {         setSelected(el, binding, vnode.context)     }    el._vOptions = [].map.call(el.options, getValue)     } else if (vnode.tag === 'textarea' || isTextInputType(el.type)) {         el._vModifiers = binding.modifiers         if (!binding.modifiers.lazy) {             el.addEventListener('compositionstart', onCompositionStart)             el.addEventListener('compositionend', onCompositionEnd)             // Safari < 10.2 & UIWebView doesn't fire compositionend when             // switching focus before confirming composition choice             // this also fixes the issue where some browsers e.g. iOS Chrome            // fires "change" instead of "input" on autocomplete.             el.addEventListener('change', onCompositionEnd) /* istanbul ignore if */             if (isIE9) {                 el.vmodel = true             }        }    }}
复制代码

scoped 样式穿透

scoped虽然避免了组件间样式污染,但是很多时候我们需要修改组件中的某个样式,但是又不想去除scoped属性


  1. 使用/deep/


<!-- Parent --><template><div class="wrap">    <Child /></div></template>
<style lang="scss" scoped>.wrap /deep/ .box{ background: red;}</style>
<!-- Child --><template> <div class="box"></div></template>
复制代码


  1. 使用两个style标签


<!-- Parent --><template><div class="wrap">    <Child /></div></template>
<style lang="scss" scoped>/* 其他样式 */</style><style lang="scss">.wrap .box{ background: red;}</style>
<!-- Child --><template> <div class="box"></div></template>
复制代码

Vue 是如何实现数据双向绑定的

Vue 数据双向绑定主要是指:数据变化更新视图,视图变化更新数据,如下图所示:



  • 输入框内容变化时,Data 中的数据同步变化。即 View => Data 的变化。

  • Data 中的数据变化时,文本节点的内容同步变化。即 Data => View 的变化


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


  • 实现一个监听器 Observer :对数据对象进行遍历,包括子属性对象的属性,利用 Object.defineProperty() 对属性都加上 settergetter。这样的话,给这个对象的某个值赋值,就会触发 setter,那么就能监听到了数据变化

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

  • 实现一个订阅者 WatcherWatcher 订阅者是 ObserverCompile 之间通信的桥梁 ,主要的任务是订阅 Observer 中的属性值变化的消息,当收到属性值变化的消息时,触发解析器 Compile 中对应的更新函数

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



Vue 数据双向绑定原理图


diff 算法

时间复杂度: 个树的完全 diff 算法是一个时间复杂度为 O(n*3) ,vue 进行优化转化成 O(n)


理解:


  • 最小量更新, key 很重要。这个可以是这个节点的唯一标识,告诉 diff 算法,在更改前后它们是同一个 DOM 节点

  • 扩展 v-for 为什么要有 key ,没有 key 会暴力复用,举例子的话随便说一个比如移动节点或者增加节点(修改 DOM),加 key 只会移动减少操作 DOM。

  • 只有是同一个虚拟节点才会进行精细化比较,否则就是暴力删除旧的,插入新的。

  • 只进行同层比较,不会进行跨层比较。


diff 算法的优化策略:四种命中查找,四个指针


  1. 旧前与新前(先比开头,后插入和删除节点的这种情况)

  2. 旧后与新后(比结尾,前插入或删除的情况)

  3. 旧前与新后(头与尾比,此种发生了,涉及移动节点,那么新前指向的节点,移动到旧后之后)

  4. 旧后与新前(尾与头比,此种发生了,涉及移动节点,那么新前指向的节点,移动到旧前之前)

Watch 中的 deep:true 是如何实现的

当用户指定了 watch 中的 deep 属性为 true 时,如果当前监控的值是数组类型。会对对象中的每一项进行求值,此时会将当前 watcher存入到对应属性的依赖中,这样数组中对象发生变化时也会通知数据更新


源码相关


get () {     pushTarget(this) // 先将当前依赖放到 Dep.target上     let value     const vm = this.vm     try {         value = this.getter.call(vm, vm)     } catch (e) {         if (this.user) {             handleError(e, vm, `getter for watcher "${this.expression}"`)         } else {             throw e         }     } finally {         if (this.deep) { // 如果需要深度监控         traverse(value) // 会对对象中的每一项取值,取值时会执行对应的get方法     }popTarget() }
复制代码

Vue 性能优化

编码优化


  • 事件代理

  • keep-alive

  • 拆分组件

  • key 保证唯一性

  • 路由懒加载、异步组件

  • 防抖节流


Vue 加载性能优化


  • 第三方模块按需导入( babel-plugin-component

  • 图片懒加载


用户体验


  • app-skeleton 骨架屏

  • shellap p 壳

  • pwa


SEO 优化


  • 预渲染

Vue.js 的 template 编译

简而言之,就是先转化成 AST 树,再得到的 render 函数返回 VNode(Vue 的虚拟 DOM 节点),详细步骤如下:


首先,通过 compile 编译器把 template 编译成 AST 语法树(abstract syntax tree 即 源代码的抽象语法结构的树状表现形式),compile 是 createCompiler 的返回值,createCompiler 是用以创建编译器的。另外 compile 还负责合并 option。


然后,AST 会经过 generate(将 AST 语法树转化成 render funtion 字符串的过程)得到 render 函数,render 的返回值是 VNode,VNode 是 Vue 的虚拟 DOM 节点,里面有(标签名、子节点、文本等等)

既然 Vue 通过数据劫持可以精准探测数据变化,为什么还需要虚拟 DOM 进行 diff 检测差异

  • 响应式数据变化,Vue确实可以在数据变化时,响应式系统可以立刻得知。但是如果给每个属性都添加watcher用于更新的话,会产生大量的watcher从而降低性能

  • 而且粒度过细也得导致更新不准确的问题,所以vue采用了组件级的watcher配合diff来检测差异

Vue 组件渲染和更新过程

渲染组件时,会通过 Vue.extend 方法构建子组件的构造函数,并进行实例化。最终手动调用$mount() 进行挂载。更新组件时会进行 patchVnode 流程,核心就是diff算法


什么是 mixin ?

  • Mixin 使我们能够为 Vue 组件编写可插拔和可重用的功能。

  • 如果希望在多个组件之间重用一组组件选项,例如生命周期 hook、 方法等,则可以将其编写为 mixin,并在组件中简单的引用它。

  • 然后将 mixin 的内容合并到组件中。如果你要在 mixin 中定义生命周期 hook,那么它在执行时将优化于组件自已的 hook。

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 压缩等。

了解 history 有哪些方法吗?说下它们的区别

history 这个对象在html5的时候新加入两个api history.pushState()history.repalceState() 这两个API可以在不进行刷新的情况下,操作浏览器的历史纪录。唯一不同的是,前者是新增一个历史记录,后者是直接替换当前的历史记录。


从参数上来说:


window.history.pushState(state,title,url)//state:需要保存的数据,这个数据在触发popstate事件时,可以在event.state里获取//title:标题,基本没用,一般传null//url:设定新的历史纪录的url。新的url与当前url的origin必须是一样的,否则会抛出错误。url可以时绝对路径,也可以是相对路径。//如 当前url是 https://www.baidu.com/a/,执行history.pushState(null, null, './qq/'),则变成 https://www.baidu.com/a/qq/,//执行history.pushState(null, null, '/qq/'),则变成 https://www.baidu.com/qq/
window.history.replaceState(state,title,url)//与pushState 基本相同,但她是修改当前历史纪录,而 pushState 是创建新的历史纪录
复制代码


另外还有:


  • window.history.back() 后退

  • window.history.forward()前进

  • window.history.go(1) 前进或者后退几步


从触发事件的监听上来说:


  • pushState()replaceState()不能被popstate事件所监听

  • 而后面三者可以,且用户点击浏览器前进后退键时也可以

Vue.extend 作用和原理

官方解释:Vue.extend 使用基础 Vue 构造器,创建一个“子类”。参数是一个包含组件选项的对象。


其实就是一个子类构造器 是 Vue 组件的核心 api 实现思路就是使用原型继承的方法返回了 Vue 的子类 并且利用 mergeOptions 把传入组件的 options 和父类的 options 进行了合并


  • extend是构造一个组件的语法器。然后这个组件你可以作用到Vue.component这个全局注册方法里还可以在任意vue模板里使用组件。 也可以作用到vue实例或者某个组件中的components属性中并在内部使用apple组件。

  • Vue.component你可以创建 ,也可以取组件。


相关代码如下


export default function initExtend(Vue) {  let cid = 0; //组件的唯一标识  // 创建子类继承Vue父类 便于属性扩展  Vue.extend = function (extendOptions) {    // 创建子类的构造函数 并且调用初始化方法    const Sub = function VueComponent(options) {      this._init(options); //调用Vue初始化方法    };    Sub.cid = cid++;    Sub.prototype = Object.create(this.prototype); // 子类原型指向父类    Sub.prototype.constructor = Sub; //constructor指向自己    Sub.options = mergeOptions(this.options, extendOptions); //合并自己的options和父类的options    return Sub;  };}
复制代码

Vue 组件之间通信方式有哪些

Vue 组件间通信是面试常考的知识点之一,这题有点类似于开放题,你回答出越多方法当然越加分,表明你对 Vue 掌握的越熟练。 Vue 组件间通信只要指以下 3 类通信父子组件通信隔代组件通信兄弟组件通信,下面我们分别介绍每种通信方式且会说明此种方法可适用于哪类组件间通信


组件传参的各种方式



组件通信常用方式有以下几种


  • props / $emit 适用 父子组件通信

  • 父组件向子组件传递数据是通过 prop 传递的,子组件传递数据给父组件是通过$emit 触发事件来做到的

  • ref$parent / $children(vue3废弃) 适用 父子组件通信

  • ref:如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件实例

  • $parent / $children:访问访问父组件的属性或方法 / 访问子组件的属性或方法

  • EventBus ($emit / $on) 适用于 父子、隔代、兄弟组件通信

  • 这种方法通过一个空的 Vue 实例作为中央事件总线(事件中心),用它来触发事件和监听事件,从而实现任何组件间的通信,包括父子、隔代、兄弟组件

  • $attrs / $listeners(vue3废弃) 适用于 隔代组件通信

  • $attrs:包含了父作用域中不被 prop 所识别 (且获取) 的特性绑定 ( classstyle 除外 )。当一个组件没有声明任何 prop时,这里会包含所有父作用域的绑定 ( classstyle 除外 ),并且可以通过 v-bind="$attrs" 传入内部组件。通常配合 inheritAttrs 选项一起使用

  • $listeners:包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。它可以通过 v-on="$listeners" 传入内部组件

  • provide / inject 适用于 隔代组件通信

  • 祖先组件中通过 provider 来提供变量,然后在子孙组件中通过 inject 来注入变量。 provide / inject API 主要解决了跨级组件间的通信问题, 不过它的使用场景,主要是子组件获取上级组件的状态 ,跨级组件间建立了一种主动提供与依赖注入的关系

  • $root 适用于 隔代组件通信 访问根组件中的属性或方法,是根组件,不是父组件。$root只对根组件有用

  • Vuex 适用于 父子、隔代、兄弟组件通信

  • Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。每一个 Vuex 应用的核心就是 store(仓库)。“store” 基本上就是一个容器,它包含着你的应用中大部分的状态 ( state )

  • Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。

  • 改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样使得我们可以方便地跟踪每一个状态的变化。


根据组件之间关系讨论组件通信最为清晰有效


  • 父子组件:props/$emit/$parent/ref

  • 兄弟组件:$parent/eventbus/vuex

  • 跨层级关系:eventbus/vuex/provide+inject/$attrs + $listeners/$root


下面演示组件之间通讯三种情况: 父传子、子传父、兄弟组件之间的通讯


1. 父子组件通信


使用props,父组件可以使用props向子组件传递数据。


父组件vue模板father.vue:


<template>  <child :msg="message"></child></template>
<script>import child from './child.vue';export default { components: { child }, data () { return { message: 'father message'; } }}</script>
复制代码


子组件vue模板child.vue:


<template>    <div>{{msg}}</div></template>
<script>export default { props: { msg: { type: String, required: true } }}</script>
复制代码


回调函数(callBack)


父传子:将父组件里定义的method作为props传入子组件


// 父组件Parent.vue:<Child :changeMsgFn="changeMessage">methods: {    changeMessage(){        this.message = 'test'    }}
复制代码


// 子组件Child.vue:<button @click="changeMsgFn">props:['changeMsgFn']
复制代码


子组件向父组件通信


父组件向子组件传递事件方法,子组件通过$emit触发事件,回调给父组件


父组件vue模板father.vue:


<template>    <child @msgFunc="func"></child></template>
<script>import child from './child.vue';export default { components: { child }, methods: { func (msg) { console.log(msg); } }}</script>
复制代码


子组件vue模板child.vue:


<template>    <button @click="handleClick">点我</button></template>
<script>export default { props: { msg: { type: String, required: true } }, methods () { handleClick () { //........ this.$emit('msgFunc'); } }}</script>
复制代码


2. provide / inject 跨级访问祖先组件的数据


父组件通过使用provide(){return{}}提供需要传递的数据


export default {  data() {    return {      title: '我是父组件',      name: 'poetry'    }  },  methods: {    say() {      alert(1)    }  },  // provide属性 能够为后面的后代组件/嵌套的组件提供所需要的变量和方法  provide() {    return {      message: '我是祖先组件提供的数据',      name: this.name, // 传递属性      say: this.say    }  }}
复制代码


子组件通过使用inject:[“参数1”,”参数2”,…]接收父组件传递的参数


<template>  <p>曾孙组件</p>  <p>{{message}}</p></template><script>export default {  // inject 注入/接收祖先组件传递的所需要的数据即可   //接收到的数据 变量 跟data里面的变量一样 可以直接绑定到页面 {{}}  inject: [ "message","say"],  mounted() {    this.say();  },};</script>
复制代码


3. children 获取父组件实例和子组件实例的集合


  • this.$parent 可以直接访问该组件的父实例或组件

  • 父组件也可以通过 this.$children 访问它所有的子组件;需要注意 $children 并不保证顺序,也不是响应式的


<!-- parent.vue --><template><div>  <child1></child1>     <child2></child2>   <button @click="clickChild">$children方式获取子组件值</button></div></template><script>import child1 from './child1'import child2 from './child2'export default {  data(){    return {      total: 108    }  },  components: {    child1,    child2    },  methods: {    funa(e){      console.log("index",e)    },    clickChild(){      console.log(this.$children[0].msg);      console.log(this.$children[1].msg);    }  }}</script>
复制代码


<!-- child1.vue --><template>  <div>    <button @click="parentClick">点击访问父组件</button>  </div></template><script>export default {  data(){    return {      msg:"child1"    }  },  methods: {    // 访问父组件数据    parentClick(){      this.$parent.funa("xx")      console.log(this.$parent.total);    }  }}</script>
复制代码


<!-- child2.vue --><template>  <div>    child2  </div></template><script>export default {  data(){    return {     msg: 'child2'    }  }}</script>
复制代码


4. listeners 多级组件通信


$attrs 包含了从父组件传过来的所有props属性


// 父组件Parent.vue:<Child :name="name" :age="age"/>
// 子组件Child.vue:<GrandChild v-bind="$attrs" />
// 孙子组件GrandChild<p>姓名:{{$attrs.name}}</p><p>年龄:{{$attrs.age}}</p>
复制代码


$listeners包含了父组件监听的所有事件


// 父组件Parent.vue:<Child :name="name" :age="age" @changeNameFn="changeName"/>
// 子组件Child.vue:<button @click="$listeners.changeNameFn"></button>
复制代码


5. ref 父子组件通信


// 父组件Parent.vue:<Child ref="childComp"/><button @click="changeName"></button>changeName(){    console.log(this.$refs.childComp.age);    this.$refs.childComp.changeAge()}
// 子组件Child.vue:data(){ return{ age:20 }},methods(){ changeAge(){ this.age=15 }}
复制代码


6. 非父子, 兄弟组件之间通信


vue2中废弃了broadcast广播和分发事件的方法。父子组件中可以用props$emit()。如何实现非父子组件间的通信,可以通过实例一个vue实例Bus作为媒介,要相互通信的兄弟组件之中,都引入Bus,然后通过分别调用 Bus 事件触发和监听来实现通信和参数传递。Bus.js可以是这样:


// Bus.js
// 创建一个中央时间总线类 class Bus { constructor() { this.callbacks = {}; // 存放事件的名字 } $on(name, fn) { this.callbacks[name] = this.callbacks[name] || []; this.callbacks[name].push(fn); } $emit(name, args) { if (this.callbacks[name]) { this.callbacks[name].forEach((cb) => cb(args)); } } }
// main.js Vue.prototype.$bus = new Bus() // 将$bus挂载到vue实例的原型上 // 另一种方式 Vue.prototype.$bus = new Vue() // Vue已经实现了Bus的功能
复制代码


<template>    <button @click="toBus">子组件传给兄弟组件</button></template>
<script>export default{ methods: { toBus () { this.$bus.$emit('foo', '来自兄弟组件') } }}</script>
复制代码


另一个组件也在钩子函数中监听on事件


export default {  data() {    return {      message: ''    }  },  mounted() {    this.$bus.$on('foo', (msg) => {      this.message = msg    })  }}
复制代码


7. $root 访问根组件中的属性或方法


  • 作用:访问根组件中的属性或方法

  • 注意:是根组件,不是父组件。$root只对根组件有用


var vm = new Vue({  el: "#app",  data() {    return {      rootInfo:"我是根元素的属性"    }  },  methods: {    alerts() {      alert(111)    }  },  components: {    com1: {      data() {        return {          info: "组件1"        }      },      template: "<p>{{ info }} <com2></com2></p>",      components: {        com2: {          template: "<p>我是组件1的子组件</p>",          created() {            this.$root.alerts()// 根组件方法            console.log(this.$root.rootInfo)// 我是根元素的属性          }        }      }    }  }});
复制代码


8. vuex


  • 适用场景: 复杂关系的组件数据传递

  • Vuex 作用相当于一个用来存储共享变量的容器



  • state用来存放共享变量的地方

  • getter,可以增加一个getter派生状态,(相当于store中的计算属性),用来获得共享变量的值

  • mutations用来存放修改state的方法。

  • actions也是用来存放修改 state 的方法,不过action是在mutations的基础上进行。常用来做一些异步操作


小结


  • 父子关系的组件数据传递选择 props$emit进行传递,也可选择ref

  • 兄弟关系的组件数据传递可选择$bus,其次可以选择$parent进行传递

  • 祖先与后代组件数据传递可选择attrslisteners或者 ProvideInject

  • 复杂关系的组件数据传递可以通过vuex存放共享的变量


用户头像

bb_xiaxia1998

关注

还未添加个人签名 2022-09-01 加入

还未添加个人简介

评论

发布
暂无评论
熬夜整理的vue面试题_Vue_bb_xiaxia1998_InfoQ写作社区