前端面试复习指南【代码演示多多版】之——Vue

0 阅读5分钟

一、Vue 基础核心

1. 生命周期钩子

// 创建阶段
beforeCreate()    // 实例初始化,数据观测前
created()         // 数据观测完成,可访问 data,但未挂载 DOM

// 挂载阶段  
beforeMount()     // 模板编译完成,未渲染
mounted()         // DOM 渲染完成,可操作 DOM

// 更新阶段
beforeUpdate()    // 数据变化,DOM 更新前
updated()         // DOM 更新完成

// 销毁阶段
beforeDestroy()   // 实例销毁前,清理定时器、取消订阅
destroyed()       // 实例销毁后

// 特殊(keep-alive)
activated()       // 组件激活时
deactivated()     // 组件失活时

// 面试题:mounted 和 created 区别?
// created: 能访问 data,不能操作 DOM
// mounted: DOM 已渲染,可操作 DOM、发请求

2. 组件通信方式

// 1. props / $emit (父子组件)
// 父组件
<Child :msg="message" @update="handleUpdate" />

// 子组件
props: ['msg'],
methods: {
  sendToParent() {
    this.$emit('update', '新数据')
  }
}

// 2. $refs (父调子)
<Child ref="childComp" />
this.$refs.childComp.childMethod()

// 3. $parent / $children (不推荐)

// 4. provide / inject (祖先后代)
// 祖先
provide() {
  return {
    theme: 'dark',
    user: this.user
  }
}
// 后代
inject: ['theme', 'user']

// 5. eventBus (任意组件)
// bus.js
export const bus = new Vue()
// 发送
bus.$emit('event-name', data)
// 接收
bus.$on('event-name', data => {})

// 6. Vuex (全局状态管理)

// 7. $attrs / $listeners (跨级传递)
// 父传孙,中间组件用 v-bind="$attrs" v-on="$listeners"

// 8. slot (内容分发)
<!-- 子组件 Child.vue -->
<template>
  <div>
    <h3>子组件</h3>
    <!-- 把 user 数据传给父组件 -->
    <slot :user="userData"></slot>
  </div>
</template>

<script>
export default {
  data() {
    return {
      userData: { name: 'Alice', age: 25 }
    }
  }
}
</script>

<!-- 父组件 Parent.vue -->
<template>
  <Child>
    <!-- 接收子组件传来的数据,随便起名 slotProps -->
    <template #default="slotProps">
      <p>姓名:{{ slotProps.user.name }}</p>
      <p>年龄:{{ slotProps.user.age }}</p>
    </template>
  </Child>
</template>

<script>
import Child from './Child.vue'
export default { components: { Child } }
</script>

3. 指令

<!-- 常用指令 -->
v-if        <!-- 条件渲染(销毁/重建) -->
v-show      <!-- 条件显示(display: none) -->
v-for       <!-- 列表渲染,必须绑定 key -->
v-bind      <!-- 动态绑定属性,简写 : -->
v-on        <!-- 事件绑定,简写 @ -->
v-model     <!-- 双向绑定 -->
v-html      <!-- 插入 HTML(小心 XSS) -->
v-text      <!-- 插入文本 -->
v-pre       <!-- 跳过编译 -->
v-once      <!-- 只渲染一次 -->
v-cloak     <!-- 防止闪烁 -->

<!-- 面试题:v-if vs v-show -->
v-if: 条件不成立时不渲染,适合运行时很少改变
v-show: 始终渲染,切换 display,适合频繁切换

二、Vue 进阶核心

1. 响应式原理

// Vue 2 响应式 (Object.defineProperty)
function defineReactive(obj, key, val) {
  // 依赖收集器(存有哪些地方用到了这个数据)
  const dep = new Dep()
  
  // 核心:拦截属性的读取和修改
  Object.defineProperty(obj, key, {
    get() {
      // 读取时:记录谁在用这个数据
      if (Dep.target) dep.add(Dep.target)
      return val
    },
    set(newVal) {
      if (newVal !== val) {
        val = newVal
        // 修改时:通知所有用到的地方更新
        dep.notify()
      }
    }
  })
}

// 缺点:
// - 无法检测对象属性的添加/删除 (需用 Vue.set/Vue.delete)
// - 无法直接监听数组变化 (需重写数组方法)

// Vue 3 响应式 (Proxy)
function reactive(obj) {
  return new Proxy(obj, {
    get(target, key) {
      // 读取时收集依赖
      track(target, key)
      return target[key]
    },
    set(target, key, value) {
      if (target[key] !== value) {
        target[key] = value
        // 修改时触发更新
        trigger(target, key)
      }
      return true
    },
    deleteProperty(target, key) {
      // 删除属性也能监听
      delete target[key]
      trigger(target, key)
      return true
    }
  })
}

// 优点:
// - 可检测属性添加/删除
// - 可检测数组索引/length 变化
// - 性能更好

Vue 响应式核心是数据劫持 + 依赖收集 + 发布订阅
1.数据劫持
-   Vue 2:用 `Object.defineProperty` 递归遍历对象属性,拦截 get/set
-   Vue 3:用 `Proxy` 代理整个对象,拦截所有操作
2.依赖收集
-   模板编译时,会读取数据 → 触发 get
-   每个数据有一个 dep,记录哪些地方用到了它(watcher/effect)
3.发布订阅
-   数据修改时 → 触发 set/proxy
-   通知 dep 中记录的所有依赖 → 执行更新

2. 计算属性 vs 方法 vs 侦听器

// 计算属性 (computed)
computed: {
  fullName() {
    return this.firstName + ' ' + this.lastName
  }
}
// 特点:有缓存,依赖不变不重新计算

// 方法 (methods)
methods: {
  getFullName() {
    return this.firstName + ' ' + this.lastName
  }
}
// 特点:无缓存,每次调用都执行

// 侦听器 (watch)
watch: {
  firstName(newVal, oldVal) {
    this.fullName = newVal + ' ' + this.lastName
  }
}
// 特点:适合异步/开销大的操作

// 面试题:computed 和 watch 区别?
// computed: 依赖多个数据,同步,有缓存
// watch: 监听单个数据变化,异步/复杂逻辑

3. nextTick 原理

// 为什么需要 nextTick?
// Vue 是异步更新 DOM 的,数据变化后 DOM 不会立即更新

methods: {
  async updateData() {
    this.message = '新消息'
    console.log(this.$el.textContent) // 旧值 ❌
    
    this.$nextTick(() => {
      console.log(this.$el.textContent) // 新值 ✅
    })
    
    // 或 await 版本
    await this.$nextTick()
    console.log(this.$el.textContent) // 新值 ✅
  }
}

// 原理:将回调放入微任务队列
// 优先 Promise.then > MutationObserver > setImmediate > setTimeout

三、Vue Router

1. 路由模式

// hash 模式
https://example.com/#/user/1
// 特点:兼容性好,无需服务器配置

// history 模式
https://example.com/user/1
// 特点:URL 美观,需服务器配置回退

// 面试题:两种模式区别?
// hash: 使用 URL hash 值,onhashchange 监听
// history: 使用 HTML5 History API,pushState/replaceState

2. 路由守卫

// 全局守卫
<!-- router/index.js -->
router.beforeEach((to, from, next) => {
  if (to.meta.requiresAuth && !isLogin) {
    next('/login')  // 跳转登录
  } else {
    next()  // 放行
  }
})

router.afterEach((to, from) => {
  // 日志记录等
})

// 路由独享守卫
<!-- router/index.js -->
{
  path: '/admin',
  component: Admin,
  beforeEnter: (to, from, next) => {}
}

// 组件内守卫
<!-- routeDemo.vue -->
beforeRouteEnter(to, from, next) {
  // 不能访问 this
  next(vm => { /* 可访问 vm */ })
},
beforeRouteUpdate(to, from, next) {},
beforeRouteLeave(to, from, next) {}

3. 路由传参

// 1. params (需配合 name)
// URL 示例
/user/123
// 路由配置
{ path: '/user/:id', name: 'user', component: User }
// 跳转
this.$router.push({ name: 'user', params: { id: 123 } })
// 获取
this.$route.params.id

// 2. query (URL 参数)
// URL 示例
/user?id=123
// 跳转
this.$router.push({ path: '/user', query: { id: 123 })
// 获取
this.$route.query.id

// 3. props 解耦
{ path: '/user/:id', component: User, props: true }
// 组件内用 props 接收
props: ['id']

四、Vuex

1. 核心概念

// store.js
export default new Vuex.Store({
  // 状态
  state: {
    user: null,
    count: 0
  },
  
  // 获取器(类似 computed)
  getters: {
    isLogin: state => !!state.user,
    doubleCount: state => state.count * 2
  },
  
  // 同步修改(唯一修改 state 的方式)
  mutations: {
    SET_USER(state, user) {
      state.user = user
    },
    INCREMENT(state) {
      state.count++
    }
  },
  
  // 异步操作
  actions: {
    async login({ commit }, userData) {
      const user = await api.login(userData)
      commit('SET_USER', user)
    }
  },
  
  // 模块化
  modules: {
    cart: cartModule,
    product: productModule
  }
})

// 组件中使用
computed: {
  user() {
    return this.$store.state.user
  },
  ...mapGetters(['isLogin', 'doubleCount'])
},
methods: {
  ...mapMutations(['INCREMENT']),
  ...mapActions(['login'])
}

2. 数据流

// 单向数据流
View (组件) 
    ↓ dispatch
Action (异步)
    ↓ commit
Mutation (同步) 
    ↓ mutate
State (状态) 
    ↓ render
View (组件)

// 面试题:为什么 mutation 必须同步?
// 为了 devtools 能够追踪状态变化

五、Vue 3 新特性

1. Composition API

// Vue 2 (Options API)
export default {
  data() {
    return { count: 0 }
  },
  methods: {
    increment() { this.count++ }
  },
  computed: {
    double() { return this.count * 2 }
  }
}

// Vue 3 (Composition API)
import { ref, computed, watch } from 'vue'

export default {
  setup() {
    // 响应式数据
    const count = ref(0)
    
    // 方法
    const increment = () => count.value++
    
    // 计算属性
    const double = computed(() => count.value * 2)
    
    // 侦听器
    watch(count, (newVal, oldVal) => {
      console.log('count变化:', newVal)
    })
    
    // 生命周期
    onMounted(() => {
      console.log('组件挂载')
    })
    
    return { count, increment, double }
  }
}

2. 响应式 API

import { ref, reactive, toRefs } from 'vue'

// ref:处理基本类型
const count = ref(0)
count.value++  // 通过 .value 访问

// reactive:处理对象
const state = reactive({
  name: 'Alice',
  age: 25
})
state.name = 'Bob'  // 直接访问

// toRefs:解构时保持响应式
const { name, age } = toRefs(state)
// 现在 name.value, age.value 是响应式的

3. 新特性

// 1. 多个根节点
<template>
  <header>...</header>
  <main>...</main>
  <footer>...</footer>
</template>

// 2. Teleport(传送门)
<teleport to="body">
  <div class="modal">弹窗</div>
</teleport>

// 3. Suspense(异步组件)
<Suspense>
  <template #default>
    <AsyncComponent />
  </template>
  <template #fallback>
    <div>加载中...</div>
  </template>
</Suspense>

六、常见面试题

1. v-for 为什么需要 key?

<!-- 错误 -->
<div v-for="item in list">{{ item }}</div>

<!-- 正确 -->
<div v-for="item in list" :key="item.id">{{ item }}</div>

原因:Vue 的 diff 算法通过 key 来识别节点,提高更新性能,避免渲染错误。


2. 组件中 data 为什么是函数?

// 错误
data: {
  count: 0
}

// 正确
data() {
  return {
    count: 0
  }
}

原因:保证每个组件实例有独立的数据,避免多个实例共享 data。


3. Vue 2 中改变数组无法触发视图更新?

// 不会触发更新
this.list[0] = '新值'      // 索引赋值
this.list.length = 0       // 修改长度

// 会触发更新的方法
this.list.push('新值')
this.list.pop()
this.list.shift()
this.list.unshift()
this.list.splice()
this.list.sort()
this.list.reverse()

// 或使用 Vue.set
Vue.set(this.list, 0, '新值')
this.$set(this.list, 0, '新值')

4. 虚拟 DOM 是什么?

// 虚拟 DOM 是一个 JS 对象,描述真实 DOM
const vnode = {
  tag: 'div',
  props: { id: 'app', class: 'container' },
  children: [
    { tag: 'h1', props: {}, children: ['标题'] }
  ]
}

// 优点:
// - 跨平台(渲染到 DOM、Native、小程序)
// - 批量更新,减少 DOM 操作
// - 抽象了渲染过程

5. keep-alive 的作用

<keep-alive>
  <component :is="currentTab">
    <!-- 被切换的组件会被缓存,不销毁 -->
  </component>
</keep-alive>

作用:缓存组件状态,避免重复渲染。组件会多两个生命周期:activateddeactivated