Vue3.x
Proxy响应式原理
shallowRef与shallowReactive的适用场景
watch与computed原理
watchEffect
vue3.0diff算法
Vue 3 的 Diff 算法(快速 Diff)通过 双端比较 + 最长递增子序列(LIS)优化,将时间复杂度从 O(n²) 降至 O(n log n),核心分为五步:预处理前置节点(头头比对)、预处理后置节点(尾尾比对)、处理仅有新增节点、处理仅有卸载节点、剩余节点存在新增、卸载、移动混合操作(LIS优化)

setup的上下文和使用
<script setup>里面的代码会被编译成组件 setup() 函数的内容。这意味着与普通的 <script> 只在组件被首次引入的时候仅执行一次不同,<script setup>中的代码会在每次组件实例被创建的时候执行,相当于beforeCreate,Created。
setup,defineProps,defineEmit,defineSlots
defineModel
defineExpose
defineOptions
与普通script一起用
顶层await支持
ref获取子组件实例的使用useTemplateRef
获取父组件实例
Vue通用
v-model双向绑定使用和原理defineModel
- vue中双向绑定是一个指令
v-model,可以绑定一个响应式数据到视图,同时视图中变化能改变该值。默认情况下相当于:value和@input。 - 通常在表单项上使用
v-model,还可以在自定义组件上使用,表示某个值的输入和输出控制。v-model用在自定义组件上时又会有很大不同,vue3中它类似于sync修饰符(:visible.sync="isVisible"),最终展开的结果是:modelValue属性和@update:modelValue事件;vue3中我们甚至可以用参数形式指定多个不同的绑定,例如v-model:foo和v-model:bar,非常强大!
nextTick的原理及vue异步更新
vue性能优化技巧
按需加载、动态绑定、内存缓存、异步处理
透传 Attributes
- “透传 attribute”指的是传递给一个组件,却没有被该组件声明为 props或 emits 的 attribute 或者
v-on事件监听器。最常见的例子就是class、style和id。 - 当一个组件以单个元素为根作渲染时,透传的 attribute 会自动被添加到根元素上。
- 如果一个子组件的根元素已经有了
class或styleattribute,它会和从父组件上继承的值合并 v-on事件监听器click监听器会被添加到<MyButton>的根元素,即那个原生的<button>元素之上。当原生的<button>被点击,会触发父组件的onClick方法。同样的,如果原生button元素自身也通过v-on绑定了一个事件监听器,则这个监听器和从父组件继承的监听器都会被触发。- 禁用 Attributes 继承,件选项中设置
inheritAttrs: false。透传进来的 attribute 可以在模板的表达式中直接用$attrs,这个$attrs对象包含了除组件所声明的props和emits之外的所有其他 attribute,例如class,style,v-on监听器等等;在<script setup>中使用useAttrs()API 来访问一个组件的所有透传 attribute。
组件通信常用方式
- 父子组件,
props/$emit/$parent/ref/$attrs - 兄弟组件,
$parent/$root/eventbus(on,$off)/vuex - 跨层级关系,
eventbus/vuex/provide+inject
provide和inject依赖注入
// 提供方
<script setup>
import { provide, ref } from 'vue'
const location = ref(0)
function updateLocation() {
location.value = 'South Pole'
}
provide('location', { location, updateLocation })
</script>
// 注入方
<script setup>
import { inject } from 'vue'
const { location, updateLocation } = inject('location')
</script>
- 一个组件可以多次调用
provide(),使用不同的注入名,注入不同的依赖值。 - 供的值是一个 ref,注入进来的会是该 ref 对象,这使得注入方组件能够通过 ref 对象保持了和供给方的响应性链接。
pinia的使用
如果子组件改变props里的数据会发生什么
- 如果修改的是基本类型,则会报错vue warning,prop 是只读的!
- 当对象或数组作为props被传入时,虽然子组件无法更改 props 绑定,但仍然可以更改对象或数组内部的值。这是因为 JavaScript 的对象和数组是按引用传递,而对 Vue 来说,禁止这样的改动,虽然可能生效,但有很大的性能损耗,比较得不偿失,并且父级数据会跟着变。改变引用类型地址也会报错。
自定义指令
<script setup>
const vFocus = {
// 在绑定元素的父组件及他自己的所有子节点都挂载完成后调用(binding传递给指令的值)
mounted: (el, binding, vnode, prevVnode) => el.focus()
// 在绑定元素的父组件及他自己的所有子节点都更新后调用(binding传递给指令的值)
updated: (el, binding, vnode, prevVnode) => {}
}
</script>
<template> <input v-focus /> </template>
// 将一个自定义指令全局注册到应用层级也是一种常见的做法
const app = createApp({})
// 使 v-focus 在所有组件中都可用
app.directive('focus', { /* ... */ })
- 在
<script setup>中,任何以v开头的驼峰式命名的变量都可以被用作一个自定义指令,vFocus即可以在模板中以v-focus的形式使用。 - 在没有使用
<script setup>的情况下,export default时自定义指令需要通过directives选项注册。 - 将一个自定义指令全局注册,
app.directive('focus', { /* ... */ }); - 当在组件上使用自定义指令时,它会始终应用于组件的根节点,和透传attrs(传入当前组件的所有属性和v-on事件)类似,和 attribute 不同,指令不能通过
v-bind="$attrs"来传递给一个不同的元素。 v-once是vue的内置指令,作用是仅渲染指定组件或元素一次,并跳过未来对其更新。编译器发现元素上面有v-once时,会将首次计算结果存入缓存对象,组件再次渲染时就会从缓存获取,从而避免再次计算。- vue3.2之后,又增加了
v-memo指令,可以有条件缓存部分模板并控制它们的更新,可以说控制力更强了。
Vue要做权限管理该怎么做?控制到按钮级别的权限怎么做?
- 页面权限,配置一个
asyncRoutes数组,需要认证的页面在其路由的meta中添加一个roles字段,通过路由守卫要求用户登录后获取用户角色,根据角色过滤出路由表,通过router.addRoutes(asyncRoutes)方式动态添加路由即可。 - 按钮权限,实现一个指令,例如
v-permission,将按钮要求角色通过值传给v-permission指令,在指令的moutned钩子中可以判断当前用户角色和按钮是否存在交集,有则保留按钮,无则移除按钮。
组合式函数
// fetch.js
import { ref, watchEffect, toValue } from 'vue'
export function useFetch(url) {
const data = ref(null)
const error = ref(null)
watchEffect(() => {
// 在 fetch 之前重置状态
data.value = null
error.value = null
// toValue() 将可能的 ref 或 getter 解包,规范化为值
fetch(toValue(url))
.then((res) => res.json())
.then((json) => (data.value = json))
.catch((err) => (error.value = err))
})
return { data, error }
}
<script setup>
import { ref, watchEffect, toValue } from 'vue'
import { useFetch } from './fetch.js'
const url = ref('/initial-url')
const { data, error } = useFetch(url)
// 这将会重新触发fetch
url.value = '/new-url'
</script>
- 这个版本的
useFetch()现在能接收静态 URL 字符串、ref 和 getter,使其更加灵活。watch effect 会立即运行,并且会跟踪toValue(url)期间访问的任何依赖项。如果没有跟踪到依赖项(例如 url 已经是字符串),则 effect 只会运行一次;否则,它将在跟踪到的任何依赖项更改时重新运行。 - 每一个调用
useFetch()的组件实例会创建其独有的data、error状态拷贝,因此他们不会互相影响。如果你想要在组件之间共享状态,请阅读状态管理这一章。 - 我们一直在组合式函数中使用
ref()而不是reactive()。我们推荐的约定是组合式函数始终返回一个包含多个 ref 的普通的非响应式对象,这样该对象在组件中被解构为 ref 之后仍可以保持响应性 - 在组合式函数中的确可以执行副作用 (例如:添加 DOM 事件监听器或者请求数据),但请注意以下规则:onMounted()确保在组件挂载后才调用的生命周期钩子中执行 DOM 相关的副作用,确保在
onUnmounted()时清理副作用。 - 组合式函数只能在
<script setup>或setup()钩子中被调用。在这些上下文中,它们也只能被同步调用。在某些情况下,你也可以在像onMounted()这样的生命周期钩子中调用它们。这些是 Vue 用于确定当前活跃的组件实例的上下文。访问活跃的组件实例很有必要,将生命周期钩子,1. 将计算属性和监听器注册到该组件实例上,以便在该组件被卸载时停止监听,避免内存泄漏。 <script setup>是唯一在调用await之后仍可调用组合式函数的地方。编译器会在异步操作之后自动为你恢复当前的组件实例。
递归组件
<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>
异步组件
import { defineAsyncComponent } from 'vue'
// defineAsyncComponent定义异步组件
const AsyncComp = defineAsyncComponent(() => {
// 加载函数返回Promise
return new Promise((resolve, reject) => {
// ...可以从服务器加载组件
resolve(/* loaded component */)
})
})
// 借助打包工具实现ES模块动态导入
const AsyncComp = defineAsyncComponent({
loader: () => import('./components/MyComponent.vue'),
loadingComponent: LoadingComponent, // 加载异步组件时使用的组件
delay: 200, // 展示加载组件前的延迟时间,默认为 200ms
errorComponent: ErrorComponent, // 加载失败后展示的组件
timeout: 3000
})
- 不仅可以在路由切换时懒加载组件,还可以在页面组件中继续使用异步组件,从而实现更细的分割粒度。(首次加载页面组件不会加载异步组件,分开打包,分割应用为更小的块,并且在需要组件时再加载)
- defineAsyncComponent定义了一个高阶组件,返回一个包装组件。包装组件根据加载器的状态决定渲染什么内容。
Suspense组件
插槽的使用以及原理
- 具名插槽,
v-slot有对应的简写#,因此<template v-slot:header>可以简写为<template #header>
- 作用域插槽,插槽的内容可能想要同时使用父组件域内和子组件域内的数据
- 原理在插槽函数slot调用时传入 props,MyComponent类比成函数调用,接收参数,
v-slot="slotProps"可以类比这里的函数签名,和函数的参数类似。

Vue的生命周期,讲一讲?
不改变子组件,父组件如何监听子组件的生命周期
父子组件生命周期顺序
说说你对虚拟 DOM 的理解
- 将真实元素节点抽象成VNode的一个
JavaScript对象,只不过它是通过不同的属性去描述一个视图结构,保存在内存中。

Vue的模板编译原理
KeepAlive组件怎么缓存当前的组件?缓存后怎么更新?
- LRU-使用map数据结构(先删除再设置),每当get和set操作map数据时,先判断map.has(key),map.delete(key),map.set(key, temp),当map.size大于固定长度时,删除前面多余的this.map.delete(this.map.keys().next().value)
Teleport组件
MVVM是什么?和MVC有何区别呢?
- Model(模型):负责从数据库中取数据
- View(视图):负责展示数据的地方
- Controller(控制器):用户交互的地方,例如点击事件等等。思想:Controller将Model的数据展示在View上
- VM:也就是View-Model,【视图】和【模型】之间数据的双向绑定。思想:实现了 View 和 Model 的自动同步,也就是当 Model 的属性改变时,对应 View 层显示会自动改变。
为什么data是个函数并且返回一个对象呢?
data之所以是一个函数,是因为一个组件可能会多处调用,而每一次调用就会执行data函数并返回新的数据对象,这样,可以避免多处调用之间的数据污染。
不需要响应式的数据应该怎么处理?
- Object.freeze()
Vue常用的修饰符有哪些有什么应用场景
表单修饰符:lazy(光标离开标签的时候,才会将值赋予给value) trim number;
事件修饰符:stop prevent self(只当在 event.target 是当前元素自身时触发处理函数) once(绑定了事件以后只能触发一次,第二次就不会触发)capture(使事件触发从包含这个元素的顶层开始往下触发) passive(相当于给onscroll事件整了一个.lazy修饰符) native(让组件变成像html内置标签那样监听根元素的原生事件,否则组件上使用 v-on 只会监听自定义事件)
Vue中的过滤器了解吗?过滤器的应用场景有哪些?
v-if和v-show有何区别
v-if是通过控制dom元素的删除和生成来实现显隐,每一次显隐都会使组件重新跑一遍生命周期v-show是通过控制dom元素的css样式来实现显隐,不会销毁- 频繁或者大数量显隐使用
v-show,否则使用v-if
为什么v-if和v-for不建议用在同一标签?
- 永远不要把
v-if和v-for同时用在同一个元素上
为什么不建议用index做key,为什么不建议用随机数做key?
- index:或用index拼接其他值作为key,改变列表的顺序就无法复用(key相同,type不同)
- 随机数:改变列表的顺序就无法复用(key不相同)
- 正确用法 :用唯一值id做key
相同的路由组件如何重新渲染?
- 改变组件的key即可
Vue组件为什么只能有一个根元素
- vue2中组件确实只能有一个根,因为
vdom是一颗单根树形结构,patch方法在遍历的时候从根节点开始遍历 - vue3中之所以可以写多个根节点,是因为引入了
Fragment的概念,这是一个抽象的节点,如果发现组件是多根的,就创建一个Fragment节点,把多个根节点作为它的children。
vue-loader是什么?它有什么作用
vue-loader是用于处理单文件组件(SFC,Single-File Component),vue-loader被执行时,它会对SFC中的template、script和style每个语言块用单独的loader链处理。vue-loader会调用@vue/compiler-sfc模块解析SFC源码为一个描述符(Descriptor),然后为每个语言块生成import代码,最后将这些单独的块装配成最终的组件模块。- 必须交由 @vue/compiler-sfc 编译为标准的 JavaScript 和 CSS,一个编译后的 SFC 是一个标准的 JavaScript(ES) 模块
- SFC 中的
<style>标签一般会在开发时注入成原生的<style>标签以支持热更新,而生产环境下它们会被抽取、合并成单独的 CSS 文件。 - Vite 提供 Vue SFC 支持的官方插件
@vitejs/plugin-vue。
// 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 lang="ts",webpack会展开成
import script from 'babel-loader!vue-loader!source.vue?vue&type=script' - style scoped lang="scss",webpack会展开成
import 'style-loader!vue-loader/style-post-loader!css-loader!sass-loader!vue-loader!source.vue?vue&type=style&index=1&scoped&lang=scss' - template lang="pug",webpack会展开成
import 'vue-loader/template-loader!pug-loader!source.vue?vue&type=template'需要用Vue 模板编译器编译template,从而得到render函数
审查元素时发现data-v-xxxxx
- 这是在标记vue文件中style时使用scoped标记产生的,因为要保证各文件中的css不相互影响,给每个component都做了唯一的标记
- vue 中的 scoped 属性的效果主要通过 PostCSS 转译实现的。PostCSS 给一个组件中的所有 DOM 添加了一个独一无二的动态属性,然后,给 CSS 选择器额外添加一个对应的属性选择器。
- scoped内如何实现样式穿透的
- ::v-deep 操作符( >>> 的别名),
.a >>> .b { /* ... */ }编译成.a[data-v-f3f3eg9] .b { /* ... */ } - 定义一个含有 scoped 属性的 style 标签之外,再定义一个不含有 scoped 属性的 style 标签
- ::v-deep 操作符( >>> 的别名),
插件
// plugins/i18n.js
export default {
install: (app, options) => {
// 注入一个全局可用的 $translate() 方法
app.config.globalProperties.$translate = (key) => {
return key.split('.').reduce((o, i) => {
if (o) return o[i]
}, options)
}
// 插件中的 Provide / Inject
app.provide('i18n', options)
}
}
//
import { createApp } from 'vue'
const app = createApp({})
import i18nPlugin from './plugins/i18n'
app.use(i18nPlugin, {
// options
greetings: { hello: 'Bonjour!' }
})
//
<h1>{{ $translate('greetings.hello') }}</h1>
- 通过
app.component()和app.directive()注册一到多个全局组件或自定义指令。 - 通过
app.provide()使一个资源可被注入进整个应用。 - 向
app.config.globalProperties中添加一些全局实例属性或方法
高阶组件
vue中如何扩展一个组件
vue-router原理
- router-link默认生成一个a标签,设置to属性定义跳转path。原理阻止a标签默认事件,调用的是navigate方法,avigate内部依然调用router.push()。
- router-view是要显示组件的占位组件,可以嵌套,对应路由配置的嵌套关系。原理上是在router-view组件内部判断当前router-view处于嵌套层级的深度,讲这个深度作为匹配组件数组matched的索引,获取对应渲染组件,渲染之。(默认0,加1之后传给后代,同时根据深度获取匹配路由)

-
vue-router有3个模式,其中history和hash更为常用。hash模式使用和部署简单,但是不会被搜索引擎处理,seo有问题;history模式则建议用在大部分web项目上,但是它要求应用在部署时做特殊配置,服务器需要做回退处理,否则会出现刷新页面404的问题。
-
路由守卫有三个级别:全局beforeEach,路由独享beforeEnter,组件级beforeRouteEnter。原理用户的任何导航行为都会走navigate方法,内部有个guards队列,runGuardQueue(guards)链式的执行用户在各级别注册的守卫钩子函数,通过则继续下一个级别的守卫,不通过进入catch流程取消原本导航。
-
从头开始实现一个简单的路由
<script setup>
import { ref, computed } from 'vue'
import Home from './Home.vue'
import About from './About.vue'
import NotFound from './NotFound.vue'
const routes = {
'/': Home,
'/about': About
}
const currentPath = ref(window.location.hash)
window.addEventListener('hashchange', () => {
currentPath.value = window.location.hash
})
const currentView = computed(() => {
return routes[currentPath.value.slice(1) || '/'] || NotFound
})
</script>
<template>
<a href="#/">Home</a> |
<a href="#/about">About</a> |
<a href="#/non-existent-path">Broken Link</a>
<component :is="currentView" />
</template>