vue面试

82 阅读3分钟

vue2的defineProperty和vue3的Proxy的区别

1. defineProperty和Proxy的本质

  • Proxy可以为一个对象创建一个代理,这个代理可以拦截重定义对对像的一些基本操作,基本操作是值:读取,删除,冻结对象,不可继承的对象,设置对象的原型,读取对象的原型,或者去循环对象中的每一个健,无论是通过语法执行或者api都行它最终在执行引擎的内部(内部方法),这些操作都转成基本操作。
  • Proxy是在拦截对象的基本方法或者是对像的内部的方法
  • defineProperty只是众多内部方法的其中之一。

2. 框架应用方面:

  • vue2:defineProperty 拦截现有对象的读写,拦截有缺陷有很多拦截不到,不精准有遗漏的,所以vue中用setset和get用手动触发
  • vue3:Proxy 针对对象的所有内部方法都可以重定义拦截全面无死角的,不管是这个对象是函数对象还是普通对象,不管对对象的任何操作全面拦截,所以在vue3中set,get都没有了因为不需要了

二、生命周期

2. Vue 子组件和父组件执行顺序

加载渲染过程:

  1. 父组件 beforeCreate
  2. 父组件 created
  3. 父组件 beforeMount
  4. 子组件 beforeCreate
  5. 子组件 created
  6. 子组件 beforeMount
  7. 子组件 mounted
  8. 父组件 mounted

更新过程:

  1. 父组件 beforeUpdate
  2. 子组件 beforeUpdate
  3. 子组件 updated
  4. 父组件 updated

销毁过程:

  1. 父组件 beforeDestroy
  2. 子组件 beforeDestroy
  3. 子组件 destroyed
  4. 父组件 destoryed

3. created和mounted的区别

  • created:在模板渲染成html前调用,即通常初始化某些属性值,然后再渲染成视图。
  • mounted:在模板渲染成html后调用,通常是初始化页面完成后,再对html的dom节点进行一些需要的操作。

4. 一般在哪个生命周期请求异步数据

我们可以在钩子函数 created、beforeMount、mounted 中进行调用,因为在这三个钩子函数中,data 已经创建,可以将服务端端返回的数据进行赋值。 ​

推荐在 created 钩子函数中调用异步请求,因为在 created 钩子函数中调用异步请求有以下优点:

  • 能更快获取到服务端数据,减少页面加载时间,用户体验更好;
  • SSR不支持 beforeMount 、mounted 钩子函数,放在 created 中有助于一致性。

三、组件通信

 1. props/$emit

父组件通过props向子组件传递数据,子组件通过$emit和父组件通信

2. eventBus事件总线($emit / $on

eventBus事件总线适用于父子组件非父子组件等之间的通信

3. 依赖注入(provide / inject)

Vue中的依赖注入,该方法用于父子组件之间的通信。当然这里所说的父子不一定是真正的父子,也可以是祖孙组件,在层数很深的情况下,可以使用这种方法来进行传值。就不用一层一层的传递了

4. ref / $refs

ref: 这个属性用在子组件上,它的引用就指向了子组件的实例。可以通过实例来访问组件的数据和方法

四、路由

路由的hash模式与history模式的核心区别 Vue-Router提供两种路由模式,分别基于不同的浏览器特性实现前端路由跳转,核心区别如下:

1. hash模式(默认模式)

定义:URL中包含#符号(如http://www.abc.com/#/vue),#后的部分称为hash值,其变化不会触发页面重新加载。

核心原理: 依赖浏览器的onhashchange事件,当hash值(#后的内容)变化时,事件被触发,前端可通过监听该事件实现路由切换,无需向后端发送请求。

特点

  • URL特性:hash值不会被包含在HTTP请求中,仅作为前端标识,对后端无影响。
  • 兼容性:支持所有浏览器(包括低版本IE),兼容性极佳。
  • 功能限制
    • 仅能修改#后的部分,URL范围受限(同文档内)。
    • hash值变化时,若与原hash相同则不会触发历史记录更新。
    • 仅支持字符串类型的hash值存储。
  • 优势:无需后端配置,刷新页面不会出现404(因请求URL始终为#前的部分)。

2. history模式

定义:URL中无#符号(如http://abc.com/user/id),通过HTML5的History API实现路由管理。 核心原理: 借助History API的pushState()replaceState()方法修改浏览器历史记录,实现URL变化但不刷新页面;通过forward()back()go()方法切换历史状态。 特点

  • URL特性:URL与传统路由一致,美观且符合用户习惯。
  • 功能扩展
    • 可设置与当前URL同源的任意URL(不限于同文档)。
    • 支持添加任意类型数据到历史记录(通过stateObject参数)。
    • 可重复添加相同URL到历史栈,且支持自定义title属性。
  • 兼容性:仅支持HTML5标准的浏览器(IE10+)。
  • 依赖后端:刷新页面时,浏览器会按当前URL向后端发起请求,若后端无对应路由配置,会返回404错误(需后端配合配置fallback页面,如指向index.html)。

3. 核心对比表

维度hash 模式history 模式
URL 格式#(如xxx.com/#/path#(如xxx.com/path
底层依赖onhashchange事件HTML5 History API(pushState等)
后端依赖无需配置,无 404 风险需后端配置路由 fallback,否则刷新可能 404
兼容性所有浏览器(包括 IE 低版本)仅支持 HTML5 的浏览器(IE10+)
URL 修改范围#后部分,同文档内同源下任意 URL
历史记录数据仅支持短字符串支持任意类型数据(通过stateObject
相同 URL 处理相同 hash 值不触发历史记录更新相同 URL 可添加到历史栈

4. 适用场景

  • hash模式:适合兼容性要求高(如需支持IE9及以下)、无需后端配合的场景(如静态站点)。
  • history模式:适合追求URL美观、需要复杂历史记录管理的场景(如多页面交互应用),需后端同步配置。

5. 模式切换配置

// 切换为history模式(需后端配合)
const router = new VueRouter({
  mode: 'history', // 默认是'hash'
  routes: [...]
})

总结:两种模式均通过前端技术实现无刷新路由跳转,hash模式简单且兼容性强,history模式更符合URL规范但依赖后端配置,需根据项目需求选择。

Vue-router 路由钩子

1. 路由钩子分类

Vue Router 的钩子分为三类,分别在不同阶段执行:

全局守卫

  • 全局前置守卫router.beforeEach
    在路由导航开始时触发,可用于登录验证、权限控制。
  • 全局解析守卫router.beforeResolve
    在所有组件内守卫和异步路由组件被解析后触发。
  • 全局后置钩子router.afterEach
    在导航完成后触发,不影响导航流程,常用于页面分析、日志记录。

路由独享守卫

  • 路由配置守卫beforeEnter
    定义在路由配置中的守卫,只对当前路由生效。

组件内守卫

  • 组件内钩子

    • beforeRouteEnter:在路由进入前触发,此时组件实例尚未创建。
    • beforeRouteUpdate:在当前路由改变但组件被复用时触发(如参数变化)。
    • beforeRouteLeave:在导航离开当前组件时触发,可用于阻止用户意外离开(如表单未保存)。

2. 钩子与生命周期的结合

当触发路由导航时,钩子的执行顺序与组件生命周期的关系如下:

完整导航流程(新路由加载)

1. 导航触发
2. 调用当前路由的beforeRouteLeave守卫(如果存在)
3. 调用全局前置守卫:router.beforeEach
4. 调用路由配置中的beforeEnter守卫(如果存在)
5. 解析异步路由组件(如果存在)
6. 调用全局解析守卫:router.beforeResolve
7. 导航被确认
8. 调用全局后置钩子:router.afterEach
9. 触发DOM更新
10. 调用组件内的beforeRouteEnter守卫中的next回调(此时组件实例已创建)

路由参数变化(组件复用)

当仅路由参数变化(如/user/1 → /user/2)时,组件实例会被复用,触发以下钩子:

1. 调用组件内的beforeRouteUpdate守卫
2. 调用全局前置守卫:router.beforeEach
3. 调用路由配置中的beforeEnter守卫(如果存在)
4. 解析异步路由组件(如果存在)
5. 调用全局解析守卫:router.beforeResolve
6. 导航被确认
7. 调用全局后置钩子:router.afterEach
8. 触发DOM更新

导航离开当前组件

当用户从当前路由导航到其他路由时:

1. 调用组件内的beforeRouteLeave守卫
2. 后续流程与新路由加载一致

3. 关键钩子详解

beforeRouteEnter

  • 特点:在导航确认前触发,此时组件实例尚未创建,无法访问this

  • 用途:可通过next回调访问组件实例,常用于路由进入前的数据预加载。

  • 示例

    export default {
      beforeRouteEnter(to, from, next) {
        // 通过next回调获取组件实例
        next(vm => {
          // vm 即组件实例,可访问data、methods等
          vm.fetchData(to.params.id);
        });
      }
    }
    

beforeRouteUpdate

  • 特点:在组件复用时触发,可访问this

  • 用途:处理路由参数变化(如更新组件数据),避免重复创建组件。

  • 示例

    javascript

    export default {
      beforeRouteUpdate(to, from, next) {
        // 当路由参数变化时更新数据
        this.fetchData(to.params.id);
        next();
      }
    }
    

beforeRouteLeave

  • 特点:在导航离开前触发,可访问this

  • 用途:阻止用户意外离开(如表单未保存),需显式调用next()

  • 示例

    export default {
      beforeRouteLeave(to, from, next) {
        if (this.formDirty) {
          // 阻止导航并提示用户
          if (confirm('表单尚未保存,确定离开?')) {
            next(); // 允许离开
          } else {
            next(false); // 取消导航
          }
        } else {
          next(); // 无修改,直接离开
        }
      }
    }
    

4. 异步操作与守卫

若在守卫中执行异步操作(如 API 请求),需等待操作完成后再调用next()
示例:权限校验

router.beforeEach(async (to, from, next) {
  const isAuthenticated = await checkAuth(); // 异步校验登录状态
  if (to.meta.requiresAuth && !isAuthenticated) {
    next('/login'); // 未登录则跳转登录页
  } else {
    next(); // 允许访问
  }
});

5. 总结

路由钩子通过精确控制导航流程,与组件生命周期紧密配合,实现了以下功能:

  • 权限控制:全局前置守卫和路由独享守卫。
  • 数据预加载beforeRouteEnterbeforeRouteUpdate
  • 用户体验优化beforeRouteLeave阻止意外操作。
  • 性能优化:复用组件时避免重复渲染。

vue框架的虚拟dom 为什么要用虚拟dom

Vue 框架使用虚拟 DOM(Virtual DOM)的核心目的是在保持声明式编程体验的同时,高效地更新真实 DOM。以下从性能优化、开发体验和跨平台能力三个维度详细解析其必要性:

1. 虚拟 DOM 的核心价值

(1)性能优化:最小化 DOM 操作

  • 直接操作 DOM 的代价
    浏览器的 DOM 是一个复杂的树形结构,每次修改都会触发重排(reflow)和重绘(repaint),性能开销大。

  • 虚拟 DOM 的解决方案

    • 抽象层:用 JavaScript 对象(虚拟 DOM)表示真实 DOM 结构。
    • 差异计算:通过对比新旧虚拟 DOM 的差异(diff 算法),只更新需要变化的真实 DOM 节点。
    • 批量更新:将多次虚拟 DOM 的修改合并为一次真实 DOM 操作,减少浏览器重排重绘次数。

示例对比
修改一个包含 100 个节点的列表时:

  • 直接操作 DOM:需执行 100 次独立 DOM 操作。
  • 虚拟 DOM:计算差异后可能只需执行 3 次 DOM 操作(如插入、移动、删除)。

(2)开发体验:声明式编程

  • 虚拟 DOM 支持声明式语法
    通过模板或 JSX 描述 UI 状态,Vue 自动将状态变化映射到虚拟 DOM 的更新,开发者无需手动操作 DOM。
  • 状态管理与视图分离
    虚拟 DOM 使组件状态与 DOM 操作解耦,代码更易维护(如 Vue 的响应式系统与虚拟 DOM 配合)。

(3)跨平台能力

  • 虚拟 DOM 是抽象层
    不仅可以渲染为浏览器 DOM,还能转换为其他平台的 UI(如移动端的原生组件)。

  • Vue 的多平台支持

    • Vue SSR(服务器端渲染):将虚拟 DOM 渲染为 HTML 字符串,提升 SEO 和首屏加载速度。
    • Vue Native:将虚拟 DOM 转换为 iOS/Android 原生组件,实现跨平台移动应用开发。

2. 虚拟 DOM 的工作流程

1. 组件初始化时,Vue将模板编译为渲染函数(render function)
2. 渲染函数执行后返回虚拟DOM树(JavaScript对象)
3. 当组件状态变化时,重新执行渲染函数生成新的虚拟DOM树
4. 通过diff算法对比新旧虚拟DOM树,计算出最小差异
5. 将差异应用到真实DOM上

3. 虚拟 DOM 的性能优势

(1)高效的差异计算

  • Vue 的 diff 算法优化

    • 同层比较:只比较同一层级的节点,避免跨层级比较的复杂计算。
    • 静态节点优化:编译时标记不变的静态节点(如纯文本),跳过比较。
    • PatchFlag(Vue3):编译时为动态节点添加标记,仅对比变化的类型(如文本、属性)。

(2)减少不必要的 DOM 操作

  • 示例:修改数组中的一个元素时:

    // 原始数组
    const list = ['a', 'b', 'c'];
    
    // 修改后
    const newList = ['a', 'x', 'c'];
    

    虚拟 DOM 只会更新b → x的变化,而非重新渲染整个列表。

4. 虚拟 DOM vs 直接操作 DOM

场景直接操作 DOM虚拟 DOM
代码复杂度高(需手动管理 DOM 操作)低(声明式描述 UI)
大规模数据更新性能差(频繁重排重绘)优(批量更新、最小化 DOM 操作)
维护成本高(DOM 操作与状态管理耦合)低(状态与视图分离)
跨平台能力仅支持浏览器支持 SSR、移动端等多平台

5. 常见误区

(1)虚拟 DOM 一定比直接操作 DOM 快?

  • 真相
    对于小规模 DOM 更新,虚拟 DOM 可能稍慢(因存在 diff 计算开销)。但在大规模复杂应用中,虚拟 DOM 的批量优化效果显著,总体性能更优。

(2)虚拟 DOM 就是 JS 对象?

  • 本质
    虚拟 DOM 不仅是 JS 对象,更是一种编程范式。它通过抽象层将 UI 描述与渲染分离,使框架具备更强的灵活性和可维护性。

6. 总结

Vue 使用虚拟 DOM 的核心原因是在保持开发体验的同时,平衡性能与可维护性

  • 性能:通过 diff 算法和批量更新,减少真实 DOM 操作次数。
  • 开发体验:声明式语法降低 DOM 操作复杂度,提升代码可维护性。
  • 跨平台:虚拟 DOM 作为中间层,支持多平台渲染。

虚拟 DOM 是现代前端框架的基石之一,它让开发者既能享受声明式编程的简洁,又能获得接近原生的性能表现。

vuex与pinia

Vuex 和 Pinia 均为 Vue.js 的状态管理库,用于管理应用的共享状态。Pinia 是 Vuex 的继任者,在保持核心功能的同时进行了诸多改进。以下从设计理念、API 风格、类型支持和性能等维度对比两者的差异:

1. 设计理念

Vuex

  • 基于 Flux 架构:单向数据流(State → View → Action → Mutations → State),强制使用 mutations 修改状态。
  • 模块系统:通过 modules 组织代码,但嵌套层级过深时结构复杂。
  • 插件机制:依赖插件实现时间旅行调试、持久化等功能。

Pinia

  • 简化设计:移除 mutations 概念,仅保留 state、getters、actions,更贴近自然的 Vue 使用方式。
  • 扁平化结构:以 store 为单位组织代码,避免深层嵌套,提高可维护性。
  • 组合式 API 优先:原生支持 TypeScript 和 Composition API,无需额外配置。

2. API 风格对比

Vuex

javascript

// Vuex 示例
import { createStore } from 'vuex';

const store = createStore({
  state() {
    return {
      count: 0
    };
  },
  mutations: {
    increment(state) {
      state.count++;
    }
  },
  actions: {
    incrementAsync({ commit }) {
      setTimeout(() => {
        commit('increment');
      }, 1000);
    }
  },
  getters: {
    doubleCount(state) {
      return state.count * 2;
    }
  }
});

Pinia

javascript

// Pinia 示例
import { defineStore } from 'pinia';

export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0
  }),
  actions: {
    increment() {
      this.count++;
    },
    incrementAsync() {
      setTimeout(() => {
        this.increment();
      }, 1000);
    }
  },
  getters: {
    doubleCount: (state) => state.count * 2,
  }
});

3. 核心差异点

特性VuexPinia
Mutations强制使用(同步操作)移除,直接在 actions 中修改状态
类型支持需额外配置(如 vuex-module-decorators)原生支持,类型自动推导
模块化方式嵌套 modules(结构复杂)扁平化 store(无嵌套)
代码结构按功能拆分(state/mutations/actions)按 store 拆分(每个 store 包含完整逻辑)
异步处理依赖 actions直接在 actions 中使用 async/await
插件系统自定义插件机制更灵活的插件 API
DevTools 支持需手动集成内置支持时间旅行调试、状态快照

4. TypeScript 支持

Vuex

  • 类型定义繁琐,需手动为 state、getters 等编写类型。

  • 示例:

    import { GetterTree, MutationTree, ActionTree } from 'vuex';
    
    interface State {
      count: number;
    }
    
    const state: State = {
      count: 0
    };
    
    const getters: GetterTree<State, State> = {
      doubleCount: (state) => state.count * 2
    };
    

Pinia

  • 类型自动推导,无需额外配置。

  • 示例:

    export const useCounterStore = defineStore('counter', {
      state: () => ({
        count: 0,
        name: 'John',
      }),
      getters: {
        // 自动推导返回类型为number
        doubleCount: (state) => state.count * 2,
      },
      actions: {
        increment() {
          this.count++; // 类型安全
        },
      },
    });
    

5. 性能对比

  • Pinia

    • 初始化速度更快(无 mutations 处理)。
    • 内存占用更低(扁平化结构)。
    • 状态更新时直接触发响应式,无需额外订阅。
  • Vuex

    • 在大型应用中因模块嵌套和 mutations 机制,可能存在性能损耗。

6. 适用场景

  • Pinia

    • 新项目或 Vue 3 应用。
    • 需要更好的 TypeScript 支持。
    • 偏好简洁 API 和组合式写法。
  • Vuex

    • 现有 Vue 2 项目的维护。
    • 严格遵循 Flux 架构的团队。
    • 需要依赖 Vuex 生态插件(如持久化、中间件)。

7. 迁移指南

从 Vuex 迁移至 Pinia 的关键点:

  1. 移除 mutations:将所有 mutations 逻辑合并到 actions 中。

  2. 模块化调整:将嵌套 modules 转换为扁平化的 store。

  3. API 更新

    • mapState → storeToRefs
    • mapGetters → storeToRefs(getters 自动转为 ref)
    • mapActions → 直接调用 store 的 actions
  4. 类型定义简化:移除冗余的类型声明,依赖 Pinia 的自动推导。

总结

Pinia 作为 Vuex 的演进,通过简化 API、强化类型支持和提升性能,成为 Vue 状态管理的首选方案。对于新项目,推荐使用 Pinia;对于现有 Vuex 项目,可根据团队技术栈和维护成本决定是否迁移

Vue 插槽(Slots)

Vue 插槽(Slots)是组件复用和内容分发的核心机制,允许父组件向子组件注入自定义内容,实现灵活的布局和逻辑分离。以下从基础用法、作用域插槽到高级应用全面解析:

1. 基础插槽:内容分发

单插槽(默认插槽)

子组件通过 <slot> 定义插槽位置,父组件可直接传入内容:

<!-- 子组件:MyComponent.vue -->
<div class="child">
  <slot></slot> <!-- 父组件内容将插入此处 -->
</div>

<!-- 父组件使用 -->
<MyComponent>
  <p>这是父组件传入的内容</p>
</MyComponent>

具名插槽

通过 name 属性定义多个插槽,父组件使用 v-slot 或简写 # 指定内容位置:

<!-- 子组件 -->
<div class="child">
  <header>
    <slot name="header"></slot> <!-- 头部插槽 -->
  </header>
  <main>
    <slot></slot> <!-- 默认插槽 -->
  </main>
  <footer>
    <slot name="footer"></slot> <!-- 底部插槽 -->
  </footer>
</div>

<!-- 父组件使用 -->
<MyComponent>
  <template #header>
    <h1>标题</h1>
  </template>
  <p>主要内容</p> <!-- 自动匹配默认插槽 -->
  <template #footer>
    <p>页脚信息</p>
  </template>
</MyComponent>

2. 作用域插槽:子组件数据暴露

原理

子组件通过 <slot> 的 v-bind 传递数据,父组件在接收时使用 (props) 解构数据:

<!-- 子组件:UserList.vue -->
<ul>
  <li v-for="user in users">
    <slot :user="user"> <!-- 暴露user数据给父组件 -->
      {{ user.name }} <!-- 默认内容 -->
    </slot>
  </li>
</ul>

<script>
export default {
  data() {
    return {
      users: [{ name: 'Alice' }, { name: 'Bob' }]
    }
  }
}
</script>

<!-- 父组件使用 -->
<UserList v-slot="slotProps">
  <template #default="slotProps"> <!-- 完整写法 -->
    <span>{{ slotProps.user.name.toUpperCase() }}</span>
  </template>
</UserList>

<!-- 简写:直接解构props -->
<UserList #default="{ user }">
  <span>{{ user.name.toUpperCase() }}</span>
</UserList>

具名作用域插槽

每个插槽可独立传递不同数据:

vue

<!-- 子组件 -->
<slot name="header" :title="pageTitle"></slot>
<slot :user="currentUser"></slot>

<!-- 父组件 -->
<MyComponent>
  <template #header="{ title }">
    <h1>{{ title }}</h1>
  </template>
  <template #default="{ user }">
    <p>{{ user.name }}</p>
  </template>
</MyComponent>

3. 高级应用

动态插槽名(Vue 3)

使用表达式作为插槽名:

vue

<template v-slot:[dynamicSlotName]>
  ...
</template>

<!-- 简写 -->
<template #[dynamicSlotName]>
  ...
</template>

作用域插槽的高级解构

支持默认值和别名:

vue

<UserList #default="{ user = { name: 'Guest' } }">
  <!-- 当user为null时使用默认值 -->
</UserList>

<UserList #default="{ user: person }">
  <!-- 将user重命名为person -->
</UserList>

后备内容(Fallback Content)

当父组件未提供内容时,显示插槽内的默认内容:

vue

<slot>
  <p>默认内容</p>
</slot>

4. 插槽与组件设计

组合式组件

通过插槽实现灵活的组件组合:

<!-- 模态框组件 -->
<div class="modal">
  <header>
    <slot name="title">默认标题</slot>
  </header>
  <div class="content">
    <slot></slot>
  </div>
  <footer>
    <slot name="footer">
      <button @click="close">关闭</button>
    </slot>
  </footer>
</div>

<!-- 使用 -->
<Modal>
  <template #title>自定义标题</template>
  <p>自定义内容</p>
  <template #footer>
    <button @click="submit">提交</button>
    <button @click="cancel">取消</button>
  </template>
</Modal>

渲染函数中的插槽

在 render 函数中访问插槽内容:

// Vue 3
export default {
  render() {
    return h('div', [
      this.$slots.header?.(), // 具名插槽
      this.$slots.default?.()  // 默认插槽
    ]);
  }
}

5. Vue 3 与 Vue 2 的差异

特性Vue 3Vue 2
语法v-slot 或 #slot 和 slot-scope(已废弃)
作用域插槽简写直接解构 #default="{ user }"需使用 slot-scope
动态插槽名支持 #[expression]不支持
渲染函数this.$slots.header()this.$scopedSlots.header()

6. 最佳实践

  1. 合理使用具名插槽:当组件有多个内容区域时,使用具名插槽提高可读性。
  2. 暴露必要数据:通过作用域插槽将子组件数据暴露给父组件,避免过度耦合。
  3. 避免深层嵌套:插槽嵌套过深会降低代码可读性,可考虑组件拆分。
  4. 提供后备内容:为插槽添加默认内容,增强组件健壮性。

总结

Vue 插槽通过内容分发和作用域机制,实现了组件的高度复用和灵活扩展:

  • 基础插槽:实现内容的简单注入。
  • 作用域插槽:让父组件能够访问子组件数据,动态定制渲染逻辑。
  • 具名插槽:支持多区域内容分发,构建复杂布局。

掌握插槽是构建可复用组件库和复杂应用的关键,尤其在开发 UI 组件库时,插槽设计直接影响组件的灵活性和扩展性。