• Home
  • Archives
  Evan的博客
  • Home
  • Archives
  • 面试
  • 原理笔记
  • 项目实践
  • 其他

常见 Vue 面试题

2020/10/09 posted in  面试

总结了一些常见的 Vue 面试题,分享给大家,也供自己复习的时候看。

其他类型的常见面试题:

  • 常见 HTML 和 CSS 面试题
  • 常见 JS 面试题
  • 常见 Vue 面试题
  • 常见性能相关面试题
  • 常见大前端相关面试题

1. vue 组件传值有哪些方式

  • 最简单的 props 和 emit
  • 利用一个新的 vue 实例做监听
// bus.js
import Vue from 'vue'
export default new Vue()

// 组件 a
import Bus from ./bus.js
methods: {emitSth() {Bus.$emit('fromA')
  }
}

// 组件 b
import Bus from ./bus.js
mounted() {Bus.$on('fromA', () => {...})
}
  • 利用 vuex

2. vue router 有哪些钩子,分别是什么作用

  • 全局前置守卫 router.beforeEach((to,from,next)=>{})

    • to 是去哪个路由,from 是来自哪个路由,next() 交接钩子状态
    • 路由守卫是一个 promise,一定要交接 next(),不然不会触发 resolve
    • 在 next 中可以传递一个路由,如 next('/') 去根路径
    • to 和 from 中除了有 path,name 等基础信息之外,还有个 matched 数组,存储所有匹配到的路由,在这里可以查看更多的详细信息,包括 meta 中的自定义信息
  • 全局后置钩子 router.afterEach((to,from)=>{})

    • 后置钩子不存在 next 了,有 to 和 from,与前置钩子类似
  • 全局解析守卫 roter.beforeResolve

    • 全局守卫是自定义的一个守卫,在导航被确认之前,同时在所有组件内守卫和异步路由组件被解析之后,解析守卫就被调用。
  • 独享路由前置守卫 beforeEnter((to,from,next)=>{})

    • 独享路由守卫就是某个路由专有的守卫,用法与全局路由一致,只不过定义在具体路由内
  • 组件前置路由守卫 beforeRouteEnter((to,from,next)=>{})

    • 组件内路由指的是,当触发某个路由后,路由会依次加载路由页面内所需的组件,这些组件都可以在自身组件内定义前置路由。
    • 在路由被 comfirm 之前,每个组件都会依次调用组件内前置路由。在这个钩子中,是没办法用 this(Vue 实例)的,因为这个时候 DOM 还没初始化,还没挂载。
    • 可以在 next 中将实例传进去 next(vm=>{}),next 之后已经初始化完成。
  • 组件变更路由守卫 beforeRouteUpdate((to,from,next)=>{})

    • 当路由改变,但是组件被复用的时候会调用 beforeRouteUpdate,比如一个页面调用了三次相同的 button 组件,第一次是 beforeRouteEnter,后面两次调用 beforeRouteUpdate
    • 这个钩子中可以用 this,因为 DOM 以及挂载好了,同时 next 中已经不能接受 vue 实例了(没意义)
  • 组件离开路由守卫 beforeRouteLeave((to,from,next)=>{})

    • 当导航(路由)离开的时候,路由内的组件开始注销,这个时候每个组件会执行这个路由守卫
    • 同样可以用 this,也不能在 next 中接受 vue 实例

3. vuex 为什么要用单向数据流,单向数据流指的是什么

单项数据流就是数据流动方向是固定的,不能倒流。在 vue 中,数据流动方向都是由 vue 实例发起开始,最后状态池 state 数据变更后通知 vue 实例重新渲染:

sequenceDiagram
Vue Components->>Actions: dispatch
Actions->>Mutations: commit
Mutations->>State: mutate
State->>Vue Components: Render

单项数据流是数据驱动视图的一个核心。因为数据的变更会导致视图的重新渲染,所以数据的变更需要是可追踪的。单项数据流的好处是可以追踪所有的数据(在 state 中)的变更都是单向的,这样保证变更状态可追溯,每次数据的变更都是由 Vue 实例派发出来的。保证了每个组件都是无副作用的(纯函数,函数式编程)

4. 为什么要通过 actions 来触发 mutations,直接用 vue 实例触发 mutations 不可以吗?

首先一个概念就是,mutation 必须是同步的,因为 mutation 会修改 state 数据,异步的 mutation 状态是无法追踪的。假如 mutation 是异步,那 action 或 vue 实例 commit 一个 mutation 之后(比如请求数据),那不知道什么时候回调会有响应,也就无法得知 state 啥时候被修改。

因为 mutation 必须是同步的,所以如果涉及到异步的操作,那么需要一个东西能异步提交 mutation,action 就是这个东西。vue 实例也可以发布 commit 触发 mutation,只不过如果直接 commit 的 mutation 就必须是同步的了。

既然 commit 一个 mutation 必须是同步的,那就异步操作(dispatch)action,然后让 action 去 commit 同步的 mutation 就好了。dispatch 可以是异步的也没关系,啥时候回调回来也没关系,反正最终他会 commit,只需要追踪 commit 一个 mutation 的时间点的快照就好了,这样数据流仍然是可追踪的。

5. vuex 中如何使用异步

vuex 中,actions 都是可异步的。并且每一个 action 都是返回一个 promise,配合 promise.then 或者 await/async 写异步还是很舒服

// 异步 action
actions: {incrementAsync ({ commit}) {setTimeout(() => {commit('increment')
    }, 1000)}}

// await / async
// 假设 getData()和 getOtherData() 返回的是 Promise
actions: {async actionA ({ commit}) {commit('gotData', await getData())
  },
  async actionB ({dispatch, commit}) {await dispatch('actionA') // 等待 actionA 完成
    commit('gotOtherData', await getOtherData())
  }
}

6. vuex 中如何使用 module

vuex 支持模块化,模块化的时候,各个模块通过 state 访问自己的状态中心,通过 rootState 访问根状态中心

// 定义 vuex
const moduleA = {state: { ...},
  mutations: {...},
  actions: {...},
  getters: {...}
}

const moduleB = {state: { ...},
  mutations: {...},
  actions: {...}
}

const store = new Vuex.Store({
  modules: {
    a: moduleA,
    b: moduleB
  }
})
store.state.a // -> moduleA 的状态
store.state.b // -> moduleB 的状态

// 使用时用 map 映射更方便
computed: {
  ...mapState('some/nested/module', {
    a: state => state.a,
    b: state => state.b
  })
},
methods: {
  ...mapActions('some/nested/module', ['foo', // -> this.foo()
    'bar' // -> this.bar()])
}

7. 如何监听第三方组件的声明周期

<Child @hook:mounted="childMounted"></Child>

8. watch / methods / computed 的区别

  • watch
    • watch 是监听某个数据的变化,是一个 watcher,订阅一份数据,当数据改变的时候执行一个回调。如果要监听对象深层次的属性,可以用 deep 关键字,但是这样会深度遍历所有的属性。也可以用下面的方法针对某个深层次属性做监听
    • watch 和 vue 响应式的 watcher 是类似的,只不过响应式是 vue 实例帮我们收集响应式 data 中的依赖,而 watch 是用户手动绑定的响应式
    • watch 的初始化在 data 初始化之后会立马触发 get 获取 value,此时如果有 immediate 属性那么立马执行 watch 对应的回调函数
    • 当 data 对应的 key 发生变化时,触发 setter,watch 感知到后执行回调函数
// 针对某个属性
watch: {
  'queryData.name': {handler: function() {// do something}
  }
}

// 或者结合 computed
computed: {getName: function() {return this.queryData.name;}
},
watch: {
  getName: {handler: function() {// do something}
  }
}
  • methods

    • methods 是方法的集合,每次调用都会执行一次方法
  • computed

    • computed 是计算属性,他会依赖其他属性计算并且 return 一个值。和 methods 最大的区别是,computed 会缓存,如果参数条件不变,将把缓存结果 return 出去,如果条件变了再重新计算。当然,也可以利用闭包保证每次执行的独立作用域,这样缓存就失效了,和 methods 就没差别了
    • 假设 computed c 依赖 data a 和 b,当 data 初始化之后,开始初始化 computed。这时候触发 c 的 getter,c 会去读取 a 和 b,触发 a 和 b 的 getter
    • 在触发 c 的 getter 的时候,就可以缓存 a 和 b。当下次再触发 c 的 getter,c 的 getter 同样去读取 a 和 b,比对后如果发现值没有变更,那么 c 不触发视图更新。如果发现依赖变更,则触发更新

9. 什么是 mvvm

mvvm 是 Model-View-ViewModel 的缩写

  • Model 是模型,是数据
  • View 是视图
  • ViewModel 是 Model 和 View 的通信层,它不关心 View 如何处理数据如何渲染(但是他会帮 View 整理好需要的数据格式),也不关心 Model 变更,它只管告知双方彼此发生了变更(所谓的双向绑定)

传统的 mvc 模式是 Model-View-Controller 的缩写,Controller 是在 View 和 Model 中间并且只能单向连接 View 和 Model。Controller 需要去修改 Model 的状态,同时还要告知 View 状态变了,你要如何渲染。这就导致 Controller 过于臃肿和庞大

一句话总结就是 mvvm 使得 view 和 model 之间是双向通信的,而 mvc 是单向通信的

10. 什么是虚拟 dom,优势是什么

虚拟 dom 就是用 js 模拟一颗 dom 树的结构。

虚拟 dom 的优势:

  • 操作 js 比操作实际的 dom 性能好得多,避免大量的重绘回流
  • 在没有 dom 结构的地方(比如 ssr)也能用 js 模拟 dom 的结构然后解析成 html 的结构,直接返回给前端
  • 利用 diff 算法(先序遍历深度优先)还能比对新旧 dom 树(js 树)的不同,定向的修改 dom 节点

在 vue 3.x 中还多了一些静态虚拟 dom,事件缓存,dom 提升等性能相关的优化

11. 什么是 diff 算法

diff 是一个先序遍历,深度优先的算法,他主要是对比两个虚拟 dom(obj 树)的不同,然后只修改不同的树枝

12. history 和 hash 模式的异同

hash 模式 url 后带有 #号,通过哈希值的变更来确定路由的变更;history 模式更像原生的 url

history 模式依赖 h5 的一些新特性,pushState 和 replaceState 等,在不刷新当前页面的基础上修改路由

13. vue 和 react 的区别

  • vue 原生支持 v-model 的双向绑定,修改数据和变更视图都更容易,react 有完善的 state 状态管理,使用 jsx,可以完全用 js 控制整个页面,自由度更高
  • vue 相对来说更简单上手,对新手和小团队快速搭建更友好,react 自由度更高,从脚手架到开发到构建都提供了充分的可配自由度
  • diff 和性能的选择有些差异,比如 react 把虚拟 dom 细化成一个个链表,在浏览器空闲的时候一个个慢慢 diff,而 vue 则是利用一个 Watcher 做整体的发布订阅管控,包括 3.x 中一些静态 dom 提升等优化

14. beforeCreated,created,mounted 之间的区别

  • beforeCreated 时 vue 实例还没初始化,this 是不可用的,这里只能做一些和 vue 无关的东西
  • created 的时候 vue 实例已经初始化完毕了,但是 dom 还没挂载,这里可以操作 this 但是不能操作 dom
  • mounted 的时候 dom 已经挂载了,可以用 ref 操作 dom

15. vue 的生命周期有哪些

  • beforeCreate
  • created
  • beforeMount
  • mounted
  • beforeUpdate
  • updated
  • beforeActivate
  • activated
  • beforeDestroy
  • destroyed

16. extend 的作用

extend 生成一个构造器,能实例化一个 vue。一般用来做组件(比如 toast)每次实例化一个 vue 挂载到 dom 上

17. provide/inject 的作用

provide/inject 是为了提供一个全局的,父子组件通信的能力,这一般是常量且不是响应式的

// 父级组件提供 'foo'
var Provider = {
  provide: {foo: 'bar'},
  // ...
}

// 子组件注入 'foo'
var Child = {inject: ['foo'],
  created () {console.log(this.foo) // => "bar"
  }
  // ...
}
  • provide 和 inject 绑定并不是可响应的。这是刻意为之的。然而,如果你传入了一个可监听的对象,那么其对象的 property 还是可响应的。
  • 但是非常不建议用 provide/inject 去做响应式的监听变更,因为这样数据流就不是单向流动的,你不知道是哪个组件修改了这个全局状态。

18. mixin 和 mixins 的区别

mixin 是全局的混入,每个组件都会受到影响。mixins 是单一组件混入。

当组件状态和混入状态冲突的时候,优先使用组件的状态

19. watch 的对象写法

watch 有对象写法,能深度监听,immediate 立即执行等

watch: {
  initData: {handler(newV) {this.initSpuData(newV);
    },
    immediate: true,
    deep: true
  }
}

20. v-show 和 v-if 的区别,分别适用于什么场景

v-if是 vue 不渲染这个 dom 节点,在虚拟 dom 树中就不存在这个 dom 节点。

v-show 是 display: none,dom 中是有这个节点的,只是这个节点不渲染

如果一个元素频繁切换状态,就用 v-show,因为不需要 vue 做 diff 去判断节点是否要挂载到 dom 树上

如果一个元素很大但不会频繁切换状态,那就用 v-if,因为一开始不渲染还能减少 diff 的时间,而渲染过一次之后也不怎么变动

21. 组件中的 data 为什么是函数,什么时候可以用对象?

如果一个组件是复用的,那如果 data 写成对象,那所有组件将共用这个对象的状态,毕竟是引用类型。

所以写成函数,每次返回一个新的对象,就算组件复用,返回的对象也是独享的

如果是 new Vue() 那就可以用对象了,因为 new 的都是一个 Vue 的实例,不存在共用 data 一说

22. vue 的响应式原理是什么,手写一个

Vue 响应式原理请参考这篇文章

// vue 2.x
const observe = obj => {if (!obj || typeof obj !== 'object') return obj
  Reflect.ownKeys(obj).forEach(key => {reactiveFn(obj, key, obj[key])
  })
}

const reactiveFn = (obj, key, val) => {observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get() {console.log('get', val)
      return val
    },
    set(newV) {console.log('set', newV)
      val = newV
    }
  })
}

let userInfo = {
  name: 'Evan',
  age: 24
}

observe(userInfo)

const name = userInfo.name
userInfo.age = 25
// vue 3.x
const proxyGenerator = obj => {if (!obj || typeof obj !== 'object') return obj
  const handler = {get(obj, key) {console.log('get', obj[key])
      return proxyGenerator(obj[key])
    },
    set(obj, key, newV) {console.log('set', newV)
      obj[key] = newV
    }
  }
  return new Proxy(obj, handler)
}

let userInfo = {
  name: 'Evan',
  age: 24
}

let userInfoProxy = proxyGenerator(userInfo)

const name = userInfoProxy.name
userInfoProxy.age = 25

23. nexttick 原理

看一段代码:

export default {data () {
    return {msg: 0}
  },
  mounted () {
    this.msg = 1
    this.msg = 2
    this.msg = 3
  },
  watch: {msg () {console.log(this.msg)
    }
  }
}

对于上面的代码,watch 只会执行一次。这是因为 vue 处理响应式是批量操作而不是每一次都操作的。

vue 双向绑定的 watcher 源码中有这么一句 queueWatcher(this),而这玩意内又有这么一句 nextTick(flushSchedulerQueue)。这个 flushSchedulerQueue 其实就是更新视图的函数。nextTick 的源码参考这里

抽象概括一下 nextTick 的原理:

  • nextTick 接收一个回调,返回一个闭包
  • 回调的执行时机 nextTick 闭包中的 timerFunc 控制

timerFunc 用以下优先级来处理出队时机。setImmediate -> MessageChannel -> Promise -> setTimeout。优先使用宏任务,如果不支持就使用 Promise 的微任务,如果还不支持就用 setTimeout 兼容。虽然 setTimeout 是宏任务,但是在 http 声明中 setTimeout 有 4ms 的延时,所以只拿来做兼容处理,宏任务交给 setImmediate 和 MessageChannel

也就是说用于更新视图的 nextTick(flushSchedulerQueue) 其实以 setImmediate -> MessageChannel -> Promise -> setTimeout 优先级塞入了 Event loop 中

这样更新的好处是:

假设有个值 test 被 while 循环执行 1000 次 ++ 操作。每次循环时,都会根据响应式触发 compile -> setter -> Dep -> Watcher -> update -> run 如果没有异步更新,而是每次都更新 DOM,那会十分消耗性能。所以 Vue 实现了一个 queue 队列,先把所有的 watcher 塞到队列里。在下一个 Tick(或者是当前 Tick 的微任务阶段)的时候会统一执行 queue 中 Watcher 的 run

整个过程如下:

  • 第一阶段:queueWatcher(this) 收集 watcher 队列

    • 响应式触发 update,把 watcher 塞到 queue 队列,并且根据 watcher.id 去重
  • 第二阶段:nextTick(flushSchedulerQueue) 更新视图

    • nextTick 会返回一个闭包,通过 timerFunc 执行 flushSchedulerQueue 回调
    • flushSchedulerQueue 执行 watcher 的 run,更新视图

回到第一段代码中,虽然修改了 msg 三次,但是因为 vue 更新视图的机制,首先把三次 this.msg 的变动塞到队列里,根据 id 去重只要了最后一次变更。之后异步执行视图更新,保证在下一个 Tick(或者在当前 Tick 的微任务中)更新视图,也就直接把 msg 从 0 -> 3 而不是 0 -> 1 -> 2 -> 3

24. Vue 插件的原理是什么

Vue 插件的的使用方法是 Vue.use(xx)

所有被 Vue.use 的对象,都必须包含一个 install 的方法,Vue.use 的本质就是执行这个方法罢了

  • 如果 use 的是一个 function 而不是一个对象,那这个方法会被直接当成 install 方法来执行。
  • Vue use 必须在 vue 实例化之前执行

« 常见 JS 面试题

Vue 响应式原理 »

Evan的博客

Evan 的博客 - 非典型码农,bug永动机
Instagram Weibo GitHub Email RSS

Categories

面试 原理笔记 项目实践 其他 JS Vue 性能优化 算法 计算机网络

Recent Posts

  • 从 HTTP 发展历程重学计算机网络
  • 应届前端的逆袭(中)
  • 应届前端的逆袭(上)
  • 应届前端的逆袭(下)
  • 前端面经复盘

Copyright © 2015 Powered by MWeb,  Theme used GitHub CSS.