本篇文章给大家总结分享29+个Vue经典面试题(附源码级详解),带你梳理基础知识,增强Vue知识储备,值得收藏,快来看看吧!
本篇文章给大家总结分享29+个Vue经典面试题(附源码级详解),带你梳理基础知识,增强Vue知识储备,值得收藏,快来看看吧!01-Vue 3.0的设计目标是什么?做了哪些优化?分析还是问新特性,陈述典型新特性,分析其给你带来的变化即可。(学习视频分享:vue视频教程) 思路从以下几方面分门别类阐述:易用性、性能、扩展性、可维护性、开发体验等 范例
可能的追问
02-你了解哪些Vue性能优化方法?分析这是一道综合实践题目,写过一定数量的代码之后小伙伴们自然会开始关注一些优化方法,答得越多肯定实践经验也越丰富,是很好的题目。 答题思路:根据题目描述,这里主要探讨Vue代码层面的优化 回答范例
03-Vue组件为什么只能有一个根元素?这题现在有些落伍, 体验一下vue2直接报错,test-v2.html new Vue({
components: {
comp: {
template: `
<div>root1</div>
<div>root2</div>
`
}
}
}).$mount('#app')
vue3中没有问题,test-v3.html Vue.createApp({
components: {
comp: {
template: `
<div>root1</div>
<div>root2</div>
`
}
}
}).mount('#app')
回答思路
范例
知其所以然
04-这是基本应用能力考察,稍微上点规模的项目都要拆分vuex模块便于维护。体验https://vuex.vuejs.org/zh/guide/modules.html const moduleA = {
state: () => ({ ... }),
mutations: { ... },
actions: { ... },
getters: { ... }
}
const moduleB = {
state: () => ({ ... }),
mutations: { ... },
actions: { ... }
}
const store = createStore({
modules: {
a: moduleA,
b: moduleB
}
})
store.state.a // -> moduleA 的状态
store.state.b // -> moduleB 的状态
store.getters.c // -> moduleA里的getters
store.commit('d') // -> 能同时触发子模块中同名mutation
store.dispatch('e') // -> 能同时触发子模块中同名action思路
范例
可能的追问
05-怎么实现路由懒加载呢?分析这是一道应用题。当打包应用时,JavaScript 包会变得非常大,影响页面加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问时才加载对应组件,这样就会更加高效。 // 将
// import UserDetails from './views/UserDetails'
// 替换为
const UserDetails = () => import('./views/UserDetails')
const router = createRouter({
// ...
routes: [{ path: '/users/:id', component: UserDetails }],
})
思路
回答范例
知其所以然
06-ref和reactive异同这是 体验ref:https://vuejs.org/api/reactivity-core.html#ref const count = ref(0) console.log(count.value) // 0 count.value++ console.log(count.value) // 1 reactive:https://vuejs.org/api/reactivity-core.html#reactive const obj = reactive({ count: 0 })
obj.count++回答思路
回答范例
知其所以然
07-watch和watchEffect异同我们经常性需要侦测响应式数据的变化,vue3中除了watch之外又出现了watchEffect,不少同学会混淆这两个api。 体验
const count = ref(0) watchEffect(() => console.log(count.value)) // -> logs 0 count.value++ // -> logs 1
const state = reactive({ count: 0 })
watch(
() => state.count,
(count, prevCount) => {
/* ... */
}
)思路
范例
知其所以然
export function watchEffect(
effect: WatchEffect,
options?: WatchOptionsBase
): WatchStopHandle {
return doWatch(effect, null, options)
}
export function watch<T = any, Immediate extends Readonly<boolean> = false>(
source: T | WatchSource<T>,
cb: any,
options?: WatchOptions<Immediate>
): WatchStopHandle {
return doWatch(source as any, cb, options)
}很明显 08-SPA、SSR的区别是什么我们现在编写的Vue、React和Angular应用大多数情况下都会在一个页面中,点击链接跳转页面通常是内容切换而非页面跳转,由于良好的用户体验逐渐成为主流的开发模式。但同时也会有首屏加载时间长,SEO不友好的问题,因此有了SSR,这也是为什么面试中会问到两者的区别。 思路分析
回答范例
知其所以然内容生成上的区别: SSR
SPA
部署上的区别
09-vue-loader是什么?它有什么作用?分析这是一道工具类的原理题目,相当有深度,具有不错的人才区分度。 体验使用官方提供的SFC playground可以很好的体验 sfc.vuejs.org 有了 <template>
<div class="example">{{ msg }}</div>
</template>
<script>
export default {
data() {
return {
msg: 'Hello world!',
}
},
}
</script>
<style>
.example {
color: red;
}
</style>思路
回答范例
知其所以然1、 // source.vue被vue-loader处理之后返回的代码 // import the <template> block import render from 'source.vue?vue&type=template' // import the <script> block import script from 'source.vue?vue&type=script' export * from 'source.vue?vue&type=script' // import <style> blocks import 'source.vue?vue&type=style&index=1' script.render = render export default script 2、我们想要script块中的内容被作为js处理(当然如果是 import script from 'source.vue?vue&type=script' 将被展开为: import script from 'babel-loader!vue-loader!source.vue?vue&type=script' 类似的,如果我们对 <style scoped lang="scss">
import 'source.vue?vue&type=style&index=1&scoped&lang=scss' 然后webpack会展开如下: import 'style-loader!css-loader!sass-loader!vue-loader!source.vue?vue&type=style&index=1&scoped&lang=scss' 1)当处理展开请求时, 2)对于
实现上这些附加的loader需要被注入到已经展开的loader链上,最终的请求会像下面这样: // <template lang="pug"> import 'vue-loader/template-loader!pug-loader!source.vue?vue&type=template' // <style scoped lang="scss"> import 'style-loader!vue-loader/style-post-loader!css-loader!sass-loader!vue-loader!source.vue?vue&type=style&index=1&scoped&lang=scss' 10-你写过自定义指令吗?使用场景有哪些?分析这是一道API题,我们可能写的自定义指令少,但是我们用的多呀,多举几个例子就行。 体验定义一个包含类似组件生命周期钩子的对象,钩子函数会接收指令挂钩的dom元素: const focus = {
mounted: (el) => el.focus()
}
export default {
directives: {
// enables v-focus in template
focus
}
}
<input v-focus /><input v-focus /> 思路
回答范例
知其所以然编译后的自定义指令会被withDirective函数装饰,进一步处理生成的vnode,添加到特定属性中。 https://sfc.vuejs.org/#eyJBcHAudnVlIjoiPHNjcmlwdCBzZXR1cD5cbmltcG9ydCB7IHJlZiB9IGZyb20gJ3Z1ZSdcblxuY29uc3QgbXNnID0gcmVmKCdIZWxsbyBXb3JsZCEnKVxuXG5jb25zdCB2Rm9jdXMgPSB7XG4gIG1vdW50ZWQoZWwpIHtcbiAgICAvLyDojrflj5ZpbnB1dO+8jOW5tuiwg+eUqOWFtmZvY3VzKCnmlrnms5VcbiAgICBlbC5mb2N1cygpXG4gIH1cbn1cbjwvc2NyaXB0PlxuXG48dGVtcGxhdGU+XG4gIDxoMT57eyBtc2cgfX08L2gxPlxuICA8aW5wdXQgdi1tb2RlbD1cIm1zZ1wiIHYtZm9jdXM+XG48L3RlbXBsYXRlPiIsImltcG9ydC1tYXAuanNvbiI6IntcbiAgXCJpbXBvcnRzXCI6IHtcbiAgICBcInZ1ZVwiOiBcImh0dHBzOi8vc2ZjLnZ1ZWpzLm9yZy92dWUucnVudGltZS5lc20tYnJvd3Nlci5qc1wiXG4gIH1cbn0ifQ== 11-说下$attrs和$listeners的使用场景分析API考察,但$attrs和$listeners是比较少用的边界知识,而且vue3有变化,$listeners已经移除,还是有细节可说的。 思路
体验一个包含组件透传属性的对象。
<template>
<child-component v-bind="$attrs">
将非属性特性透传给内部的子组件
</child-component>
</template>范例
原理查看透传属性foo和普通属性bar,发现vnode结构完全相同,这说明vue3中将分辨两者工作由框架完成而非用户指定: <template>
<h1>{{ msg }}</h1>
<comp foo="foo" bar="bar" />
</template><template>
<div>
{{$attrs.foo}} {{bar}}
</div>
</template>
<script setup>
defineProps({
bar: String
})
</script>_createVNode(Comp, {
foo: "foo",
bar: "bar"
})https://sfc.vuejs.org/#eyJBcHAudnVlIjoiPHNjcmlwdCBzZXR1cD5cbmltcG9ydCB7IHJlZiB9IGZyb20gJ3Z1ZSdcbmltcG9ydCBDb21wIGZyb20gJy4vQ29tcC52dWUnXG5jb25zdCBtc2cgPSByZWYoJ0hlbGxvIFdvcmxkIScpXG48L3NjcmlwdD5cblxuPHRlbXBsYXRlPlxuICA8aDE+e3sgbXNnIH19PC9oMT5cbiAgPGNvbXAgZm9vPVwiZm9vXCIgYmFyPVwiYmFyXCIgLz5cbjwvdGVtcGxhdGU+IiwiaW1wb3J0LW1hcC5qc29uIjoie1xuICBcImltcG9ydHNcIjoge1xuICAgIFwidnVlXCI6IFwiaHR0cHM6Ly9zZmMudnVlanMub3JnL3Z1ZS5ydW50aW1lLmVzbS1icm93c2VyLmpzXCJcbiAgfVxufSIsIkNvbXAudnVlIjoiPHRlbXBsYXRlPlxuXHQ8ZGl2PlxuICAgIHt7JGF0dHJzLmZvb319IHt7YmFyfX1cbiAgPC9kaXY+XG48L3RlbXBsYXRlPlxuPHNjcmlwdCBzZXR1cD5cbmRlZmluZVByb3BzKHtcbiAgYmFyOiBTdHJpbmdcbn0pXG48L3NjcmlwdD4ifQ== 12-v-once的使用场景有哪些?分析
体验仅渲染元素和组件一次,并且跳过未来更新
<!-- single element -->
<span v-once>This will never change: {{msg}}</span>
<!-- the element have children -->
<div v-once>
<h1>comment</h1>
<p>{{msg}}</p>
</div>
<!-- component -->
<my-component v-once :comment="msg"></my-component>
<!-- `v-for` directive -->
<ul>
<li v-for="i in list" v-once>{{i}}</li>
</ul>思路
回答范例
知其所以然下面例子使用了v-once: <script setup>
import { ref } from 'vue'
const msg = ref('Hello World!')
</script>
<template>
<h1 v-once>{{ msg }}</h1>
<input v-model="msg">
</template>我们发现v-once出现后,编译器会缓存作用元素或组件,从而避免以后更新时重新计算这一部分: // ...
return (_ctx, _cache) => {
return (_openBlock(), _createElementBlock(_Fragment, null, [
// 从缓存获取vnode
_cache[0] || (
_setBlockTracking(-1),
_cache[0] = _createElementVNode("h1", null, [
_createTextVNode(_toDisplayString(msg.value), 1 /* TEXT */)
]),
_setBlockTracking(1),
_cache[0]
),
// ...13-什么是递归组件?举个例子说明下?分析递归组件我们用的比较少,但是在Tree、Menu这类组件中会被用到。 体验组件通过组件名称引用它自己,这种情况就是递归组件。
<template>
<li>
<div> {{ model.name }}</div>
<ul v-show="isOpen" v-if="isFolder">
<!-- 注意这里:组件递归渲染了它自己 -->
<TreeItem
class="item"
v-for="model in model.children"
:model="model">
</TreeItem>
</ul>
</li>
<script>
export default {
name: 'TreeItem',
// ...
}
</script>思路
回答范例
知其所以然递归组件编译结果中,获取组件时会传递一个标识符 const _component_Comp = _resolveComponent("Comp", true)就是在传递 export function resolveComponent(
name: string,
maybeSelfReference?: boolean
): ConcreteComponent | string {
return resolveAsset(COMPONENTS, name, true, maybeSelfReference) || name
}resolveAsset中最终返回的是组件自身: if (!res && maybeSelfReference) {
// fallback to implicit self-reference
return Component
}https://github1s.com/vuejs/core/blob/HEAD/packages/runtime-core/src/helpers/resolveAssets.ts#L22-L23 https://github1s.com/vuejs/core/blob/HEAD/packages/runtime-core/src/helpers/resolveAssets.ts#L110-L111 https://sfc.vuejs.org/#eyJBcHAudnVlIjoiPHNjcmlwdCBzZXR1cD5cbmltcG9ydCB7IHJlZiB9IGZyb20gJ3Z1ZSdcbmltcG9ydCBjb21wIGZyb20gJy4vQ29tcC52dWUnXG5jb25zdCBtc2cgPSByZWYoJ+mAkuW9kue7hOS7ticpXG5jb25zdCBtb2RlbCA9IHtcbiAgbGFiZWw6ICdub2RlLTEnLFxuICBjaGlsZHJlbjogW1xuICAgIHtsYWJlbDogJ25vZGUtMS0xJ30sXG4gICAge2xhYmVsOiAnbm9kZS0xLTInfVxuICBdXG59XG48L3NjcmlwdD5cblxuPHRlbXBsYXRlPlxuICA8aDE+e3sgbXNnIH19PC9oMT5cbiAgPGNvbXAgOm1vZGVsPVwibW9kZWxcIj48L2NvbXA+XG48L3RlbXBsYXRlPiIsImltcG9ydC1tYXAuanNvbiI6IntcbiAgXCJpbXBvcnRzXCI6IHtcbiAgICBcInZ1ZVwiOiBcImh0dHBzOi8vc2ZjLnZ1ZWpzLm9yZy92dWUucnVudGltZS5lc20tYnJvd3Nlci5qc1wiXG4gIH1cbn0iLCJDb21wLnZ1ZSI6Ijx0ZW1wbGF0ZT5cbiAgPGRpdj5cbiAgICB7e21vZGVsLmxhYmVsfX1cbiAgPC9kaXY+XG4gIDxDb21wIHYtZm9yPVwiaXRlbSBpbiBtb2RlbC5jaGlsZHJlblwiIDptb2RlbD1cIml0ZW1cIj48L0NvbXA+XG4gIDxjb21wMj48L2NvbXAyPlxuPC90ZW1wbGF0ZT5cbjxzY3JpcHQ+XG5cdGV4cG9ydCBkZWZhdWx0IHtcbiAgICBuYW1lOiAnQ29tcCcsXG4gICAgcHJvcHM6IHtcbiAgICAgIG1vZGVsOiBPYmplY3RcbiAgICB9LFxuICAgIGNvbXBvbmVudHM6IHtcbiAgICAgIGNvbXAyOiB7XG4gICAgICAgIHJlbmRlcigpe31cbiAgICAgIH1cbiAgICB9XG4gIH1cbjwvc2NyaXB0PiJ9 14-异步组件是什么?使用场景有哪些?分析因为异步路由的存在,我们使用异步组件的次数比较少,因此还是有必要两者的不同。 体验大型应用中,我们需要分割应用为更小的块,并且在需要组件时再加载它们。
import { defineAsyncComponent } from 'vue'
// defineAsyncComponent定义异步组件
const AsyncComp = defineAsyncComponent(() => {
// 加载函数返回Promise
return new Promise((resolve, reject) => {
// ...可以从服务器加载组件
resolve(/* loaded component */)
})
})
// 借助打包工具实现ES模块动态导入
const AsyncComp = defineAsyncComponent(() =>
import('./components/MyComponent.vue')
)思路
范例
知其所以然defineAsyncComponent定义了一个高阶组件,返回一个包装组件。包装组件根据加载器的状态决定渲染什么内容。
15-你是怎么处理vue项目中的错误的?分析这是一个综合应用题目,在项目中我们常常需要将App的异常上报,此时错误处理就很重要了。 这里要区分错误的类型,针对性做收集。 然后是将收集的的错误信息上报服务器。 思路
回答范例
实践axios拦截器中处理捕获异常: // 响应拦截器
instance.interceptors.response.use(
(response) => {
return response.data;
},
(error) => {
// 存在response说明服务器有响应
if (error.response) {
let response = error.response;
if (response.status >= 400) {
handleError(response);
}
} else {
handleError(null);
}
return Promise.reject(error);
},
);vue中全局捕获异常: import { createApp } from 'vue'
const app = createApp(...)
app.config.errorHandler = (err, instance, info) => {
// report error to tracking services
}处理接口请求错误: function handleError(error, type) {
if(type == 1) {
// 接口错误,从config字段中获取请求信息
let { url, method, params, data } = error.config
let err_data = {
url, method,
params: { query: params, body: data },
error: error.data?.message || JSON.stringify(error.data),
})
}
}处理前端逻辑错误: function handleError(error, type) {
if(type == 2) {
let errData = null
// 逻辑错误
if(error instanceof Error) {
let { name, message } = error
errData = {
type: name,
error: message
}
} else {
errData = {
type: 'other',
error: JSON.strigify(error)
}
}
}
}16-如果让你从零开始写一个vuex,说说你的思路思路分析这个题目很有难度,首先思考
回答范例
实践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中Store的实现:https://github1s.com/vuejs/vuex/blob/HEAD/src/store.js#L19-L20 17-vuex中actions和mutations有什么区别?题目分析
我们只需记住修改状态只能是 体验看下面例子可知,
const store = createStore({
state: {
count: 0
},
mutations: {
increment (state) {
state.count++
}
},
actions: {
increment (context) {
context.commit('increment')
}
}
})答题思路
回答范例
知其所以然我们可以像下面这样简单实现 class Store {
constructor(options) {
this.state = reactive(options.state)
this.options = options
}
commit(type, payload) {
// 传入上下文和参数1都是state对象
this.options.mutations[type].call(this.state, this.state, payload)
}
dispatch(type, payload) {
// 传入上下文和参数1都是store本身
this.options.actions[type].call(this, this, payload)
}
}18-使用vue渲染大量数据时应该怎么优化?说下你的思路!分析企业级项目中渲染大量数据的情况比较常见,因此这是一道非常好的综合实践题目。 思路
回答
19-怎么监听vuex数据的变化?分析vuex数据状态是响应式的,所以状态变视图跟着变,但是有时还是需要知道数据状态变了从而做一些事情。 既然状态都是响应式的,那自然可以 思路
回答范例
实践watch方式 const app = createApp({
watch: {
'$store.state.counter'() {
console.log('counter change!');
}
}
})subscribe方式: store.subscribe((mutation, state) => {
if (mutation.type === 'add') {
console.log('counter change in subscribe()!');
}
})20-router-link和router-view是如何起作用的?分析vue-router中两个重要组件 思路
回答范例
知其所以然
https://github1s.com/vuejs/router/blob/HEAD/src/RouterLink.ts#L184-L185
https://github1s.com/vuejs/router/blob/HEAD/src/RouterView.ts#L43-L44 21-Vue-router 除了 router-link 怎么实现跳转分析vue-router导航有两种方式: 体验声明式导航 <router-link to="/about">Go to About</router-link> 编程导航 // literal string path
router.push('/users/eduardo')
// object with path
router.push({ path: '/users/eduardo' })
// named route with params to let the router build the url
router.push({ name: 'user', params: { username: 'eduardo' } })思路
回答范例
知其所以然https://github1s.com/vuejs/router/blob/HEAD/src/RouterLink.ts#L240-L241 routerlink点击跳转,调用的是navigate方法 navigate内部依然调用的push 22-Vue3.0 性能提升体现在哪些方面?分析vue3在设计时有几个目标:更小、更快、更友好,这些多数适合性能相关,因此可以围绕介绍。 思路
回答范例
体验通过playground体验编译优化:sfc.vuejs.org 知其所以然为什么基于Proxy更快了:初始化时懒处理,用户访问才做拦截处理,初始化更快: https://github1s.com/vuejs/core/blob/HEAD/packages/reactivity/src/baseHandlers.ts#L136-L137 轻量的依赖关系保存:利用WeakMap、Map和Set保存响应式数据和副作用之间的依赖关系 https://github1s.com/vuejs/core/blob/HEAD/packages/reactivity/src/effect.ts#L19-L20 23-Vue3.0里为什么要用 Proxy 替代 defineProperty ?分析Vue3中最重大的更新之一就是响应式模块 此变化主要是从性能方面考量。 思路
回答范例
知其所以然Proxy属性拦截的原理:利用get、set、deleteProperty这三个trap实现拦截 function reactive(obj) {
return new Proxy(obj, {
get(target, key) {},
set(target, key, val) {},
deleteProperty(target, key){}
})
}Object.defineProperty属性拦截原理:利用get、set这两个trap实现拦截 function defineReactive(obj, key, val) {
Object.defineReactive(obj, key, {
get(key) {},
set(key, val) {}
})
}很容易看出两者的区别! 24-History模式和Hash模式有何区别?分析vue-router有3个模式,其中两个更为常用,那便是history和hash。 两者差别主要在显示形式和部署上。 体验vue-router4.x中设置模式已经变化: const router = createRouter({
history: createWebHashHistory(), // hash模式
history: createWebHistory(), // history模式
})用起来一模一样 <router-link to="/about">Go to About</router-link> 区别只在url形式 // hash // 浏览器里的形态:http://xx.com/#/about // history // 浏览器里的形态:http://xx.com/about 思路
回答范例
知其所以然hash是一种特殊的history实现: https://github1s.com/vuejs/router/blob/HEAD/src/history/hash.ts#L31-L32 25-在什么场景下会用到嵌套路由?分析应用的有些界面是由多层级组件组合而来的,这种情况下,url各部分通常对应某个嵌套的组件,vue-router中就可以使用嵌套路由表示这种关系:router.vuejs.org/guide/essen…
体验定义嵌套路由,对应上图嵌套关系: const routes = [
{
path: '/user/:id',
component: User,
children: [
{
// UserProfile 会被渲染在 User 组件中的 <router-view> 里
path: 'profile',
component: UserProfile,
},
{
// UserPosts 会被渲染在 User 组件中的 <router-view> 里
path: 'posts',
component: UserPosts,
},
],
},
]思路
回答范例
知其所以然router-view获取自己所在的深度:默认0,加1之后传给后代,同时根据深度获取匹配路由。
26-页面刷新后vuex的state数据丢失怎么解决?分析这是一道应用题目,很容易想到使用 但是如何优雅编写代码还是能体现认知水平。 体验可以从 const store = createStore({
state () {
return {
count: localStorage.getItem('count')
}
}
})业务代码中,提交修改状态同时保存最新值:虽说实现了,但是每次还要手动刷新localStorage不太优雅 store.commit('increment')
localStorage.setItem('count', store.state.count)思路
回答范例
知其所以然可以看一下vuex-persist内部确实是利用subscribe实现的 https://github.com/championswimmer/vuex-persist/blob/master/src/index.ts#L277 27-你觉得vuex有什么缺点?分析相较于redux,vuex已经相当简便好用了。但模块的使用比较繁琐,对ts支持也不好。 体验使用模块:用起来比较繁琐,使用模式也不统一,基本上得不到类型系统的任何支持 const store = createStore({
modules: {
a: moduleA
}
})
store.state.a // -> 要带上 moduleA 的key,内嵌模块的话会很长,不得不配合mapState使用
store.getters.c // -> moduleA里的getters,没有namespaced时又变成了全局的
store.getters['a/c'] // -> 有namespaced时要加path,使用模式又和state不一样
store.commit('d') // -> 没有namespaced时变成了全局的,能同时触发多个子模块中同名mutation
store.commit('a/d') // -> 有namespaced时要加path,配合mapMutations使用感觉也没简化思路
回答范例
知其所以然下面我们来看看vuex中 首先是子模块安装过程:父模块状态 |
