前端面试vue
目录Vue生命周期钩子函数Vue2响应式原理检测变化的注意事项Vue动态组件Vue Router中动态路由传参query和params的区别Watch、Computed、方法的区别以及Computed如何传参数Vue异步组件Vue组件通信的几种方式vue 组件定义data为什么必须是函数v-if 和 v-show 的区别v-if 和 v-for为啥不能一起使用自定义组件的v-model导航守卫Object.defineProperty() 和 proxy的区别Vue 中diff算法Vue2和Vue3的区别虚拟DOM是什么vue插槽有哪些vue性能优化watch 和 watchEffect 的区别如果用户反馈页面加载慢你会如何排查和优化ref 和 reactive 的区别和使用场景如何实现一个权限管理系统vue中有哪些内置指令1211111111Vue生命周期钩子函数vue2 生命周期构造函数 beforeCreate: 创建之前调用此时data和el都还未初始化无法通过vm访问到data中的数据、 methods中的方法 created: 实例创建完成之后调用此时data已经完成了初始化el还未初始化可以通过vm访问到data 中的数据、methods中的方法 beforeMount: 实例挂载开始之前调用data和el都已经初始化页面呈现的是虚拟DOM还没渲染到 HTML页面中 mounted: 实例挂载之后调用模版中的HTML渲染到HTML页面中 beforeUpdate: 数据更新前调用 updated: 数据更新后调用避免在此期间更新状态可能会造成无限循环 beforeDestroy: 组件销毁前调用还能访问组件实例。在此可以清除定时器以及dom绑定的事件 destroyed: 组件销毁后调用销毁后组件实例不可访问 activated被 keep-alive 缓存的组件激活时调用 deactivated被 keep-alive 缓存的组件失活时调用 errorCaptured在捕获一个来自后代组件的错误时被调用 vue3 选项式 beforeCreate在实例初始化完成并且 props 被解析后立即调用 created在组件实例处理完所有与状态相关的选项后调用当这个钩子被调用时响应式数据、计算属性、方法和侦听器已经设置完成。然而此时挂载阶段还未开始因此 $el 属性仍不可用 beforeMount在组件被挂载之前调用当这个钩子被调用时组件已经完成了其响应式状态的设置但还没有创建 DOM 节点。它即将首次执行 DOM 渲染过程。 mounted在组件被挂载之后调用这个钩子通常用于执行需要访问组件所渲染的 DOM 树相关的副作用。 beforeUpdate在组件即将因为一个响应式状态变更而更新其 DOM 树之前调用这个钩子可以用来在 Vue 更新 DOM 之前访问 DOM 状态 updated在组件因为一个响应式状态变更而更新其 DOM 树之后调用父组件的更新钩子将在其子组件的更新钩子之后调用 beforeUnmount在一个组件实例被卸载之前调用当这个钩子被调用时组件实例依然还保有全部的功能 unmounted在一个组件实例被卸载之后调用可以在这个钩子中手动清理一些副作用例如计时器、DOM 事件监听器或者与服务器的连接 activated组件实例是 KeepAlive 缓存树的一部分当组件被插入到 DOM 中时调用 deactivated若组件实例是 KeepAlive 缓存树的一部分当组件从 DOM 中被移除时调用 errorCaptured在捕获了后代组件传递的错误时调用 renderTracked在一个响应式依赖被组件的渲染作用追踪后调用 renderTriggered在一个响应式依赖被组件触发了重新渲染之后调用 ***与vue2的区别就是销毁钩子的命名改变*** vue3 组合式 onBeforeMount()在组件被挂载之前被调用当这个钩子被调用时组件已经完成了其响应式状态的设置但还没有创建 DOM 节点。它即将首次执行 DOM 渲染过程。 onMounted()在组件挂载完成后执行个钩子通常用于执行需要访问组件所渲染的 DOM 树相关的副作用。 onBeforeUpdate()在组件即将因为响应式状态变更而更新其 DOM 树之前调用这个钩子可以用来在 Vue 更新 DOM 之前访问 DOM 状态。 onUpdated()在组件因为响应式状态变更而更新其 DOM 树之后调用父组件的更新钩子将在其子组件的更新钩子之后调用。 onBeforeUnmount()在组件实例被卸载之前调用。 onUnmounted()在组件实例被卸载之后调用可以在这个钩子中手动清理一些副作用例如计时器、DOM 事件监听器或者与服务器的连接。 onActivated()若组件实例是 KeepAlive 缓存树的一部分当组件被插入到 DOM 中时调用。 onDeactivated()若组件实例是 KeepAlive 缓存树的一部分当组件从 DOM 中被移除时调用。 onErrorCaptured()在捕获了后代组件传递的错误时调用。 onRenderTracked() 当组件渲染过程中追踪到响应式依赖时调用。 onRenderTriggered() 当响应式依赖的变更触发了组件渲染时调用。Vue2响应式原理检测变化的注意事项Vue 2 的响应式原理核心是 Object.defineProperty 1、通过 Object.defineProperty 劫持对象的getter/setter 2、在getter 中收集依赖 3、在setter 中通知依赖更新 4、每个响应式属性都有一个对应的 Dep 实例 5、通过 Watcher 连接视图和数据 Vue 2 响应式有什么缺陷 1、无法检测对象属性的添加/删除 → 需要 Vue.set/Vue.delete 由于 Vue 会在初始化实例时对 property 执行 getter/setter 转化所以 property 必须在 data 对象上存在才能让 Vue 将它转换为响应式的。例如 var vm new Vue({ data:{ a:1 } }) // vm.a 是响应式的 vm.b 2 // vm.b 是非响应式的 对于已经创建的实例Vue 不允许动态添加根级别的响应式 property。但是可以使用 Vue.set(object, propertyName, value) 方法向嵌套对象添加响应式 property。 也可以使用 this.$set(this.someObject,b,2) 有时你可能需要为已有对象赋值多个新 property比如使用 Object.assign() 或 _.extend()。但是这样添加到对象上的新 property 不会触发更新。在这种情况下你应该用原对象与要混合进去的对象的 property 一起创建一个新的对象。 // 代替 Object.assign(this.someObject, { a: 1, b: 2 }) this.someObject Object.assign({}, this.someObject, { a: 1, b: 2 }) 2、对于数组 Vue 不能检测以下数组的变动 当你利用索引直接设置一个数组项时例如vm.items[indexOfItem] newValue 当你修改数组的长度时例如vm.items.length newLength 解决第一类问题可以使用 vm.$set(vm.items, indexOfItem, newValue) vm.items.splice(indexOfItem, 1, newValue) 解决第二类问题可以使用 vm.items.splice(newLength) 3. 初始化时递归遍历所有属性性能较差 4. 对 ES6 的数据结构Map、Set支持不好Vue动态组件通过 Vue 的 component 元素和特殊的 is attribute 实现 component :istabs[currentTab]/componentVue Router中动态路由传参query和params的区别1、写法上的区别params传值必须使用路由的name属性query传值用path和name属性都可以 router.push({ name: user, params: { username: eduardo } }) router.push({ path: /register, query: { plan: private } }) router.push({ name: register, query: { plan: private } }) 2、params 传值需要在路由定义时指定相应的路径参数如果不指定路由就会跳转不过去Vue Router4 Vue Router3 不在路由定义指定相应参数也可以跳转传参 3、如果不在路由定义时指定相应的路径参数params传值在url看不到并且如果页面刷新的话参数会丢失 4、query传值能在url上看到传递的参数且页面刷新参数不会丢失Watch、Computed、方法的区别以及Computed如何传参数1、computed是计算属性watch是监听一个值的变化而执行对应的回调 2、computed函数所依赖的属性不变的时候会调用缓存watch每次监听的值发生变化时候都会调用回调 3、computed必须有returnwatch可以没有 4、computed函数不能有异步watch可以 5、computed当一个属性受多个属性影响的时候使用watch一条数据影响多条数据的时候使用 6、computed 传参数使用闭包 computed: { asyncRules () { return function (record, key) { return } }, }, 计算属性缓存 vs 方法 计算属性是基于响应式依赖缓存的只在相关响应式依赖发生改变时它们才会重新求值。 方法每当触发重新渲染时调用方法总会再次执行。 计算属性默认只有 getter不过在需要时你也可以提供一个 setter computed: { fullName: { // getter get: function () { return this.firstName this.lastName }, // setter set: function (newValue) { var names newValue.split( ) this.firstName names[0] this.lastName names[names.length - 1] } } }Vue异步组件1、vue2 实现 // 全局注册 Vue.component(async-example, () import(./MyComponent.vue)) // 局部注册 export default { components: { async-example: () import(./MyComponent.vue) } } 2、vue3使用defineAsyncComponent函数 defineAsyncComponent 方法接收一个返回 Promise 的加载函数ES 模块动态导入也会返回一个 Promise import { defineAsyncComponent } from vue const AsyncComp defineAsyncComponent(() import(./components/MyComponent.vue) )Vue组件通信的几种方式1、Props / Emits父子通信 // 父组件 Child :titleparentTitle updatehandleUpdate / // 子组件 defineProps([title]) defineEmits([update]) emit(update, newValue) 2、Provide / Inject跨层级 //祖先组件 provide(theme, ref(dark)) //后代组件 const theme inject(theme, light) // 默认值 3、Event Bus事件总线 //使用 mitt 或自定义事件中心 emitter.on(event, callback): 订阅事件 emitter.emit(event, data): 触发事件 emitter.off(event, callback): 取消订阅事件 4、Vuex / Pinia状态管理全局状态管理 5、v-model 双向绑定 3.4 版本之前使用 defineProps({firstName: String, lastName: String}) defineEmits([update:firstName, update:lastName]) template input typetext :valuefirstName input$emit(update:firstName, $event.target.value)/ input typetext :valuelastName input$emit(update:lastName, $event.target.value)/ /template 3.4 版本之后使用 defineModel script setup const firstName defineModel(firstName) const lastName defineModel(lastName) /script template input typetext v-modelfirstName / input typetext v-modellastName / /template 父子组件通信props/emit vue的基础设计保持单向数据流 兄弟组件通信逻辑简单通过父组件做状态提升逻辑复杂的话可以采用数据共享处理pina vuex 跨多层嵌套的组件可以采用依赖注入 完全不相关的全局模块采用pina统一管理事件总线vue3已经不建议使用 总之优先使用props/emit 其次用provide/inject 复杂状态使用pina避免使用事件总线vue 组件定义data为什么必须是函数当一个组件被定义data必须声明为返回一个初始数据对象的函数因为组件可能被用来创建多个实例。 如果data是一个纯粹的对象则所有实例都将共享引用同一个数据对象。当你修改其中一个实例的数据时这个改变会影响所有的实例。通过提供data函数每次创建一个新实例后我们都会调用data函数就会返回初始数据的一个全新副本数据对象。v-if 和 v-show 的区别相同点都是用于条件性的渲染一块内容这块内容只有在表达式返回值为真值时渲染 不同点 1、v-if 在切换时条件区块内的事件监听器和子组件都会被销毁与重建 2、v-if 也是惰性的如果在初次渲染时条件值为 false则不会做任何事。只有当条件首次变为 true 时才被渲染 3、v-show 无论初始条件如何始终会被渲染只是通过 CSS display 属性会切换隐藏与显示 4、v-if 有更高的切换开销而 v-show 有更高的初始渲染开销。因此如果需要频繁切换则使用 v-show 较好如果在运行时绑定条件很少改变则 v-if 会更合适 5、v-show 不支持在 template 元素上使用也不能和 v-else 搭配使用 6、v-if 可以在template 元素上使用而且最后渲染的结果不包含template 元素v-if 和 v-for为啥不能一起使用同时使用 v-if 和 v-for 是不推荐的因为这样二者的优先级不明显 vue2: 当 v-if 与 v-for 一起使用时v-for 具有比 v-if 更高的优先级 vue3: 当 v-if 和 v-for 一起使用时v-if 具有比 v-for 更高的优先级 vue2中的执行逻辑 先遍历整个list数据 对每一项数据都做v-if判断 即使最终只渲染部分满足条件的数据也会先遍历全部 vue3中的执行逻辑 v-if的优先级高但是依然会在v-for的作用域内使用 同样存在性能问题且容易产生逻辑混乱 正确的解决方案 1、使用计算属性过滤出满足条件的数据同时计算属性有缓存数据不变化时不会重新计算 2、可以使用 v-for 外层包裹template Vue 2/3 通用自定义组件的v-model一个组件上的 v-model 默认会利用名为 value 的 prop 和名为 input 的事件。 // 父组件 template div CustomInput v-modelmessage / pMessage: {{ message }}/p /div /template script import CustomInput from ../components/CustomInput.vue; export default { components: { CustomInput }, data() { return { message: }; } } /script //子组件 template div input :valuevalue inputupdateValue($event.target.value) / /div /template script export default { props: [value], methods: { updateValue(value) { this.$emit(input, value); } } } /script导航守卫导航守卫分为三类全局的, 单个路由独享的, 组件级的 一、全局守卫 1、全局前置守卫 beforeEach const router createRouter({ ... }) // to: 即将要进入的目标from: 当前导航正要离开的路由 // next: 用该方法来 resolve 这个钩子进行管道中的下一个钩子 router.beforeEach((to, from, next) { if (to.name ! Login !isAuthenticated) next({ name: Login }) else next() }) 2、全局解析守卫 beforeResolve router.beforeResolve 和 router.beforeEach 类似区别是在导航被确认之前同时在所有组件 内守卫和异步路由组件被解析之后解析守卫就被调用 3、全局后置钩子 afterEach 和守卫不同的是这些些钩子不会接受 next 函数也不会改变导航本身 router.afterEach((to, from) { // ... }) 它们对于分析、更改页面标题、声明页面等辅助功能以及许多其他事情都很有用 二、路由独享的守卫 beforeEnter 可以在路由配置上直接定义 beforeEnter 守卫只在进入路由时触发 const router new VueRouter({ routes: [{ path: /foo, component: Foo, beforeEnter: (to, from, next) { // ...} }]}) 三、组件内的守卫 可以在路由组件内直接定义以下路由导航守卫 beforeRouteEnter beforeRouteUpdate (2.2 新增) beforeRouteLeave const Foo { template: ..., beforeRouteEnter(to, from, next) { // 在渲染该组件的对应路由被 confirm 前调用 // 不能获取组件实例 this // 因为当守卫执行前组件实例还没被创建 // 可以通过传一个回调给 next来访问组件实例 // 导航被确认的时候执行回调并且把组件实例作为回调方法的参数 next(vm { // 通过 vm 访问组件实例 }) }, beforeRouteUpdate(to, from, next) { // 在当前路由改变但是该组件被复用时调用 // 举例来说对于一个带有动态参数的路径 /foo/:id在 /foo/1 和 /foo/2 之间跳转的时候 // 由于会渲染同样的 Foo 组件因此组件实例会被复用。而这个钩子就会在这个情况下被调用。 // 可以访问组件实例 this }, beforeRouteLeave(to, from, next) { // 导航离开该组件的对应路由时调用 // 可以访问组件实例 this } }vue2 和 vue3 响应式原理的区别(Object.defineProperty() 和 proxy)1、Object.defineProperty 是属性拦截Proxy 是对象代理 2、vue2需要递归遍历对象上的所有属性单独劫持进行依赖收集和触发更新。vue3直接代理 整个对象实现全功能响应式 3、vue2 不能监听对象属性的新增、删除数组通过下标操作和修改length长度的操作vue3 不存在这些问题 4、vue2初始化需要遍历所有属性深度越大对象开销越大vue3 初始化时只代理顶层对象懒代理子对象只有在属性被访问时才代理子对象Vue 中diff算法diff算法是一种对比新旧虚拟dom树找出最小差异并只更新变化部分的算法。它的目标是 用最少的dom操作来更新视图 为什么需要diff 如果每次数据变化都要重新渲染整个页面性能比较差 diff算法让vue可以复用已有的dom节点只更新变化的部分 diff算法的核心逻辑 当数据变化重新触发组件渲染时vue会 1、生成新的虚拟dom树 2、与旧的虚拟dom树进行diff比较 3、根据差异打补丁更新真是dom diff算法的核心策略 1、同层比较只比较同一层级节点跨层不比较 2、类型判断类型不同直接替换整个节点 3、key复用通过key标识节点尽可能复用已有节点 vue2的实现采用双端比较头头、头尾、尾尾、尾头高效处理列表更新 vue3的优化 1、静态提升静态节点只创建一次复用即可 2、补丁标志编译标记动态属性更新时只对比标记的部分 3、最长递增子序列优化列表移动操作 这些优化让 Vue 3 的 diff 性能比 Vue 2 提升了约 50% React 的 diff 差异React 也是同层比较但采用仅向前比较没有 Vue 的双端比较 虚拟dom即一个纯 JavaScript 的对象它代表着一个 div 元素包含我们创建实际 元素需要的所有信息 const vnode { type: div, props: { id: hello }, children: [ /* 更多 vnode */ ] }Vue2和Vue3的区别1、数据响应式原理发生改变 vue2采用Object.defineProperty(), vue3 采用proxy对属性操作进行拦截。vue2不能 检测对象属性新增、删除数组通过下标修改数组项、数组长度改变的操作vue3 不存在这些问题。 2、vue3支持碎片(Fragments), 即可以拥有多个根节点vue2只能拥有一个。 3、vue3支持组合式api Composition API 4、生命周期钩子函数不同具体参考20 Vue生命周期钩子函数 5、vue2 把数据存放在data中vue3放在setup 函数中 6、setup函数只能是同步的不能是异步的虚拟DOM是什么虚拟 DOM (Virtual DOM简称 VDOM) 是一种编程概念意为将目标所需要的ui通过数据结构虚拟的表示出来保存在内存中然后将真实的DOM与之保持同步。 与其说虚拟 DOM 是一种具体的技术不如说是一种模式所以并没有一个标准的实现。我们可以用一个简单的例子来说明 const vnode { type: div, props: { id: hello }, children: [ /* 更多 vnode */ ] } 这里所说的 vnode 即一个纯 JavaScript 的对象 (一个“虚拟节点”)它代表着一个 div 元素。它包含我们创建实际元素所需的所有信息如标签名、属性、子节点信息这使它成为虚拟 DOM 树的根节点。vue插槽有哪些1、具名插槽 #default 插槽定义name 属性 2、条件插槽 $slots.header 判断 3、作用域插槽 v-slotslotProps 或者解构 #default{ message }vue性能优化1. 代码优化使用组件懒加载减少初始包体积合理代码分割利用 Tree Shaking 移除未使用代码 1.组件懒加载 const LazyComponent defineAsyncComponent(() import(./components/HeavyComponent.vue)) 2.路由懒加载 const routes [{ path: /dashboard, component: () import(./views/Dashboard.vue) // 按需加载 }] 2. 组件优化合理选择 v-if 和 v-show优化列表渲染使用 computed 缓存计算结果 1.始终为 v-for 提供唯一 key ListItem v-foritem in items :keyitem.id / 2.长列表使用虚拟滚动处理 3.使用computed缓存结果 4.使用v-show代替频繁切换的v-if 5.大型静态数据可以使用 响应式容器数据冻结 const list ref([]) list.value Object.freeze(data) // 跳过不必要的响应式转换 6.使用 watchEffect 替代深度 watch 3. 构建优化配置压缩、分包、Gzip 压缩优化图片等静态资源 1.按需引入UI组件库 import { ElButton, ElInput } from element-plus 2.压缩配置、制定分包策略 build:{ minify: terser, // 默认esbuild 可以删除console、debugger terserOptions: { compress: { drop_console: process.env.NODE_ENV production, drop_debugger: true } }, // Rollup 打包选项、分包策略 rollupOptions: { output: { manualChunks: { // 将 Vue 的核心库打包成一个单独的 chunk vue-vendor: [vue, vue-router, pinia], // 将 Element Plus 单独拆分 element-plus: [element-plus], // 将工具库单独拆分 utils: [lodash-es, axios], }, }, }, } 3.图片优化assetsInlineLimit 4kb以下转为base64 4. 网络优化利用浏览器缓存、CDN 加速、预加载关键资源 5. 运行时优化及时清理事件监听器使用防抖节流复杂计算使用 Web Worker 1.组件销毁及时清理定时器和事件监听器 2.使用防抖节流 6. 监控优化建立性能监控体系及时发现和修复性能问题watch 和 watchEffect 的区别1、依赖追踪方式不同 watch: 需要明确指定监听源 watchEffect: 自动追踪函数内的响应式依赖 2、立即执行行为 watch: 默认不会立即执行需要配置属性 watchEffect: 默认会立即执行一次 3、获取旧值的能力 watch: 可以获取旧值和新值 watchEffect: 只能获取当前值每次执行前清理上一次的副作用如果用户反馈页面加载慢你会如何排查和优化1、首先第一步复现问题观察象限 2、使用 Lighthouse 和 Performance 进行页面性能分析根据分析结果进行优化 体积过大: 代码分割、按需加载、移除未使用的代码 请求过多: 请求合并、配置缓存 渲染卡顿: 优化列表渲染减少不必要的列表渲染ref 和 reactive 的区别和使用场景1、数据类型支持 ref 可包装任意类型基本类型数字、字符串、布尔值对象类型对象、数组 reactive 仅支持对象或数组无法处理基本类型 2、访问与修改方式 ref 必须通过 .value 读写如 count.value但在模板中自动解包无需 .value reactive 直接操作属性如 state.count无需额外语法 3、重新赋值行为 ref 允许整体替换如 refObj.value { new: 1 }保持响应性 reactive 禁止整体替换如 reactiveObj { new: 1 } 会丢失响应性只能修改内部属性 如果需要替换整个对象需要使用 Object.assign(refObj, {new: 1}) 4、实现原理。 ref 对基本类型用轻量劫持对对象内部调用 reactive 的 Proxy reactive 基于 Proxy 深度代理整个对象自动追踪嵌套属性变化如何实现一个权限管理系统前端权限控制可以分为4个方面: 接口权限、按钮权限、菜单权限、路由权限 1、接口权限: 创建菜单API接口与菜单ID关联接口请求先通过auth鉴权鉴权失败返回错误码前端根据 错误码进行业务处理 2、按钮权限: 可以使用 v-if hooks 和 自定义指令实现 自定义指令核心代码 button v-permissionuser:add添加用户/button if (!userStore.hasPermission(value)) { // 1. 直接移除元素 el.parentNode?.removeChild(el) // 或 2. 禁用元素 // el.disabled true // el.classList.add(disabled) // 或 3. 隐藏元素 // el.style.display none } hooks核心代码 // usePermission 文件 const hasPermission (permission: string) { return userStore.hasPermission(permission)} button v-ifcanAddUser.value clickaddUser // 方式1基础权限检查 const { hasPermission, checkPermissions, filterByPermission } usePermission() // 检查单个权限 const canAddUser hasPermission(user:add) 3、菜单权限: 菜单数据由后端返回后端根据用户权限和角色权限返回对应的菜单数据前端把单数据处理成 对应的路由数据 如: router.addRoute(routes) //routes 处理后的路由数据 4、路由权限: 初始化加载全部路由并且在路由上标记相应的权限信息每次路由跳转前做校验vue中有哪些内置指令vue内置的指令大概有10几个我按照共说一下 1、条件渲染v-if v-else-if v-else v-show, 其中v-if是真正的条件渲染v-show只是css切换 适合于频繁切换的场景 2、列表循环v-for 通常配合 :key使用, 用于性能优化 3、属性绑定v-bind v-model, v-bind 用于动态属性绑定 v-model 用于表单双向绑定 4、事件绑定v-on 支持 .stop .noce .prevent 等修饰符 5、内容渲染v-text v-html v-html 可以渲染html但是要注意 xss 风险 6、内容分发v-slot 7、优化类 v-once 只渲染一次 v-pre 跳过编译 v-cloak 配合css解决模板闪烁的问题 v-model的本质是 :value 和 input 的语法糖vue2或者 :modelValue 和 update:modelValueVue 3v-show 算不算重排v-show的本质是切换 display: none 的值是否发生重排取决于元素是否占据文档流中的空间 如果元素是块级元素且处于正常的文档流空间隐藏时其他元素会填补他的位置这会发生重排。 如果元素是绝对定位或者脱离文档流则不会影响其他元素的布局不会触发重排只会触发重绘。 但无论是否重排v-if会直接删除/新增dom元素一定会触发重排。所以对于频繁切换的场景 即使v-show可能会触发重排也比v-if性能更好因为它避免了dom的重建和销毁 触发重排的操作元素的位置、尺寸、布局发生改变11111111