乐趣区

关于前端:前端vue面试题持续更新中

vue-router 中如何爱护路由

剖析

路由爱护在利用开发过程中十分重要,简直每个利用都要做各种路由权限治理,因而相当考查使用者基本功。

体验

全局守卫:

const router = createRouter({...})
​
router.beforeEach((to, from) => {
  // ...
  // 返回 false 以勾销导航
  return false
})

路由独享守卫:

const routes = [
  {
    path: '/users/:id',
    component: UserDetails,
    beforeEnter: (to, from) => {
      // reject the navigation
      return false
    },
  },
]

组件内的守卫:

const UserDetails = {
  template: `...`,
  beforeRouteEnter(to, from) {// 在渲染该组件的对应路由被验证前调用},
  beforeRouteUpdate(to, from) {// 在以后路由扭转,然而该组件被复用时调用},
  beforeRouteLeave(to, from) {// 在导航来到渲染该组件的对应路由时调用},
}

答复

  • vue-router中爱护路由的办法叫做路由守卫,次要用来通过跳转或勾销的形式守卫导航。
  • 路由守卫有三个级别:全局 路由独享 组件级。影响范畴由大到小,例如全局的router.beforeEach(),能够注册一个全局前置守卫,每次路由导航都会通过这个守卫,因而在其外部能够退出管制逻辑决定用户是否能够导航到指标路由;在路由注册的时候能够退出单路由独享的守卫,例如beforeEnter,守卫只在进入路由时触发,因而只会影响这个路由,管制更准确;咱们还能够为路由组件增加守卫配置,例如beforeRouteEnter,会在渲染该组件的对应路由被验证前调用,管制的范畴更准确了。
  • 用户的任何导航行为都会走 navigate 办法,外部有个 guards 队列按程序执行用户注册的守卫钩子函数,如果没有通过验证逻辑则会勾销原有的导航。

原理

runGuardQueue(guards)链式的执行用户在各级别注册的守卫钩子函数,通过则持续下一个级别的守卫,不通过进入 catch 流程勾销本来导航

// 源码
runGuardQueue(guards)
  .then(() => {
    // check global guards beforeEach
    guards = []
    for (const guard of beforeGuards.list()) {guards.push(guardToPromiseFn(guard, to, from))
    }
    guards.push(canceledNavigationCheck)

    return runGuardQueue(guards)
  })
  .then(() => {
    // check in components beforeRouteUpdate
    guards = extractComponentsGuards(
      updatingRecords,
      'beforeRouteUpdate',
      to,
      from
    )

    for (const record of updatingRecords) {
      record.updateGuards.forEach(guard => {guards.push(guardToPromiseFn(guard, to, from))
      })
    }
    guards.push(canceledNavigationCheck)

    // run the queue of per route beforeEnter guards
    return runGuardQueue(guards)
  })
  .then(() => {
    // check the route beforeEnter
    guards = []
    for (const record of to.matched) {
      // do not trigger beforeEnter on reused views
      if (record.beforeEnter && !from.matched.includes(record)) {if (isArray(record.beforeEnter)) {for (const beforeEnter of record.beforeEnter)
            guards.push(guardToPromiseFn(beforeEnter, to, from))
        } else {guards.push(guardToPromiseFn(record.beforeEnter, to, from))
        }
      }
    }
    guards.push(canceledNavigationCheck)

    // run the queue of per route beforeEnter guards
    return runGuardQueue(guards)
  })
  .then(() => {// NOTE: at this point to.matched is normalized and does not contain any () => Promise<Component>

    // clear existing enterCallbacks, these are added by extractComponentsGuards
    to.matched.forEach(record => (record.enterCallbacks = {}))

    // check in-component beforeRouteEnter
    guards = extractComponentsGuards(
      enteringRecords,
      'beforeRouteEnter',
      to,
      from
    )
    guards.push(canceledNavigationCheck)

    // run the queue of per route beforeEnter guards
    return runGuardQueue(guards)
  })
  .then(() => {
    // check global guards beforeResolve
    guards = []
    for (const guard of beforeResolveGuards.list()) {guards.push(guardToPromiseFn(guard, to, from))
    }
    guards.push(canceledNavigationCheck)

    return runGuardQueue(guards)
  })
  // catch any navigation canceled
  .catch(err =>
    isNavigationFailure(err, ErrorTypes.NAVIGATION_CANCELLED)
      ? err
      : Promise.reject(err)
  )

源码地位(opens new window)

应用 Object.defineProperty() 来进行数据劫持有什么毛病?

在对一些属性进行操作时,应用这种办法无奈拦挡,比方通过下标形式批改数组数据或者给对象新增属性,这都不能触发组件的从新渲染,因为 Object.defineProperty 不能拦挡到这些操作。更准确的来说,对于数组而言,大部分操作都是拦挡不到的,只是 Vue 外部通过重写函数的形式解决了这个问题。

在 Vue3.0 中曾经不应用这种形式了,而是通过应用 Proxy 对对象进行代理,从而实现数据劫持。应用 Proxy 的益处是它能够完满的监听到任何形式的数据扭转,惟一的毛病是兼容性的问题,因为 Proxy 是 ES6 的语法。

用 VNode 来形容一个 DOM 构造

虚构节点就是用一个对象来形容一个实在的 DOM 元素。首先将 template(实在 DOM)先转成 ast ast 树通过 codegen 生成 render 函数, render 函数里的 _c 办法将它转为虚构 dom

如何从实在 DOM 到虚构 DOM

波及到 Vue 中的模板编译原理,次要过程:

  1. 将模板转换成 ast 树, ast 用对象来形容实在的 JS 语法(将实在 DOM 转换成虚构 DOM)
  2. 优化树
  3. ast 树生成代码

Vue 为什么没有相似于 React 中 shouldComponentUpdate 的生命周期?

考点: Vue 的变动侦测原理

前置常识: 依赖收集、虚构 DOM、响应式零碎

根本原因是 Vue 与 React 的变动侦测形式有所不同

React 是 pull 的形式侦测变动, 当 React 晓得发生变化后, 会应用 Virtual Dom Diff 进行差别检测, 然而很多组件实际上是必定不会发生变化的, 这个时候须要用 shouldComponentUpdate 进行手动操作来缩小 diff, 从而进步程序整体的性能.

Vue 是 pull+push 的形式侦测变动的, 在一开始就晓得那个组件产生了变动, 因而在 push 的阶段并不需要手动管制 diff, 而组件外部采纳的 diff 形式实际上是能够引入相似于 shouldComponentUpdate 相干生命周期的, 然而通常正当大小的组件不会有适量的 diff, 手动优化的价值无限, 因而目前 Vue 并没有思考引入 shouldComponentUpdate 这种手动优化的生命周期.

Vue 组件间通信有哪几种形式?

​ Vue 组件间通信是面试常考的知识点之一,这题有点相似于凋谢题,你答复出越多办法当然越加分,表明你对 Vue 把握的越纯熟。Vue 组件间通信只有指以下 3 类通信:父子组件通信、隔代组件通信、兄弟组件通信,上面咱们别离介绍每种通信形式且会阐明此种办法可实用于哪类组件间通信。

(1)props / $emit 实用 父子组件通信

这种办法是 Vue 组件的根底,置信大部分同学耳闻能详,所以此处就不举例开展介绍。

(2)ref$parent / $children 实用 父子组件通信

  • ref:如果在一般的 DOM 元素上应用,援用指向的就是 DOM 元素;如果用在子组件上,援用就指向组件实例
  • $parent / $children:拜访父 / 子实例

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

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

(4)$attrs/$listeners 实用于 隔代组件通信

  • $attrs:蕴含了父作用域中不被 prop 所辨认 (且获取) 的个性绑定 (class 和 style 除外)。当一个组件没有申明任何 prop 时,这里会蕴含所有父作用域的绑定 (class 和 style 除外),并且能够通过 v-bind="$attrs" 传入外部组件。通常配合 inheritAttrs 选项一起应用。
  • $listeners:蕴含了父作用域中的 (不含 .native 润饰器的) v-on 事件监听器。它能够通过 v-on="$listeners" 传入外部组件

(5)provide / inject 实用于 隔代组件通信

先人组件中通过 provider 来提供变量,而后在子孙组件中通过 inject 来注入变量。provide / inject API 次要解决了跨级组件间的通信问题,不过它的应用场景,次要是子组件获取下级组件的状态,跨级组件间建设了一种被动提供与依赖注入的关系。

(6)Vuex 实用于 父子、隔代、兄弟组件通信

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

  • Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地失去高效更新。
  • 扭转 store 中的状态的惟一路径就是显式地提交 (commit) mutation。这样使得咱们能够不便地跟踪每一个状态的变动。

参考 前端进阶面试题具体解答

keep-alive 应用场景和原理

keep-alive 是 Vue 内置的一个组件,能够实现组件缓存,当组件切换时不会对以后组件进行卸载。

  • 罕用的两个属性 include/exclude,容许组件有条件的进行缓存。
  • 两个生命周期 activated/deactivated,用来得悉以后组件是否处于沉闷状态。
  • keep-alive 的中还使用了 LRU(最近起码应用) 算法,抉择最近最久未应用的组件予以淘汰。

computed 和 watch 有什么区别?

computed:

  1. computed是计算属性, 也就是计算值, 它更多用于计算值的场景
  2. computed具备缓存性,computed 的值在 getter 执行后是会缓存的,只有在它依赖的属性值扭转之后,下一次获取 computed 的值时才会从新调用对应的 getter 来计算
  3. computed实用于计算比拟耗费性能的计算场景

watch:

  1. 更多的是「察看」的作用, 相似于某些数据的监听回调, 用于察看 props $emit 或者本组件的值, 当数据变动时来执行回调进行后续操作
  2. 无缓存性,页面从新渲染时值不变动也会执行

小结:

  1. 当咱们要进行数值计算, 而且依赖于其余数据,那么把这个数据设计为 computed
  2. 如果你须要在某个数据变动时做一些事件,应用 watch 来察看这个数据变动

Vue 组件如何通信?

Vue 组件通信的办法如下:

  • props/$emit+v-on: 通过 props 将数据自上而下传递,而通过 $emit 和 v -on 来向上传递信息。
  • EventBus: 通过 EventBus 进行信息的公布与订阅
  • vuex: 是全局数据管理库,能够通过 vuex 治理全局的数据流
  • $attrs/$listeners: Vue2.4 中退出的 $attrs/$listeners 能够进行跨级的组件通信
  • provide/inject:以容许一个先人组件向其所有子孙后代注入一个依赖,不管组件档次有多深,并在起上下游关系成立的工夫里始终失效,这成为了跨组件通信的根底

还有一些用 solt 插槽或者 ref 实例进行通信的,应用场景过于无限就不赘述了。

v-show 与 v-if 有什么区别?

v-if 真正 的条件渲染,因为它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建;也是 惰性的:如果在初始渲染时条件为假,则什么也不做——直到条件第一次变为真时,才会开始渲染条件块。

v-show 就简略得多——不论初始条件是什么,元素总是会被渲染,并且只是简略地基于 CSS 的“display”属性进行切换。

所以,v-if 实用于在运行时很少扭转条件,不须要频繁切换条件的场景;v-show 则实用于须要十分频繁切换条件的场景。

个别在哪个生命周期申请异步数据

咱们能够在钩子函数 created、beforeMount、mounted 中进行调用,因为在这三个钩子函数中,data 曾经创立,能够将服务端端返回的数据进行赋值。

举荐在 created 钩子函数中调用异步申请,因为在 created 钩子函数中调用异步申请有以下长处:

  • 能更快获取到服务端数据,缩小页面加载工夫,用户体验更好;
  • SSR 不反对 beforeMount、mounted 钩子函数,放在 created 中有助于一致性。

Vue 中的 key 到底有什么用?

key是为 Vue 中的 vnode 标记的惟一 id, 通过这个 key, 咱们的 diff 操作能够更精确、更疾速

diff 算法的过程中, 先会进行新旧节点的首尾穿插比照, 当无奈匹配的时候会用新节点的 key 与旧节点进行比对, 而后超出差别.

diff 程能够概括为:oldCh 和 newCh 各有两个头尾的变量 StartIdx 和 EndIdx,它们的 2 个变量互相比拟,一共有 4 种比拟形式。如果 4 种比拟都没匹配,如果设置了 key,就会用 key 进行比拟,在比拟的过程中,变量会往两头靠,一旦 StartIdx>EndIdx 表明 oldCh 和 newCh 至多有一个曾经遍历完了,就会完结比拟, 这四种比拟形式就是首、尾、旧尾新头、旧头新尾.

  • 精确: 如果不加key, 那么 vue 会抉择复用节点(Vue 的就地更新策略), 导致之前节点的状态被保留下来, 会产生一系列的 bug.
  • 疾速: key 的唯一性能够被 Map 数据结构充分利用, 相比于遍历查找的工夫复杂度 O(n),Map 的工夫复杂度仅仅为 O(1).

应用 vue 渲染大量数据时应该怎么优化?说下你的思路!

剖析

企业级我的项目中渲染大量数据的状况比拟常见,因而这是一道十分好的综合实际题目。

答复

  1. 在大型企业级我的项目中常常须要渲染大量数据,此时很容易呈现卡顿的状况。比方大数据量的表格、树
  2. 解决时要依据状况做不同解决:
  3. 能够采取分页的形式获取,防止渲染大量数据
  • vue-virtual-scroller (opens new window)等虚构滚动计划,只渲染视口范畴内的数据
  • 如果不须要更新,能够应用 v -once 形式只渲染一次
  • 通过 v -memo (opens new window)能够缓存后果,联合 v-for 应用,防止数据变动时不必要的 VNode 创立
  • 能够采纳懒加载形式,在用户须要的时候再加载数据,比方 tree 组件子树的懒加载
  • 还是要看具体需要,首先从设计上防止大数据获取和渲染;切实须要这样做能够采纳虚表的形式优化渲染;最初优化更新,如果不须要更新能够 v-once 解决,须要更新能够 v-memo 进一步优化大数据更新性能。其余能够采纳的是交互方式优化,无线滚动、懒加载等计划

虚构 DOM 真的比实在 DOM 性能好吗

  • 首次渲染大量 DOM 时,因为多了一层虚构 DOM 的计算,会比 innerHTML 插入慢。
  • 正如它能保障性能上限,在实在 DOM 操作的时候进行针对性的优化时,还是更快的。

Vue 组件间通信有哪几种形式?

​ Vue 组件间通信是面试常考的知识点之一,这题有点相似于凋谢题,你答复出越多办法当然越加分,表明你对 Vue 把握的越纯熟。Vue 组件间通信只有指以下 3 类通信:父子组件通信、隔代组件通信、兄弟组件通信,上面咱们别离介绍每种通信形式且会阐明此种办法可实用于哪类组件间通信。

(1)props / $emit 实用 父子组件通信

这种办法是 Vue 组件的根底,置信大部分同学耳闻能详,所以此处就不举例开展介绍。

(2)ref$parent / $children 实用 父子组件通信

  • ref:如果在一般的 DOM 元素上应用,援用指向的就是 DOM 元素;如果用在子组件上,援用就指向组件实例
  • $parent / $children:拜访父 / 子实例

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

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

(4)$attrs/$listeners 实用于 隔代组件通信

  • $attrs:蕴含了父作用域中不被 prop 所辨认 (且获取) 的个性绑定 (class 和 style 除外)。当一个组件没有申明任何 prop 时,这里会蕴含所有父作用域的绑定 (class 和 style 除外),并且能够通过 v-bind="$attrs" 传入外部组件。通常配合 inheritAttrs 选项一起应用。
  • $listeners:蕴含了父作用域中的 (不含 .native 润饰器的) v-on 事件监听器。它能够通过 v-on="$listeners" 传入外部组件

(5)provide / inject 实用于 隔代组件通信

先人组件中通过 provider 来提供变量,而后在子孙组件中通过 inject 来注入变量。provide / inject API 次要解决了跨级组件间的通信问题,不过它的应用场景,次要是子组件获取下级组件的状态,跨级组件间建设了一种被动提供与依赖注入的关系。

(6)Vuex 实用于 父子、隔代、兄弟组件通信

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

  • Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地失去高效更新。
  • 扭转 store 中的状态的惟一路径就是显式地提交 (commit) mutation。这样使得咱们能够不便地跟踪每一个状态的变动。

如果让你从零开始写一个 vuex,说说你的思路

思路剖析

这个题目很有难度,首先思考 vuex 解决的问题:存储用户全局状态并提供治理状态 API。

  • vuex需要剖析
  • 如何实现这些需要

答复范例

  1. 官网说 vuex 是一个状态管理模式和库,并确保这些状态以可预期的形式变更。可见要实现一个vuex
  2. 要实现一个 Store 存储全局状态
  3. 要提供批改状态所需 API:commit(type, payload), dispatch(type, payload)
  4. 实现 Store 时,能够定义 Store 类,构造函数接管选项 options,设置属性state 对外裸露状态,提供 commitdispatch批改属性 state。这里须要设置state 为响应式对象,同时将 Store 定义为一个 Vue 插件
  5. commit(type, payload)办法中能够获取用户传入 mutations 并执行它,这样能够按用户提供的办法批改状态。dispatch(type, payload)相似,但须要留神它可能是异步的,须要返回一个 Promise 给用户以解决异步后果

实际

Store的实现:

class Store {constructor(options) {this.state = reactive(options.state)
        this.options = options
    }
    commit(type, payload) {this.options.mutations[type].call(this, this.state, payload)
    }
}

vuex 简易版

/**
 * 1 实现插件,挂载 $store
 * 2 实现 store
 */

let Vue;

class Store {constructor(options) {
    // state 响应式解决
    // 内部拜访:this.$store.state.***
    // 第一种写法
    // this.state = new Vue({
    //   data: options.state
    // })

    // 第二种写法:避免外界间接接触外部 vue 实例,避免内部强行变更
    this._vm = new Vue({
      data: {$$state: options.state}
    })

    this._mutations = options.mutations
    this._actions = options.actions
    this.getters = {}
    options.getters && this.handleGetters(options.getters)

    this.commit = this.commit.bind(this)
    this.dispatch = this.dispatch.bind(this)
  }

  get state () {return this._vm._data.$$state}

  set state (val) {return new Error('Please use replaceState to reset state')
  }

  handleGetters (getters) {Object.keys(getters).map(key => {
      Object.defineProperty(this.getters, key, {get: () => getters[key](this.state)
      })
    })
  }

  commit (type, payload) {let entry = this._mutations[type]
    if (!entry) {return new Error(`${type} is not defined`)
    }

    entry(this.state, payload)
  }

  dispatch (type, payload) {let entry = this._actions[type]
    if (!entry) {return new Error(`${type} is not defined`)
    }

    entry(this, payload)
  }
}

const install = (_Vue) => {
  Vue = _Vue

  Vue.mixin({beforeCreate () {if (this.$options.store) {Vue.prototype.$store = this.$options.store}
    },
  })
}


export default {Store, install}

验证形式

import Vue from 'vue'
import Vuex from './vuex'
// this.$store
Vue.use(Vuex)

export default new Vuex.Store({
  state: {counter: 0},
  mutations: {
    // state 从哪里来的
    add (state) {state.counter++}
  },
  getters: {doubleCounter (state) {return state.counter * 2}
  },
  actions: {add ({ commit}) {setTimeout(() => {commit('add')
      }, 1000)
    }
  },
  modules: {}})

简述 mixin、extends 的笼罩逻辑

(1)mixin 和 extends mixin 和 extends 均是用于合并、拓展组件的,两者均通过 mergeOptions 办法实现合并。

  • mixins 接管一个混入对象的数组,其中混入对象能够像失常的实例对象一样蕴含实例选项,这些选项会被合并到最终的选项中。Mixin 钩子依照传入程序顺次调用,并在调用组件本身的钩子之前被调用。
  • extends 次要是为了便于扩大单文件组件,接管一个对象或构造函数。

    (2)mergeOptions 的执行过程

  • 规范化选项(normalizeProps、normalizelnject、normalizeDirectives)
  • 对未合并的选项,进行判断
if (!child._base) {if (child.extends) {parent = mergeOptions(parent, child.extends, vm);
  }
  if (child.mixins) {for (let i = 0, l = child.mixins.length; i < l; i++) {parent = mergeOptions(parent, child.mixins[i], vm);
    }
  }
}
  • 合并解决。依据一个通用 Vue 实例所蕴含的选项进行分类逐个判断合并,如 props、data、methods、watch、computed、生命周期等,将合并后果存储在新定义的 options 对象里。
  • 返回合并后果 options。

Vue 中 v -html 会导致哪些问题

  • 可能会导致 xss 攻打
  • v-html 会替换掉标签外部的子元素
let template = require('vue-template-compiler'); 
let r = template.compile(`<div v-html="'<span>hello</span>'"></div>`) 

// with(this){return _c('div',{domProps: {"innerHTML":_s('<span>hello</span>')}})} 
console.log(r.render);

// _c 定义在 core/instance/render.js 
// _s 定义在 core/instance/render-helpers/index,js
if (key === 'textContent' || key === 'innerHTML') {if (vnode.children) vnode.children.length = 0 
    if (cur === oldProps[key]) continue // #6601 work around Chrome version <= 55 bug where single textNode // replaced by innerHTML/textContent retains its parentNode property 
    if (elm.childNodes.length === 1) {elm.removeChild(elm.childNodes[0]) 
    } 
}

在 Vue 中应用插件的步骤

  • 采纳 ES6import ... from ...语法或 CommonJSrequire()办法引入插件
  • 应用全局办法 Vue.use(plugin) 应用插件, 能够传入一个选项对象Vue.use(MyPlugin, { someOption: true})

Vue.extend 作用和原理

官网解释:Vue.extend 应用根底 Vue 结构器,创立一个“子类”。参数是一个蕴含组件选项的对象。

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

退出移动版