Vue.js核心知识总结01

50 阅读7分钟

一、Vue 实例与数据绑定详解

1.1 Vue 实例是什么?

Vue 实例是 Vue 应用的入口点,每个 Vue 应用都是通过创建 Vue 实例开始的。

// Vue 实例创建语法
const vm = new Vue({
  // 配置选项
  el: '#app',          // 挂载点
  data: {},           // 数据
  methods: {},        // 方法
  computed: {},       // 计算属性
  watch: {},         // 侦听器
  // ... 其他选项
})

1.2 el 选项的两种写法

写法一:创建时直接指定

// 创建实例时指定挂载点
const vm = new Vue({
  el: '#app',  // 可以是CSS选择器字符串
  data: { message: 'Hello' }
})

// 或者传入DOM元素
const appElement = document.getElementById('app')
const vm = new Vue({
  el: appElement,  // 也可以是DOM元素
  data: { message: 'Hello' }
})

写法二:创建后手动挂载

// 1. 先创建实例(不指定el)
const vm = new Vue({
  data: { message: 'Hello' }
})

// 2. 稍后手动挂载
setTimeout(() => {
  vm.$mount('#app')  // 方式一:使用选择器
  
  // 或
  const element = document.getElementById('app')
  vm.$mount(element)  // 方式二:使用DOM元素
}, 1000)

何时使用$mount?

  • 需要条件挂载:根据用户权限、设备类型等决定挂载点
  • 异步加载场景:等待某些条件满足后再挂载
  • 测试环境:更方便地控制挂载时机

1.3 data 的两种写法详解

写法一:对象式(Vue实例使用)

// 适用于根实例或简单场景
new Vue({
  el: '#app',
  data: {
    message: 'Hello Vue',
    count: 0,
    user: {
      name: '张三',
      age: 25
    }
  }
})

写法二:函数式(组件必须使用)

// 适用于组件(Vue.component 或 .vue 文件)
Vue.component('my-component', {
  data() {
    return {
      message: 'Hello',
      count: 0
    }
  }
})

// 或单文件组件中
export default {
  data() {
    return {
      message: 'Hello',
      count: 0
    }
  }
}

为什么组件必须用函数式?

//  错误示例:对象式data会导致数据共享问题
const CounterComponent = {
  data: { count: 0 },  // 所有实例共享同一个对象
  template: '<button @click="count++">{{ count }}</button>'
}

// 使用组件
<counter-component></counter-component>
<counter-component></counter-component>
// 两个组件会显示相同的count值,点击一个另一个也会变

//  正确示例:函数式保证每个实例有自己的数据
const CounterComponent = {
  data() {
    return { count: 0 }  // 每次返回新对象
  },
  template: '<button @click="count++">{{ count }}</button>'
}
// 现在每个组件实例都有独立的count

二、模板语法深度解析

2.1 插值表达式 {{}} 的完整用法

基本语法:

<div id="app">
  <!-- 显示纯文本 -->
  <p>{{ message }}</p>
  
  <!-- 支持JavaScript表达式 -->
  <p>{{ message + '!' }}</p>
  <p>{{ count + 1 }}</p>
  <p>{{ price * quantity }}</p>
  <p>{{ score >= 60 ? '及格' : '不及格' }}</p>
  
  <!-- 调用方法 -->
  <p>{{ getFullName() }}</p>
  
  <!-- 访问对象属性 -->
  <p>{{ user.name }}</p>
  <p>{{ user.address.city }}</p>
  
  <!-- 数组操作 -->
  <p>{{ list[0] }}</p>
  <p>{{ list.slice(0, 3) }}</p>
  
  <!-- 字符串操作 -->
  <p>{{ text.toUpperCase() }}</p>
  <p>{{ text.substring(0, 10) + '...' }}</p>
</div>

不允许的用法:

<!--  不能是语句 -->
{{ var a = 1 }}
{{ if(true) { return 'yes' } }}
{{ for(let i=0; i<10; i++) { console.log(i) } }}

<!--  不能是函数/类定义 -->
{{ function() { return 1 } }}
{{ class MyClass {} }}

<!--  不能是赋值表达式 -->
{{ a = 1 }}
{{ a += 1 }}

<!-- 不能在标签属性中直接使用 -->
<div id="{{ id }}">错误</div>  <!-- 应该用 v-bind -->

2.2 指令语法:v-bind 详细讲解

基础绑定:

<!-- 绑定属性 -->
<img v-bind:src="imageSrc">
<a v-bind:href="url">链接</a>
<input v-bind:value="inputValue">

<!-- 简写形式 -->
<img :src="imageSrc">
<a :href="url">链接</a>
<input :value="inputValue">

绑定class的多种方式:

<!-- 1. 对象语法(最常用) -->
<div :class="{ active: isActive, 'text-danger': hasError }">
  对象语法
</div>
<!-- 渲染为:<div class="active text-danger"></div> -->

<!-- 2. 数组语法 -->
<div :class="[activeClass, errorClass]">
  数组语法
</div>
<!-- 相当于:<div class="active text-danger"></div> -->

<!-- 3. 混合语法 -->
<div :class="['base-class', { active: isActive, disabled: isDisabled }]">
  混合语法
</div>

<!-- 4. 计算属性返回class对象 -->
<div :class="classObject">
  计算属性
</div>

<script>
new Vue({
  data: {
    isActive: true,
    hasError: false,
    activeClass: 'active',
    errorClass: 'text-danger'
  },
  computed: {
    classObject() {
      return {
        active: this.isActive && !this.hasError,
        'text-danger': this.hasError && this.error.type === 'fatal'
      }
    }
  }
})
</script>

绑定style的详细用法:

<!-- 1. 内联对象语法 -->
<div :style="{ 
  color: activeColor, 
  fontSize: fontSize + 'px',
  'font-weight': isBold ? 'bold' : 'normal'
}">
  内联样式
</div>

<!-- 2. 绑定样式对象 -->
<div :style="styleObject">
  样式对象
</div>

<script>
new Vue({
  data: {
    styleObject: {
      color: 'red',
      fontSize: '13px',
      backgroundColor: '#f5f5f5'
    }
  }
})
</script>

<!-- 3. 数组语法(合并多个样式对象) -->
<div :style="[baseStyles, overridingStyles]">
  数组语法
</div>

<!-- 4. 自动添加浏览器前缀 -->
<div :style="{ transform: 'rotate(45deg)' }">
  自动加前缀
</div>
<!-- Vue会自动处理为:transform, -webkit-transform, -ms-transform等 -->

<!-- 5. 多重值(浏览器兼容) -->
<div :style="{ display: ['-webkit-box', '-ms-flexbox', 'flex'] }">
  多重值
</div>
<!-- 浏览器会依次检查支持哪个值,使用第一个支持的 -->

2.3 v-model 双向绑定深度解析

基础用法:

<!-- 文本输入 -->
<input v-model="message" placeholder="编辑我...">
<p>输入的内容是:{{ message }}</p>

<!-- 多行文本 -->
<textarea v-model="message" placeholder="多行文本..."></textarea>

<!-- 复选框(单个) -->
<input type="checkbox" id="checkbox" v-model="checked">
<label for="checkbox">{{ checked }}</label>

<!-- 复选框(多个,绑定到数组) -->
<input type="checkbox" id="jack" value="Jack" v-model="checkedNames">
<label for="jack">Jack</label>
<input type="checkbox" id="john" value="John" v-model="checkedNames">
<label for="john">John</label>
<p>选中的名字:{{ checkedNames }}</p>

<!-- 单选按钮 -->
<input type="radio" id="one" value="One" v-model="picked">
<label for="one">One</label>
<input type="radio" id="two" value="Two" v-model="picked">
<label for="two">Two</label>
<p>选中的是:{{ picked }}</p>

<!-- 选择框(单选) -->
<select v-model="selected">
  <option disabled value="">请选择</option>
  <option value="A">选项A</option>
  <option value="B">选项B</option>
</select>
<p>选中的是:{{ selected }}</p>

<!-- 选择框(多选,绑定到数组) -->
<select v-model="multiSelected" multiple style="width: 50px;">
  <option>A</option>
  <option>B</option>
  <option>C</option>
</select>
<p>多选的结果:{{ multiSelected }}</p>

修饰符详解:

<!-- .lazy:输入完成后同步(失去焦点或按回车时) -->
<input v-model.lazy="msg">
<p>lazy值:{{ msg }}</p>
<!-- 普通v-model:每输入一个字符就同步一次 -->
<!-- lazy:输入完成后才同步 -->

<!-- .number:自动将输入转为数值类型 -->
<input v-model.number="age" type="number">
<p>age的类型:{{ typeof age }}</p>
<!-- 输入"25"会转为数字25,而不是字符串"25" -->

<!-- .trim:自动去除首尾空白字符 -->
<input v-model.trim="username">
<p>用户名:"{{ username }}"</p>
<!-- 输入"  admin "会自动转为"admin" -->

<!-- 修饰符可以串联使用 -->
<input v-model.lazy.trim="searchText">

v-model 原理(自定义组件中实现):

Vue.component('custom-input', {
  props: ['value'],  // 接收父组件传递的值
  template: `
    <input
      :value="value"
      @input="$emit('input', $event.target.value)"
    >
  `
})

// 使用
<custom-input v-model="message"></custom-input>
<!-- 相当于 -->
<custom-input 
  :value="message"
  @input="message = $event"
></custom-input>

三、事件处理完整指南

3.1 事件绑定语法详解

基本绑定:

<!-- 方法处理器 -->
<button v-on:click="greet">打招呼</button>

<!-- 简写 -->
<button @click="greet">打招呼</button>

<!-- 内联语句 -->
<button @click="count += 1">增加:{{ count }}</button>

<!-- 调用方法 -->
<button @click="say('Hello')">说Hello</button>

<!-- 传递事件对象 -->
<button @click="warn('Form cannot be submitted yet.', $event)">
  提交
</button>

<!-- 访问原始DOM事件 -->
<input @input="onInput">

methods 配置详解:

new Vue({
  data: {
    count: 0,
    name: 'Vue.js'
  },
  methods: {
    // 基本方法
    greet() {
      alert('Hello ' + this.name + '!')
    },
    
    // 带参数的方法
    say(message) {
      alert(message)
    },
    
    // 使用事件对象
    warn(message, event) {
      // 阻止默认行为
      if (event) {
        event.preventDefault()
      }
      alert(message)
    },
    
    // 访问事件目标
    onInput(event) {
      console.log('输入的值:', event.target.value)
      console.log('事件类型:', event.type)
      console.log('触发元素:', event.target.tagName)
    },
    
    // 在方法中使用数据
    increment() {
      this.count++  // 可以访问实例数据
      console.log('当前计数:', this.count)
    }
  }
})

3.2 事件修饰符完整用法

.prevent:阻止默认行为

<!-- 阻止链接跳转 -->
<a @click.prevent="doSomething">链接</a>

<!-- 阻止表单提交 -->
<form @submit.prevent="onSubmit">
  <button type="submit">提交</button>
</form>

<!-- 阻止右键菜单 -->
<div @contextmenu.prevent>禁用右键</div>

.stop:阻止事件冒泡

<!-- 父元素也有click事件 -->
<div @click="parentClick">
  <!-- 点击按钮不会触发父元素的click -->
  <button @click.stop="childClick">按钮</button>
</div>

<!-- 实际应用:模态框关闭 -->
<div class="modal" @click="closeModal">
  <div class="modal-content" @click.stop>
    <!-- 点击内容区域不会关闭模态框 -->
    模态框内容
  </div>
</div>

其他修饰符:

<!-- .capture:使用捕获模式 -->
<!-- 事件从外向内捕获,先执行父元素事件 -->
<div @click.capture="parentClick">
  <button @click="childClick">按钮</button>
</div>
<!-- 点击按钮:先执行parentClick,再执行childClick -->

<!-- .self:只有事件在元素自身触发时才调用 -->
<!-- 点击子元素不会触发 -->
<div @click.self="handleSelf">
  <p>点击这个p不会触发div的click事件</p>
</div>

<!-- .once:事件只触发一次 -->
<button @click.once="init">初始化(只执行一次)</button>

<!-- .passive:提升滚动性能 -->
<!-- 告诉浏览器你不想阻止事件的默认行为 -->
<div @scroll.passive="onScroll">
  滚动内容...
</div>
<!-- 特别适合移动端,提升滚动流畅度 -->

修饰符串联使用:

<!-- 顺序很重要:会按顺序执行 -->
<a @click.stop.prevent="doSomething">
  阻止跳转和冒泡
</a>
<!-- 先执行.stop,再执行.prevent -->

<!-- 多个修饰符 -->
<form @submit.prevent.stop.once="handleSubmit">
  只提交一次的表单
</form>

3.3 按键修饰符和系统修饰符

按键修饰符:

<!-- Vue提供的按键别名 -->
<input @keyup.enter="submit">      <!-- 回车 -->
<input @keyup.tab="nextField">     <!-- Tab -->
<input @keyup.delete="deleteItem"> <!-- 删除 -->
<input @keyup.esc="cancel">        <!-- ESC -->
<input @keyup.space="playPause">   <!-- 空格 -->
<input @keyup.up="moveUp">         <!-- 上箭头 -->
<input @keyup.down="moveDown">     <!-- 下箭头 -->
<input @keyup.left="moveLeft">     <!-- 左箭头 -->
<input @keyup.right="moveRight">   <!-- 右箭头 -->

<!-- 数字键 -->
<input @keyup.1="selectOption1">   <!-- 数字1 -->
<input @keyup.2="selectOption2">   <!-- 数字2 -->

<!-- 字母键 -->
<input @keyup.a="pressA">          <!-- A键 -->
<input @keyup.b="pressB">          <!-- B键 -->

<!-- 使用键码(不推荐,因为键码可能变动) -->
<input @keyup.13="submit">         <!-- 13是回车键码 -->

<!-- 自定义按键别名 -->
<script>
Vue.config.keyCodes = {
  f1: 112,
  f2: 113,
  custom: 86,      // v键
  pageUp: 33,
  pageDown: 34
}
</script>
<input @keyup.f1="showHelp">       <!-- F1键 -->
<input @keyup.custom="doCustom">   <!-- 自定义键 -->

系统修饰键:

<!-- Ctrl + 点击 -->
<button @click.ctrl="doSomething">Ctrl+点击</button>

<!-- Alt + 点击 -->
<button @click.alt="doSomething">Alt+点击</button>

<!-- Shift + 点击 -->
<button @click.shift="doSomething">Shift+点击</button>

<!-- Meta(Windows键或Command键) -->
<button @click.meta="doSomething">Meta+点击</button>

<!-- 精确修饰符 .exact -->
<!-- 只有Ctrl被按下时才触发 -->
<button @click.ctrl.exact="ctrlOnly">仅Ctrl</button>

<!-- 没有任何系统修饰符被按下时才触发 -->
<button @click.exact="noModifiers">无修饰键</button>

鼠标按钮修饰符:

<!-- 左键点击 -->
<button @click.left="leftClick">左键</button>

<!-- 右键点击 -->
<button @click.right="rightClick">右键</button>

<!-- 中键点击 -->
<button @click.middle="middleClick">中键</button>

四、计算属性与侦听器深度解析

4.1 计算属性(Computed)完整指南

计算属性的特点:

  1. 缓存机制:依赖不变时不重新计算
  2. 响应式:依赖的响应式数据变化时自动更新
  3. 声明式:像普通属性一样使用,不用关心如何计算

基本用法:

new Vue({
  data: {
    firstName: '张',
    lastName: '三',
    price: 100,
    quantity: 5,
    discount: 0.1
  },
  computed: {
    // 1. 简写形式(只有getter)
    fullName() {
      return this.firstName + this.lastName
    },
    
    // 2. 完整形式(包含getter和setter)
    fullNameWithSetter: {
      get() {
        return this.firstName + ' ' + this.lastName
      },
      set(newValue) {
        const names = newValue.split(' ')
        this.firstName = names[0]
        this.lastName = names[names.length - 1]
      }
    },
    
    // 3. 依赖多个数据
    totalPrice() {
      return this.price * this.quantity * (1 - this.discount)
    },
    
    // 4. 格式化数据
    formattedPrice() {
      return '¥' + this.totalPrice.toFixed(2)
    },
    
    // 5. 过滤列表
    activeUsers() {
      return this.users.filter(user => user.isActive)
    },
    
    // 6. 排序列表
    sortedItems() {
      return [...this.items].sort((a, b) => a.price - b.price)
    }
  }
})

在模板中使用计算属性:

<div id="app">
  <!-- 像普通属性一样使用 -->
  <p>全名:{{ fullName }}</p>
  <p>总价:{{ formattedPrice }}</p>
  
  <!-- 双向绑定计算属性(需要有setter) -->
  <input v-model="fullNameWithSetter">
  
  <!-- 在v-for中使用 -->
  <ul>
    <li v-for="user in activeUsers" :key="user.id">
      {{ user.name }}
    </li>
  </ul>
  
  <!-- 在样式绑定中使用 -->
  <div :class="{ 'high-price': totalPrice > 1000 }">
    价格:{{ formattedPrice }}
  </div>
  
  <!-- 在条件渲染中使用 -->
  <div v-if="hasActiveUsers">
    有活跃用户
  </div>
</div>

计算属性的缓存机制:

computed: {
  // 这个计算属性会被缓存
  expensiveCalculation() {
    console.log('重新计算...')
    // 假设这是一个耗时的计算
    let result = 0
    for (let i = 0; i < 1000000; i++) {
      result += Math.sqrt(i)
    }
    return result
  }
}

// 多次访问expensiveCalculation,只计算一次
console.log(vm.expensiveCalculation) // 输出"重新计算...",然后返回结果
console.log(vm.expensiveCalculation) // 直接返回缓存结果,不输出
vm.someDependency = 'new value'      // 依赖变化
console.log(vm.expensiveCalculation) // 再次输出"重新计算..."

4.2 侦听器(Watch)完整指南

侦听器的特点:

  1. 观察数据变化:响应特定数据的变化
  2. 执行副作用:适合执行异步操作或复杂逻辑
  3. 无缓存:每次变化都会执行

基本语法:

new Vue({
  data: {
    message: 'Hello',
    user: {
      name: '张三',
      age: 25,
      address: {
        city: '北京',
        street: '朝阳路'
      }
    },
    searchText: '',
    formData: {
      title: '',
      content: ''
    }
  },
  watch: {
    // 1. 基本侦听(函数形式)
    message(newVal, oldVal) {
      console.log(`消息从 "${oldVal}" 变为 "${newVal}"`)
    },
    
    // 2. 深度侦听对象(对象形式)
    user: {
      deep: true,        // 深度侦听对象内部变化
      immediate: true,   // 立即执行一次handler
      handler(newVal, oldVal) {
        console.log('用户信息发生变化')
        // 保存到本地存储
        localStorage.setItem('user', JSON.stringify(newVal))
      }
    },
    
    // 3. 侦听对象属性(字符串形式)
    'user.name': function(newVal, oldVal) {
      console.log(`用户名从 ${oldVal} 改为 ${newVal}`)
    },
    
    // 4. 侦听嵌套属性
    'user.address.city': function(newCity) {
      console.log('城市改为:', newCity)
    },
    
    // 5. 侦听计算属性
    fullName(newVal) {
      console.log('全名更新为:', newVal)
    }
  }
})

实际应用场景:

场景一:搜索功能防抖

watch: {
  searchText: {
    handler(newVal) {
      // 清除之前的定时器
      if (this.searchTimer) {
        clearTimeout(this.searchTimer)
      }
      
      // 设置新的定时器
      this.searchTimer = setTimeout(() => {
        this.performSearch(newVal)
      }, 500) // 500毫秒防抖
    },
    immediate: true // 组件创建时立即执行
  }
},
methods: {
  async performSearch(keyword) {
    if (!keyword.trim()) {
      this.searchResults = []
      return
    }
    
    try {
      this.isLoading = true
      const response = await fetch(`/api/search?q=${keyword}`)
      this.searchResults = await response.json()
    } catch (error) {
      console.error('搜索失败:', error)
    } finally {
      this.isLoading = false
    }
  }
}

场景二:表单验证

watch: {
  email: {
    handler(newVal) {
      this.validateEmail(newVal)
    },
    immediate: true
  },
  
  password: {
    handler(newVal) {
      this.validatePassword(newVal)
    },
    immediate: true
  }
},
methods: {
  validateEmail(email) {
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
    if (!email) {
      this.emailError = '邮箱不能为空'
    } else if (!emailRegex.test(email)) {
      this.emailError = '邮箱格式不正确'
    } else {
      this.emailError = ''
    }
  },
  
  validatePassword(password) {
    if (!password) {
      this.passwordError = '密码不能为空'
    } else if (password.length < 6) {
      this.passwordError = '密码至少6位'
    } else {
      this.passwordError = ''
    }
  }
}

场景三:路由参数变化

watch: {
  // 侦听路由参数变化
  '$route.params.id': {
    handler(newId) {
      this.loadProductDetails(newId)
    },
    immediate: true
  },
  
  // 侦听查询参数变化
  '$route.query': {
    handler(newQuery) {
      this.applyFilters(newQuery)
    },
    deep: true
  }
},
methods: {
  async loadProductDetails(productId) {
    try {
      this.isLoading = true
      const response = await fetch(`/api/products/${productId}`)
      this.product = await response.json()
    } catch (error) {
      console.error('加载失败:', error)
      this.error = '加载产品信息失败'
    } finally {
      this.isLoading = false
    }
  },
  
  applyFilters(query) {
    // 根据查询参数过滤数据
    this.filters = { ...this.filters, ...query }
    this.filterProducts()
  }
}

API方式使用侦听器:

const vm = new Vue({
  data: { count: 0 }
})

// 使用$watch API
const unwatch = vm.$watch('count', function(newVal, oldVal) {
  console.log(`count从${oldVal}变为${newVal}`)
}, {
  deep: false,
  immediate: false
})

// 修改数据,触发侦听器
vm.count = 1  // 输出:count从0变为1

// 停止侦听
unwatch()

vm.count = 2  // 不会触发侦听器

4.3 Computed vs Watch 选择指南

何时使用计算属性?

// 场景1:需要基于现有数据计算新值
computed: {
  // 全名 = 姓 + 名
  fullName() {
    return this.firstName + ' ' + this.lastName
  },
  
  // 过滤并排序列表
  sortedActiveUsers() {
    return this.users
      .filter(user => user.isActive)
      .sort((a, b) => a.name.localeCompare(b.name))
  },
  
  // 格式化显示
  formattedDate() {
    return new Date(this.timestamp).toLocaleDateString()
  }
}

何时使用侦听器?

// 场景1:需要执行异步操作
watch: {
  searchText(newVal) {
    // 发送API请求
    this.fetchSearchResults(newVal)
  }
},

// 场景2:数据变化需要执行多个操作
watch: {
  formData: {
    deep: true,
    handler(newVal) {
      // 1. 保存草稿
      this.saveDraft(newVal)
      // 2. 验证表单
      this.validateForm(newVal)
      // 3. 发送分析事件
      this.sendAnalytics('form_changed', newVal)
    }
  }
},

// 场景3:需要访问新旧值
watch: {
  price(newPrice, oldPrice) {
    const change = ((newPrice - oldPrice) / oldPrice * 100).toFixed(2)
    console.log(`价格变化:${change}%`)
  }
}

性能优化建议:

  1. 能用计算属性就不用侦听器:计算属性有缓存,性能更好
  2. 避免在计算属性中执行异步操作:计算属性应该是同步的
  3. 深度侦听要谨慎deep: true 会遍历对象所有属性,性能开销大
  4. 及时清理定时器:在beforeDestroy中清理侦听器中的定时器
beforeDestroy() {
  if (this.searchTimer) {
    clearTimeout(this.searchTimer)
  }
  if (this.unwatch) {
    this.unwatch()
  }
}

 5.条件渲染

5.1 v-if 指令

语法

<div v-if="表达式">内容</div>
<div v-else-if="表达式">内容</div>
<div v-else>内容</div>

特点

  • 基于条件判断是否创建或移除DOM元素
  • 切换频率较低的场景使用
  • v-if、v-else-if、v-else必须连续使用,中间不能被打断

示例

<div id="root">
    <div v-if="type === 'A'">显示A</div>
    <div v-else-if="type === 'B'">显示B</div>
    <div v-else>显示其他</div>
</div>

5.2 v-show 指令

语法

<div v-show="表达式">内容</div>

特点

  • 通过CSS的display: none控制显示/隐藏
  • DOM元素始终存在,只是视觉上隐藏
  • 切换频率高的场景使用
  • 初始渲染开销大,切换开销小

5.3 v-if vs v-show 对比

特性v-ifv-show
DOM操作添加/删除DOM节点切换CSS的display属性
初始渲染条件为false时不渲染无论条件都渲染,然后隐藏
切换开销高(需要重建DOM)低(只修改CSS)
适用场景不频繁切换的场景频繁切换的场景
性能影响减少初始DOM节点数量增加初始DOM节点数量

5.4 template 标签的使用

<template v-if="n === 1">
    <h1>标题1</h1>
    <p>段落1</p>
    <span>内容1</span>
</template>

特点

  • 作为不可见的包裹元素,不会在最终HTML中显示
  • 只能与v-ifv-else-ifv-elsev-for配合使用
  • 不能v-show一起使用

6、列表渲染

6.1 v-for 指令基础

语法

<li v-for="(item, index) in items" :key="item.id">
    {{ item.name }} - 索引: {{ index }}
</li>

可遍历的数据结构

遍历数组(最常用):

<ul>
    <li v-for="(person, index) in persons" :key="person.id">
        {{ index }} - {{ person.name }} ({{ person.age }})
    </li>
</ul>

遍历对象

<ul>
    <li v-for="(value, key, index) in car" :key="key">
        {{ index }} - {{ key }}: {{ value }}
    </li>
</ul>

遍历字符串(很少用):

<span v-for="(char, index) in 'hello'" :key="index">
    {{ char }}
</span>

遍历数字(很少用):

<span v-for="n in 5" :key="n">{{ n }}</span>
<!-- 输出:1 2 3 4 5 -->

6.2 :key 的重要性

为什么需要key?

key是Vue识别节点的标识,用于高效的DOM更新。

key的内部原理(虚拟DOM Diff算法)

数据更新流程

数据变化 → 响应式系统检测 → 重新生成虚拟DOM → 新旧虚拟DOM差异对比 → 最小化更新真实DOM

Diff算法对比规则

  1. 找到相同key

    • 内容未变 → 复用之前的真实DOM
    • 内容变化 → 生成新真实DOM,替换旧DOM
  2. 未找到相同key

    • 创建新真实DOM,添加到页面

key的选择策略

推荐使用

  • 数据的唯一标识:id、手机号、身份证号等
<li v-for="person in persons" :key="person.id">
    {{ person.name }}
</li>

谨慎使用index作为key

<!-- 可能引发问题的场景 -->
<li v-for="(person, index) in persons" :key="index">
    {{ person.name }}
</li>

index作为key的问题

  1. 效率问题:逆序添加/删除时,产生不必要的真实DOM更新
  2. 数据错乱:包含输入类DOM时,可能产生错误的DOM更新

何时可以使用index

  • 仅用于展示的静态列表
  • 没有逆序操作
  • 没有输入类表单元素

6.3 综合应用

列表过滤与排序

<div id="app">
    <!-- 搜索功能 -->
    <input type="text" v-model="keyword" placeholder="搜索姓名">
    
    <!-- 排序按钮 -->
    <button @click="sortType = 1">年龄升序 ↑</button>
    <button @click="sortType = 2">年龄降序 ↓</button>
    <button @click="sortType = 0">原顺序</button>
    
    <!-- 列表渲染 -->
    <ul>
        <li v-for="p in filteredPersons" :key="p.id">
            {{ p.id }} - {{ p.name }} ({{ p.age }}岁)
        </li>
    </ul>
</div>

<script>
new Vue({
    el: '#app',
    data: {
        keyword: '',
        sortType: 0, // 0:原顺序, 1:升序, 2:降序
        persons: [
            { id: '001', name: '张三', age: 25 },
            { id: '002', name: '李四', age: 22 },
            { id: '003', name: '王五', age: 28 },
            { id: '004', name: '赵六', age: 20 }
        ]
    },
    computed: {
        filteredPersons() {
            let arr = this.persons
            
            // 1. 过滤(根据关键字)
            if (this.keyword) {
                arr = arr.filter(person => 
                    person.name.includes(this.keyword)
                )
            }
            
            // 2. 排序
            if (this.sortType === 1) {
                arr = [...arr].sort((a, b) => a.age - b.age) // 升序
            } else if (this.sortType === 2) {
                arr = [...arr].sort((a, b) => b.age - a.age) // 降序
            }
            
            return arr
        }
    }
})
</script>

数组排序方法详解

sort()方法比较函数

arr.sort((a, b) => {
    // 返回值 < 0: a排在b前面
    // 返回值 > 0: b排在a前面  
    // 返回值 = 0: 保持原顺序
})

// 数字排序口诀:前减后得升序,后减前得降序
numbers.sort((a, b) => a - b)   // 升序
numbers.sort((a, b) => b - a)   // 降序

条件渲染与列表渲染结合

<div id="app">
    <!-- 根据数据是否为空显示不同内容 -->
    <div v-if="items.length === 0">
        <p>暂无数据</p>
        <button @click="loadData">加载数据</button>
    </div>
    
    <template v-else>
        <h3>数据列表 (共{{ items.length }}条)</h3>
        <ul>
            <li v-for="item in items" :key="item.id">
                <span v-if="item.isNew" class="new-badge">NEW</span>
                {{ item.title }}
                <span v-show="item.hot"></span>
            </li>
        </ul>
    </template>
</div>

6.4实践与注意事项

性能优化

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

    • 初始不需要显示 → 使用v-if
    • 需要频繁切换 → 使用v-show
  2. 避免v-if和v-for同时用在同一个元素

    <!-- 不推荐 -->
    <li v-for="user in users" v-if="user.isActive">
        {{ user.name }}
    </li>
    
    <!-- 推荐:使用计算属性过滤 -->
    <li v-for="user in activeUsers" :key="user.id">
        {{ user.name }}
    </li>
    
  3. 为v-for始终添加key

    • 使用唯一标识,避免使用index

常见问题解决

问题1:列表更新视图不刷新

  • 确保使用Vue的响应式方法修改数组
// 正确
this.items.push(newItem)
this.items.splice(index, 1, newItem)
Vue.set(this.items, index, newValue)

// 错误(不会触发视图更新)
this.items[index] = newValue
this.items.length = 0

问题2:嵌套循环的key

<div v-for="category in categories" :key="category.id">
    <h3>{{ category.name }}</h3>
    <div v-for="product in category.products" 
         :key="product.id">
        {{ product.name }}
    </div>
</div>

7. 响应式原理回顾

Vue的响应式系统工作流程

  1. 数据被Object.defineProperty()代理
  2. 数据变化触发setter
  3. 通知所有依赖的Watcher
  4. Watcher调用更新函数
  5. 重新生成虚拟DOM
  6. 执行Diff算法对比新旧虚拟DOM
  7. 最小化更新真实DOM

虚拟DOM的优势

  • 减少直接操作DOM的次数
  • 批量更新,提高性能
  • 跨平台能力(可渲染到不同平台)

总结

特性条件渲染列表渲染
主要指令v-if, v-else-if, v-else, v-showv-for
核心概念条件判断显示/隐藏数据遍历
性能关键合理选择v-if/v-show正确使用:key
常用场景模态框、选项卡、权限控制数据列表、表格、菜单

key的正确使用:

<!-- 使用唯一标识 -->
<li v-for="user in users" :key="user.id">
  {{ user.name }}
</li>

<!-- 组合唯一键 -->
<li v-for="item in items" :key="`${item.category}-${item.id}`">
  {{ item.name }}
</li>

<!-- 特殊情况:使用index(要谨慎) -->
<li v-for="(item, index) in staticList" :key="index">
  {{ item }}
</li>
<!-- 仅在以下情况可以使用index:
     1. 列表是静态的(不会重新排序、添加、删除)
     2. 列表项没有表单元素
     3. 列表项没有自己的状态 -->

数组更新检测:

// Vue能够检测到的数组变更方法:
// 1. push()     - 末尾添加
// 2. pop()      - 末尾删除
// 3. shift()    - 开头删除
// 4. unshift()  - 开头添加
// 5. splice()   - 添加/删除
// 6. sort()     - 排序
// 7. reverse()  - 反转

// Vue能检测到变化:
vm.items.push({ id: 4, name: 'D' })
vm.items.splice(0, 1)  // 删除第一个
vm.items.sort((a, b) => a.price - b.price)

// Vue检测不到的变化:
vm.items[0] = { id: 1, name: '新的A' }  // 直接设置索引
vm.items.length = 0                     // 直接修改length

// 解决方案:
Vue.set(vm.items, 0, { id: 1, name: '新的A' })
vm.items.splice(0, 1, { id: 1, name: '新的A' })  // 替换
vm.items = []  // 重新赋值

嵌套循环:

<!-- 外层循环 -->
<div v-for="category in categories" :key="category.id">
  <h3>{{ category.name }}</h3>
  
  <!-- 内层循环 -->
  <ul>
    <li v-for="product in category.products" 
        :key="product.id"
        :class="{ 'new': product.isNew }">
      {{ product.name }}
      <span v-if="product.isHot"></span>
    </li>
  </ul>
</div>

8.Vue数据代理

数据代理 是 Vue 中一个重要的机制,它允许开发者通过 this.属性名 直接访问 data 中的数据,而不需要写 this.data.属性名

Vue 实例this代理了 data() 函数返回的对象中的所有属性

代理过程

第一步:原始数据存储

// 1. Vue 先执行你的 data() 函数,得到一个对象
const rawData = {
  message: 'Hello Vue',
  count: 0,
  user: { name: 'John' }
};

// 2. Vue 把这个对象保存到实例的 _data 属性
this._data = rawData;
// 现在可以通过 this._data.message 访问

第二步:创建代理连接

// 3. Vue 在自身(this)上创建同名属性
// 对 message 属性的代理
Object.defineProperty(this, 'message', {
  get() {
    // 当有人访问 this.message 时
    // 实际上返回的是 this._data.message
    return this._data.message;
  },
  set(newValue) {
    // 当有人设置 this.message = 'xxx' 时
    // 实际上设置的是 this._data.message = 'xxx'
    this._data.message = newValue;
  }
});

// 对 count 做同样的代理
Object.defineProperty(this, 'count', {
  get() { return this._data.count; },
  set(val) { this._data.count = val; }
});

// 对 user 做同样的代理  
Object.defineProperty(this, 'user', {
  get() { return this._data.user; },
  set(val) { this._data.user = val; }
});

第三步:使用时的效果

// 你写的代码
this.message = 'New Value';

// Vue 实际执行的操作
this._data.message = 'New Value';
// 这就是代理的核心:转发操作

实际工作流程:

  1. Vue 实例初始化时,将原始 data 保存到 _data 私有属性
  2. 遍历 _data 的所有属性名(如 'message', 'count')
  3. 为每个属性在 Vue 实例上定义同名属性
  4. 设置 getter:访问 this.message 时,实际返回 this._data.message
  5. 设置 setter:设置 this.message = 'new' 时,实际设置 this._data.message = 'new'
  • 谁代理谁?

  • 代理者:Vue 实例(this

  • 被代理者data() 返回的对象中的每个属性

  • 代理什么:属性的读取设置操作

  • 代理方式:通过 Object.defineProperty 创建 getter/setter

  • 代理目的:提供简洁统一的访问接口

9.Vue数据监测

Vue 的核心特性之一就是数据驱动视图。当数据变化时,视图会自动更新。

Vue 2.x 的实现原理(基于 Object.defineProperty)

数据劫持

当你在 Vue 的 data 选项中定义数据时,Vue 会遍历这些数据的所有属性,并使用 JavaScript 的 Object.defineProperty 方法对每个属性进行"劫持"。

每个属性都被 Vue 安装了一个"监听器"。这个监听器有两项主要功能:

  • getter(获取拦截器):当你读取这个属性时,Vue 会记录下来"谁读取了我"
  • setter(设置拦截器):当你修改这个属性时,Vue 会通知所有依赖这个属性的地方进行更新

依赖收集

当一个组件或计算属性使用某个数据时,Vue 会创建一个"观察者"(Watcher)。在数据被读取的过程中,这个观察者会把自己"注册"到该数据的依赖列表中。

就好比你订阅了一份杂志:

  • 当你第一次读取数据时,相当于你告诉杂志社:"我对这个数据感兴趣,请把我加入订阅名单"
  • 数据变化时,杂志社(Vue)会给所有订阅者(Watcher)发送通知

发布-订阅模式

当数据发生变化时,setter 会被触发。这时 Vue 会:

  1. 检查新值是否与旧值相同(避免不必要的更新)
  2. 如果值确实改变了,通知所有依赖这个数据的观察者
  3. 观察者收到通知后,执行更新操作(比如重新渲染组件)

递归观测

如果数据的属性值是对象或数组,Vue 会递归地对它们进行同样的观测处理。这就是为什么嵌套对象和数组也能实现响应式。

Vue 2 响应式的局限性

Vue 2 使用 Object.defineProperty 实现响应式,这带来了一个关键限制:

data() {
  return {
    user: {
      name: 'John',
      age: 30
    },
    list: ['a', 'b', 'c']
  };
},
created() {
  //  问题1:添加新属性不会触发更新
  this.user.email = 'john@example.com';  // 视图不会更新
  
  // 问题2:通过索引设置数组项不会触发更新
  this.list[0] = 'new value';  // 视图不会更新
  
  // 问题3:修改数组长度不会触发更新
  this.list.length = 0;  // 视图不会更新
}

根本原因

  • Vue 2 只能在初始化时对已有的属性进行响应式处理
  • 新增的属性没有经过 Object.defineProperty 处理
  • 数组的索引和长度修改也无法被劫持

Vue 2.x 的方法有几个局限性:

  1. 无法检测到对象新属性的添加或删除 (需要通过vue.set)
  2. 对数组的某些操作不敏感(比如通过索引直接修改)
  3. 需要对每个属性单独劫持,性能开销较大

数组的特殊处理

由于历史原因和浏览器兼容性,Vue 对数组进行了特殊处理:

在 Vue 2.x 中:

  • 重写了数组的 7 个变更方法(push、pop、shift、unshift、splice、sort、reverse)

  • 当你调用这些方法时,Vue 能够检测到变化

  • 但通过索引直接设置项(arr[index] = value)或修改长度(arr.length = 0)不会被检测到

数组索引不能用 Object.defineProperty

技术限制

const arr = ['a', 'b', 'c'];

// 理论上可以对索引使用 defineProperty
Object.defineProperty(arr, '0', {
  get() { /* ... */ },
  set() { /* ... */ }
});

// 但问题来了:
arr[100] = 'new';  // 创建了第100个元素
// 难道要对 0-100 每个索引都预先 defineProperty 吗

根本问题

  1. 数组长度动态变化:无法预知会有多少个索引

  2. 性能不可行:如果数组有10000个元素,难道要定义10000个 getter/setter?

  3. 内存浪费:大多数索引可能永远不会被访问

数组访问的实际情况

场景分析

export default {
  data() {
    return {
      list: ['a', 'b', 'c'],
      users: [
        { name: 'John', age: 30 },
        { name: 'Jane', age: 25 }
      ]
    };
  },
  created() {
    // 1. 访问数组本身 - 有 getter
    console.log(this.list);  // 触发 list 的 getter
    
    // 2. 访问数组索引 - 没有 getter
    console.log(this.list[0]);  // 直接访问数组索引,无 getter
    
    // 3. 访问数组中的对象属性 - 有 getter
    console.log(this.users[0].name);  
    // 流程:
    // - users[0]:无 getter(直接数组访问)
    // - .name:有 getter(对象属性有响应式)
    
    // 4. 通过索引修改 - 问题所在
    this.list[0] = 'x';  //  不会触发更新
    
    // 5. 使用方法修改 - 有效
    this.list.push('d');  // 触发更新(重写的方法)
  }
};

数组索引的特殊访问路径

虽然索引没有 getter/setter,但 Vue 通过另一种方式"感知"到数组元素的访问:

// 当你在模板中使用数组索引时:
<template>
  <div>{{ list[0] }}</div>
  <div>{{ users[0].name }}</div>
</template>

// Vue 的编译过程:
1. 编译模板时发现 list[0]
2. 创建对应的 Watcher(观察者)
3. 这个 Watcher 会:
   - 读取 list(触发 listgetter,收集依赖)
   - 然后读取 list[0](直接数组访问)
   
// 关键点:依赖是收集在「数组本身」上,而不是「数组索引」上

vue2能被检测到的数组操作

// 1. 使用重写的7个方法
this.list.push('new');      // 
this.list.pop();            // 
this.list.shift();          // 
this.list.unshift('new');   // 
this.list.splice(0, 1, 'x'); // 
this.list.sort();           // 
this.list.reverse();        // 

// 2. 替换整个数组
this.list = ['new', 'array'];  // 

// 3. 使用 Vue.set(内部用 splice)
Vue.set(this.list, 0, 'new');  // 

// 4. 修改 length(但有限制)
this.list.splice(newLength);  //  通过 splice 修改长度

数组操作规范

// 正确:使用变异方法或 $set
this.list.push('new');               // 添加元素
this.list.splice(index, 1);          // 删除元素
this.$set(this.list, index, 'new');  // 修改元素

// 避免:直接操作索引
this.list[index] = 'new';            // 不会触发更新
this.list.length = 0;                // 不会触发更新

解决方案:Vue.set / this.$set

Vue 提供了 Vue.set(全局API)和 this.$set(实例方法)来解决这个问题:

// 正确做法
this.$set(this.user, 'email', 'john@example.com');  // 视图会更新
this.$set(this.list, 0, 'new value');               // 视图会更新

Vue.set 和 this.$set 的关系

它们是同一个函数

// Vue 源码中的实现
Vue.set = function (obj, key, value) {
  // ... 实现逻辑
};

// 在组件实例上暴露为 $set
Vue.prototype.$set = Vue.set;

// 也就是说:
Vue.set === Vue.prototype.$set  // true

使用场景区别

// 1. 在 Vue 组件内部:使用 this.$set(推荐)
export default {
  methods: {
    addProperty() {
      this.$set(this.user, 'email', 'test@example.com');
    }
  }
};

// 2. 在 Vue 组件外部:使用 Vue.set
import Vue from 'vue';

const app = new Vue({ /* ... */ });
Vue.set(app.user, 'email', 'test@example.com');

// 3. 在非 Vue 上下文中:使用 Vue.set
const plainObject = { name: 'John' };
Vue.set(plainObject, 'age', 30);  // 也可以用于普通对象

什么时候必须用 $set?

// 必须用 $set 的情况:
// 1. 给对象添加新属性
// 2. 通过索引修改数组项(当索引超出原长度或修改已有项)

// 不需要 $set 的情况:
// 1. 修改已有属性:this.user.name = 'new'
// 2. 使用数组变异方法:push, pop, splice 等
// 3. 替换整个数组:this.list = newList

10.Vue 内置指令

10.1 v-text 指令

作用:向所在节点渲染文本内容。

<div v-text="name">你好</div>
<div v-text="str"></div>

特点

  • 替换节点中的所有内容

  • 不识别 HTML 结构(纯文本显示)

  • 与 {{ }} 插值语法的区别:

    • v-text:完全替换内容
    • {{ }}:只替换占位符,保留其他内容

10.2 v-html 指令

作用:向指定节点渲染包含 HTML 结构的内容。

<div v-html="str"></div>

安全性警告

  • XSS 攻击:跨站脚本攻击,恶意脚本注入可能盗取 Cookie 等用户信息

  • 使用原则

    1. 永远不要用在用户提交的内容上
    2. 只在完全可信的内容上使用

11.自定义指令

11.1 定义语法

// 局部指令
new Vue({
  directives: { 指令名: 配置对象 }
})

// 全局指令
Vue.directive('指令名', 配置对象)

11.2 三个核心钩子函数

Vue.directive('fbind', {
  // 1. 指令与元素成功绑定时
  bind(element, binding) {
    element.value = binding.value
  },
  // 2. 元素被插入页面时
  inserted(element, binding) {
    element.focus()
  },
  // 3. 模板被重新解析时
  update(element, binding) {
    element.value = binding.value
  }
})

11.3 参数详解

element:绑定的真实 DOM 元素。

binding 对象

{
  name: 'fbind',        // 指令名(不带 v-)
  value: 1,             // 绑定的值
  expression: 'n',      // 表达式字符串
  arg: 'value',         // 参数(v-fbind:value 中的 value)
  modifiers: {}         // 修饰符对象
}

11.4 命名规范

  • 指令定义时不加 v-,使用时要加 v-

  • 多单词指令使用 kebab-case(如:v-big-number

  • 指令名要加引号的情况:命名中间有-

    directives: {
      'big-number': function(element, binding) {
        // ...
      }
    }
    

12.Vue 生命周期

12.1 生命周期概念

  • 别名:生命周期回调函数、生命周期函数、生命周期钩子

  • 定义:Vue 在关键时刻自动调用的特殊函数

  • 特点

    • 函数名不可更改
    • 函数内容由程序员根据需求编写
    • this 指向 vm 或组件实例对象

12.2 完整的生命周期流程

创建阶段:
beforeCreate → created → beforeMount → mounted

更新阶段:
beforeUpdate → updated

销毁阶段:
beforeDestroy → destroyed

12.3 各阶段详解

(1) 创建阶段

  • beforeCreate

    • 实例刚创建,数据观测和事件配置还未开始
    • datamethods 等不可用
  • created

    • 实例创建完成,数据观测、属性和方法运算完成
    • datamethods 已可用,但 DOM 未生成
    • 适合:发送异步请求、初始化数据
  • beforeMount

    • 模板编译完成,但未挂载到页面
    • 虚拟 DOM 已创建
  • mounted

    • 实例挂载到 DOM 上,页面首次渲染完成
    • 真实 DOM 已生成,可通过 this.$el 访问

(2) 更新阶段

  • beforeUpdate

    • 数据更新时调用,DOM 未重新渲染
    • 可以获取更新前的 DOM 状态
  • updated

    • 数据更新导致 DOM 重新渲染后调用
    • 避免在此阶段修改数据,可能导致无限循环

(3) 销毁阶段

  • beforeDestroy

    • 实例销毁前调用,实例完全可用
    • 必须进行清理工作
  • destroyed

    • 实例销毁后调用,所有绑定和监听被移除
    • 子实例也被销毁

​编辑

12.4 最常用的两个钩子详解

(1) mounted(挂载完成时)

调用时机:组件第一次显示在页面上之后。

应该做什么

mounted() {
  // 发送请求获取初始数据
  this.fetchData()
  
  // 启动定时器
  this.timer = setInterval(() => {
    // 轮询、倒计时等
  }, 1000)
  
  // 绑定自定义事件
  this.$bus.$on('event', this.handleEvent)
  
  // 操作 DOM(初始化第三方库)
  this.initChart()
  
  // 订阅消息
  this.$store.subscribe(mutation => {
    // 处理订阅
  })
}

不应该做什么

  • 修改大量数据(可能导致频繁重新渲染)
  • 执行耗时同步操作(会阻塞页面)

(2) beforeDestroy(销毁前)

调用时机:组件即将被销毁前。

必须做什么(防止内存泄漏):

beforeDestroy() {
  //  清除定时器
  clearInterval(this.timer)
  clearTimeout(this.timeout)
  
  // 解绑自定义事件
  this.$bus.$off('event', this.handleEvent)
  
  // 取消订阅
  this.unsubscribe()
  
  // 清理其他资源
  this.websocket.close()
}

重要性:如果不清理,即使组件销毁了,这些资源还在后台运行,会造成内存泄漏


13.自定义指令中的 this 指向问题

13.1 问题现象

在自定义指令的钩子函数中,this 指向的是 window(全局对象),而不是 Vue 实例。

directives: {
  big(element, binding) {
    console.log(this) // window,不是 Vue 实例
    console.log(this.message) // undefined,无法访问组件数据
  }
}

13.2 原因分析

  • 设计原因:保持指令的独立性和复用性

  • 指令钩子是独立函数,不是 Vue 实例的方法

  • Vue 内部调用方式:

    // 伪代码
    function callHook(hookFn, el, binding, vnode) {
      hookFn.call(window, el, binding, vnode)  // 用 window 作为 this
    }
    

13.3 解决方案

方案一:通过 binding.value 传递数据(推荐)

<div v-demo="dataValue"></div>
directives: {
  demo: {
    bind(el, binding) {
      // 所有数据通过 binding.value 传递
      el.innerText = binding.value
    }
  }
}

方案二:通过 vnode 访问 Vue 实例(不推荐,破坏封装性)

directives: {
  demo: {
    bind(el, binding, vnode) {
      const vm = vnode.context  // 获取 Vue 实例
      console.log(vm.message)   // 可以访问组件数据
    }
  }
}

13.4为什么这样设计?

  1. 保持指令独立:指令只依赖传入参数,不依赖外部状态
  2. 提高复用性:指令可在任何组件中使用
  3. 明确数据来源:所有数据通过 binding.value 显式传递,代码更清晰

对比其他地方的 this

位置this 指向示例
methods 中的方法Vue 实例this.message 可以访问
computed 计算属性Vue 实例this.count 可以访问
watch 监听器Vue 实例this.$emit() 可以用
生命周期钩子Vue 实例this.$el 可以访问
自定义指令钩子windowthis 没用

13.5常见问题

Q1:为什么组件的 data 必须是函数?

A:如果 data 是对象,所有组件实例会共享同一个 data 对象,修改一个实例的数据会影响所有实例。使用函数返回新对象可保证每个实例有独立的数据。

Q2:什么时候使用 v-text 而不是 {{ }}?

A

  • 当需要完全替换元素内容时用 v-text
  • 当需要保留元素原有内容时用 {{ }}
  • 当内容安全可信且需要显示 HTML 时用 v-html

Q3:生命周期函数可以异步吗?

A:可以,但需要注意:

  • created 中可以发送异步请求
  • mounted 中可以进行异步 DOM 操作
  • 异步操作不会影响生命周期流程

Q4:为什么 beforeDestroy 中必须清理资源?

A:如果不清理:

  • 定时器继续运行,占用内存
  • 事件监听器未移除,可能触发错误
  • 订阅未取消,可能产生内存泄漏
  • 第三方库实例未销毁,可能冲突

Q5:如何选择使用局部指令还是全局指令?

A

  • 局部指令:只在当前组件或特定组件中使用
  • 全局指令:在多个不相关的组件中都需要使用

Q6:自定义指令中如何传递复杂参数?

A

<!-- 传递对象 -->
<div v-demo="{ color: 'red', size: 20 }"></div>

<!-- 传递多个参数 -->
<div v-demo:[arg].modifier="value"></div>
bind(el, binding) {
  console.log(binding.value)    // { color: 'red', size: 20 }
  console.log(binding.arg)      // 'arg'
  console.log(binding.modifiers) // { modifier: true }
}

Q7:mounted 和 created 的区别是什么?

A

  • created:实例创建完成,数据可用,但 DOM 未生成

  • mounted:实例已挂载到 DOM,可进行 DOM 操作

  • 选择原则

    • 需要 DOM 操作 → mounted
    • 不需要 DOM,只需数据 → created(更早获取数据)