学习笔记-Vue2知识总结

37 阅读12分钟

一、基础概念篇

1. 什么是Vue2?它的核心特点有哪些?

你想了解的Vue2是一套用于构建用户界面的渐进式JavaScript框架(发布于2016年,目前仍有大量存量项目在使用),所谓“渐进式”指的是可以按需引入Vue的功能模块,无需一次性全盘接受,适合从简单页面到复杂单页应用(SPA)的逐步迭代。

它的核心特点如下:

  • 数据驱动视图(MVVM思想):无需手动操作DOM,只需维护数据,Vue会自动完成数据到DOM的映射更新
  • 组件化开发:将页面拆分为可复用、独立的组件,提高开发效率和代码可维护性
  • 双向数据绑定(v-model):简化表单交互,实现数据与视图的双向同步
  • 虚拟DOM:通过抽象DOM结构减少真实DOM操作,提高渲染性能
  • 指令系统:提供一系列内置指令(如v-if、v-for),简化DOM操作逻辑
  • 生命周期钩子:提供完整的组件生命周期回调,方便在不同阶段执行业务逻辑

2. Vue2与Vue3的核心区别是什么?(高频面试题)

两者的核心差异集中在底层架构、性能、语法体验三个方面,核心区别如下:

对比维度Vue2Vue3
核心架构选项式API(Options API),按属性分类(data、methods、computed)组合式API(Composition API),按业务逻辑组织代码,更适合复杂组件
响应式原理Object.defineProperty(),无法监听对象新增属性、数组下标修改Proxy + Reflect,全方位监听对象/数组变化,无需特殊处理
虚拟DOM传统虚拟DOM,更新粒度较粗优化后的虚拟DOM,结合编译期优化,更新效率更高
组件写法单根节点限制支持多根节点(片段)
生命周期beforeCreate/created等(无setup)新增setup钩子,对应替换beforeCreate/created,其余钩子添加on前缀(如onMounted)
体积相对较大,无法按需引入核心功能支持Tree-Shaking,体积更小,按需引入API

3. Vue2中的MVVM模式是什么?如何理解?

MVVM是Model-View-ViewModel的缩写,是一种软件架构模式,Vue2正是基于MVVM思想实现的,各部分职责如下:

  • Model(模型) :对应Vue中的dataprops等数据层,存储业务数据和状态,不关心视图展示

  • View(视图) :对应DOM模板层,负责展示数据,是被动的,仅根据Model数据更新自身

  • ViewModel(视图模型) :Vue实例本身就是ViewModel,作为Model和View之间的桥梁,核心职责:

    • 监听Model数据变化(通过Object.defineProperty()
    • 当Model数据变化时,更新View(数据驱动视图)
    • 监听View的用户交互(如表单输入),更新Model数据(视图驱动数据,双向绑定)
  • 核心优势:解耦View和Model,开发者无需操作DOM,只需专注于数据和业务逻辑。

二、核心语法篇

1. Vue2中如何实现数据响应式?有什么局限性?

实现原理

Vue2通过Object.defineProperty()方法对data中的属性进行数据劫持,结合发布-订阅模式实现响应式,核心步骤如下:

  1. 初始化时,遍历data中的所有属性(递归遍历嵌套对象)
  2. 对每个属性使用Object.defineProperty()定义gettersetter
  3. getter:当属性被访问时,收集依赖(即当前使用该属性的组件/Watcher)
  4. setter:当属性值被修改时,通知所有收集到的依赖,触发对应的更新逻辑,更新视图

局限性

  • 无法监听对象新增的属性(只能劫持初始化时已存在的属性)
  • 无法监听数组的下标修改和长度修改(如arr[0] = 10arr.length = 0
  • 对于复杂嵌套对象,递归劫持会有一定的性能开销

解决方案

  • 新增对象属性:使用Vue.set(object, key, value)this.$set(object, key, value)
  • 修改数组:使用Vue封装的数组方法(pushpopshiftunshiftsplicesortreverse),或Vue.set(arr, index, value)
  • 替换整个对象/数组:直接重新赋值(如this.obj = { ...this.obj, newKey: newValue }

2. v-model的工作原理是什么?如何在自定义组件上使用v-model?

工作原理

v-model是Vue提供的语法糖,本质是结合了v-bind(绑定value属性)和v-on(监听input事件),核心逻辑如下:

<!-- 原生表单元素上的v-model -->
<input v-model="message">

<!-- 等价于手动绑定和监听 -->
<input :value="message" @input="message = $event.target.value">

对于不同的表单元素,v-model对应的属性和事件略有差异:

  • 输入框(text/textarea):value属性 + input事件
  • 单选框/复选框:checked属性 + change事件
  • 下拉框(select):value属性 + change事件

自定义组件上使用v-model

自定义组件默认通过value属性接收数据,通过input事件传递更新后的数据,步骤如下:

  1. 子组件:通过props接收value属性
  2. 子组件:当内部数据变化时,通过$emit('input', 新值)触发事件
  3. 父组件:直接在子组件标签上使用v-model绑定数据即可

示例代码:

<!-- 子组件 Child.vue -->
<template>
  <input :value="value" @input="$emit('input', $event.target.value)">
</template>

<script>
export default {
  props: ['value'] // 接收父组件传递的value
}
</script>

<!-- 父组件 Parent.vue -->
<template>
  <Child v-model="parentMessage"></Child>
</template>

<script>
import Child from './Child.vue'
export default {
  components: { Child },
  data() {
    return {
      parentMessage: ''
    }
  }
}
</script>

如果想修改默认的valueinput,可以通过子组件的model选项配置:

// 子组件中配置
export default {
  model: {
    prop: 'msg', // 替换默认的value
    event: 'updateMsg' // 替换默认的input
  },
  props: ['msg']
}

3. v-for和v-if为什么不建议同时使用?有什么替代方案?

不建议同时使用的原因

v-for的优先级高于v-if,当两者同时作用在同一个元素上时,会导致性能问题

  1. 每次渲染时,都会先执行v-for遍历整个数组,再对每个遍历项执行v-if判断
  2. 即使只有少量数据需要展示,也会遍历全部数据,造成不必要的性能开销
  3. 逻辑上不清晰,v-for用于遍历,v-if用于条件判断,分离职责更易维护

替代方案

  1. 使用计算属性(推荐) :先通过计算属性过滤掉不需要展示的数据,再进行v-for遍历
<template>
  <div v-for="item in activeItems" :key="item.id">{{ item.name }}</div>
</template>

<script>
export default {
  data() {
    return {
      items: [
        { id: 1, name: 'Vue2', active: true },
        { id: 2, name: 'Vue3', active: false }
      ]
    }
  },
  computed: {
    activeItems() {
      // 先过滤,再遍历
      return this.items.filter(item => item.active)
    }
  }
}
</script>
  1. 在外层包裹元素上使用v-if:如果是整体显示/隐藏,而非单个遍历项,可在外层加v-if
<template>
  <div v-if="showItems">
    <div v-for="item in items" :key="item.id">{{ item.name }}</div>
  </div>
</template>

4. Vue2中的指令有哪些分类?常用指令的作用是什么?

Vue2的指令以v-开头,用于在视图上添加特殊行为,主要分为以下几类:

1. 数据绑定指令

  • v-text:渲染纯文本,替代{{ }},不会解析HTML(比{{ }}更安全,无闪烁问题)
  • v-html:渲染HTML内容,存在XSS安全风险,不建议在用户输入内容上使用
  • v-bind:绑定HTML属性或组件props,缩写为:,如:src="imgUrl":class="className"
  • v-model:双向数据绑定,仅适用于表单元素和自定义组件

2. 条件渲染指令

  • v-if:根据条件动态创建/销毁元素,惰性渲染(初始条件为假时,不渲染)
  • v-else:配合v-if使用,无独立条件,对应v-if的相反场景
  • v-else-if:配合v-if使用,多条件判断
  • v-show:根据条件显示/隐藏元素(修改CSS的display属性),初始渲染时无论条件真假都会渲染

3. 列表渲染指令

  • v-for:遍历数组、对象、字符串、数字进行渲染,必须配合key属性使用(提高diff算法效率)

    • 遍历数组:v-for="(item, index) in arr" :key="item.id"
    • 遍历对象:v-for="(value, key, index) in obj" :key="key"

4. 事件绑定指令

  • v-on:绑定DOM事件或组件自定义事件,缩写为@,如@click="handleClick"@input="handleInput"

    • 支持事件修饰符:.stop(阻止冒泡)、.prevent(阻止默认行为)、.once(只触发一次)等
    • 支持按键修饰符:.enter(回车)、.tab(制表符)等

5. 其他常用指令

  • v-cloak:解决{{ }}插值表达式的闪烁问题(配合CSS[v-cloak] { display: none; }使用)
  • v-once:只渲染一次,后续数据变化不会重新渲染,用于静态内容优化性能
  • v-pre:跳过当前元素及其子元素的编译过程,直接显示原始内容,用于优化大量静态内容的渲染

5. 计算属性(computed)和侦听器(watch)的区别是什么?各自的使用场景?

核心区别

对比维度computed(计算属性)watch(侦听器)
本质基于依赖缓存的属性,返回一个计算结果监听数据变化,执行自定义逻辑(无返回值)
缓存机制依赖的数据不变时,多次访问会返回缓存结果,不重复计算无缓存,只要监听的数据变化,就会执行回调
语法形式声明式语法,像使用普通属性一样调用(无需加括号)命令式语法,需要定义回调函数
适用场景简单的同步计算,依赖多个数据推导一个结果复杂的异步操作、数据变化后的后续逻辑(如请求接口)
参数无参数,自动依赖data/props中的数据可接收两个参数(新值、旧值)

各自的使用场景

  1. computed 适用场景

    1. 数据格式化(如将时间戳转为日期字符串)
    2. 多数据联动计算(如购物车总价 = 商品单价 * 数量 + 运费)
    3. 数据过滤/转换(如将数组按条件过滤后返回新数组)

示例:

computed: {
  // 购物车总价
  totalPrice() {
    return this.goods.reduce((total, item) => total + item.price * item.quantity, 0) + this.freight
  }
}
  1. watch 适用场景

    1. 监听数据变化,发起异步请求(如根据搜索关键词请求接口数据)
    2. 数据变化后执行复杂的业务逻辑(如表单数据变化后验证表单)
    3. 监听路由变化($route),执行页面跳转后的逻辑

示例:

watch: {
  // 监听搜索关键词,发起接口请求
  searchKey(newVal, oldVal) {
    if (newVal) {
      // 异步请求
      this.$axios.get(`/api/search?keyword=${newVal}`).then(res => {
        this.searchResult = res.data
      })
    }
  },
  // 深度监听对象内部属性变化
  userInfo: {
    handler(newVal) {
      console.log('用户信息变化', newVal)
    },
    deep: true, // 开启深度监听
    immediate: true // 初始化时立即执行一次回调
  }
}

三、组件化开发篇

1. Vue2中组件的通信方式有哪些?分别适用于什么场景?

Vue2组件通信分为父子组件通信、兄弟组件通信、跨层级/全局通信三大类,具体方式及场景如下:

1. 父子组件通信(最常用)

  • 方式1:父传子(props)

    • 原理:父组件通过v-bind传递数据,子组件通过props接收数据
    • 适用场景:父组件向子组件传递静态/动态数据、配置项
    • 注意:props是单向数据流,子组件不能直接修改props,如需修改,可通过$emit通知父组件修改

示例:

<!-- 父组件 -->
<Child :title="pageTitle" :list="goodsList"></Child>

<!-- 子组件 -->
<script>
export default {
  props: {
    // 类型验证 + 默认值
    title: {
      type: String,
      default: '默认标题'
    },
    list: {
      type: Array,
      default: () => [] // 数组/对象默认值需用函数返回,避免共享引用
    }
  }
}
</script>
  • 方式2:子传父($emit / 自定义事件)

    • 原理:子组件通过this.$emit('事件名', 传递数据)触发事件,父组件通过v-on监听事件并接收数据
    • 适用场景:子组件向父组件传递用户交互结果、数据更新通知

示例:

<!-- 子组件 -->
<button @click="handleClick">点击传递数据</button>
<script>
export default {
  methods: {
    handleClick() {
      // 触发自定义事件,传递数据
      this.$emit('child-data', { id: 1, name: 'Vue2' })
    }
  }
}
</script>

<!-- 父组件 -->
<Child @child-data="handleChildData"></Child>
<script>
export default {
  methods: {
    handleChildData(data) {
      console.log('接收子组件数据', data)
    }
  }
}
</script>
  • 方式3:父组件直接访问子组件($refs)

    • 原理:父组件通过ref给子组件命名,通过this.$refs.xxx访问子组件的属性和方法
    • 适用场景:父组件需要主动调用子组件的方法(如子组件的重置表单方法)
    • 注意:$refs仅在组件挂载完成后生效,无法在created钩子中访问

2. 兄弟组件通信

  • 方式1:通过父组件中转

    • 原理:兄弟A通过$emit将数据传递给父组件,父组件再通过props传递给兄弟B
    • 适用场景:兄弟组件关系简单,层级较浅
  • 方式2:通过EventBus(事件总线)

    • 原理:创建一个全局的Vue实例作为事件中心,兄弟组件通过$on监听事件,$emit触发事件

    • 适用场景:兄弟组件较多、层级较深,无需父组件中转

    • 步骤:

      • 创建事件总线(src/utils/bus.js

        1. import Vue from 'vue'
          export default new Vue()
          
      • 发送数据的组件(兄弟A)

        1. import Bus from '@/utils/bus'
          Bus.$emit('brother-data', { msg: '来自兄弟A的数据' })
          
      • 接收数据的组件(兄弟B)

        1. import Bus from '@/utils/bus'
          export default {
            mounted() {
              // 监听事件
              this.busListener = Bus.$on('brother-data', data => {
                console.log('接收兄弟A数据', data)
              })
            },
            beforeDestroy() {
              // 销毁事件监听,防止内存泄漏
              Bus.$off('brother-data', this.busListener)
            }
          }
          

3. 跨层级/全局通信

  • 方式1:Vuex/Pinia(推荐)

    • 原理:集中式状态管理库,将全局共享数据存储在state中,组件通过dispatch/commit修改数据,通过mapState/getters获取数据
    • 适用场景:中大型项目,大量组件共享全局数据(如用户信息、购物车数据)
  • 方式2:provide / inject(依赖注入)

    • 原理:父组件通过provide提供数据,所有后代组件(无论层级多深)都可以通过inject注入数据

    • 适用场景:深层级组件通信,无需逐层传递props

    • 注意:非响应式(默认),如需响应式,可提供一个包含响应式数据的对象

    • 示例:

      • // 父组件
        export default {
          provide() {
            return {
              // 提供响应式数据(绑定this,确保数据更新)
              userInfo: this.userInfo
            }
          },
          data() {
            return {
              userInfo: { name: '张三', age: 25 }
            }
          }
        }
        
        // 后代组件
        export default {
          inject: ['userInfo'], // 注入数据
          mounted() {
            console.log('接收父组件提供的数据', this.userInfo)
          }
        }
        
  • 方式3:全局变量(Vue.prototype / window)

    • 原理:将数据挂载到Vue.prototype(全局Vue实例)或window上,所有组件均可访问

    • 适用场景:简单的全局配置(如接口基础地址、全局常量)

    • 示例:

      • // main.js
        import Vue from 'vue'
        Vue.prototype.$baseUrl = 'https://api.example.com'
        
        // 组件中访问
        console.log(this.$baseUrl)
        

2. Vue2中组件的props验证有哪些方式?如何使用?

为了提高组件的健壮性和可维护性,Vue2支持对props进行类型、格式、必要性等验证,验证方式分为简单验证复杂验证

1. 简单验证(直接指定类型)

适用于简单场景,直接指定props的类型(String、Number、Boolean、Array、Object、Function、Symbol)。

export default {
  props: {
    title: String, // 字符串类型
    count: Number, // 数字类型
    isShow: Boolean, // 布尔类型
    list: Array, // 数组类型
    config: Object, // 对象类型
    handleClick: Function // 函数类型
  }
}

2. 复杂验证(对象形式,包含多属性配置)

适用于复杂场景,可配置type(类型)、required(是否必传)、default(默认值)、validator(自定义验证函数)。

export default {
  props: {
    // 必传的字符串类型
    name: {
      type: String,
      required: true, // 必传,未传递会在控制台报警告
      default: '未知名称' // 仅在required为false时生效
    },
    // 数字类型,有默认值
    age: {
      type: Number,
      default: 18
    },
    // 数组类型,默认值需用函数返回(避免多个组件共享同一引用)
    hobbies: {
      type: Array,
      default: () => ['读书', '运动']
    },
    // 对象类型,默认值需用函数返回
    userInfo: {
      type: Object,
      default: () => ({
        id: '',
        name: ''
      })
    },
    // 自定义验证函数
    score: {
      type: Number,
      validator: (value) => {
        // 验证分数是否在0-100之间
        return value >= 0 && value <= 100
      },
      message: '分数必须在0到100之间' // 自定义报错信息(Vue2.x需配合第三方插件,默认控制台显示默认信息)
    }
  }
}

注意事项

  • type可以是单个类型,也可以是多个类型的数组(如type: [String, Number]
  • default对于基本类型可以直接赋值,对于数组/对象必须用函数返回,避免组件间共享数据引用
  • 验证失败时,Vue会在控制台抛出警告(开发环境),但不会阻止程序运行
  • props验证仅在开发环境生效,生产环境会被忽略,以提高性能

3. Vue2中如何实现组件的插槽(Slot)?有哪些类型的插槽?

插槽(Slot)是Vue实现的内容分发机制,允许父组件向子组件的指定位置插入自定义内容,提高组件的灵活性和复用性,Vue2支持三种插槽类型:默认插槽、具名插槽、作用域插槽。

1. 默认插槽(匿名插槽)

  • 原理:子组件中用<slot></slot>定义插槽位置,父组件直接在子组件标签内写入内容,会自动填充到默认插槽中

  • 适用场景:子组件只有一个插槽,无需区分位置

  • 示例:

    • <!-- 子组件 Child.vue -->
      <template>
        <div class="child-container">
          <h3>子组件标题</h3>
          <!-- 默认插槽 -->
          <slot>默认内容(父组件未传递内容时显示)</slot>
        </div>
      </template>
      
      <!-- 父组件 Parent.vue -->
      <template>
        <Child>
          <!-- 插入到默认插槽中的内容 -->
          <p>这是父组件传递给子组件的默认插槽内容</p>
        </Child>
      </template>
      

2. 具名插槽

  • 原理:子组件通过slotname属性定义多个具名插槽,父组件通过v-slot:name(缩写#name)指定内容插入到对应的插槽中

  • 适用场景:子组件有多个插槽,需要区分不同位置的内容

  • 示例:

    • <!-- 子组件 Child.vue -->
      <template>
        <div class="child-container">
          <!-- 头部插槽 -->
          <slot name="header"></slot>
          <!-- 主体插槽(默认插槽,name可省略) -->
          <slot></slot>
          <!-- 底部插槽 -->
          <slot name="footer"></slot>
        </div>
      </template>
      
      <!-- 父组件 Parent.vue -->
      <template>
        <Child>
          <!-- 具名插槽:头部 -->
          <template #header>
            <h1>这是页面头部</h1>
          </template>
          <!-- 默认插槽 -->
          <p>这是页面主体内容</p>
          <!-- 具名插槽:底部 -->
          <template #footer>
            <div>这是页面底部</div>
          </template>
        </Child>
      </template>
      

3. 作用域插槽

  • 原理:子组件通过slot绑定属性(传递数据),父组件在插槽中通过v-slot接收子组件传递的数据,实现“子传父”的内容分发

  • 适用场景:父组件需要控制插槽内容的渲染样式,但数据来源于子组件(如自定义列表项的渲染)

  • 示例:

    • <!-- 子组件 Child.vue -->
      <template>
        <div class="child-container">
          <ul>
            <li v-for="item in list" :key="item.id">
              <!-- 插槽绑定子组件数据,传递给父组件 -->
              <slot :item="item" :index="index"></slot>
            </li>
          </ul>
        </div>
      </template>
      
      <script>
      export default {
        data() {
          return {
            list: [
              { id: 1, name: 'Vue2', author: '尤雨溪' },
              { id: 2, name: 'React', author: 'Facebook' }
            ]
          }
        }
      }
      </script>
      
      <!-- 父组件 Parent.vue -->
      <template>
        <Child>
          <!-- 接收子组件传递的插槽数据 -->
          <template v-slot="slotProps">
            <!-- 自定义渲染样式,使用子组件数据 -->
            <span>{{ slotProps.index + 1 }}. {{ slotProps.item.name }} - {{ slotProps.item.author }}</span>
          </template>
        </Child>
      </template>
      
    •   简化写法:解构赋值
    • <template v-slot="{ item, index }">
        <span>{{ index + 1 }}. {{ item.name }} - {{ item.author }}</span>
      </template>
      

四、生命周期篇

1. Vue2组件的生命周期钩子有哪些?各自的执行时机和作用是什么?

Vue2组件的生命周期分为四个阶段,共8个核心钩子函数,执行顺序固定,每个钩子对应不同的组件状态,具体如下:

阶段1:初始化阶段(组件创建前/后,未挂载到DOM)

  • beforeCreate(创建前)

    • 执行时机:Vue实例初始化之后,数据劫持(data/props)和事件配置之前
    • 组件状态:datapropsmethods$el均未初始化,无法访问
    • 作用:极少使用,可用于执行一些无需依赖组件数据的初始化操作(如全局事件监听的初始化)
  • created(创建后)

    • 执行时机:Vue实例初始化完成,数据劫持、事件配置已完成,但DOM未生成($el仍为undefined
    • 组件状态:可访问datapropsmethods,但无法访问DOM元素
    • 作用:常用,用于初始化数据、发起异步请求(如获取组件初始化所需的接口数据)、监听事件

阶段2:挂载阶段(组件挂载到DOM)

  • beforeMount(挂载前)

    • 执行时机:模板编译完成,即将开始挂载DOM($el已生成,但未挂载到页面)
    • 组件状态:可访问dataprops$el已存在,但对应的DOM元素尚未渲染到页面
    • 作用:可用于在DOM挂载前修改组件数据,不会触发额外的渲染
  • mounted(挂载后)

    • 执行时机:组件已成功挂载到页面DOM中,模板渲染完成
    • 组件状态:可访问DOM元素($el$refs),数据已完成渲染
    • 作用:常用,用于操作DOM(如初始化第三方插件、获取DOM元素尺寸、绑定DOM事件)

阶段3:更新阶段(组件数据变化,触发视图更新)

  • beforeUpdate(更新前)

    • 执行时机:组件data/props数据变化,虚拟DOM重新渲染之前
    • 组件状态:数据已更新,但视图尚未更新(仍为旧的DOM)
    • 作用:可用于在视图更新前获取旧的DOM状态,或取消一些不必要的更新
  • updated(更新后)

    • 执行时机:虚拟DOM重新渲染完成,视图已更新为最新数据
    • 组件状态:数据和视图均已更新,可访问最新的DOM元素
    • 作用:可用于在视图更新后执行DOM操作,但需避免在此修改data(会导致无限更新循环)

阶段4:销毁阶段(组件从DOM中卸载)

  • beforeDestroy(销毁前)

    • 执行时机:组件即将被销毁,实例仍处于可用状态
    • 组件状态:datapropsmethods、DOM均仍可访问
    • 作用:常用,用于清理资源(如取消全局事件监听、清除定时器、销毁第三方插件实例),防止内存泄漏
  • destroyed(销毁后)

    • 执行时机:组件已被完全销毁,实例所有属性和方法均已失效,DOM已从页面中移除
    • 组件状态:无法访问dataprops、DOM及组件实例
    • 作用:极少使用,可用于执行最终的清理操作

生命周期执行顺序总结

beforeCreate → created → beforeMount → mounted → (数据变化触发)beforeUpdate → updated → (组件销毁触发)beforeDestroy → destroyed

2. 父子组件的生命周期执行顺序是怎样的?

父子组件的生命周期执行遵循**“先父后子,先挂载后销毁”**的原则,具体顺序如下:

1. 挂载阶段(初始化渲染)

父beforeCreate → 父created → 父beforeMount → 子beforeCreate → 子created → 子beforeMount → 子mounted → 父mounted
  • 解析:父组件先完成自身的初始化,再开始子组件的初始化和挂载,子组件挂载完成后,父组件才最终完成挂载

2. 更新阶段(父/子组件数据变化)

  • 情况1:父组件数据变化,触发更新

    • 父beforeUpdate → 子beforeUpdate → 子updated → 父updated
      
  • 情况2:子组件数据变化,触发更新

    • 子beforeUpdate → 子updated
      
  • 解析:父组件更新会带动子组件更新,子组件先完成更新,父组件再完成更新;子组件自身数据变化仅触发子组件的更新钩子

3. 销毁阶段(父组件销毁)

父beforeDestroy → 子beforeDestroy → 子destroyed → 父destroyed
  • 解析:父组件开始销毁前,先销毁所有子组件,子组件完全销毁后,父组件再完成自身销毁

3. 为什么不建议在created钩子中操作DOM?为什么在mounted钩子中可以?

  • created钩子中无法操作DOM的原因

    • created钩子执行时,Vue实例仅完成了datapropsmethods的初始化,模板编译尚未完成, $el (组件根DOM元素)尚未生成,更未将DOM挂载到页面中
    • 此时页面中还不存在当前组件对应的DOM元素,操作DOM会返回nullundefined,无法达到预期效果
  • mounted钩子中可以操作DOM的原因

    • mounted钩子执行时,Vue已完成模板编译,生成了对应的虚拟DOM,并将虚拟DOM渲染为真实DOM,成功挂载到页面的DOM树中
    • 此时$el已指向组件的根DOM元素,$refs也已绑定对应的子DOM元素,能够正常访问和操作DOM,适合初始化第三方依赖DOM的插件(如ECharts、Element UI的弹窗等)

示例:

export default {
  created() {
    // 无法操作DOM,this.$el 为 undefined
    console.log(this.$el) // undefined
    document.getElementById('app') // 可能为 null(若组件未挂载)
  },
  mounted() {
    // 可以正常操作DOM
    console.log(this.$el) // 组件根DOM元素
    this.initEcharts() // 初始化ECharts图表(依赖DOM)
  }
}

五、状态管理篇(Vuex)

1. Vuex是什么?它的核心组成部分有哪些?各自的作用是什么?

什么是Vuex

Vuex是专为Vue.js应用设计的集中式状态管理库,用于解决组件间全局数据共享和状态管理的问题,它采用单向数据流的设计,将全局共享的数据集中存储和管理,确保数据变更的可预测性和可维护性。

核心组成部分(5个核心模块)

Vuex的核心是Store(仓库),包含StateGetterMutationActionModule五个部分,各自作用如下:

  1. State(状态)

    1. 作用:存储全局共享的核心数据(如用户信息、购物车数据、全局配置)
    2. 特点:单一状态树(一个应用只有一个Store,所有全局状态都存储在State中)
    3. 访问方式:组件中通过this.$store.state.xxxmapState辅助函数访问

示例:

// store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    userInfo: null, // 用户信息
    cartList: [] // 购物车列表
  }
})
  1. Getter(派生状态)

    1. 作用:类似于组件的computed,对State中的数据进行加工处理(过滤、转换、计算),返回派生数据,且具有缓存机制
    2. 特点:依赖State数据,State数据变化时,Getter会重新计算
    3. 访问方式:组件中通过this.$store.getters.xxxmapGetters辅助函数访问

示例:

export default new Vuex.Store({
  state: { cartList: [] },
  getters: {
    // 购物车商品总数
    cartTotalCount: state => {
      return state.cartList.reduce((total, item) => total + item.quantity, 0)
    },
    // 带参数的Getter(返回一个函数)
    cartItemById: state => (id) => {
      return state.cartList.find(item => item.id === id)
    }
  }
})
  1. Mutation(同步修改状态)

    1. 作用:唯一允许修改 State 数据的方式,用于执行同步的状态修改逻辑
    2. 特点:必须是同步函数(异步操作会导致状态变更不可追踪,无法进行DevTools调试)
    3. 触发方式:组件中通过this.$store.commit('mutation名', 载荷)mapMutations辅助函数触发

示例:

export default new Vuex.Store({
  state: { cartList: [] },
  mutations: {
    // 添加商品到购物车
    ADD_TO_CART(state, payload) {
      state.cartList.push(payload)
    },
    // 修改商品数量
    UPDATE_CART_QUANTITY(state, { id, quantity }) {
      const item = state.cartList.find(item => item.id === id)
      if (item) item.quantity = quantity
    }
  }
})
  1. Action(异步操作/批量修改状态)

    1. 作用:用于执行异步操作(如接口请求、定时器),异步操作完成后,通过commit触发Mutation修改State,不允许直接修改State
    2. 特点:可以是异步函数,支持批量触发多个Mutation
    3. 触发方式:组件中通过this.$store.dispatch('action名', 载荷)mapActions辅助函数触发

示例:

export default new Vuex.Store({
  mutations: { SET_USER_INFO(state, userInfo) { state.userInfo = userInfo } },
  actions: {
    // 异步获取用户信息
    async getUserInfo({ commit }) {
      const res = await Vue.axios.get('/api/user/info')
      // 触发Mutation修改State
      commit('SET_USER_INFO', res.data)
    }
  }
})
  1. Module(模块拆分)

    1. 作用:将复杂的Store按业务模块拆分(如用户模块、购物车模块、商品模块),每个模块拥有独立的StateGetterMutationAction,提高代码可维护性
    2. 特点:默认情况下,模块内部的MutationAction是全局的(可直接触发),StateGetter是局部的(需通过store.state.模块名访问);可通过namespaced: true开启命名空间,使模块内部的所有成员私有化

示例:

// 模块1:user.js
const userModule = {
  namespaced: true, // 开启命名空间
  state: { userInfo: null },
  mutations: { SET_USER_INFO(state, data) { state.userInfo = data } },
  actions: { async getUserInfo({ commit }) { /* ... */ } }
}

// 模块2:cart.js
const cartModule = {
  namespaced: true,
  state: { cartList: [] },
  mutations: { ADD_TO_CART(state, data) { /* ... */ } }
}

// store/index.js
export default new Vuex.Store({
  modules: {
    user: userModule,
    cart: cartModule
  }
})

// 组件中访问命名空间模块的状态
this.$store.state.user.userInfo
// 组件中触发命名空间模块的Action
this.$store.dispatch('user/getUserInfo')

2. Vuex中为什么必须通过Mutation修改State,而不能直接修改?

Vuex要求必须通过Mutation修改State,核心目的是保证状态变更的可追踪性、可预测性和可调试性,具体原因如下:

  1. 集中式状态变更管理:所有的状态修改都集中在Mutation中,便于统一管理和维护,开发者可以快速定位到状态变更的逻辑,避免多个组件随意修改全局状态导致的混乱
  2. 支持DevTools调试:Vuex的DevTools可以记录每一次Mutation的触发记录(包括Mutation名称、载荷、修改前后的State状态),支持时间旅行(回滚到任意历史状态),如果直接修改State,DevTools无法捕获到状态变更,失去调试能力
  3. 强制同步操作Mutation要求必须是同步函数,确保状态变更的同步性,避免异步操作导致的状态变更顺序混乱、不可预测(异步操作放在Action中执行,完成后再触发Mutation
  4. 便于实现中间件和插件:Vuex的中间件(如日志中间件、持久化插件)都是基于Mutation的触发机制实现的,直接修改State会绕过这些中间件,导致插件失效

3. Vuex的持久化如何实现?

Vuex的State数据存储在内存中,页面刷新后会丢失,实现Vuex持久化的核心思路是 State 数据同步存储到本地存储(localStorage/sessionStorage)中,页面刷新时从本地存储中读取数据并恢复到State中,常用实现方式有两种:

方式1:手动实现(简单场景)

通过Vuex的subscribe方法监听Mutation的触发,在Mutation执行后,将State数据存储到本地存储;在Store初始化时,从本地存储中读取数据并初始化State

示例:

// store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)

// 从localStorage中读取持久化数据
const getLocalStorageState = () => {
  const stateStr = localStorage.getItem('vuexState')
  return stateStr ? JSON.parse(stateStr) : { userInfo: null, cartList: [] }
}

const store = new Vuex.Store({
  state: getLocalStorageState(), // 初始化时读取本地存储
  mutations: { /* ... */ },
  actions: { /* ... */ }
})

// 监听Mutation,同步数据到localStorage
store.subscribe((mutation, state) => {
  localStorage.setItem('vuexState', JSON.stringify(state))
})

export default store

方式2:使用第三方插件(推荐,复杂场景)

使用vuex-persistedstate插件,自动完成State数据与本地存储的同步,无需手动编写监听逻辑。

步骤:

  1. 安装插件

    1. npm install vuex-persistedstate --save
      
  2. 配置插件

    1. // store/index.js
      import Vue from 'vue'
      import Vuex from 'vuex'
      import createPersistedState from 'vuex-persistedstate'
      Vue.use(Vuex)
      
      export default new Vuex.Store({
        state: { /* ... */ },
        mutations: { /* ... */ },
        actions: { /* ... */ },
        plugins: [
          createPersistedState({
            // 配置项
            storage: window.localStorage, // 存储方式(localStorage/sessionStorage)
            key: 'vuexState', // 本地存储的key名
            paths: ['user', 'cart'] // 需要持久化的模块(默认持久化整个State)
          })
        ]
      })
      

六、路由篇(Vue Router 3.x)

1. Vue Router 3.x的核心概念有哪些?各自的作用是什么?

Vue Router 3.x是Vue2官方配套的路由管理器,用于构建单页应用(SPA),核心概念如下:

1. Router(路由实例)

  • 作用:创建路由实例,配置路由规则、全局守卫、模式等核心配置
  • 创建方式:通过new VueRouter({ ...config })创建,挂载到Vue实例上

示例:

// router/index.js
import Vue from 'vue'
import Router from 'vue-router'
import Home from '@/views/Home'
Vue.use(Router)

export default new Router({
  mode: 'history', // 路由模式(hash/history)
  routes: [/* 路由规则 */]
})

2. Route(路由规则/当前路由信息)

  • 作用:routes配置中的单个路由规则(描述路径与组件的映射关系),或当前激活的路由信息对象

  • 路由规则配置项:

    • path:路由路径(如/home/user/:id(动态路由))
    • name:路由名称(用于命名路由跳转,避免硬编码路径)
    • component:路由对应的组件(如Home() => import('@/views/User')(懒加载))
    • props:是否将路由参数转为组件props
    • meta:路由元信息(用于存储路由附加信息,如是否需要登录、页面标题)
    • children:嵌套路由规则(用于页面内嵌套组件)

示例:

routes: [
  {
    path: '/',
    name: 'Home',
    component: Home,
    meta: { requiresAuth: false, title: '首页' }
  },
  {
    path: '/user/:id', // 动态路由
    name: 'User',
    component: () => import('@/views/User'), // 组件懒加载
    props: true, // 路由参数转为props
    meta: { requiresAuth: true, title: '用户中心' }
  }
]

3. Router Link(路由链接)

  • 作用:替代原生<a>标签,实现无刷新的路由跳转,自动添加激活状态类名
  • 常用属性:to(跳转路径/路由对象)、name(路由名称)、params/query(传递参数)

示例:

<!-- 路径跳转 -->
<router-link to="/home">首页</router-link>
<!-- 命名路由跳转,传递参数 -->
<router-link :to="{ name: 'User', params: { id: 1 }, query: { tab: 'info' } }">
  用户中心
</router-link>

4. Router View(路由出口)

  • 作用:路由匹配到的组件将渲染在router-view中,支持嵌套使用(对应嵌套路由)

  • 示例:

    • <!-- 根组件 App.vue -->
      <template>
        <div id="app">
          <router-view></router-view> <!-- 根路由出口 -->
        </div>
      </template>
      
      <!-- 嵌套路由出口(Home组件) -->
      <template>
        <div class="home">
          <h2>首页</h2>
          <router-view></router-view> <!-- 嵌套路由组件渲染位置 -->
        </div>
      </template>
      

5. 路由模式(mode)

  • hash模式(默认):路径以#开头(如http://localhost:8080/#/home),基于URL的hash值实现,无需后端配置,兼容性好
  • history模式:路径无#(如http://localhost:8080/home),基于HTML5 History API实现,URL更美观,但需要后端配置(解决页面刷新404问题,配置所有请求转发到index.html

2. 如何实现Vue Router的路由懒加载?有什么好处?

什么是路由懒加载

路由懒加载(也叫组件懒加载)是指在路由被访问时,才动态加载对应的组件文件,而不是在应用初始化时一次性加载所有组件,是Vue项目优化的重要手段。

实现方式(3种常用方式)

  1. ES6的import()语法(推荐)

    1. // router/index.js
      routes: [
        {
          path: '/home',
          name: 'Home',
          // 路由被访问时,才加载Home组件
          component: () => import('@/views/Home')
        },
        {
          path: '/user',
          name: 'User',
          // 按需加载并指定chunk名称(打包时会生成单独的chunk文件,便于打包优化)
          component: () => import(/* webpackChunkName: "user" */ '@/views/User')
        }
      ]
      
  2. Vue的异步组件( Vue.component

    1. import Vue from 'vue'
      routes: [
        {
          path: '/about',
          name: 'About',
          component: Vue.component('About', () => import('@/views/About'))
        }
      ]
      
  3. webpack的 require.ensure (旧版方式,不推荐)

    1. routes: [
        {
          path: '/contact',
          name: 'Contact',
          component: resolve => require.ensure([], () => resolve(require('@/views/Contact')), 'contact')
        }
      ]
      

路由懒加载的好处

  1. 减小首屏加载体积:应用初始化时,只加载核心路由(如首页)的组件,不加载其他未访问的路由组件,减小首屏打包文件大小,提高首屏加载速度
  2. 优化用户体验:首屏加载更快,减少用户等待时间,避免白屏现象
  3. 按需加载资源:只有当用户访问对应路由时,才请求对应的组件资源,合理利用网络带宽,降低服务器初始请求压力
  4. 便于打包优化:通过指定webpackChunkName,可以将多个路由组件打包到同一个chunk文件中,避免生成过多小文件,提高打包效率和资源加载效率

3. Vue Router的导航守卫有哪些?各自的作用和执行顺序是什么?

Vue Router的导航守卫用于拦截路由跳转过程,执行自定义逻辑(如登录验证、页面权限控制、页面标题修改) ,分为三大类:全局守卫、路由独享守卫、组件内守卫。

1. 全局守卫(全局生效,所有路由跳转都会触发)

  • router.beforeEach(全局前置守卫)

    • 作用:路由跳转前拦截,最常用(如登录验证、权限判断)
    • 执行时机:每次路由跳转前(在组件内守卫之前执行)
    • 参数:to(即将进入的路由对象)、from(即将离开的路由对象)、next(继续跳转的回调函数)
    • 注意:必须调用next()才能继续跳转,next(false)取消跳转,next('/login')重定向到其他路由

示例:

// 登录验证
router.beforeEach((to, from, next) => {
  // 判断路由是否需要登录
  if (to.meta.requiresAuth) {
    const token = localStorage.getItem('token')
    if (token) {
      next() // 已登录,继续跳转
    } else {
      next('/login') // 未登录,重定向到登录页
    }
  } else {
    next() // 无需登录,直接跳转
  }
})
  • router.beforeResolve(全局解析守卫)

    • 作用:与beforeEach类似,但在所有组件内守卫和异步路由组件加载完成后执行
    • 执行时机:beforeEach之后,afterEach之前
    • 适用场景:需要等待异步组件加载完成后执行的逻辑
  • router.afterEach(全局后置守卫)

    • 作用:路由跳转完成后执行,无next参数(无法拦截跳转)
    • 执行时机:每次路由跳转完成后(最后执行)
    • 适用场景:修改页面标题、统计页面访问量、隐藏加载动画

示例:

router.afterEach((to, from) => {
  // 修改页面标题
  document.title = to.meta.title || 'Vue2项目'
})

2. 路由独享守卫(单个路由生效)

  • beforeEnter

    • 作用:仅对当前路由生效,拦截当前路由的跳转
    • 配置位置:在路由规则中直接配置
    • 执行时机:beforeEach之后,组件内守卫之前
    • 适用场景:单个路由的特殊权限控制

示例:

routes: [
  {
    path: '/admin',
    name: 'Admin',
    component: () => import('@/views/Admin'),
    meta: { requiresAuth: true },
    // 路由独享守卫
    beforeEnter: (to, from, next) => {
      const isAdmin = localStorage.getItem('isAdmin')
      if (isAdmin) {
        next()
      } else {
        next('/403') // 无权限,重定向到403页面
      }
    }
  }
]

3. 组件内守卫(组件内部生效)

  • beforeRouteEnter(组件进入前)

    • 作用:组件被路由渲染前拦截
    • 执行时机:beforeEnter之后,mounted之前
    • 注意:此时组件实例尚未创建(thisundefined),如需访问组件实例,可通过next(vm => { /* 操作vm(组件实例) */ })
    • 适用场景:组件进入前的初始化逻辑、获取组件所需数据
  • beforeRouteUpdate(组件更新时)

    • 作用:路由参数变化但组件复用(如/user/1/user/2)时触发
    • 执行时机:路由参数变化后,组件更新前
    • 注意:此时组件实例已存在(this可访问)
    • 适用场景:路由参数变化后,更新组件数据

示例:

export default {
  beforeRouteUpdate(to, from, next) {
    // 路由参数id变化,重新获取用户信息
    this.userId = to.params.id
    this.getUserInfo()
    next()
  }
}
  • beforeRouteLeave(组件离开前)

    • 作用:组件被路由离开前拦截
    • 执行时机:组件离开前,beforeEach之前(针对下一个路由)
    • 注意:此时组件实例已存在(this可访问)
    • 适用场景:提示用户保存未提交的表单、取消定时器

示例:

export default {
  beforeRouteLeave(to, from, next) {
    if (this.formIsModified) {
      const confirm = window.confirm('表单内容未保存,是否确认离开?')
      if (confirm) {
        next()
      } else {
        next(false)
      }
    } else {
      next()
    }
  }
}

导航守卫执行顺序(完整路由跳转流程)

  1. 触发路由跳转(如router.pushrouter-link
  2. 全局前置守卫:router.beforeEach
  3. 组件内守卫(离开当前组件):beforeRouteLeave
  4. 路由独享守卫:beforeEnter
  5. 解析异步路由组件(如有)
  6. 组件内守卫(进入目标组件):beforeRouteEnter
  7. 全局解析守卫:router.beforeResolve
  8. 导航完成,更新DOM(渲染目标组件)
  9. 全局后置守卫:router.afterEach
  10. 组件内守卫(目标组件):beforeRouteEnternext(vm => {})回调(组件实例创建后)

七、性能优化篇

1. Vue2项目有哪些常见的性能优化手段?

Vue2项目的性能优化分为开发阶段优化、打包阶段优化、运行阶段优化三大类,覆盖项目全生命周期,具体手段如下:

一、开发阶段优化(编码层面)

  1. 合理使用v-if和v-show

    1. 频繁切换显示/隐藏:使用v-show(修改CSS,性能更高)
    2. 条件不常变化或需要销毁组件:使用v-if(创建/销毁DOM,节省内存)
  2. 避免v-for和v-if同时使用

    1. 优先使用计算属性过滤数据,再进行v-for遍历(详见核心语法篇)
  3. 为v-for设置唯一key

    1. 使用唯一且稳定的key(如后端返回的id,不建议使用index),帮助Vue的diff算法高效对比虚拟DOM,减少不必要的DOM操作
  4. 合理使用计算属性和侦听器

    1. 复杂数据推导使用computed(利用缓存,避免重复计算)
    2. 异步操作和复杂业务逻辑使用watch(避免在computed中执行异步操作)
  5. 组件懒加载(路由懒加载+组件按需加载)

    1. 路由懒加载:减少首屏打包体积(详见路由篇)
    2. 组件按需加载:非核心组件(如弹窗、图表)采用懒加载,避免初始化时加载
  6. 减少不必要的DOM操作

    1. 利用Vue的数据驱动视图,避免手动操作DOM
    2. 复杂DOM结构使用虚拟列表(如vue-virtual-scroller),只渲染可视区域内的DOM元素(适用于长列表)
  7. 避免不必要的响应式数据

    1. 对于不需要响应式的数据(如静态配置、大型第三方数据),直接挂载到this上(如this.config = { ... }),而非放入data中(避免Object.defineProperty()劫持,节省性能)
  8. 合理使用插槽和组件复用

    1. 提取公共组件(如按钮、输入框、卡片),提高复用性,减少冗余代码
    2. 复杂组件使用插槽分发内容,提高灵活性,避免组件过于臃肿

二、打包阶段优化(webpack配置层面)

  1. 优化webpack配置

    1. 开启productionSourceMap: false(生产环境不生成sourceMap,减小打包体积)
    2. 配置chainWebpackconfigureWebpack,拆分chunk(如将第三方库打包为单独chunk)
    3. 开启代码压缩(terser-webpack-plugin压缩JS,css-minimizer-webpack-plugin压缩CSS)
  2. CDN引入第三方库

    1. 将大型第三方库(如Vue、Vuex、Vue Router、Element UI)通过CDN引入,不打包到项目中,减小打包体积,提高加载速度
    2. 配置:vue.config.jsexternals排除第三方库,public/index.html中引入CDN链接

示例:

// vue.config.js
module.exports = {
  configureWebpack: {
    externals: {
      vue: 'Vue',
      'vuex': 'Vuex',
      'vue-router': 'VueRouter',
      'element-ui': 'ELEMENT'
    }
  }
}
  1. 图片资源优化

    1. 小图片转为base64(url-loader配置limit),减少HTTP请求
    2. 大图片使用WebP格式,或采用图片懒加载(vue-lazyload
    3. 使用CDN存储图片资源,提高图片加载速度
  2. 开启Gzip压缩

    1. 配置compression-webpack-plugin,打包生成Gzip压缩文件
    2. 后端配置(Nginx/Apache)支持Gzip,减少文件传输体积

三、运行阶段优化(项目上线后)

  1. 利用浏览器缓存

    1. 配置静态资源的缓存策略(Nginx配置Cache-ControlETag),长期缓存不变的静态资源(如JS、CSS、图片)
    2. 打包时为静态资源添加哈希值(如app.[hash].js),确保资源更新时浏览器能获取最新版本
  2. 服务端渲染(SSR)/静态站点生成(SSG)

    1. 对于SEO要求高、首屏加载速度要求高的项目,采用Vue SSR(nuxt.js),在服务端渲染页面,返回完整的HTML给浏览器,提高首屏加载速度和SEO效果
  3. 监控性能瓶颈

    1. 使用Vue DevTools的Performance面板,分析组件渲染和更新性能
    2. 使用浏览器DevTools的Performance面板,分析页面加载、脚本执行、DOM渲染的性能瓶颈
    3. 接入前端性能监控平台(如Sentry、Fundebug),实时监控线上项目的性能问题和报错

2. 如何解决Vue2中长列表的性能问题?

长列表(如1000+条数据)直接渲染会导致DOM元素过多、虚拟DOM diff算法耗时过长、页面卡顿、首屏加载缓慢等性能问题,解决方案如下:

方案1:虚拟列表(推荐,适用于超长长列表)

虚拟列表的核心思想是只渲染可视区域内的DOM元素,滚动时动态替换可视区域内的数据和DOM元素,不渲染不可见区域的内容,从而减少DOM数量,提高渲染性能和滚动流畅度。

常用的Vue2虚拟列表插件:

  • vue-virtual-scroller(轻量、易用,支持无限滚动)
  • vue-virtual-list(简单高效,适用于固定高度列表)

使用示例(vue-virtual-scroller):

  1. 安装插件

    1. npm install vue-virtual-scroller --save
      
  2. 全局注册

    1. // main.js
      import Vue from 'vue'
      import VueVirtualScroller from 'vue-virtual-scroller'
      import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'
      Vue.use(VueVirtualScroller)
      
  3. 组件中使用

    1. <template>
        <div class="long-list">
          <RecycleScroller
            :items="longList"
            :item-size="80" // 每个列表项的高度固定高度key-field="id" // 列表项的唯一key
            class="scroller"
          >
            <template v-slot="{ item }">
              <!-- 列表项内容 -->
              <div class="list-item">
                <h3>{{ item.name }}</h3>
                <p>{{ item.desc }}</p>
              </div>
            </template>
          </RecycleScroller>
        </div>
      </template>
      
      <script>
      export default {
        data() {
          return {
            longList: [] // 超长列表数据(10000条+)
          }
        },
        mounted() {
          // 模拟获取超长列表数据
          for (let i = 0; i < 10000; i++) {
            this.longList.push({
              id: i,
              name: `列表项${i + 1}`,
              desc: `这是第${i + 1}条列表数据`
            })
          }
        }
      }
      </script>
      
      <style scoped>
      .long-list {
        height: 600px;
        overflow: auto;
      }
      .scroller {
        height: 100%;
      }
      .list-item {
        height: 80px;
        padding: 10px;
        border-bottom: 1px solid #eee;
      }
      </style>
      

方案2:分页加载(适用于可分页的长列表)

分页加载的核心思想是将长列表数据按页码拆分,每次只渲染一页数据(如10/20条),用户点击页码或滚动到底部时加载下一页数据,减少单次渲染的DOM数量。

实现方式:

  1. 后端支持分页接口(返回当前页数据和总页数)
  2. 前端维护pageNum(当前页码)、pageSize(每页条数)、total(总条数)
  3. 滚动到底部时(或点击下一页),请求下一页数据,追加到列表中

示例(滚动到底部加载下一页):

<template>
  <div class="long-list" @scroll="handleScroll">
    <div class="list-item" v-for="item in list" :key="item.id">
      <h3>{{ item.name }}</h3>
      <p>{{ item.desc }}</p>
    </div>
    <div v-if="loading">加载中...</div>
    <div v-if="noMore">没有更多数据了</div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      list: [],
      pageNum: 1,
      pageSize: 20,
      total: 0,
      loading: false,
      noMore: false
    }
  },
  mounted() {
    this.getListData()
  },
  methods: {
    // 获取列表数据
    async getListData() {
      this.loading = true
      const res = await this.$axios.get('/api/list', {
        params: { pageNum: this.pageNum, pageSize: this.pageSize }
      })
      this.list = [...this.list, ...res.data.list]
      this.total = res.data.total
      this.loading = false
      // 判断是否有更多数据
      this.noMore = this.list.length >= this.total
    },
    // 滚动到底部加载下一页
    handleScroll(e) {
      const { scrollTop, scrollHeight, clientHeight } = e.target
      // 滚动到底部(距离底部20px时触发)
      if (scrollTop + clientHeight >= scrollHeight - 20 && !this.loading && !this.noMore) {
        this.pageNum++
        this.getListData()
      }
    }
  }
}
</script>

方案3:数据懒加载+防抖(适用于中等长度列表)

对于中等长度列表(如500条以内),可采用数据懒加载+防抖的方式,减少一次性渲染的压力:

  1. 初始化时只渲染部分数据(如前50条)
  2. 监听滚动事件,滚动到底部时,追加渲染下一部分数据(如再渲染50条)
  3. 对滚动事件添加防抖处理,避免频繁触发渲染逻辑

八、常见问题与排错篇

1. Vue2中修改了data中的数据,视图为什么没有更新?

这是Vue2开发中高频问题,核心原因是数据修改未被Vue的响应式系统检测到,具体场景及解决方案如下:

场景1:修改了对象新增的属性(未被Object.defineProperty劫持)

// 初始化data
data() {
  return {
    user: { name: '张三' }
  }
}

// 错误修改:新增age属性,视图不更新
this.user.age = 25

// 解决方案
this.$set(this.user, 'age', 25) // 方式1:使用$set
this.user = { ...this.user, age: 25 } // 方式2:重新赋值对象

场景2:修改了数组的下标或长度(Vue未封装该操作)

// 初始化data
data() {
  return {
    arr: [1, 2, 3]
  }
}

// 错误修改:下标修改/长度修改,视图不更新
this.arr[0] = 10
this.arr.length = 0

// 解决方案
this.$set(this.arr, 0, 10) // 方式1:$set修改下标
this.arr.splice(0, 1, 10) // 方式2:使用Vue封装的数组方法
this.arr = [...this.arr.slice(0, 0), 10, ...this.arr.slice(1)] // 方式3:重新赋值数组

场景3:修改了深层嵌套对象的属性(未触发setter)

// 初始化data
data() {
  return {
    obj: { a: { b: { c: 1 } } }
  }
}

// 错误修改:深层嵌套属性,视图可能不更新(尤其是多层嵌套)
this.obj.a.b.c = 2

// 解决方案
this.$set(this.obj.a.b, 'c', 2) // 方式1:$set修改深层属性
this.obj = JSON.parse(JSON.stringify(this.obj)) // 方式2:深拷贝后重新赋值(简单粗暴,不推荐复杂对象)
this.obj.a.b = { ...this.obj.a.b, c: 2 } // 方式3:逐层重新赋值(推荐)

场景4:异步操作中修改数据,未正确触发更新

// 错误示例:定时器中修改数据,视图可能不更新(罕见,Vue通常能检测到)
setTimeout(() => {
  this.message = '异步修改数据'
}, 1000)

// 解决方案(若出现不更新):强制触发更新
setTimeout(() => {
  this.message = '异步修改数据'
  this.$forceUpdate() // 强制组件重新渲染(不推荐频繁使用,会跳过响应式系统)
}, 1000)

场景5:数据被冻结(Object.freeze())

// 错误示例:冻结对象后,修改数据不更新
data() {
  return {
    obj: Object.freeze({ name: '张三' })
  }
}

this.obj.name = '李四' // 视图不更新

// 解决方案:不要冻结需要响应式更新的对象,或重新赋值一个未冻结的对象
this.obj = { ...this.obj, name: '李四' }

2. Vue2中如何解决跨域问题?

跨域是指浏览器的同源策略限制(协议、域名、端口任一不同即为跨域),Vue2项目中解决跨域的方式主要有以下3种:

方式1:开发环境(本地调试)- 配置Vue CLI代理(推荐)

vue.config.js中配置devServer.proxy,通过本地开发服务器转发请求,避免浏览器跨域限制(仅适用于开发环境)。

示例:

// vue.config.js
module.exports = {
  devServer: {
    proxy: {
      // 匹配以/api开头的请求
      '/api': {
        target: 'https://api.example.com', // 目标后端接口地址
        changeOrigin: true, // 开启跨域模拟(修改请求头的Host为目标地址)
        pathRewrite: { '^/api': '' } // 路径重写(去掉请求路径中的/api)
      }
    }
  }
}

使用:

// 前端请求(无需写完整地址,直接写/api开头)
this.$axios.get('/api/user/info').then(res => { /* ... */ })

// 实际转发后的请求地址:https://api.example.com/user/info

方式2:生产环境 - 后端配置CORS(跨域资源共享)

由后端在响应头中添加允许跨域的配置,允许前端域名访问,常用响应头如下:

Access-Control-Allow-Origin: https://www.frontend.com // 允许的前端域名(*表示允许所有域名)
Access-Control-Allow-Methods: GET, POST, PUT, DELETE // 允许的请求方法
Access-Control-Allow-Headers: Content-Type, Authorization // 允许的请求头
Access-Control-Allow-Credentials: true // 允许携带Cookie(需前后端同时配置)

方式3:生产环境 - 反向代理(Nginx/Apache)

通过Nginx配置反向代理,将前端的请求转发到后端接口,避免浏览器跨域(生产环境常用方案)。

Nginx配置示例:

# nginx.conf
server {
  listen 80;
  server_name www.frontend.com; // 前端域名

  # 转发/api开头的请求到后端接口
  location /api/ {
    proxy_pass https://api.example.com/; // 后端接口地址
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
  }

  # 前端静态资源配置
  location / {
    root /path/to/frontend/dist; // 前端打包后的dist目录
    index index.html;
    try_files $uri $uri/ /index.html; // 解决SPA路由刷新404问题
  }
}

3. Vue2中如何解决组件之间的样式冲突?

Vue2组件中的样式默认是全局生效的,多个组件使用相同的类名会导致样式冲突,解决方案如下:

方式1:使用scoped属性(推荐,组件内样式隔离)

在组件的<style>标签上添加scoped属性,Vue会为当前组件的所有DOM元素添加一个唯一的data-v-xxx属性,并为样式添加对应的属性选择器,使样式仅对当前组件生效,不会污染其他组件。

示例:

<template>
  <div class="container">组件内容</div>
</template>

<!-- scoped样式,仅当前组件生效 -->
<style scoped>
.container {
  width: 100%;
  padding: 20px;
}
</style>

注意事项:

  • scoped样式不会影响子组件的根元素(便于父组件控制子组件根元素样式)
  • 如需修改子组件内部样式,可使用深度选择器(/deep/::v-deep,Vue2中推荐/deep/

示例:

<style scoped>
/* 深度选择器:修改子组件内部样式 */
.parent-container /deep/ .child-item {
  color: red;
}
</style>

方式2:使用CSS Modules(模块化CSS)

CSS Modules将CSS样式模块化,每个类名会被编译为唯一的哈希值,避免样式冲突,适用于复杂项目。

使用示例:

<template>
  <!-- 绑定模块化样式 -->
  <div :class="$style.container">组件内容</div>
</template>

<!-- 开启CSS Modules -->
<style module>
.container {
  width: 100%;
  padding: 20px;
}
</style>

方式3:使用BEM命名规范(手动避免冲突)

BEM(Block-Element-Modifier)是一种CSS命名规范,通过统一的命名格式避免类名冲突,适用于不使用scoped和CSS Modules的场景。

命名格式:块名__元素名--修饰符

  • 块名:组件名称(如user-card
  • 元素名:组件内的元素(如user-card__avatar
  • 修饰符:元素的状态(如user-card__avatar--large

示例:

<template>
  <div class="user-card">
    <img class="user-card__avatar user-card__avatar--large" src="" alt="">
    <div class="user-card__name"></div>
  </div>
</template>

<style>
.user-card {
  width: 300px;
  border: 1px solid #eee;
}
.user-card__avatar {
  width: 80px;
  height: 80px;
}
.user-card__avatar--large {
  width: 100px;
  height: 100px;
}
</style>