怎么缓存当前的组件?缓存后怎么更新?
缓存组件使用keep-alive组件,这是一个非常常见且有用的优化手段,vue3中keep-alive有比较大的更新,能说的点比较多。
思路
- 缓存用keep-alive,它的作用与用法
- 使用细节,例如缓存指定/排除、结合router和transition
- 组件缓存后更新可以利用activated或者beforeRouteEnter
- 原理阐述
回答范例
-
开发中缓存组件使用keep-alive组件,keep-alive是vue内置组件,keep-alive包裹动态组件component时,会缓存不活动的组件实例,而不是销毁它们,这样在组件切换过程中将状态保留在内存中,防止重复渲染DOM。
<keep-alive> <component :is="view"></component> </keep-alive>
-
结合属性include和exclude可以明确指定缓存哪些组件或排除缓存指定组件。vue3中结合vue-router时变化较大,之前是
keep-alive
包裹router-view
,现在需要反过来用router-view
包裹keep-alive
:<router-view v-slot="{ Component }"> <keep-alive> <component :is="Component"></component> </keep-alive> </router-view>
-
缓存后如果要获取数据,解决方案可以有以下两种:
-
beforeRouteEnter:在有vue-router的项目,每次进入路由的时候,都会执行
beforeRouteEnter
beforeRouteEnter(to, from, next){ next(vm=>{ console.log(vm) // 每次进入路由执行 vm.getData() // 获取数据 }) },
-
actived:在
keep-alive
缓存的组件被激活的时候,都会执行actived
钩子activated(){ this.getData() // 获取数据 },
-
- keep-alive是一个通用组件,它内部定义了一个map,缓存创建过的组件实例,它返回的渲染函数内部会查找内嵌的component组件对应组件的vnode,如果该组件在map中存在就直接返回它。由于component的is属性是个响应式数据,因此只要它变化,keep-alive的render函数就会重新执行。
测试缓存特性,test-v3.html
说说从 template 到 render 处理过程
分析
问我们template到render过程,其实是问vue编译器
工作原理。
思路
- 引入vue编译器概念
- 说明编译器的必要性
- 阐述编译器工作流程
回答范例
- Vue中有个独特的编译器模块,称为“compiler”,它的主要作用是将用户编写的template编译为js中可执行的render函数。
- 之所以需要这个编译过程是为了便于前端程序员能高效的编写视图模板。相比而言,我们还是更愿意用HTML来编写视图,直观且高效。手写render函数不仅效率底下,而且失去了编译期的优化能力。
- 在Vue中编译器会先对template进行解析,这一步称为parse,结束之后会得到一个JS对象,我们成为抽象语法树AST,然后是对AST进行深加工的转换过程,这一步成为transform,最后将前面得到的AST生成为JS代码,也就是render函数。
知其所以然
vue3编译过程窥探:
https://github1s.com/vuejs/core/blob/HEAD/packages/compiler-core/src/compile.ts#L61-L62
测试,test-v3.html
可能的追问
- Vue中编译器何时执行?
- react有没有编译器?
Vue 3.0的设计目标是什么?做了哪些优化?
分析
还是问新特性,陈述典型新特性,分析其给你带来的变化即可。
思路
从以下几方面分门别类阐述:易用性、性能、扩展性、可维护性、开发体验等
范例
- 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
有什么不同?
25-你了解哪些Vue性能优化方法?
分析
这是一道综合实践题目,写过一定数量的代码之后小伙伴们自然会开始关注一些优化方法,答得越多肯定实践经验也越丰富,是很好的题目。
答题思路:
根据题目描述,这里主要探讨Vue代码层面的优化
回答范例
-
我这里主要从Vue代码编写层面说一些优化手段,例如:代码分割、服务端渲染、组件缓存、长列表优化等
-
最常见的路由懒加载:有效拆分App尺寸,访问时才异步加载
const router = createRouter({ routes: [ // 借助webpack的import()实现异步组件 { path: '/foo', component: () => import('./Foo.vue') } ] })
-
keep-alive
缓存页面:避免重复创建组件实例,且能保留缓存组件状态<router-view v-slot="{ Component }"> <keep-alive> <component :is="Component"></component> </keep-alive> </router-view>
-
使用
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>
-
v-for
遍历避免同时使用v-if
:实际上在Vue3中已经是个错误写法<template> <ul> <li v-for="user in activeUsers" <!-- 避免同时使用,vue3中会报错 --> <!-- v-if="user.isActive" --> :key="user.id"> {{ user.name }} </li> </ul> </template> <script> export default { computed: { activeUsers: function () { return this.users.filter(user => user.isActive) } } } </script>
-
v-once和v-memo:不再变化的数据使用
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-momo
:下面这个列表只会更新选中状态变化项<div v-for="item in list" :key="item.id" v-memo="[item.id === selected]"> <p>ID: {{ item.id }} - selected: {{ item.id === selected }}</p> <p>...more child nodes</p> </div>
https://vuejs.org/api/built-in-directives.html#v-memo
-
长列表性能优化:如果是大数据长列表,可采用虚拟滚动,只渲染少部分区域的内容
<recycle-scroller class="items" :items="items" :item-size="24" > <template v-slot="{ item }"> <FetchItemView :item="item" @vote="voteItem(item)" /> </template> </recycle-scroller>
-
事件的销毁:Vue 组件销毁时,会自动解绑它的全部指令及事件监听器,但是仅限于组件本身的事件。
export default { created() { this.timer = setInterval(this.refresh, 2000) }, beforeUnmount() { clearInterval(this.timer) } }
-
图片懒加载
对于图片过多的页面,为了加速页面加载速度,所以很多时候我们需要将页面内未出现在可视区域内的图片先不做加载, 等到滚动到可视区域后再去加载。
<img v-lazy="/static/img/1.png">
参考项目:vue-lazyload
-
第三方插件按需引入
像
element-plus
这样的第三方组件库可以按需引入避免体积太大。import { createApp } from 'vue'; import { Button, Select } from 'element-plus'; const app = createApp() app.use(Button) app.use(Select)
-
子组件分割策略:较重的状态组件适合拆分
<template> <div> <ChildComp/> </div> </template> <script> export default { components: { ChildComp: { methods: { heavy () { /* 耗时任务 */ } }, render (h) { return h('div', this.heavy()) } } } } </script>
但同时也不宜过度拆分组件,尤其是为了所谓组件抽象将一些不需要渲染的组件特意抽出来,组件实例消耗远大于纯dom节点。参考:https://vuejs.org/guide/best-practices/performance.html#avoid-unnecessary-component-abstractions
-
服务端渲染/静态网站生成:SSR/SSG
如果SPA应用有首屏渲染慢的问题,可以考虑SSR、SSG方案优化。参考SSR Guide
26-Vue组件为什么只能有一个根元素?
这题现在有些落伍,vue3
已经不用一个根了。因此这题目很有说头!
体验一下
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')
回答思路
- 给一条自己的结论
- 解释为什么会这样
vue3
解决方法原理
范例
vue2
中组件确实只能有一个根,但vue3
中组件已经可以多根节点了。- 之所以需要这样是因为
vdom
是一颗单根树形结构,patch
方法在遍历的时候从根节点开始遍历,它要求只有一个根节点。组件也会转换为一个vdom
,自然应该满足这个要求。 vue3
中之所以可以写多个根节点,是因为引入了Fragment
的概念,这是一个抽象的节点,如果发现组件是多根的,就创建一个Fragment节点,把多个根节点作为它的children。将来patch的时候,如果发现是一个Fragment节点,则直接遍历children创建或更新。
27-你有使用过vuex的module吗?
这是基本应用能力考察,稍微上点规模的项目都要拆分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
思路
- 概念和必要性
- 怎么拆
- 使用细节
- 优缺点
范例
- 用过module,项目规模变大之后,单独一个store对象会过于庞大臃肿,通过模块方式可以拆分开来便于维护
- 可以按之前规则单独编写子模块代码,然后在主文件中通过
modules
选项组织起来:createStore({modules:{...}})
- 不过使用时要注意访问子模块状态时需要加上注册时模块名:
store.state.a.xxx
,但同时getters
、mutations
和actions
又在全局空间中,使用方式和之前一样。如果要做到完全拆分,需要在子模块加上namespace
选项,此时再访问它们就要加上命名空间前缀。 - 很显然,模块的方式可以拆分代码,但是缺点也很明显,就是使用起来比较繁琐复杂,容易出错。而且类型系统支持很差,不能给我们带来帮助。pinia显然在这方面有了很大改进,是时候切换过去了。
可能的追问
- 用过pinia吗?都做了哪些改善?
28-怎么实现路由懒加载呢?
分析
这是一道应用题。当打包应用时,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中结合rollupOptions定义分块
-
路由中不能使用异步组件
29-ref和reactive异同
这是Vue3
数据响应式中非常重要的两个概念,自然的,跟我们写代码关系也很大。
体验
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++
回答思路
- 两者概念
- 两者使用场景
- 两者异同
- 使用细节
- 原理
回答范例
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,拦截用户对值的访问,从而实现响应式。
30-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 0
count.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})
31-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反而浪费资源,我们可以考虑预渲染(prerender)方案。另外nuxt.js/next.js中给我们提供了SSG(Static Site Generate)静态网站生成方案也是很好的静态站点解决方案,结合一些CI手段,可以起到很好的优化效果,且能节约服务器资源。
32-你写过自定义指令吗?使用场景有哪些?
分析
这是一道API题,我们可能写的自定义指令少,但是我们用的多呀,多举几个例子就行。
体验
定义一个包含类似组件生命周期钩子的对象,钩子函数会接收指令挂钩的dom元素:
const focus = {
mounted: (el) => el.focus()
}
export default {
directives: {
// enables v-focus in template
focus
}
}
<input v-focus />
<input v-focus />
思路
- 定义
- 何时用
- 如何用
- 常用指令
- vue3变化
回答范例
- Vue有一组默认指令,比如
v-mode
l或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开头方便的定义自定义指令,更简单了!
33-说下$attrs和$listeners的使用场景
分析
API考察,但$attrs和 l i s t e n e r s 是比较少用的边界知识,而且 v u e 3 有变化, listeners是比较少用的边界知识,而且vue3有变化, listeners是比较少用的边界知识,而且vue3有变化,listeners已经移除,还是有细节可说的。
思路
- 这两个api的作用
- 使用场景分析
- 使用方式和细节
- vue3变化
体验
一个包含组件透传属性的对象。
An object that contains the component’s fallthrough attributes.
<template>
<child-component v-bind="$attrs">
将非属性特性透传给内部的子组件
</child-component>
</template>
范例
- 我们可能会有一些属性和事件没有在props中定义,这类称为非属性特性,结合v-bind指令可以直接透传给内部的子组件。
- 这类“属性透传”常常用于包装高阶组件时往内部传递属性,常用于爷孙组件之间传参。比如我在扩展A组件时创建了组件B组件,然后在C组件中使用B,此时传递给C的属性中只有props里面声明的属性是给B使用的,其他的都是A需要的,此时就可以利用v-bind="$attrs"透传下去。
- 最常见用法是结合v-bind做展开;$attrs本身不是响应式的,除非访问的属性本身是响应式对象。
- vue2中使用 l i s t e n e r s 获取事件, v u e 3 中已移除,均合并到 listeners获取事件,vue3中已移除,均合并到 listeners获取事件,vue3中已移除,均合并到attrs中,使用起来更简单了。
原理
查看透传属性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"
})
34-v-once的使用场景有哪些?
分析
v-once
是Vue中内置指令,很有用的API,在优化方面经常会用到,不过小伙伴们平时可能容易忽略它。
体验
仅渲染元素和组件一次,并且跳过未来更新
Render the element and component once only, and skip future updates.
<!-- 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
是什么- 什么时候使用
- 如何使用
- 扩展
v-memo
- 探索原理
回答范例
v-once
是vue的内置指令,作用是仅渲染指定组件或元素一次,并跳过未来对其更新。- 如果我们有一些元素或者组件在初始化渲染之后不再需要变化,这种情况下适合使用
v-once
,这样哪怕这些数据变化,vue也会跳过更新,是一种代码优化手段。 - 我们只需要作用的组件或元素上加上v-once即可。
- vue3.2之后,又增加了
v-memo
指令,可以有条件缓存部分模板并控制它们的更新,可以说控制力更强了。 - 编译器发现元素上面有v-once时,会将首次计算结果存入缓存对象,组件再次渲染时就会从缓存获取,从而避免再次计算。
知其所以然
下面例子使用了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]
),
// ...
35-什么是递归组件?举个例子说明下?
分析
递归组件我们用的比较少,但是在Tree、Menu这类组件中会被用到。
体验
组件通过组件名称引用它自己,这种情况就是递归组件。
An SFC can implicitly refer to itself via its filename.
<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>
思路
- 下定义
- 使用场景
- 使用细节
- 原理阐述
回答范例
- 如果某个组件通过组件名称引用它自己,这种情况就是递归组件。
- 实际开发中类似Tree、Menu这类组件,它们的节点往往包含子节点,子节点结构和父节点往往是相同的。这类组件的数据往往也是树形结构,这种都是使用递归组件的典型场景。
- 使用递归组件时,由于我们并未也不能在组件内部导入它自己,所以设置组件
name
属性,用来查找组件定义,如果使用SFC,则可以通过SFC文件名推断。组件内部通常也要有递归结束条件,比如model.children这样的判断。 - 查看生成渲染函数可知,递归组件查找时会传递一个布尔值给
resolveComponent
,这样实际获取的组件就是当前组件本身。
知其所以然
递归组件编译结果中,获取组件时会传递一个标识符 _resolveComponent("Comp", true)
const _component_Comp = _resolveComponent("Comp", true)
就是在传递maybeSelfReference
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
}
36-异步组件是什么?使用场景有哪些?
分析
因为异步路由的存在,我们使用异步组件的次数比较少,因此还是有必要两者的不同。
体验
大型应用中,我们需要分割应用为更小的块,并且在需要组件时再加载它们。
In large applications, we may need to divide the app into smaller chunks and only load a component from the server when it’s needed.
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指定一个loader函数,结合ES模块动态导入函数import可以快速实现。我们甚至可以指定loadingComponent和errorComponent选项从而给用户一个很好的加载反馈。另外Vue3中还可以结合Suspense组件使用异步组件。
- 异步组件容易和路由懒加载混淆,实际上不是一个东西。异步组件不能被用于定义懒加载路由上,处理它的是vue框架,处理路由组件加载的是vue-router。但是可以在懒加载的路由组件中使用异步组件。
知其所以然
defineAsyncComponent定义了一个高阶组件,返回一个包装组件。包装组件根据加载器的状态决定渲染什么内容。
https://github1s.com/vuejs/core/blob/HEAD/packages/runtime-core/src/apiAsyncComponent.ts#L43-L44
37-你是怎么处理vue项目中的错误的?
分析
这是一个综合应用题目,在项目中我们常常需要将App的异常上报,此时错误处理就很重要了。
这里要区分错误的类型,针对性做收集。
然后是将收集的的错误信息上报服务器。
思路
- 首先区分错误类型
- 根据错误不同类型做相应收集
- 收集的错误是如何上报服务器的
回答范例
- 应用中的错误类型分为"
接口异常
"和“代码逻辑异常
” - 我们需要根据不同错误类型做相应处理:
接口异常
是我们请求后端接口过程中发生的异常,可能是请求失败,也可能是请求获得了服务器响应,但是返回的是错误状态。以Axios为例,这类异常我们可以通过封装Axios,在拦截器中统一处理整个应用中请求的错误。代码逻辑异常
是我们编写的前端代码中存在逻辑上的错误造成的异常,vue应用中最常见的方式是使用全局错误处理函数app.config.errorHandler
收集错误。 - 收集到错误之后,需要统一处理这些异常:分析错误,获取需要错误信息和数据。这里应该有效区分错误类型,如果是请求错误,需要上报接口信息,参数,状态码等;对于前端逻辑异常,获取错误名称和详情即可。另外还可以收集应用名称、环境、版本、用户信息,所在页面等。这些信息可以通过vuex存储的全局状态和路由信息获取。
实践
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)
}
}
}
}
子组件可以直接改变父组件的数据么,说明原因
分析
这是一个实践知识点,组件化开发过程中有个单项数据流原则,不在子组件中修改父组件是个常识问题。
参考文档:https://staging.vuejs.org/guide/components/props.html#one-way-data-flow
思路
- 讲讲单项数据流原则,表明为何不能这么做
- 举几个常见场景的例子说说解决方案
- 结合实践讲讲如果需要修改父组件状态应该如何做
回答范例
- 所有的 prop 都使得其父子之间形成了一个单向下行绑定:父级 prop 的更新会向下流动到子组件中,但是反过来则不行。这样会防止从子组件意外变更父级组件的状态,从而导致你的应用的数据流向难以理解。另外,每次父级组件发生变更时,子组件中所有的 prop 都将会刷新为最新的值。这意味着你不应该在一个子组件内部改变 prop。如果你这样做了,Vue 会在浏览器控制台中发出警告。
const props = defineProps([‘foo’])
// ❌ 下面行为会被警告, props是只读的!
props.foo = ‘bar’
- 实际开发过程中有两个场景会想要修改一个属性:
- 这个 prop 用来传递一个初始值;这个子组件接下来希望将其作为一个本地的 prop 数据来使用。在这种情况下,最好定义一个本地的 data,并将这个 prop 用作其初始值:
const props = defineProps([‘initialCounter’])
const counter = ref(props.initialCounter) - 这个 prop 以一种原始的值传入且需要进行转换。在这种情况下,最好使用这个 prop 的值来定义一个计算属性:
const props = defineProps([‘size’])
// prop变化,计算属性自动更新
const normalizedSize = computed(() => props.size.trim().toLowerCase())
- 这个 prop 用来传递一个初始值;这个子组件接下来希望将其作为一个本地的 prop 数据来使用。在这种情况下,最好定义一个本地的 data,并将这个 prop 用作其初始值:
- 实践中如果确实想要改变父组件属性应该emit一个事件让父组件去做这个变更。注意虽然我们不能直接修改一个传入的对象或者数组类型的prop,但是我们还是能够直接改内嵌的对象或属性。
Vue要做权限管理该怎么做?控制到按钮级别的权限怎么做?
分析
综合实践题目,实际开发中经常需要面临权限管理的需求,考查实际应用能力。
权限管理一般需求是两个:页面权限和按钮权限,从这两个方面论述即可。
思路
- 权限管理需求分析:页面和按钮权限
- 权限管理的实现方案:分后端方案和前端方案阐述
- 说说各自的优缺点
回答范例
-
权限管理一般需求是页面权限和按钮权限的管理
-
具体实现的时候分后端和前端两种方案:
前端方案会把所有路由信息在前端配置,通过路由守卫要求用户登录,用户登录后根据角色过滤出路由表。比如我会配置一个
asyncRoutes
数组,需要认证的页面在其路由的meta
中添加一个roles
字段,等获取用户角色之后取两者的交集,若结果不为空则说明可以访问。此过滤过程结束,剩下的路由就是该用户能访问的页面,最后通过router.addRoutes(accessRoutes)
方式动态添加路由即可。后端方案会把所有页面路由信息存在数据库中,用户登录的时候根据其角色查询得到其能访问的所有页面路由信息返回给前端,前端再通过
addRoutes
动态添加路由信息按钮权限的控制通常会实现一个指令,例如
v-permission
,将按钮要求角色通过值传给v-permission指令,在指令的moutned
钩子中可以判断当前用户角色和按钮是否存在交集,有则保留按钮,无则移除按钮。 -
纯前端方案的优点是实现简单,不需要额外权限管理页面,但是维护起来问题比较大,有新的页面和角色需求就要修改前端代码重新打包部署;服务端方案就不存在这个问题,通过专门的角色和权限管理页面,配置页面和按钮权限信息到数据库,应用每次登陆时获取的都是最新的路由信息,可谓一劳永逸!
可能的追问
-
类似
Tabs
这类组件能不能使用v-permission
指令实现按钮权限控制?<el-tabs> <el-tab-pane label="⽤户管理" name="first">⽤户管理</el-tab-pane> <el-tab-pane label="⻆⾊管理" name="third">⻆⾊管理</el-tab-pane> </el-tabs>
-
服务端返回的路由信息如何添加到路由器中?
// 前端组件名和组件映射表 const map = { //xx: require('@/views/xx.vue').default // 同步的⽅式 xx: () => import('@/views/xx.vue') // 异步的⽅式 } // 服务端返回的asyncRoutes const asyncRoutes = [ { path: '/xx', component: 'xx',... } ] // 遍历asyncRoutes,将component替换为map[component] function mapComponent(asyncRoutes) { asyncRoutes.forEach(route => { route.component = map[route.component]; if(route.children) { route.children.map(child => mapComponent(child)) } }) } mapComponent(asyncRoutes)
19-从0到1自己构架一个vue项目,说说有哪些步骤、哪些重要插件、目录结构你会怎么组织
综合实践类题目,考查实战能力。没有什么绝对的正确答案,把平时工作的重点有条理的描述一下即可。
思路
- 构建项目,创建项目基本结构
- 引入必要的插件:
- 代码规范:prettier,eslint
- 提交规范:husky,lint-staged
- 其他常用:svg-loader,vueuse,nprogress
- 常见目录结构
回答范例
-
从0创建一个项目我大致会做以下事情:项目构建、引入必要插件、代码规范、提交规范、常用库和组件
-
目前vue3项目我会用vite或者create-vue创建项目
-
接下来引入必要插件:路由插件vue-router、状态管理vuex/pinia、ui库我比较喜欢element-plus和antd-vue、http工具我会选axios
-
其他比较常用的库有vueuse,nprogress,图标可以使用vite-svg-loader
-
下面是代码规范:结合prettier和eslint即可
-
最后是提交规范,可以使用husky,lint-staged,commitlint
-
目录结构我有如下习惯:
.vscode
:用来放项目中的 vscode 配置plugins
:用来放 vite 插件的 plugin 配置public
:用来放一些诸如 页头icon 之类的公共文件,会被打包到dist根目录下src
:用来放项目代码文件api
:用来放http的一些接口配置assets
:用来放一些 CSS 之类的静态资源components
:用来放项目通用组件layout
:用来放项目的布局router
:用来放项目的路由配置store
:用来放状态管理Pinia的配置utils
:用来放项目中的工具方法类views
:用来放项目的页面文件
实际工作中,你总结的vue最佳实践有哪些?
思路
查看vue官方文档:
风格指南:https://vuejs.org/style-guide/
性能:https://vuejs.org/guide/best-practices/performance.html#overview
安全:https://vuejs.org/guide/best-practices/security.html
访问性:https://vuejs.org/guide/best-practices/accessibility.html
发布:https://vuejs.org/guide/best-practices/production-deployment.html
回答范例
我从编码风格、性能、安全等方面说几条:
- 编码风格方面:
- 命名组件时使用“多词”风格避免和HTML元素冲突
- 使用“细节化”方式定义属性而不是只有一个属性名
- 属性名声明时使用“驼峰命名”,模板或jsx中使用“肉串命名”
- 使用v-for时务必加上key,且不要跟v-if写在一起
- 性能方面:
- 路由懒加载减少应用尺寸
- 利用SSR减少首屏加载时间
- 利用v-once渲染那些不需要更新的内容
- 一些长列表可以利用虚拟滚动技术避免内存过度占用
- 对于深层嵌套对象的大数组可以使用shallowRef或shallowReactive降低开销
- 避免不必要的组件抽象
- 安全:
- 不使用不可信模板,例如使用用户输入拼接模板:
template: <div> + userProvidedString + </div>
- 小心使用v-html,:url,:style等,避免html、url、样式等注入
- 不使用不可信模板,例如使用用户输入拼接模板:
- 等等…
Vue实例挂载的过程中发生了什么?
分析
挂载过程完成了最重要的两件事:
- 初始化
- 建立更新机制
把这两件事说清楚即可!
回答范例
- 挂载过程指的是app.mount()过程,这个是个初始化过程,整体上做了两件事:初始化和建立更新机制
- 初始化会创建组件实例、初始化组件状态、创建各种响应式数据
- 建立更新机制这一步会立即执行一次组件更新函数,这会首次执行组件渲染函数并执行patch将前面获得vnode转换为dom;同时首次执行渲染函数会创建它内部响应式数据和组件更新函数之间的依赖关系,这使得以后数据变化时会执行对应的更新函数。
可能的追问
- 响应式数据怎么创建
- 依赖关系如何建立
vue-loader是什么?它有什么作用?
分析
这是一道工具类的原理题目,相当有深度,具有不错的人才区分度。
体验
使用官方提供的SFC playground可以很好的体验vue-loader。
有了vue-loader加持,我们才可以以SFC的方式快速编写代码。
<template>
<div class="example">{{ msg }}</div>
</template>
<script>
export default {
data() {
return {
msg: 'Hello world!',
}
},
}
</script>
<style>
.example {
color: red;
}
</style>
思路
vue-loader
是什么东东vue-loader
是做什么用的vue-loader
何时生效vue-loader
如何工作
回答范例
vue-loader
是用于处理单文件组件(SFC,Single-File Component)的webpack loader- 因为有了
vue-loader
,我们就可以在项目中编写SFC格式的Vue组件,我们可以把代码分割为、
知其所以然
vue-loader
会调用@vue/compiler-sfc
模块解析SFC源码为一个描述符(Descriptor),然后为每个语言块生成import代码,返回的代码类似下面:
// 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
- 我们想要script块中的内容被作为js处理(当然如果是
<script lang="ts">
被作为ts处理),这样我们想要webpack把配置中跟.js匹配的规则都应用到形如source.vue?vue&type=script
的这个请求上。例如我们对所有*.js配置了babel-loader,这个规则将被克隆并应用到所在Vue SFC的
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,下面的代码:
<style scoped lang="scss">
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'
- 当处理展开请求时,vue-loader将被再次调用。这次,loader将会关注那些有查询串的请求,且仅针对特定块,它会选中特定块内部的内容并传递给后面匹配的loader。
- 对于
<script>
块,处理到这就可以了,但是<template>
和<style>
还有一些额外任务要做,比如:
- 需要用Vue 模板编译器编译template,从而得到render函数
- 需要对
<style scoped>
中的CSS做后处理(post-process),该操作在css-loader之后但在style-loader之前
实现上这些附加的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'
vue-loader是什么?它有什么作用?
分析
这是一道工具类的原理题目,相当有深度,具有不错的人才区分度。
体验
使用官方提供的SFC playground可以很好的体验vue-loader。
有了vue-loader加持,我们才可以以SFC的方式快速编写代码。
<template>
<div class="example">{{ msg }}</div>
</template>
<script>
export default {
data() {
return {
msg: 'Hello world!',
}
},
}
</script>
<style>
.example {
color: red;
}
</style>
思路
vue-loader
是什么东东vue-loader
是做什么用的vue-loader
何时生效vue-loader
如何工作
回答范例
vue-loader
是用于处理单文件组件(SFC,Single-File Component)的webpack loader- 因为有了
vue-loader
,我们就可以在项目中编写SFC格式的Vue组件,我们可以把代码分割为、
知其所以然
vue-loader
会调用@vue/compiler-sfc
模块解析SFC源码为一个描述符(Descriptor),然后为每个语言块生成import代码,返回的代码类似下面:
// 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
- 我们想要script块中的内容被作为js处理(当然如果是
<script lang="ts">
被作为ts处理),这样我们想要webpack把配置中跟.js匹配的规则都应用到形如source.vue?vue&type=script
的这个请求上。例如我们对所有*.js配置了babel-loader,这个规则将被克隆并应用到所在Vue SFC的
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,下面的代码:
<style scoped lang="scss">
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'
- 当处理展开请求时,vue-loader将被再次调用。这次,loader将会关注那些有查询串的请求,且仅针对特定块,它会选中特定块内部的内容并传递给后面匹配的loader。
- 对于
<script>
块,处理到这就可以了,但是<template>
和<style>
还有一些额外任务要做,比如:
- 需要用Vue 模板编译器编译template,从而得到render函数
- 需要对
<style scoped>
中的CSS做后处理(post-process),该操作在css-loader之后但在style-loader之前
实现上这些附加的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'
12 - 怎么定义动态路由?怎么获取传过来的动态参数?
分析
API题目,考查基础能力,不容有失,尽可能说的详细。
思路
- 什么是动态路由
- 什么时候使用动态路由,怎么定义动态路由
- 参数如何获取
- 细节、注意事项
回答范例
- 很多时候,我们需要将给定匹配模式的路由映射到同一个组件,这种情况就需要定义动态路由。
- 例如,我们可能有一个
User
组件,它应该对所有用户进行渲染,但用户 ID 不同。在 Vue Router 中,我们可以在路径中使用一个动态字段来实现,例如:{ path: '/users/:id', component: User }
,其中:id
就是路径参数 - 路径参数 用冒号
:
表示。当一个路由被匹配时,它的 params 的值将在每个组件中以this.$route.params
的形式暴露出来。 - 参数还可以有多个,例如
/users/:username/posts/:postId
;除了$route.params
之外,$route
对象还公开了其他有用的信息,如$route.query
、$route.hash
等。
可能的追问
- 如何响应动态路由参数的变化
https://router.vuejs.org/zh/guide/essentials/dynamic-matching.html#%E5%93%8D%E5%BA%94%E8%B7%AF%E7%94%B1%E5%8F%82%E6%95%B0%E7%9A%84%E5%8F%98%E5%8C%96
- 我们如何处理404 Not Found路由
https://router.vuejs.org/zh/guide/essentials/dynamic-matching.html#%E6%8D%95%E8%8E%B7%E6%89%80%E6%9C%89%E8%B7%AF%E7%94%B1%E6%88%96-404-not-found-%E8%B7%AF%E7%94%B1
如果让你从零开始写一个vuex,说说你的思路
思路分析
这个题目很有难度,首先思考vuex
解决的问题:存储用户全局状态并提供管理状态API。
vuex
需求分析- 如何实现这些需求
回答范例
-
官方说
vuex
是一个状态管理模式和库,并确保这些状态以可预期的方式变更。可见要实现一个vuex
:- 要实现一个
Store
存储全局状态 - 要提供修改状态所需API:
commit(type, payload)
,dispatch(type, payload)
- 要实现一个
-
实现
Store
时,可以定义Store类,构造函数接收选项options,设置属性state对外暴露状态,提供commit和dispatch修改属性state。这里需要设置state为响应式对象,同时将Store定义为一个Vue插件。 -
commit(type, payload)
方法中可以获取用户传入mutations
并执行它,这样可以按用户提供的方法修改状态。dispatch(type, payload)
类似,但需要注意它可能是异步的,需要返回一个Promise给用户以处理异步结果。
实践
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
vuex中actions和mutations有什么区别?
题目分析
mutations
和actions
是vuex
带来的两个独特的概念。新手程序员容易混淆,所以面试官喜欢问。
我们只需记住修改状态只能是mutations
,actions
只能通过提交mutation
修改状态即可。
体验
看下面例子可知,Action
类似于 mutation
,不同在于:
Action
提交的是mutation
,而不是直接变更状态。Action
可以包含任意异步操作。
const store = createStore({
state: {
count: 0
},
mutations: {
increment (state) {
state.count++
}
},
actions: {
increment (context) {
context.commit('increment')
}
}
})
答题思路
- 给出两者概念说明区别
- 举例说明应用场景
- 使用细节不同
- 简单阐述实现上差异
回答范例
- 官方文档说:更改 Vuex 的 store 中的状态的唯一方法是提交
mutation
,mutation
非常类似于事件:每个mutation
都有一个字符串的类型 (type)**和一个**回调函数 (handler)。Action
类似于mutation
,不同在于:Action
可以包含任意异步操作,但它不能修改状态, 需要提交mutation
才能变更状态。 - 因此,开发时,包含异步操作或者复杂业务组合时使用action;需要直接修改状态则提交mutation。但由于dispatch和commit是两个API,容易引起混淆,实践中也会采用统一使用dispatch action的方式。
- 调用dispatch和commit两个API时几乎完全一样,但是定义两者时却不甚相同,mutation的回调函数接收参数是state对象。action则是与Store实例具有相同方法和属性的上下文context对象,因此一般会解构它为
{commit, dispatch, state}
,从而方便编码。另外dispatch会返回Promise实例便于处理内部异步结果。 - 实现上commit(type)方法相当于调用
options.mutations[type](state)
;dispatch(type)
方法相当于调用options.actions[type](store)
,这样就很容易理解两者使用上的不同了。
知其所以然
我们可以像下面这样简单实现commit
和dispatch
,从而辨别两者不同:
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)
}
}
使用vue渲染大量数据时应该怎么优化?说下你的思路!
分析
企业级项目中渲染大量数据的情况比较常见,因此这是一道非常好的综合实践题目。
思路
- 描述大数据量带来的问题
- 分不同情况做不同处理
- 总结一下
回答
- 在大型企业级项目中经常需要渲染大量数据,此时很容易出现卡顿的情况。比如大数据量的表格、树。
- 处理时要根据情况做不通处理:
- 可以采取分页的方式获取,避免渲染大量数据
- vue-virtual-scroller等虚拟滚动方案,只渲染视口范围内的数据
- 如果不需要更新,可以使用
v-once
方式只渲染一次 - 通过v-memo可以缓存结果,结合
v-for
使用,避免数据变化时不必要的VNode创建 - 可以采用懒加载方式,在用户需要的时候再加载数据,比如tree组件子树的懒加载
- 总之,还是要看具体需求,首先从设计上避免大数据获取和渲染;实在需要这样做可以采用虚表的方式优化渲染;最后优化更新,如果不需要更新可以v-once处理,需要更新可以v-memo进一步优化大数据更新性能。其他可以采用的是交互方式优化,无线滚动、懒加载等方案。
41-怎么监听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方式
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()!');
}
})
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在匹配数组结果中找到对应的路由并获取组件,最终将其渲染出来。