本篇文章给大家总结分享29+个vue经典面试题(附源码级详解),带你梳理基础知识,增强vue知识储备,值得收藏,快来看看吧!
01-Vue 3.0的设计目标是什么?做了哪些优化?
分析
还是问新特性,陈述典型新特性,分析其给你带来的变化即可。(学习视频分享:vue)
思路
从以下几方面分门别类阐述:易用性、性能、扩展性、可维护性、开发体验等
范例
Vue3的最大设计目标是替代Vue2(皮一下),为了实现这一点,Vue3在以下几个方面做了很大改进,如:易用性、框架性能、扩展性、可维护性、开发体验等
易用性方面主要是API简化,比如v-model在Vue3中变成了Vue2中v-model和sync修饰符的结合体,用户不用区分两者不同,也不用选择困难。类似的简化还有用于渲染函数内部生成VNode的h(type, props, children),其中props不用考虑区分属性、特性、事件等,框架替我们判断,易用性大增。
立即学习“前端免费学习笔记(深入)”;
开发体验方面,新组件Teleport传送门、Fragments 、Suspense等都会简化特定场景的代码编写,SFC Composition API语法糖更是极大提升我们开发体验。
扩展性方面提升如独立的reactivity模块,custom renderer API等
可维护性方面主要是Composition API,更容易编写高复用性的业务逻辑。还有对TypeScript支持的提升。
性能方面的改进也很显著,例如编译期优化、基于Proxy的响应式系统
。。。
可能的追问
Vue3做了哪些编译优化?Proxy和defineProperty有什么不同?
02-你了解哪些Vue性能优化方法?
分析
这是一道综合实践题目,写过一定数量的代码之后小伙伴们自然会开始关注一些优化方法,答得越多肯定实践经验也越丰富,是很好的题目。
答题思路:
根据题目描述,这里主要探讨Vue代码层面的优化
回答范例
我这里主要从Vue代码编写层面说一些优化手段,例如:代码分割、服务端渲染、组件缓存、长列表优化等
最常见的路由懒加载:有效拆分App尺寸,访问时才异步加载
const router = createRouter({ routes: [ // 借助webpack的import()实现异步组件 { path: '/foo', component: () => import('./Foo.vue') } ]})
登录后复制
keep-alive缓存页面:避免重复创建组件实例,且能保留缓存组件状态
登录后复制
使用v-show复用DOM:避免重复创建组件
登录后复制
v-for 遍历避免同时使用 v-if:实际上在Vue3中已经是个错误写法
- <li v-for="user in activeUsers" :key="user.id"> {{ user.name }}
登录后复制
v-once和v-memo:不再变化的数据使用v-once
This will never change: {{msg}}comment
{{msg}}
- {{i}}
登录后复制登录后复制
按条件跳过更新时使用v-momo:下面这个列表只会更新选中状态变化项
ID: {{ item.id }} - selected: {{ item.id === selected }}
...more child nodes
登录后复制
https://vuejs.org/api/built-in-directives.html#v-memo
长列表性能优化:如果是大数据长列表,可采用虚拟滚动,只渲染少部分区域的内容
登录后复制
一些开源库:
vue-virtual-scroller:https://github.com/Akryum/vue-virtual-scrollervue-virtual-scroll-grid:https://github.com/rocwang/vue-virtual-scroll-grid
事件的销毁:Vue 组件销毁时,会自动解绑它的全部指令及事件监听器,但是仅限于组件本身的事件。
export default { created() { this.timer = setInterval(this.refresh, 2000) }, beforeUnmount() { clearInterval(this.timer) }}
登录后复制
图片懒加载
对于图片过多的页面,为了加速页面加载速度,所以很多时候我们需要将页面内未出现在可视区域内的图片先不做加载, 等到滚动到可视区域后再去加载。
@@##@@
登录后复制
参考项目:https://github.com/hilongjw/vue-lazyload
第三方插件按需引入
像element-plus这样的第三方组件库可以按需引入避免体积太大。
import { createApp } from 'vue';import { Button, Select } from 'element-plus';const app = createApp()app.use(Button)app.use(Select)
登录后复制
子组件分割策略:较重的状态组件适合拆分
export default { components: { ChildComp: { methods: { heavy () { /* 耗时任务 */ } }, render (h) { return h('div', this.heavy()) } } }}
登录后复制
但同时也不宜过度拆分组件,尤其是为了所谓组件抽象将一些不需要渲染的组件特意抽出来,组件实例消耗远大于纯dom节点。参考:https://vuejs.org/guide/best-practices/performance.html#avoid-unnecessary-component-abstractions
服务端渲染/静态网站生成:SSR/SSG
如果SPA应用有首屏渲染慢的问题,可以考虑SSR、SSG方案优化。参考:https://vuejs.org/guide/scaling-up/ssr.html
03-Vue组件为什么只能有一个根元素?
这题现在有些落伍,vue3已经不用一个根了。因此这题目很有说头!
体验一下
vue2直接报错,test-v2.html
new Vue({ components: { comp: { template: `root1root2` } }}).$mount('#app')
登录后复制
vue3中没有问题,test-v3.html
Vue.createApp({ components: { comp: { template: `root1root2` } }}).mount('#app')
登录后复制
回答思路
给一条自己的结论解释为什么会这样vue3解决方法原理
范例
vue2中组件确实只能有一个根,但vue3中组件已经可以多根节点了。之所以需要这样是因为vdom是一颗单根树形结构,patch方法在遍历的时候从根节点开始遍历,它要求只有一个根节点。组件也会转换为一个vdom,自然应该满足这个要求。vue3中之所以可以写多个根节点,是因为引入了Fragment的概念,这是一个抽象的节点,如果发现组件是多根的,就创建一个Fragment节点,把多个根节点作为它的children。将来patch的时候,如果发现是一个Fragment节点,则直接遍历children创建或更新。
知其所以然
patch方法接收单根vdom:
https://github1s.com/vuejs/core/blob/HEAD/packages/runtime-core/src/renderer.ts#L354-L355
// 直接获取type等,没有考虑数组的可能性const { type, ref, shapeFlag } = n2
登录后复制
patch方法对Fragment的处理:
https://github1s.com/vuejs/core/blob/HEAD/packages/runtime-core/src/renderer.ts#L1091-L1092
// a fragment can only have array children// since they are either generated by the compiler, or implicitly created// from arrays.mountChildren(n2.children as VNodeArrayChildren, container, ...)
登录后复制
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里的gettersstore.commit('d') // -> 能同时触发子模块中同名mutationstore.dispatch('e') // -> 能同时触发子模块中同名action
登录后复制
思路
概念和必要性
怎么拆
使用细节
优缺点
范例
用过module,项目规模变大之后,单独一个store对象会过于庞大臃肿,通过模块方式可以拆分开来便于维护
可以按之前规则单独编写子模块代码,然后在主文件中通过modules选项组织起来:createStore({modules:{…}})
不过使用时要注意访问子模块状态时需要加上注册时模块名:store.state.a.xxx,但同时getters、mutations和actions又在全局空间中,使用方式和之前一样。如果要做到完全拆分,需要在子模块加上namespace选项,此时再访问它们就要加上命名空间前缀。
很显然,模块的方式可以拆分代码,但是缺点也很明显,就是使用起来比较繁琐复杂,容易出错。而且类型系统支持很差,不能给我们带来帮助。pinia显然在这方面有了很大改进,是时候切换过去了。
可能的追问
用过pinia吗?都做了哪些改善?
05-怎么实现路由懒加载呢?
分析
这是一道应用题。当打包应用时,JavaScript 包会变得非常大,影响页面加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问时才加载对应组件,这样就会更加高效。
// 将// import UserDetails from './views/UserDetails'// 替换为const UserDetails = () => import('./views/UserDetails')const router = createRouter({ // ... routes: [{ path: '/users/:id', component: UserDetails }],})
登录后复制
参考:https://router.vuejs.org/zh/guide/advanced/lazy-loading.html
思路
必要性
何时用
怎么用
使用细节
回答范例
当打包构建应用时,JavaScript 包会变得非常大,影响页面加载。利用路由懒加载我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样会更加高效,是一种优化手段。
一般来说,对所有的路由都使用动态导入是个好主意。
给component选项配置一个返回 Promise 组件的函数就可以定义懒加载路由。例如:
{ path: ‘/users/:id’, component: () => import(‘./views/UserDetails’) }
结合注释() => import(/* webpackChunkName: “group-user” */ ‘./UserDetails.vue’)可以做webpack代码分块
vite中结合vue定义分块
路由中不能使用异步组件
知其所以然
component (和 components) 配置如果接收一个返回 Promise 组件的函数,Vue Router 只会在第一次进入页面时才会获取这个函数,然后使用缓存数据。
https://github1s.com/vuejs/router/blob/HEAD/src/navigationGuards.ts#L292-L293
06-ref和reactive异同
这是Vue3数据响应式中非常重要的两个概念,自然的,跟我们写代码关系也很大。
体验
ref:https://vuejs.org/api/reactivity-core.html#ref
const count = ref(0)console.log(count.value) // 0count.value++console.log(count.value) // 1
登录后复制
reactive:https://vuejs.org/api/reactivity-core.html#reactive
const obj = reactive({ count: 0 })obj.count++
登录后复制
回答思路
两者概念
两者使用场景
两者异同
使用细节
原理
回答范例
ref接收内部值(inner value)返回响应式Ref对象,reactive返回响应式代理对象
从定义上看ref通常用于处理单值的响应式,reactive用于处理对象类型的数据响应式
两者均是用于构造响应式数据,但是ref主要解决原始值的响应式问题
ref返回的响应式数据在JS中使用需要加上.value才能访问其值,在视图中使用会自动脱ref,不需要.value;ref可以接收对象或数组等非原始值,但内部依然是reactive实现响应式;reactive内部如果接收Ref对象会自动脱ref;使用展开运算符(…)展开reactive返回的响应式对象会使其失去响应性,可以结合toRefs()将值转换为Ref对象之后再展开。
reactive内部使用Proxy代理传入对象并拦截该对象各种操作(trap),从而实现响应式。ref内部封装一个RefImpl类,并设置get value/set value,拦截用户对值的访问,从而实现响应式。
知其所以然
reactive实现响应式:
https://github1s.com/vuejs/core/blob/HEAD/packages/reactivity/src/reactive.ts#L90-L91
ref实现响应式:
https://github1s.com/vuejs/core/blob/HEAD/packages/reactivity/src/ref.ts#L73-L74
07-watch和watchEffect异同
我们经常性需要侦测响应式数据的变化,vue3中除了watch之外又出现了watchEffect,不少同学会混淆这两个api。
体验
watchEffect立即运行一个函数,然后被动地追踪它的依赖,当这些依赖改变时重新执行该函数。
Runs a function immediately while reactively tracking its dependencies and re-runs it whenever the dependencies are changed.
const count = ref(0)watchEffect(() => console.log(count.value))// -> logs 0count.value++// -> logs 1
登录后复制
watch侦测一个或多个响应式数据源并在数据源变化时调用一个回调函数。
Watches one or more reactive data sources and invokes a callback function when the sources change.
const state = reactive({ count: 0 })watch( () => state.count, (count, prevCount) => { /* ... */ })
登录后复制
思路
给出两者定义
给出场景上的不同
给出使用方式和细节
原理阐述
范例
watchEffect立即运行一个函数,然后被动地追踪它的依赖,当这些依赖改变时重新执行该函数。watch侦测一个或多个响应式数据源并在数据源变化时调用一个回调函数。
watchEffect(effect)是一种特殊watch,传入的函数既是依赖收集的数据源,也是回调函数。如果我们不关心响应式数据变化前后的值,只是想拿这些数据做些事情,那么watchEffect就是我们需要的。watch更底层,可以接收多种数据源,包括用于依赖收集的getter函数,因此它完全可以实现watchEffect的功能,同时由于可以指定getter函数,依赖可以控制的更精确,还能获取数据变化前后的值,因此如果需要这些时我们会使用watch。
watchEffect在使用时,传入的函数会立刻执行一次。watch默认情况下并不会执行回调函数,除非我们手动设置immediate选项。
从实现上来说,watchEffect(fn)相当于watch(fn,fn,{immediate:true})
知其所以然
watchEffect定义:https://github1s.com/vuejs/core/blob/HEAD/packages/runtime-core/src/apiWatch.ts#L80-L81
export function watchEffect( effect: WatchEffect, options?: WatchOptionsBase): WatchStopHandle { return doWatch(effect, null, options)}
登录后复制
watch定义如下:https://github1s.com/vuejs/core/blob/HEAD/packages/runtime-core/src/apiWatch.ts#L158-L159
export function watch<T = any, Immediate extends Readonly = false>( source: T | WatchSource, cb: any, options?: WatchOptions): WatchStopHandle { return doWatch(source as any, cb, options)}
登录后复制
很明显watchEffect就是一种特殊的watch实现。
08-SPA、SSR的区别是什么
我们现在编写的Vue、React和Angular应用大多数情况下都会在一个页面中,点击链接跳转页面通常是内容切换而非页面跳转,由于良好的用户体验逐渐成为主流的开发模式。但同时也会有首屏加载时间长,SEO不友好的问题,因此有了SSR,这也是为什么面试中会问到两者的区别。
思路分析
两者概念
两者优缺点分析
使用场景差异
其他选择
回答范例
SPA(Single Page Application)即单页面应用。一般也称为 客户端渲染(Client Side Render), 简称 CSR。SSR(Server Side Render)即 服务端渲染。一般也称为 多页面应用(Mulpile Page Application),简称 MPA。
SPA应用只会首次请求html文件,后续只需要请求JSON数据即可,因此用户体验更好,节约流量,服务端压力也较小。但是首屏加载的时间会变长,而且SEO不友好。为了解决以上缺点,就有了SSR方案,由于HTML内容在服务器一次性生成出来,首屏加载快,搜索引擎也可以很方便的抓取页面信息。但同时SSR方案也会有性能,开发受限等问题。
在选择上,如果我们的应用存在首屏加载优化需求,SEO需求时,就可以考虑SSR。
但并不是只有这一种替代方案,比如对一些不常变化的静态网站,SSR反而浪费资源,我们可以考虑vue(prerender)方案。另外nuxt.js/next.js中给我们提供了SSG(Static Site Generate)静态网站生成方案也是很好的静态站点解决方案,结合一些CI手段,可以起到很好的优化效果,且能节约服务器资源。
知其所以然
内容生成上的区别:
SSR
SPA
部署上的区别
09-vue-loader是什么?它有什么作用?
分析
这是一道工具类的原理题目,相当有深度,具有不错的人才区分度。
体验
使用官方提供的SFC playground可以很好的体验vue-loader。
vue
有了vue-loader加持,我们才可以以SFC的方式快速编写代码。
{{ msg }}export default { data() { return { msg: 'Hello world!', } },}.example { color: red;}
登录后复制
思路
vue-loader是什么东东vue-loader是做什么用的vue-loader何时生效vue-loader如何工作
回答范例
vue-loader是用于处理单文件组件(SFC,Single-File Component)的webpack loader
因为有了vue-loader,我们就可以在项目中编写SFC格式的Vue组件,我们可以把代码分割为、和,代码会异常清晰。结合其他loader我们还可以用Pug编写,用SASS编写,用TS编写。我们的还可以单独作用当前组件。
webpack打包时,会以loader的方式调用vue-loader
vue-loader被执行时,它会对SFC中的每个语言块用单独的loader链处理。最后将这些单独的块装配成最终的组件模块。
知其所以然
1、vue-loader会调用@vue/compiler-sfc模块解析SFC源码为一个描述符(Descriptor),然后为每个语言块生成import代码,返回的代码类似下面:
// source.vue被vue-loader处理之后返回的代码// import the blockimport render from 'source.vue?vue&type=template'// import the blockimport script from 'source.vue?vue&type=script'export * from 'source.vue?vue&type=script'// import blocksimport 'source.vue?vue&type=style&index=1'script.render = renderexport 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'
登录后复制
类似的,如果我们对.sass文件配置了style-loader + css-loader + sass-loader,对下面的代码:
登录后复制
vue-loader将会返回给我们下面结果:
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)当处理展开请求时,vue-loader将被再次调用。这次,loader将会关注那些有查询串的请求,且仅针对特定块,它会选中特定块内部的内容并传递给后面匹配的loader。
2)对于块,处理到这就可以了,但是 和 还有一些额外任务要做,比如:
需要用Vue 模板编译器编译template,从而得到render函数需要对中的CSS做后处理(post-process),该操作在css-loader之后但在style-loader之前
实现上这些附加的loader需要被注入到已经展开的loader链上,最终的请求会像下面这样:
// import 'vue-loader/template-loader!pug-loader!source.vue?vue&type=template'// 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 }}
登录后复制
登录后复制
思路
定义
何时用
如何用
常用指令
vue3变化
回答范例
Vue有一组默认指令,比如v-model或v-for,同时Vue也允许用户注册自定义指令来扩展Vue能力
自定义指令主要完成一些可复用低层级DOM操作
使用自定义指令分为定义、注册和使用三步:
定义自定义指令有两种方式:对象和函数形式,前者类似组件定义,有各种生命周期;后者只会在mounted和updated时执行注册自定义指令类似组件,可以使用app.directive()全局注册,使用{directives:{xxx}}局部注册使用时在注册名称前加上v-即可,比如v-focus
我在项目中常用到一些自定义指令,例如:
复制粘贴 v-copy长按 v-longpress防抖 v-debounce图片懒加载 v-lazy按钮权限 v-premission页面水印 v-waterMarker拖拽指令 v-draggable
vue3中指令定义发生了比较大的变化,主要是钩子的名称保持和组件一致,这样开发人员容易记忆,不易犯错。另外在v3.2之后,可以在setup中以一个小写v开头方便的定义自定义指令,更简单了!
知其所以然
编译后的自定义指令会被withDirective函数装饰,进一步处理生成的vnode,添加到特定属性中。
https://sfc.vuejs.org/#eyJBcHAudnVlIjoiPHNjcmlwdCBzZXR1cD5cbmltcG9ydCB7IHJlZiB9IGZyb20gJ3Z1ZSdcblxuY29uc3QgbXNnID0gcmVmKCdIZWxsbyBXb3JsZCEnKVxuXG5jb25zdCB2Rm9jdXMgPSB7XG4gIG1vdW50ZWQoZWwpIHtcbiAgICAvLyDojrflj5ZpbnB1dO+8jOW5tuiwg+eUqOWFtmZvY3VzKCnmlrnms5VcbiAgICBlbC5mb2N1cygpXG4gIH1cbn1cbjwvc2NyaXB0PlxuXG48dGVtcGxhdGU+XG4gIDxoMT57eyBtc2cgfX08L2gxPlxuICA8aW5wdXQgdi1tb2RlbD1cIm1zZ1wiIHYtZm9jdXM+XG48L3RlbXBsYXRlPiIsImltcG9ydC1tYXAuanNvbiI6IntcbiAgXCJpbXBvcnRzXCI6IHtcbiAgICBcInZ1ZVwiOiBcImh0dHBzOi8vc2ZjLnZ1ZWpzLm9yZy92dWUucnVudGltZS5lc20tYnJvd3Nlci5qc1wiXG4gIH1cbn0ifQ==
登录后复制
11-说下$attrs和$listeners的使用场景
分析
API考察,但$attrs和$listeners是比较少用的边界知识,而且vue3有变化,$listeners已经移除,还是有细节可说的。
思路
这两个api的作用
使用场景分析
使用方式和细节
vue3变化
体验
一个包含组件透传属性的对象。
An object that contains the component’s fallthrough attributes.
将非属性特性透传给内部的子组件
登录后复制
范例
我们可能会有一些属性和事件没有在props中定义,这类称为非属性特性,结合v-bind指令可以直接透传给内部的子组件。
这类“属性透传”常常用于包装高阶组件时往内部传递属性,常用于爷孙组件之间传参。比如我在扩展A组件时创建了组件B组件,然后在C组件中使用B,此时传递给C的属性中只有props里面声明的属性是给B使用的,其他的都是A需要的,此时就可以利用v-bind=”$attrs”透传下去。
最常见用法是结合v-bind做展开;$attrs本身不是响应式的,除非访问的属性本身是响应式对象。
vue2中使用listeners获取事件,vue3中已移除,均合并到vue等虚拟滚动方案,只渲染视口范围内的数据
如果不需要更新,可以使用v-once方式只渲染一次通过vue可以缓存结果,结合v-for使用,避免数据变化时不必要的VNode创建可以采用懒加载方式,在用户需要的时候再加载数据,比如tree组件子树的懒加载
总之,还是要看具体需求,首先从设计上避免大数据获取和渲染;实在需要这样做可以采用虚表的方式优化渲染;最后优化更新,如果不需要更新可以v-once处理,需要更新可以v-memo进一步优化大数据更新性能。其他可以采用的是交互方式优化,无线滚动、懒加载等方案。
19-怎么监听vuex数据的变化?
分析
vuex数据状态是响应式的,所以状态变视图跟着变,但是有时还是需要知道数据状态变了从而做一些事情。
既然状态都是响应式的,那自然可以watch,另外vuex也提供了订阅的API:store.subscribe()。
思路
总述知道的方法分别阐述用法选择和场景
回答范例
我知道几种方法:
可以通过watch选项或者watch方法监听状态可以使用vuex提供的API:store.subscribe()
watch选项方式,可以以字符串形式监听$store.state.xx;subscribe方式,可以调用store.subscribe(cb),回调函数接收mutation对象和state对象,这样可以进一步判断mutation.type是否是期待的那个,从而进一步做后续处理。
watch方式简单好用,且能获取变化前后值,首选;subscribe方法会被所有commit行为触发,因此还需要判断mutation.type,用起来略繁琐,一般用于vuex插件中。
实践
watch方式
{{ msg }}
登录后复制
subscribe方式:
{{$attrs.foo}} {{bar}}defineProps({ bar: String})
登录后复制
20-router-link和router-view是如何起作用的?
分析
vue-router中两个重要组件router-link和router-view,分别起到导航作用和内容渲染作用,但是回答如何生效还真有一定难度哪!
思路
两者作用阐述使用方式原理说明
回答范例
vue-router中两个重要组件router-link和router-view,分别起到路由导航作用和组件内容渲染作用使用中router-link默认生成一个a标签,设置to属性定义跳转path。实际上也可以通过custom和插槽自定义最终的展现形式。router-view是要显示组件的占位组件,可以嵌套,对应路由配置的嵌套关系,配合name可以显示具名组件,起到更强的布局作用。router-link组件内部根据custom属性判断如何渲染最终生成节点,内部提供导航方法navigate,用户点击之后实际调用的是该方法,此方法最终会修改响应式的路由变量,然后重新去routes匹配出数组结果,router-view则根据其所处深度deep在匹配数组结果中找到对应的路由并获取组件,最终将其渲染出来。
知其所以然
RouterLink定义
https://github1s.com/vuejs/router/blob/HEAD/src/RouterLink.ts#L184-L185
RouterView定义
https://github1s.com/vuejs/router/blob/HEAD/src/RouterView.ts#L43-L44
21-Vue-router 除了 router-link 怎么实现跳转
分析
vue-router导航有两种方式:声明式导航和编程方式导航
体验
声明式导航
_createVNode(Comp, { foo: "foo", bar: "bar"})
登录后复制
编程导航
https://sfc.vuejs.org/#eyJBcHAudnVlIjoiPHNjcmlwdCBzZXR1cD5cbmltcG9ydCB7IHJlZiB9IGZyb20gJ3Z1ZSdcbmltcG9ydCBDb21wIGZyb20gJy4vQ29tcC52dWUnXG5jb25zdCBtc2cgPSByZWYoJ0hlbGxvIFdvcmxkIScpXG48L3NjcmlwdD5cblxuPHRlbXBsYXRlPlxuICA8aDE+e3sgbXNnIH19PC9oMT5cbiAgPGNvbXAgZm9vPVwiZm9vXCIgYmFyPVwiYmFyXCIgLz5cbjwvdGVtcGxhdGU+IiwiaW1wb3J0LW1hcC5qc29uIjoie1xuICBcImltcG9ydHNcIjoge1xuICAgIFwidnVlXCI6IFwiaHR0cHM6Ly9zZmMudnVlanMub3JnL3Z1ZS5ydW50aW1lLmVzbS1icm93c2VyLmpzXCJcbiAgfVxufSIsIkNvbXAudnVlIjoiPHRlbXBsYXRlPlxuXHQ8ZGl2PlxuICAgIHt7JGF0dHJzLmZvb319IHt7YmFyfX1cbiAgPC9kaXY+XG48L3RlbXBsYXRlPlxuPHNjcmlwdCBzZXR1cD5cbmRlZmluZVByb3BzKHtcbiAgYmFyOiBTdHJpbmdcbn0pXG48L3NjcmlwdD4ifQ==
登录后复制
思路
两种方式分别阐述使用方式区别和选择原理说明
回答范例
vue-router导航有两种方式:声明式导航和编程方式导航声明式导航方式使用router-link组件,添加to属性导航;编程方式导航更加灵活,可传递调用router.push(),并传递path字符串或者RouteLocationRaw对象,指定path、name、params等信息如果页面中简单表示跳转链接,使用router-link最快捷,会渲染一个a标签;如果页面是个复杂的内容,比如商品信息,可以添加点击事件,使用编程式导航实际上内部两者调用的导航函数是一样的
知其所以然
https://github1s.com/vuejs/router/blob/HEAD/src/RouterLink.ts#L240-L241
routerlink点击跳转,调用的是navigate方法
navigate内部依然调用的push
22-Vue3.0 性能提升体现在哪些方面?
分析
vue3在设计时有几个目标:更小、更快、更友好,这些多数适合性能相关,因此可以围绕介绍。
思路
总述和性能相关的新特性逐个说细节能说点原理更佳
回答范例
我分别从代码、编译、打包三方面介绍vue3性能方面的提升代码层面性能优化主要体现在全新响应式API,基于Proxy实现,初始化时间和内存占用均大幅改进;编译层面做了更多编译优化处理,比如静态提升、动态标记、事件缓存,区块等,可以有效跳过大量diff过程;打包时更好的支持tree-shaking,因此整体体积更小,加载更快
体验
通过playground体验编译优化:vue
知其所以然
为什么基于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中最重大的更新之一就是响应式模块reactivity的重写。主要的修改就是Proxy替换defineProperty实现响应式。
此变化主要是从性能方面考量。
思路
属性拦截的几种方式defineProperty的问题Proxy的优点其他考量
回答范例
JS中做属性拦截常见的方式有三:: vue,vue/vue 和vue.Vue2中使用defineProperty的原因是,2013年时只能用这种方式。由于该API存在一些局限性,比如对于数组的拦截有问题,为此vue需要专门为数组响应式做一套实现。另外不能拦截那些新增、删除属性;最后defineProperty方案在初始化时需要深度递归遍历待处理的对象才能对它进行完全拦截,明显增加了初始化的时间。以上两点在Proxy出现之后迎刃而解,不仅可以对数组实现拦截,还能对Map、Set实现拦截;另外Proxy的拦截也是懒处理行为,如果用户没有访问嵌套对象,那么也不会实施拦截,这就让初始化的速度和内存占用都改善了。当然Proxy是有兼容性问题的,IE完全不支持,所以如果需要IE兼容就不合适
知其所以然
Proxy属性拦截的原理:利用get、set、deleteProperty这三个trap实现拦截
This will never change: {{msg}}comment
{{msg}}
- {{i}}
登录后复制登录后复制
Object.defineProperty属性拦截原理:利用get、set这两个trap实现拦截
import { ref } from 'vue'const msg = ref('Hello World!'){{ msg }}
登录后复制
很容易看出两者的区别!
24-History模式和Hash模式有何区别?
分析
vue-router有3个模式,其中两个更为常用,那便是history和hash。
两者差别主要在显示形式和部署上。
体验
vue-router4.x中设置模式已经变化:
// ...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] ),// ...
登录后复制
用起来一模一样
登录后复制
区别只在url形式
const _component_Comp = _resolveComponent("Comp", true)
登录后复制
思路
区别详细阐述实现
回答范例
vue-router有3个模式,其中history和hash更为常用。两者差别主要在显示形式、seo和部署上。hash模式在地址栏显示的时候是已哈希的形式:#/xxx,这种方式使用和部署简单,但是不会被搜索引擎处理,seo有问题;history模式则建议用在大部分web项目上,但是它要求应用在部署时做特殊配置,服务器需要做回退处理,否则会出现刷新页面404的问题。底层实现上其实hash是一种特殊的history实现。
知其所以然
hash是一种特殊的history实现:
https://github1s.com/vuejs/router/blob/HEAD/src/history/hash.ts#L31-L32
25-在什么场景下会用到嵌套路由?
分析
应用的有些界面是由多层级组件组合而来的,这种情况下,url各部分通常对应某个嵌套的组件,vue-router中就可以使用嵌套路由表示这种关系:vue
体验
定义嵌套路由,对应上图嵌套关系:
export function resolveComponent( name: string, maybeSelfReference?: boolean): ConcreteComponent | string { return resolveAsset(COMPONENTS, name, true, maybeSelfReference) || name}
登录后复制
思路
概念和使用场景使用方式实现原理
回答范例
平时开发中,应用的有些界面是由多层级组件组合而来的,这种情况下,url各部分通常对应某个嵌套的组件,vue-router中可以使用嵌套路由表示这种关系表现形式是在两个路由间切换时,它们有公用的视图内容。此时通常提取一个父组件,内部放上,从而形成物理上的嵌套,和逻辑上的嵌套对应起来定义嵌套路由时使用children属性组织嵌套关系原理上是在router-view组件内部判断当前router-view处于嵌套层级的深度,讲这个深度作为匹配组件数组matched的索引,获取对应渲染组件,渲染之
知其所以然
router-view获取自己所在的深度:默认0,加1之后传给后代,同时根据深度获取匹配路由。
26-页面刷新后vuex的state数据丢失怎么解决?
分析
这是一道应用题目,很容易想到使用localStorage或数据库存储并还原状态。
但是如何优雅编写代码还是能体现认知水平。
体验
可以从localStorage中获取作为状态初始值:
if (!res && maybeSelfReference) { // fallback to implicit self-reference return Component}
登录后复制
业务代码中,提交修改状态同时保存最新值:虽说实现了,但是每次还要手动刷新localStorage不太优雅
https://github1s.com/vuejs/core/blob/HEAD/packages/runtime-core/src/helpers/resolveAssets.ts#L22-L23https://github1s.com/vuejs/core/blob/HEAD/packages/runtime-core/src/helpers/resolveAssets.ts#L110-L111https://sfc.vuejs.org/#eyJBcHAudnVlIjoiPHNjcmlwdCBzZXR1cD5cbmltcG9ydCB7IHJlZiB9IGZyb20gJ3Z1ZSdcbmltcG9ydCBjb21wIGZyb20gJy4vQ29tcC52dWUnXG5jb25zdCBtc2cgPSByZWYoJ+mAkuW9kue7hOS7ticpXG5jb25zdCBtb2RlbCA9IHtcbiAgbGFiZWw6ICdub2RlLTEnLFxuICBjaGlsZHJlbjogW1xuICAgIHtsYWJlbDogJ25vZGUtMS0xJ30sXG4gICAge2xhYmVsOiAnbm9kZS0xLTInfVxuICBdXG59XG48L3NjcmlwdD5cblxuPHRlbXBsYXRlPlxuICA8aDE+e3sgbXNnIH19PC9oMT5cbiAgPGNvbXAgOm1vZGVsPVwibW9kZWxcIj48L2NvbXA+XG48L3RlbXBsYXRlPiIsImltcG9ydC1tYXAuanNvbiI6IntcbiAgXCJpbXBvcnRzXCI6IHtcbiAgICBcInZ1ZVwiOiBcImh0dHBzOi8vc2ZjLnZ1ZWpzLm9yZy92dWUucnVudGltZS5lc20tYnJvd3Nlci5qc1wiXG4gIH1cbn0iLCJDb21wLnZ1ZSI6Ijx0ZW1wbGF0ZT5cbiAgPGRpdj5cbiAgICB7e21vZGVsLmxhYmVsfX1cbiAgPC9kaXY+XG4gIDxDb21wIHYtZm9yPVwiaXRlbSBpbiBtb2RlbC5jaGlsZHJlblwiIDptb2RlbD1cIml0ZW1cIj48L0NvbXA+XG4gIDxjb21wMj48L2NvbXAyPlxuPC90ZW1wbGF0ZT5cbjxzY3JpcHQ+XG5cdGV4cG9ydCBkZWZhdWx0IHtcbiAgICBuYW1lOiAnQ29tcCcsXG4gICAgcHJvcHM6IHtcbiAgICAgIG1vZGVsOiBPYmplY3RcbiAgICB9LFxuICAgIGNvbXBvbmVudHM6IHtcbiAgICAgIGNvbXAyOiB7XG4gICAgICAgIHJlbmRlcigpe31cbiAgICAgIH1cbiAgICB9XG4gIH1cbjwvc2NyaXB0PiJ9
登录后复制
思路
问题描述解决方法谈个人理解三方库原理探讨
回答范例
vuex只是在内存保存状态,刷新之后就会丢失,如果要持久化就要存起来。localStorage就很合适,提交mutation的时候同时存入localStorage,store中把值取出作为state的初始值即可。这里有两个问题,不是所有状态都需要持久化;如果需要保存的状态很多,编写的代码就不够优雅,每个提交的地方都要单独做保存处理。这里就可以利用vuex提供的subscribe方法做一个统一的处理。甚至可以封装一个vuex插件以便复用。类似的插件有vuex-persist、vuex-persistedstate,内部的实现就是通过订阅mutation变化做统一处理,通过插件的选项控制哪些需要持久化
知其所以然
可以看一下vuex-persist内部确实是利用subscribe实现的
https://github.com/championswimmer/vuex-persist/blob/master/src/index.ts#L277
27-你觉得vuex有什么缺点?
分析
相较于redux,vuex已经相当简便好用了。但模块的使用比较繁琐,对ts支持也不好。
体验
使用模块:用起来比较繁琐,使用模式也不统一,基本上得不到类型系统的任何支持
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'))
登录后复制
思路
先夸再贬使用感受解决方案
回答范例
vuex利用响应式,使用起来已经相当方便快捷了。但是在使用过程中感觉模块化这一块做的过于复杂,用的时候容易出错,还要经常查看文档比如:访问state时要带上模块key,内嵌模块的话会很长,不得不配合mapState使用,加不加namespaced区别也很大,getters,mutations,actions这些默认是全局,加上之后必须用字符串类型的path来匹配,使用模式不统一,容易出错;对ts的支持也不友好,在使用模块时没有代码提示。之前Vue2项目中用过vue的解决方案,虽然类型支持上有所改善,但又要学一套新东西,增加了学习成本。pinia出现之后使用体验好了很多,Vue3 + pinia会是更好的组合。
知其所以然
下面我们来看看vuex中store.state.x.y这种嵌套的路径是怎么搞出来的。
首先是子模块安装过程:父模块状态parentState上面设置了子模块名称moduleName,值为当前模块state对象。放在上面的例子中相当于:store.state[‘x’] = moduleX.state。此过程是递归的,那么store.state.x.y安装时就是:store.state[‘x’][‘y’] = moduleY.state。
// 响应拦截器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); },);
登录后复制
这下大家明白了吧!
源码地址:https://github1s.com/vuejs/vuex/blob/HEAD/src/store-util.js#L102-L115
28-Composition API 与 Options API 有什么不同
分析
Vue3最重要更新之一就是Composition API,它具有一些列优点,其中不少是针对Options API暴露的一些问题量身打造。是Vue3推荐的写法,因此掌握好Composition API应用对掌握好Vue3至关重要。
https://vuejs.org/guide/extras/composition-api-faq.html
体验
Composition API能更好的组织代码,下面这个代码用options api实现
如果用composition api可以提取为useCount(),用于组合、复用
思路
总述不同点composition api动机两者选择
回答范例
Composition API是一组API,包括:Reactivity API、生命周期钩子、依赖注入,使用户可以通过导入函数方式编写vue组件。而Options API则通过声明组件选项的对象形式编写组件。Composition API最主要作用是能够简洁、高效复用逻辑。解决了过去Options API中mixins的各种缺点;另外Composition API具有更加敏捷的代码组织能力,很多用户喜欢Options API,认为所有东西都有固定位置的选项放置代码,但是单个组件增长过大之后这反而成为限制,一个逻辑关注点分散在组件各处,形成代码碎片,维护时需要反复横跳,Composition API则可以将它们有效组织在一起。最后Composition API拥有更好的类型推断,对ts支持更友好,Options API在设计之初并未考虑类型推断因素,虽然官方为此做了很多复杂的类型体操,确保用户可以在使用Options API时获得类型推断,然而还是没办法用在mixins和provide/inject上。Vue3首推Composition API,但是这会让我们在代码组织上多花点心思,因此在选择上,如果我们项目属于中低复杂度的场景,Options API仍是一个好选择。对于那些大型,高扩展,强维护的项目上,Composition API会获得更大收益。
可能的追问
Composition API能否和Options API一起使用?
29-vue-router中如何保护路由?
分析
路由保护在应用开发过程中非常重要,几乎每个应用都要做各种路由权限管理,因此相当考察使用者基本功。
体验
全局守卫:
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) } } }}
登录后复制
思路
路由守卫的概念路由守卫的使用路由守卫的原理vue-router中保护路由的方法叫做路由守卫,主要用来通过跳转或取消的方式守卫导航。路由守卫有三个级别:全局,路由独享,组件级。影响范围由大到小,例如全局的router.beforeEach(),可以注册一个全局前置守卫,每次路由导航都会经过这个守卫,因此在其内部可以加入控制逻辑决定用户是否可以导航到目标路由;在路由注册的时候可以加入单路由独享的守卫,例如beforeEnter,守卫只在进入路由时触发,因此只会影响这个路由,控制更精确;我们还可以为路由组件添加守卫配置,例如beforeRouteEnter,会在渲染该组件的对应路由被验证前调用,控制的范围更精确了。用户的任何导航行为都会走navigate方法,内部有个guards队列按顺序执行用户注册的守卫钩子函数,如果没有通过验证逻辑则会取消原有的导航。
知其所以然
runGuardQueue(guards)链式的执行用户在各级别注册的守卫钩子函数,通过则继续下一个级别的守卫,不通过进入catch流程取消原本导航。
【相关视频教程推荐:vue】
class Store { constructor(options) { this.state = reactive(options.state) this.options = options } commit(type, payload) { this.options.mutations[type].call(this, this.state, payload) }}
登录后复制
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) }}
登录后复制
const app = createApp({ watch: { '$store.state.counter'() { console.log('counter change!'); } } })
登录后复制
store.subscribe((mutation, state) => { if (mutation.type === 'add') { console.log('counter change in subscribe()!'); } })
登录后复制
Go to About
登录后复制登录后复制
// literal string pathrouter.push('/users/eduardo')// object with pathrouter.push({ path: '/users/eduardo' })// named route with params to let the router build the urlrouter.push({ name: 'user', params: { username: 'eduardo' } })
登录后复制
function reactive(obj) { return new Proxy(obj, { get(target, key) {}, set(target, key, val) {}, deleteProperty(target, key){} })}
登录后复制
function defineReactive(obj, key, val) { Object.defineReactive(obj, key, { get(key) {}, set(key, val) {} })}
登录后复制
const router = createRouter({ history: createWebHashHistory(), // hash模式 history: createWebHistory(), // history模式})
登录后复制
Go to About
登录后复制登录后复制
// hash// 浏览器里的形态:http://xx.com/#/about// history// 浏览器里的形态:http://xx.com/about
登录后复制
const routes = [ { path: '/user/:id', component: User, children: [ { // UserProfile 会被渲染在 User 组件中的 里 path: 'profile', component: UserProfile, }, { // UserPosts 会被渲染在 User 组件中的 里 path: 'posts', component: UserPosts, }, ], },]
登录后复制
const store = createStore({ state () { return { count: localStorage.getItem('count') } }})
登录后复制
store.commit('increment')localStorage.setItem('count', store.state.count)
登录后复制
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时变成了全局的,能同时触发多个子模块中同名mutationstore.commit('a/d') // -> 有namespaced时要加path,配合mapMutations使用感觉也没简化
登录后复制
if (!isRoot && !hot) { // 获取父模块state const parentState = getNestedState(rootState, path.slice(0, -1)) // 获取子模块名称 const moduleName = path[path.length - 1] store._withCommit(() => { // 把子模块state设置到父模块上 parentState[moduleName] = module.state })}
登录后复制
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) { // 在导航离开渲染该组件的对应路由时调用 },}
登录后复制
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至253000106@qq.com举报,一经查实,本站将立刻删除。
发布者:PHP中文网,转转请注明出处:https://www.chuangxiangniao.com/p/3158834.html