# Vue面试题

# 1. v-if和v-for哪个优先级更高?如果两个同时出现,应该怎么优化得到更好的性能?

v-if和v-for的解析在源码src/compiler/codegen/index.js

export function genElement (el: ASTElement, state: CodegenState): string {
  if (el.parent) {
    el.pre = el.pre || el.parent.pre
  }
  if (el.staticRoot && !el.staticProcessed) {
    return genStatic(el, state)
  } else if (el.once && !el.onceProcessed) {
    return genOnce(el, state)
  } else if (el.for && !el.forProcessed) {
    return genFor(el, state)
  } else if (el.if && !el.ifProcessed) {
    return genIf(el, state)
  } else if (el.tag === 'template' && !el.slotTarget && !state.pre) {
    return genChildren(el, state) || 'void 0'
  } else if (el.tag === 'slot') {
    return genSlot(el, state)
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
  • 显然v-for优先于v-if被解析。
  • 如果同时出现,每次渲染都会先执行循环再判断条件,无论如何循环都不可避免,浪费了性能。
  • 要避免出现这种情况,则在外层嵌套template,在这一层进行v-if判断,然后在内部进行v-for循环。
  • 如果条件出现在循环内部,可通过计算属性提前过滤掉那些不需要显示的项。

# 2. Vue组件data为什么必须是个函数而Vue的根实例则没有此限制?

Vue组件可能存在多个实例,如果使用对象形式定义data,则会导致它们共用一个data对象,那么状态变更将会影响所有组件实例,这是不合理的;采用函数形式定义,在initData时会将其作为工厂函数返回全新data对象,有效规避多实例之间状态污染问题。而在Vue根实例创建过程中则不存在该限制,也是因为根实例只能有一个,不需要担心这种情况。

# 3. 你知道vue中key的作用和工作原理吗?说说你对它的理解。

  • key的作用主要是为了高效的更新虚拟DOM,其原理是vue在patch过程中通过key可以精准判断两个节点是否是同一个,从而避免频繁更新不同元素,使得整个patch过程更加高效,减少DOM操作量,提高性能。
  • 另外,若不设置key还可能在列表更新时引发一些隐蔽的bug
  • vue中在使用相同标签名元素的过渡切换时,也会使用到key属性,其目的也是为了让vue可以区分它们,否则vue只会替换其内部属性而不会触发过渡效果。

# 4. 你怎么理解vue中的diff算法?

  • diff算法是虚拟DOM技术的必然产物:通过新旧虚拟DOM作对比(即diff),将变化的地方更新在真实DOM上;另外,也需要diff高效的执行对比过程,从而降低时间复杂度为O(n)。
  • vue 2.x中为了降低Watcher粒度,每个组件只有一个Watcher与之对应,只有引入diff才能精确找到发生变化的地方。
  • vue中diff执行的时刻是组件实例执行其更新函数时,它会比对上一次渲染结果oldVnode和新的渲染结果newVnode,此过程称为patch。
  • diff过程整体遵循深度优先、同层比较的策略;两个节点之间比较会根据它们是否拥有子节点或者文本节点做不同操作;比较两组子节点是算法的重点,首先判断头尾节点是否相同做4次比对尝试,如果没有找到相同节点才按照通用方式遍历查找,查找结束再按情况处理剩下的节点;借助key通常可以非常精确找到相同节点,因此整个patch过程非常高效。

# 5. 你了解哪些Vue性能优化方法?

  • 路由懒加载
const router = new VueRouter({
	routes: [{
		path: '/foo', component: () =>import ('./Foo.vue')
	}]
})
1
2
3
4
5
  • keep-alive缓存页面
  • v-for 遍历避免同时使用 v-if,应使用计算属性
<template>
	<ul>
		<li
		v-for="user in activeUsers"
		:key="user.id">
		{{ user.name }}
		</li>
	</ul>
</template>
<script>
export default {
	computed: {
		activeUsers: function () {
			return this.users.filter(function (user) {
				return user.isActive
			})
		}
	}
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
  • 长列表性能优化

如果列表是纯粹的数据展示,不会有任何改变,就不需要做响应化。

export default {
	data: () => ({
		users: []
	}),
	async created() {
		const users = await axios.get("/api/users");
		this.users = Object.freeze(users);
	}
};
1
2
3
4
5
6
7
8
9

如果是大数据长列表,可采用虚拟滚动,只渲染少部分区域的内容

<recycle-scroller
	class="items"
	:items="items"
	:item-size="24"
>
	<template v-slot="{ item }">
		<FetchItemView
		:item="item"
		@vote="voteItem(item)"
		/>
	</template>
</recycle-scroller>
1
2
3
4
5
6
7
8
9
10
11
12

参考vue-virtual-scrollervue-virtual-scroll-list

  • 事件的销毁

Vue 组件销毁时,会自动解绑它的全部指令及事件监听器,但是仅限于组件本身的事件。

created() {
	this.timer = setInterval(this.refresh, 2000)
},
beforeDestroy() {
	clearInterval(this.timer)
}
1
2
3
4
5
6
  • 图片懒加载

对于图片过多的页面,为了加速页面加载速度,所以很多时候我们需要将页面内未出现在可视区域 内的图片先不做加载,等到滚动到可视区域后再去加载。参考项目:vue-lazyload

<img v-lazy="/static/img/1.png">
1
  • SSR

# 6. 你对Vue3.0的新特性有没有了解?

Vue3.0改进主要在以下几点:

  • 更快。虚拟DOM重写、优化slots的生成、静态树提升、静态属性提升、基于Proxy的响应式系统
  • 更小。通过摇树优化核心库体积
  • 更容易维护。TypeScript + 模块化
  • 更加友好。跨平台:编译器核心和运行时核心与平台无关,使得Vue更容易与任何平台(Web、 Android、iOS)一起使用。
  • 更容易使用。改进的TypeScript支持,编辑器能提供强有力的类型检查和错误及警告。更好的调试支 持。独立的响应化模块。Composition API。

# 7. vue中组件之间的通信方式?

vue中常用的通信方式有4种,分别是:

  1. props ★★ (父传子)
  2. $emit/$on ★★ 事件总线 (跨层级通信)
  3. vuex ★★★(状态管理) 优点:一次存储数据,所有页面都可访问
  4. provide/inject ★★★ (高阶用法 = 推荐使用 ) 优点:使用简单 缺点:不是响应式

# 8. 什么是递归组件?

通过props从父组件拿到数据,递归组件每次进行递归的时候都会tree-menus组件传递下一级treeList数据,整个过程结束之后,递归也就完成了,对于折叠树状菜单来说,我们一般只会去渲染一级的数据,当点击一级菜单时,再去渲染一级菜单下的结构,如此往复。那么v-if就可以实现我们的这个需求,当v-if设置为false时,递归组件将不会再进行渲染,设置为true时,继续渲染。

# 9. 你知道vue双向数据绑定的原理吗?

Vue的双向数据绑定基于数据劫持+发布订阅模式。使用getter/setter代理值的读取和赋值,使得我们可以控制数据的流向。使用观察者模式设计,实现了指令和数据的依赖关系以及触发更新。

# 数据劫持

vue2基于Object.defineProperty实现数据劫持,vue3基于proxy实现数据劫持。

# 发布订阅模式

Dep对象:Dependency依赖的简写,包含有三个主要属性id, subs, target和四个主要函数addSub,removeSub, depend, notify,是观察者的依赖集合,负责在数据发生改变时,使用notify()触发保存在subs下的订阅列表,依次更新数据和DOM。

Observer对象:即观察者,包含两个主要属性value, dep。做法是使用getter/setter方法覆盖默认的取值和赋值操作,将对象封装为响应式对象,每一次调用时更新依赖列表,更新值时触发订阅者。绑定在对象的_ ob _原型链属性上。

# 10. vue如果想要扩展某个组件现有组件时怎么做?

  • 使用mixin全局混入
  • 使用slot扩展

# 11. watch和computed的区别以及怎么选用?

  • watch需要在数据变化时执行异步或开销较大的操作时使用,简单讲,当一条数据影响多条数据的时候,例如搜索数据。
  • 对于任何复杂逻辑或一个数据属性在它所依赖的属性发生变化时,也要发生变化,简单讲。当一个属性受多个属性影响的时候,例如购物车商品结算时。

# 12. 你知道nextTick的原理吗?

  • vue用异步队列的方式来控制DOM更新和nextTick回调先后执行。
  • microtask因为其高优先级特性,能确保队列中的微任务在下一次事件循环前被执行完毕。
  • 因为兼容性问题,vue不得不做了microtask向macrotask的降级方案。

timerFunc 对异步队列尝试使用原生的 Promise.thenMutationObserversetImmediate ,如果执行环境不支持,则会采用 setTimeout(fn, 0) 代替。

常见的microtask有:PromiseMutationObserverObject.observe(废弃),以及nodejs中的process.nextTick

看到了MutationObserver,vue用MutationObserver是想利用它的microtask特性,而不是想做DOM监听。核心是microtask,用不用MutationObserver都行的。事实上,vue在2.5版本中已经删去了MutationObserver相关的代码,因为它是HTML5新增的特性,在iOS上尚有bug。

那么最优的microtask策略就是Promise了,而令人尴尬的是,Promise是ES6新增的东西,也存在兼容 问题呀。所以vue就不得不降级为macrotask来做队列控制了。

macrotask有哪些可选的方案呢?前面提到了setTimeout是一种,但它不是理想的方案。因为 setTimeout执行的最小时间间隔是约4ms的样子,略微有点延迟。 在vue2.5的源码中,macrotask降级的方案依次是:setImmediateMessageChannelsetTimeoutsetImmediate是最理想的方案了,可惜的是只有IE和nodejs支持。 MessageChannel的onmessage回调也是microtask,但也是个新API,面临兼容性的尴尬。 所以最后的兜底方案就是setTimeout了,尽管它有执行延迟,可能造成多次渲染,算是没有办法的办法 了。

# 13、Vue 的父组件和子组件生命周期钩子执行顺序

image

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

  • 加载渲染过程

父 beforeCreate -> 父 created -> 父 beforeMount -> 子 beforeCreate -> 子 created -> 子 beforeMount -> 子 mounted -> 父 mounted

注意 mounted 不会保证所有的子组件也都一起被挂载。如果你希望等到整个视图都渲染完毕,可以在 mounted 内部使用 vm.$nextTick

  • 子组件更新过程

父 beforeUpdate -> 子 beforeUpdate -> 子 updated -> 父 updated

  • 父组件更新过程

父 beforeUpdate -> 父 updated

  • 销毁过程

父 beforeDestroy -> 子 beforeDestroy -> 子 destroyed -> 父 destroyed

# 14、vue created 和 watch 输入属性哪个先执行

如果watch 加了 immediate: true, 就是watch先执行,否则就是created先执行。immediate如果为true 代表如果在 wacth 里声明了之后,就会在created之前立即先去执行里面的handler方法。

使用immediate: true前,初始化的时候就执行一次,然后监听变化再执行,如下:

created(){
  this.fetchPostList()
},
watch: {
  searchInputValue(){
    this.fetchPostList()
  }
}
1
2
3
4
5
6
7
8

上面的这种写法我们可以完全如下写:

watch: {
  searchInputValue:{
    handler: 'fetchPostList',
    immediate: true
  }
}
1
2
3
4
5
6

# 15、v-show和v-if的区别

v-if,只渲染满足条件的,而v-show,不管满不满足条件,都会渲染,只是会用display:none来隐藏节点。切换频繁用v-show,否则用v-if。

# 16、v-for可以遍历对象吗?

v-for可以遍历对象,如下,遍历的时候,每项有value,key,index。

<template>
  <div id="app">
    <ul>
      <li v-for="(value,key,index) in listObj" :key="key">
        {{index}}---{{key}}---{{value.title}}
      </li>
    </ul>
  </div>
</template>

<script>
export default {
  name: "App",
  data() {
    return {
      listObj: {
        a: { title: "标题1" },
        b: { title: "标题2" }
      }
    };
  }
};
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Last Updated: 11/15/2020, 12:44:37 PM