# 常见面试题


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

答案解析

源码位置:src\compiler\codegen\index.js

function genElement() {
  ...
	else if (el.for && !el.forProcessed) {
		return genFor(el, state)
	} else if (el.if && !el.ifProcessed) {
		return genIf(el, state)
	}
  ...
}
1
2
3
4
5
6
7
8
9
  • v-for 优先于 v-if 被解析;
  • 如果同时出现,每次渲染都会先执行循环再判断条件,无论如何循环都不可避免,浪费了性能;
  • 要避免出现这种情况,则在外层嵌套 template,在这一层进行 v-if 判断,然后在内部进行 v-for 循环;
  • 如果条件出现在循环内部,可通过计算属性提前过滤掉那些不需要显示的项。

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

答案解析

源码位置:src\core\instance\state.js - initData()

function initData() {
  ...
	let data = vm.$options.data;
    data = vm._data = typeof data === "function" ? getData(data, vm) : data || {};
  ...
}
1
2
3
4
5
6
  • Vue 组件可能存在多个实例,如果使用对象形式定义 data,则会导致它们共用一个 data 对象,那么状态变更将会影响所有组件实例,这是不合理的;采用函数形式定义,在 initData 时会将其作为工厂函数返回全新 data 对象,有效规避多实例之间状态污染问题。而在 Vue 根实例创建过程中则不存在该限制,也是因为根实例只能有一个,不需要担心这种情况。

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

答案解析

源码位置:src\core\vdom\patch.js - updateChildren()

function updateChildren() {
  ...
  else if (sameVnode(oldStartVnode, newStartVnode))
  ...
}
function sameVnode(a, b) {
  return (
    a.key === b.key &&
    ((a.tag === b.tag &&
      a.isComment === b.isComment &&
      isDef(a.data) === isDef(b.data) &&
      sameInputType(a, b)) ||
      (isTrue(a.isAsyncPlaceholder) &&
        a.asyncFactory === b.asyncFactory &&
        isUndef(b.asyncFactory.error)))
  )
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
  • key 的作用主要是为了高效的更新虚拟 DOM,其原理是 Vue 在 patch 过程中通过 key 可以精准判断两个节点是否是同一个,从而避免频繁更新不同元素,使得整个 patch 过程更加高效,减少 DOM 操作量,提高性能;
  • 若不设置 key 还可能在列表更新时引发一些隐蔽的 bug;
  • Vue 中在使用相同标签名元素的过渡切换时,也会使用到 key 属性,其目的也是为了让 Vue 可以区分它们,否则 Vue 只会替换其内部属性而不会触发过渡效果。

# 4、你怎么理解 Vue 中的 diff 算法?

答案解析

源码位置:

必要性:src\core\instance\lifecycle.js - mountComponent()

执行方式:src\core\vdom\patch.js - patchVnode()

高效性:src\core\vdom\patch.js - updateChildren()

function patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly) {
	const oldCh = oldVnode.children
	const ch = vnode.children
	// 属性更新
	if (isDef(data) && isPatchable(vnode)) {
		for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
		if (isDef((i = data.hook)) && isDef((i = i.update))) i(oldVnode, vnode)
	}
	// 新节点有孩子
	if (isUndef(vnode.text)) {
		// 新旧节点都有孩子
		if (isDef(oldCh) && isDef(ch)) {
			if (oldCh !== ch)
				// patch算法,打补丁
				updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
		} else if (isDef(ch)) {
			// 新节点有孩子,老节点没有孩子,先清空老节点的文本内容,然后新增子节点
			if (process.env.NODE_ENV !== 'production') {
				checkDuplicateKeys(ch)
			}
			if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
			addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
		} else if (isDef(oldCh)) {
			// 老节点有孩子,新节点没有孩子,移除老节点所有子节点
			removeVnodes(oldCh, 0, oldCh.length - 1)
		} else if (isDef(oldVnode.text)) {
			nodeOps.setTextContent(elm, '')
		}
	} else if (oldVnode.text !== vnode.text) {
		// 新旧节点都没有孩子,直接文本内容替换
		nodeOps.setTextContent(elm, vnode.text)
	}
}

function updateChildren(...) {
  let oldStartIdx = 0;
  let newStartIdx = 0;
  let oldEndIdx = oldCh.length - 1;
  let oldStartVnode = oldCh[0];
  let oldEndVnode = oldCh[oldEndIdx];
  let newEndIdx = newCh.length - 1;
  let newStartVnode = newCh[0];
  let newEndVnode = newCh[newEndIdx];
  let oldKeyToIdx, idxInOld, vnodeToMove, refElm;
  // 游标,老的开始小于老的结束,新的开始小于新的结束
  while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
    if (isUndef(oldStartVnode)) {
      oldStartVnode = oldCh[++oldStartIdx];
    } else if (isUndef(oldEndVnode)) {
      oldEndVnode = oldCh[--oldEndIdx];
    } else if (sameVnode(oldStartVnode, newStartVnode)) {
      // 老的开始节点和新的开始节点相同,直接patchVnode
      patchVnode(...);
      oldStartVnode = oldCh[++oldStartIdx];
      newStartVnode = newCh[++newStartIdx];
    } else if (sameVnode(oldEndVnode, newEndVnode)) {
      // 老的结束节点和新的结束节点相同,直接patchVnode
      patchVnode(...);
      oldEndVnode = oldCh[--oldEndIdx];
      newEndVnode = newCh[--newEndIdx];
    } else if (sameVnode(oldStartVnode, newEndVnode)) {
      // 老的开始节点和新的结束节点相同,进行patchVnode的同时,需要将老的开始节点移动到老的结束节点之后
      patchVnode(...);
      canMove && nodeOps.insertBefore(parentElm,oldStartVnode.elm,nodeOps.nextSibling(oldEndVnode.elm));
      oldStartVnode = oldCh[++oldStartIdx];
      newEndVnode = newCh[--newEndIdx];
    } else if (sameVnode(oldEndVnode, newStartVnode)) {
      // 老的结束节点和新的开始节点相同,进行patchVnode的同时,需要将老的结束节点移动到老的开始节点之前
      patchVnode(...);
      canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm);
      oldEndVnode = oldCh[--oldEndIdx];
      newStartVnode = newCh[++newStartIdx];
    } else {
      if (isUndef(oldKeyToIdx))
        oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx); // 拿出老节点所有孩子
      idxInOld = isDef(newStartVnode.key) // 新节点第一个孩子
        ? oldKeyToIdx[newStartVnode.key]
        : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx);
      if (isUndef(idxInOld)) {
        // 在老节点中找和新的开始节点相同的节点,没找到,创建一个新的节点
        createElm(...);
      } else {
        // 在老节点中找和新的开始节点相同的节点,找到了,进行patchVnode的同时,
        // 需要将找到的老节点移动到oldStartIdx的前面
        vnodeToMove = oldCh[idxInOld];
        if (sameVnode(vnodeToMove, newStartVnode)) {
          patchVnode(...);
          oldCh[idxInOld] = undefined;
          canMove && nodeOps.insertBefore(parentElm,vnodeToMove.elm,oldStartVnode.elm);
        } else {
          // same key but different element. treat as new element
          createElm(...);
        }
      }
      newStartVnode = newCh[++newStartIdx];
    }
  }
  // 老节点先循环结束,说明新节点多,则把剩下的节点插入到dom中,执行addVnodes
  if (oldStartIdx > oldEndIdx) {
    refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm;
    addVnodes(...);
  } else if (newStartIdx > newEndIdx) {
    // 新节点先循环结束,说明老节点多,则把剩下的节点从dom中删除,执行removeVnodes
    removeVnodes(oldCh, oldStartIdx, oldEndIdx);
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
  • 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 组件化的理解?

答案解析
  • 组件是独立和可复用的代码组织单元。组件系统是 Vue 核心特性之一,它使开发者使用小型、独立和通常可复用的组件构建大型应用;
  • 组件化开发能大幅提高应用开发效率、测试性、复用性等;
  • 组件使用按分类有:页面组件、业务组件、通用组件;
  • Vue 的组件是基于配置的,我们通常编写的组件是组件配置而非组件,框架后续会生成其构造函数,它们基于 VueComponent,扩展于 Vue;
  • Vue 中常见组件化技术有:属性 prop,自定义事件,插槽等,它们主要用于组件通信、扩展等;
  • 合理的划分组件,有助于提升应用性能;
  • 组件应该是高内聚、低耦合的;
  • 遵循单向数据流的原则。

# 6、你了解哪些 Vue 性能优化方法?

答案解析
  • 路由懒加载
const router = new VueRouter({
	routes: [{ path: '/foo', component: () => import('./Foo.Vue') }],
})
1
2
3
  • keep-alive 缓存页面
<template>
	<div id="app">
		<keep-alive>
			<router-view />
		</keep-alive>
	</div>
</template>
1
2
3
4
5
6
7
  • 使用 v-show 复用 DOM
<template>
	<div class="cell">
		<!--这种情况用v-show复用DOM,比v-if效果好-->
		<div v-show="value" class="on">
			<Heavy :n="10000" />
		</div>
		<section v-show="!value" class="off">
			<Heavy :n="10000" />
		</section>
	</div>
</template>
1
2
3
4
5
6
7
8
9
10
11
  • 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() {
				return this.users.filter(user => user.isActive)
			},
		},
	}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
  • 长列表性能优化

    • 如果列表是纯粹的数据展示,不会有任何改变,就不需要做响应化
    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
    • 如果是大数据长列表,可采用虚拟滚动,只渲染少部分区域的内容
    <template>
    	<recycle-scroller class="items" :items="items" :item-size="24">
    		<template v-slot="{ item }">
    			<FetchItemView :item="item" @vote="voteItem(item)" />
    		</template>
    	</recycle-scroller>
    </template>
    
    1
    2
    3
    4
    5
    6
    7

    参考: Vue-virtual-scroller Vue-virtual-scroll-list

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

export default {
	created() {
		this.timer = setInterval(this.refresh, 2000)
	},
	beforeDestroy() {
		clearInterval(this.timer)
	},
}
// 也可以用hook,将组件销毁钩子函数和执行逻辑放一起
export default {
	created() {
		this.timer = setInterval(this.refresh, 2000)
		this.$once('hook:beforeDestory', () => {
			clearInterval(this.timer)
		})
	},
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
  • 图片懒加载(对于图片过多的页面,为了加速页面加载速度,所以很多时候我们需要将页面内未出现在可视区域内的图片先不做加载, 等到滚动到可视区域后再去加载)
<template>
	<img v-lazy="/static/img/1.png" />
</template>
1
2
3

参考: Vue-lazyload

  • 第三方插件按需引入(像 element-ui 这样的第三方组件库可以按需引入避免体积太大)
import Vue from 'Vue'
import { Button, Select } from 'element-ui'
Vue.use(Button)
Vue.use(Select)
1
2
3
4
  • 无状态的组件标记为函数式组件
<template functional>
	<div class="cell">
		<div v-if="props.value" class="on"></div>
		<section v-else class="off"></section>
	</div>
</template>
<script>
	export default {
		props: ['value'],
	}
</script>
1
2
3
4
5
6
7
8
9
10
11
  • 子组件分割
<template>
	<div>
		<ChildComp />
	</div>
</template>
<script>
	export default {
		components: {
			ChildComp: {
				methods: {
					heavy() {
						/* 耗时任务 */
					},
				},
				render(h) {
					return h('div', this.heavy())
				},
			},
		},
	}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
  • 变量本地化
<template>
	<div :style="{ opacity: start / 300 }">
		{{ result }}
	</div>
</template>
<script>
	import { heavy } from '@/utils'
	export default {
		props: ['start'],
		computed: {
			base() {
				return 42
			},
			result() {
				const base = this.base // 不要频繁引用this.base
				let result = this.start
				for (let i = 0; i < 1000; i++) {
					result += heavy(base)
				}
				return result
			},
		},
	}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
  • SSR

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

答案解析

根据尤大的 PPT 总结,Vue3.0 改进主要在以下几点:

  • 更快

    • 虚拟 DOM 重写
    • 优化 slots 的生成
    • 静态树提升
    • 静态属性提升
    • 基于 Proxy 的响应式系统
  • 更小:通过摇树优化核心库体积

  • 更容易维护:TypeScript + 模块化

  • 更加友好

    • 跨平台:编译器核心和运行时核心与平台无关,使得 Vue 更容易与任何平台(Web、Android、iOS)一起使用
  • 更容易使用

    • 改进的 TypeScript 支持,编辑器能提供强有力的类型检查和错误及警告
    • 更好的调试支持
    • 独立的响应化模块
    • Composition API

# 8、Vue 中组件之间的通信方式?

答案解析

组件可以有以下几种关系:

常见使用场景可以分为三类:

1、父子组件通信

2、兄弟组件通信

3、跨层组件通信

Vue 组件中通信有如下几种方式:

1、props

2、$emit/$on

3、Vuex

4、$parent/$children

5、$attrs/$listeners

6、provide/inject

1、props

  • 父组件 => 子组件传值

父组件 A 通过 props 向子组件 B 传递值; 子组件 B 通过 $emit 传递父组件 A,父组件 A 通过 v-on/@ 触发

// 父组件
<template>
	<div id="app">
		<Child :child="users"></Child>
	</div>
</template>
<script>
	import Child from './components/Child' // 子组件
	export default {
		name: 'App',
		data() {
			return { users: ['小明', '小红', '小王'] }
		},
		components: {
			Child: Child,
		},
	}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 子组件
<template>
	<ul>
		<li v-for="item in child">{{ item }}</li>
	</ul>
</template>
<script>
	export default {
		name: 'ChildComp',
		props: {
			child: {
				type: Array,
				required: true,
			},
		},
	}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

总结:父组件通过 props 向下传递数据给子组件。

  • 子组件 => 父组件传值
// 子组件 Header.vue
<template>
	<div>
		<h1 @click="changeTitle">{{ title }}</h1>
	</div>
</template>
<script>
	export default {
		name: 'Header',
		data() {
			return {
				title: 'Vue.js',
			}
		},
		methods: {
			changeTitle() {
				this.$emit('titleChanged', '子向父组件传值')
			},
		},
	}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 父组件
<template>
	<div id="app">
		<Header @titleChanged="updateTitle"></Header>
		//与子组件titleChanged自定义事件保持一致,updateTitle($event)接受传递过来的文字
		<h2>{{ title }}</h2>
	</div>
</template>
<script>
	import Header from './components/Header'
	export default {
		name: 'App',
		data() {
			return {
				title: '传递的是一个值',
			}
		},
		methods: {
			updateTitle(tit) {
				this.title = tit
			},
		},
		components: {
			Header: Header,
		},
	}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

总结:子组件通过 events 给父组件发送消息,实际上就是子组件把自己的数据发送到父组件。

2、$emit/$on => $bus

Vue 实例作为事件总线(事件中心)用来触发事件和监听事件,可以通过此种方式进行组件间通信包括:父子组件、兄弟组件、跨级组件

// 创建bus.js
import Vue from 'vue'
export defult new Vue()
1
2
3
// A组件
<template>
	<div>
		<h3>A组件</h3>
		<button @click="sendMsg">将数据发送给C组件</button>
	</div>
</template>
<script>
	import bus from './bus'
	export default {
		methods: {
			sendMsg() {
				bus.$emit('sendTitle', '传递的值')
			},
		},
	}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// C组件
<template>
	<div>接收A组件传递过来的值:{{ msg }}</div>
</template>
<script>
	import bus from './bus'
	export default {
		data() {
			return {
				msg: '',
			}
		},
		mounted() {
			bus.$on('sendTitle', val => {
				this.msg = val
			})
		},
	}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

3、Vuex

  • Vuex 介绍

Vuex 实现了一个单向数据流,在全局拥有一个 State 存放数据,当组件要更改 State 中的数据时,必须通过 Mutation 提交修改信息, Mutation 同时提供了订阅者模式供外部插件调用获取 State 数据的更新。

而当所有异步操作(常见于调用后端接口异步获取更新数据)或批量的同步操作需要走 Action ,但 Action 也是无法直接修改 State 的,还是需要通过 Mutation 来修改 State 的数据。最后,根据 State 的变化,渲染到视图上。

  • 2、Vuex 中核心概念

    • state: Vuex 的唯一数据源,如果获取多个 state,可以使用 ...mapState 。
    export const store = new Vuex.Store({
    	state: {
    		productList: [
    			{
    				name: 'goods 1',
    				price: 80,
    			},
    			{
    				name: 'goods 2',
    				price: 88,
    			},
    		],
    	},
    })
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    import { mapState } from 'vuex'
    export default {
    	computed: {
    		// mapState 辅助函数
    		...mapState(['productList']),
    	},
    }
    
    1
    2
    3
    4
    5
    6
    7
    • getter:可以将 getter 理解为计算属性, getter 的返回值根据他的依赖缓存起来,依赖发生变化才会被重新计算。
    export const store = new Vuex.Store({
    	getters: {
    		getSaledPrice: state => {
    			let saleProduct = state.productList.map(item => {
    				return {
    					name: '**' + item.name + '**',
    					price: item.price / 2,
    				}
    			})
    			return saleProduct
    		},
    	},
    })
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    import { mapGetters } from 'vuex'
    export default {
    	computed: {
    		// mapGetters 辅助函数
    		...mapGetters(['getSaledPrice']),
    	},
    }
    
    1
    2
    3
    4
    5
    6
    7
    • mutation:更改 Vuex 的 store 中的状态的唯一方法是提交 mutation。 每个 mutation 都有一个字符串的事件类型(type)和一个回调函(handler)。这个回调函数就是我们实际进行状态更改的地方,并且它会接受 state 作为第一个参数,payload 为第二个参数, mutation 必须是同步函数。
    export const store = new Vuex.Store({
    	mutations: {
    		reducePrice: (state, payload) => {
    			return state.productList.forEach(product => {
    				product.price -= payload
    			})
    		},
    	},
    })
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    import { mapMutations } from 'vuex'
    export default {
    	methods: {
    		// mapMutations 辅助函数
    		...mapMutations(['reducePrice']),
    	},
    }
    
    1
    2
    3
    4
    5
    6
    7
    • action: action 类似 mutation 都是修改状态,不同之处,

    action 提交的 mutation 不是直接修改状态

    action 可以包含异步操作,而 mutation 不行

    action 中的回调函数第一个参数是 context ,是一个与 store 实例具有相同属性的方法的对象

    action 通过 store.dispatch 触发, mutation 通过 store.commit 提交

    export const store = new Vuex.Store({
    	actions: {
    		// 提交的是mutation,可以包含异步操作
    		reducePriceAsync: ({ commit }, payload) => {
    			setTimeout(() => {
    				// reducePrice为上一步mutation中的属性
    				commit('reducePrice', payload)
    			}, 2000)
    		},
    	},
    })
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    import { mapActions } from 'vuex'
    export default {
    	methods: {
    		// mapMutations 辅助函数
    		...mapActions(['reducePriceAsync']),
    	},
    }
    
    1
    2
    3
    4
    5
    6
    7