一、简介

Vue是一款用于构建用户界面的JavaScript框架,它基于标准HTML、CSS和JavaScript构建,并提供了一套声明式的、组件化的编程模型,可以高效地开发用户界面。无论是简单还是复杂的界面,Vue 都可以胜任。

二、语法

1、创建应用

可以通过createApp函数创建一个应用实例,并传入一个“根组件”来创建应用。

  • 创建应用
import { createApp } from 'vue'
// 从一个单文件组件中导入根组件
import App from './App.vue'

const app = createApp(App)
// 挂载到DOM元素上
app.mount('#app') 
  • 应用配置

可以通过应用实例的.config对象进行应用级配置,例如配置错误处理器、注册全局组件或指令等。

app.config.errorHandler = (err) => { 
	/* 处理错误 */ 
}
// 全局注册
app.component('MyComponent', MyComponent) 
  • 説明

    • 挂载

      .mount()方法必须在应用配置之后调用,它返回的是根组件实例。

    • 多应用实例

      可以在同一个页面上创建多个独立的Vue应用,每个应用都有自己的作用域。

2、模板语法

Vue的模板语法基于HTML,能够声明式地将组件实例的数据绑定到DOM上。

  • 文本插值

双大括号{{ }}用于文本插值,数据会被解释为纯文本。

<span>Message: </span>
  • 原始HTML

可以使用v-html指令输出真正的HTML。但需注意XSS风险,只对可信内容使用。

<span v-html="rawHtml"></span>
  • 指令

    指令是带有v-前缀的特殊attribute,它的期望值是一个JavaScript表达式。

    一个指令的任务是在其表达式的值变化时响应式地更新DOM。例如:

      <p v-if="seen">Now you see me</p>
    
    • 参数

    某些指令会需要一个“参数”:在指令名后通过一个冒号隔开做标识。例如用v-bind指令来响应式地更新一个HTML attribute:

      <a v-bind:href="url"> ... </a>
      <!-- 简写 -->
      <a :href="url"> ... </a>
    
      <a v-on:click="doSomething"> ... </a>
      <!-- 简写 -->
      <a @click="doSomething"> ... </a>
    
    • 动态参数

    在指令参数上也可以使用一个 JavaScript 表达式,需要包含在一对方括号内:

      <a v-bind:[attributeName]="url"> ... </a>
      <!-- 简写 -->
      <a :[attributeName]="url"> ... </a>
    

    上面的attributeName会作为一个JavaScript表达式被动态执行,计算得到的值会被用作最终的参数。

  • 属性绑定

    可以使用v-bind指令响应式地绑定HTML属性,简写为:

      <div :id="dynamicId"></div>
    
    • 同名简写

    如果属性名与绑定的JavaScript变量名相同,可直接写,例如::id

      <div :id></div>
    
    • 布尔型属性

    当值为真值或空字符串时,此属性(attribute)存在;如果为假值(例如null、 undefined、 false)时,属性会被移除。

      <button :disabled="isButtonDisabled">Button</button>
    
  • 使用JavaScript表达式

Vue支持在模板中绑定完整的JavaScript表达式,但每个绑定只能包含单一表达式(一段能够被求值的 JavaScript 代码)。


{{ number + 1 }}
{{ ok ? 'YES' : 'NO' }}
<div :id="`list-${id}`"></div>

下面的例子都是无效的:


<!-- 这是一个语句,而非表达式 -->
{{ var a = 1 }}

<!-- 条件控制也不支持,请使用三元表达式 -->
{{ if (ok) { return message } }}

  • 修饰符

以点.开头的特殊后缀,表示指令以特殊方式绑定。例如.prevent告诉v-on指令调用 event.preventDefault()

<form @submit.prevent="onSubmit">...</form>

3、响应式基础

  • 声明响应式状态

    • 选项式API

    使用data选项,它返回一个对象,Vue会将其属性变为响应式。

      export default {
        data() {
          return { count: 0 }
        }
      }
    
    • 组合式API

    在组合式API中,推荐使用ref()函数来声明响应式状态:

      import { ref } from 'vue'
      // 适用于基本类型,通过 .value 访问
      const count = ref(0)      
      console.log(count) // { value: 0 }
      console.log(count.value) // 0
      count.value++
      console.log(count.value) // 1
    

    如果要在组件模板中访问ref,需要在组件的setup()函数中声明并返回它们:

      import { ref } from 'vue'
    
      export default {
        // `setup` 是一个特殊的钩子,专门用于组合式 API。
        setup() {
          const count = ref(0)
    
          // 将 ref 暴露给模板
          return {
            count
          }
        }
      }
    
      <div>8</div>
    

    ref创建了一个包含value属性的对象。通过.value属性,Vue可以追踪其访问和修改,从而实现响应性。在模板中使用时,ref会自动“解包”,无需写.value

    另一种声明响应式状态的方式是使用reactive()API,与将内部值包装在特殊对象中的ref不同,reactive()将使对象本身具有响应性:

      // 适用于对象/数组,可直接访问属性
      import { reactive } from 'vue'
      const state = reactive({ count: 0 })
    

    在模板中使用:

      <button @click="state.count++">
    	  
      </button>
    

    reactive()返回的是一个原始对象的代理Proxy,它和原始对象是不相等的;只有代理对象是响应式的,更改原始对象不会触发更新。因此,使用Vue的响应式系统的最佳实践是仅使用你声明对象的代理版本。

      const raw = {}
      const proxy = reactive(raw)
    
      // 代理对象和原始对象不是全等的
      console.log(proxy === raw) // false
    

    reactive的局限是只能用于对象类型,不能替换整个对象,而且解构时会丢失响应性。因此,官方推荐使用ref()作为主要API。

      let state = reactive({ count: 0 })
    
      // 上面的 ({ count: 0 }) 引用将不再被追踪,响应性连接已丢失!
      state = reactive({ count: 1 })
    
      // 当解构时,count 已经与 state.count 断开连接
      let { count } = state
      // 不会影响原始的 state
      count++
    
      // 该函数接收到的是一个普通的数字,并且无法追踪 state.count 的变化,必须传入整个对象以保持响应性
      callSomeFunction(state.count)
    
  • <script setup>

setup()函数中手动暴露大量的状态和方法非常繁琐,因此可以使用<script setup>来大幅度地简化代码:

<script setup>
import { ref } from 'vue'

const count = ref(0)

function increment() {
  count.value++
}
</script>

<template>
  <button @click="increment">
    8
  </button>
</template>

<script setup>中的顶层的导入、声明的变量和函数可以在同一组件的模板中直接使用。可以理解为模板是在同一作用域内声明的一个JavaScript函数。

  • DOM更新时机

Vue的DOM更新是异步的,修改响应式状态后,DOM不会立即更新;如果要等待更新完成,可以使用 nextTick()

import { nextTick } from 'vue'

async function increment() {
  count.value++
  await nextTick()
  // 现在DOM已更新
}

4、方法与计算属性

  • 方法

在选项式API中,使用methods选项为组件添加方法,方法中的this会自动绑定到当前组件实例。

export default {
  methods: {
    increment() {
      this.count++
    }
  }
}
  • 计算属性

对于包含复杂逻辑的响应式数据,应使用计算属性。它基于其响应式依赖进行缓存,只有依赖变化时才会重新计算。

<script setup>
import { reactive, computed } from 'vue'

const author = reactive({
  name: 'John Doe',
  books: [
    'Vue 2 - Advanced Guide',
    'Vue 3 - Basic Guide',
    'Vue 4 - The Mystery'
  ]
})

// 一个计算属性 ref
const publishedBooksMessage = computed(() => {
  return author.books.length > 0 ? 'Yes' : 'No'
})
</script>

<template>
  <p>Has published books:</p>
  <span></span>
</template>

上面的computed()方法接收一个getter函数,返回值为一个计算属性ref,赋值给publishedBooksMessage(计算属性);和其他一般的ref类似,可以通过publishedBooksMessage.value访问计算结果。计算属性ref也会在模板中自动解包,因此在模板表达式中引用时无需添加.value

  • 可写计算属性

计算属性默认是只读的,如果需要用到“可写”的属性,则可以通过同时提供gettersetter方法来创建:

<script setup>
import { ref, computed } from 'vue'

const firstName = ref('John')
const lastName = ref('Doe')

const fullName = computed({
  // getter
  get() {
    return firstName.value + ' ' + lastName.value
  },
  // setter
  set(newValue) {
    // 注意:这里使用的是解构赋值语法
    [firstName.value, lastName.value] = newValue.split(' ')
  }
})
</script>
  • 方法与计算属性的区别

计算属性有缓存,方法则没有。若依赖不改变,多次访问计算属性会直接返回缓存结果,性能更优。

5、类与样式绑定

数据绑定的一个常见需求场景是操纵元素的CSS class列表和内联样式,可以和其他attribute一样使用 v-bind将它们和动态的字符串绑定。但在处理比较复杂的绑定时,通过拼接生成字符串比较麻烦且容易出错。因此,Vue专门为classstylev-bind用法提供了特殊的功能增强:除了字符串外,表达式的值也可以是对象或数组。

  • 绑定HTML Class

    • 对象

    根据数据动态切换class。

      <div :class="{ active: isActive, 'text-danger': hasError }"></div>
    
    • 数组

    绑定多个class。

      <div :class="[activeClass, errorClass]"></div>
    
    • 在组件上使用

    class会被自动添加到组件的根元素上,并与其已有class合并。例如:

    组件MyComponent:

      <p class="foo bar">Hi!</p>
    

    在使用时添加一些 class:

      <!-- 在使用组件时 -->
      <MyComponent class="baz boo" />
    

    渲染出的HTML为:

      <p class="foo bar baz boo">Hi!</p>
    
  • 绑定内联样式

    • 对象

    推荐使用camelCase

      const activeColor = ref('red')
      const fontSize = ref(30)
    
      <div :style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>
    

    也支持kebab-cased形式的CSS属性key (对应其 CSS 中的实际名称),例如:

      const styleObject = reactive({
        color: 'red',
        fontSize: '30px'
      })
    
      <div :style="{ 'font-size': fontSize + 'px' }"></div>
      <!--直接绑定一个样式对象可以使模板更加简洁-->
      <div :style="styleObject"></div>
    
    • 数组

    绑定一个包含多个样式对象的数组,这些对象会被合并后渲染到同一元素上:

      <div :style="[baseStyles, overridingStyles]"></div>
    
    • 自动前缀

    Vue会为需要浏览器特殊前缀的CSS属性自动添加前缀。Vue是在运行时检查该属性是否支持在当前浏览器中使用。如果浏览器不支持某个属性,那么将尝试加上各个浏览器特殊前缀,以找到哪一个是被支持的。

6、条件渲染

  • v-if/v-else-if/v-else

“真实的”按条件渲染,确保了在切换时,条件区块内的事件监听器和子组件都会被销毁与重建。同时也是惰性的:如果在初次渲染时条件值为false,则不会做任何事;只有当条件首次变为true时才被渲染。适合切换频率低的场景。

<div v-if="type === 'A'">
  A
</div>
<div v-else-if="type === 'B'">
  B
</div>
<div v-else-if="type === 'C'">
  C
</div>
<div v-else>
  Not A/B/C
</div>

<template>上的v-if:可用于包裹多个元素,但最终渲染结果不会包含<template>本身。

<template v-if="ok">
  <h1>Title</h1>
  <p>Paragraph 1</p>
  <p>Paragraph 2</p>
</template>

v-elsev-else-if也可以在

  • v-show

元素始终被渲染,仅切换其display CSS属性,适合频繁切换的场景。

<h1 v-show="ok">Hello!</h1>

v-show不支持在<template>元素上使用。

7、列表渲染

  • v-for

    基于数组或对象渲染列表,语法为item in items,第二个参数为索引或键名(可选)。

    • 数组
      const items = ref([{ message: 'Foo' }, { message: 'Bar' }])
    
      <li v-for="item in items">
    	  
      </li>
    
      <li v-for="(item, index) in items">
          - 
      </li>
    
    • 对象
      const myObject = reactive({
        title: 'How to do lists in Vue',
        author: 'Jane Doe',
        publishedAt: '2016-04-10'
      })
    
      <ul>
        <li v-for="value in myObject">
    		
        </li>
      </ul>
    
      <!--第二个参数表示属性名-->
      <li v-for="(value, key) in myObject">
        : 
      </li>
    
      <!--第三个参数表示位置索引-->
      <li v-for="(value, key, index) in myObject">
        . : 
      </li>
    
  • key

为每个列表项提供一个唯一的key,可以帮助Vue高效地跟踪节点状态,实现重用和重新排序。推荐在任何可能的时候都使用key。

<div v-for="item in items" :key="item.id">
  <!-- 内容 -->
</div>

使用<template v-for>时,key应该被放置在<template>这个容器上:

<template v-for="todo in todos" :key="todo.name">
  <li></li>
</template>
  • 数组变化侦测

    • 变更方法(改变原数组)

    Vue 能够侦听响应式数组的变更方法,并在它们被调用时触发相关的更新。这些变更方法包括:push()pop()shift()unshift()splice()sort()reverse()

    • 替换方法(返回新数组)

    filter()concat()slice()不会更改原数组,而总是返回一个新数组。

      // `items` 是一个数组的 ref
      items.value = items.value.filter((item) => item.message.match(/Foo/))
    
  • 展示过滤或排序后的结果

建议使用计算属性来处理显示过滤或排序后的列表,以避免直接修改原始数据。

const numbers = ref([1, 2, 3, 4, 5])

const evenNumbers = computed(() => {
  return numbers.value.filter((n) => n % 2 === 0)
})
<li v-for="n in evenNumbers"></li>

8、事件处理

可以使用v-on指令(简写为@)来监听DOM事件,并在事件触发时执行对应的JavaScript语句。用法:v-on:click="handler"@click="handler"

  • 事件处理器

    事件处理器(handler)的值可以是:内联事件处理器(直接执行JavaScript语句)或方法事件处理器(指向组件中定义的方法名)。

    • 内联事件处理器

    事件被触发时执行的内联JavaScript语句(与onclick类似),内联事件处理器通常用于简单场景,例如:

      const count = ref(0)
    
      <button @click="count++">Add 1</button>
      <p>Count is: 8</p>
    
    • 方法事件处理器

    当事件处理器的逻辑比较复杂时,内联代码方式变得不够灵活,此时可以使用v-on指定一个方法名或对某个方法的调用:

      const name = ref('Vue.js')
    
      function greet(event) {
        alert(`Hello ${name.value}!`)
        // `event` 是 DOM 原生事件
        if (event) {
          alert(event.target.tagName)
        }
      }
    
      <!-- `greet` 是上面定义过的方法名 -->
      <button @click="greet">Greet</button>
    
  • 事件修饰符

    事件修饰符简化了DOM事件细节的处理,例如:stop(停止冒泡)、.prevent(阻止默认行为)、.once(只触发一次)等,可以链式调用。

      <!-- 单击事件将停止传递 -->
      <a @click.stop="doThis"></a>
    
      <!-- 提交事件将不再重新加载页面 -->
      <form @submit.prevent="onSubmit"></form>
    
      <!-- 修饰语可以使用链式书写 -->
      <a @click.stop.prevent="doThat"></a>
    
      <!-- 也可以只有修饰符 -->
      <form @submit.prevent></form>
    
      <!-- 仅当 event.target 是元素本身时才会触发事件处理器 -->
      <!-- 例如:事件处理器不来自子元素 -->
      <div @click.self="doThat">...</div>
    
  • 按键修饰符

在监听键盘事件时,经常需要检查特定的按键。Vue允许在v-on@监听按键事件时添加按键修饰符。例如:

<!-- 仅在 `key` 为 `Enter` 时调用 `submit` -->
<input @keyup.enter="submit" />

可以直接使用KeyboardEvent.key暴露的按键名称作为修饰符,但需要转为kebab-case形式。例如:

<input @keyup.page-down="onPageDown" />
  • 按键别名

Vue为一些常用的按键提供了别名:.enter.tab.delete.esc.space.up.down.left.right

  • 系统按键修饰符

也可以使用.ctrl.alt.shift.meta等系统按键修饰符来触发鼠标或键盘事件监听器,只有当按键被按下时才会触发。

  • .exact

使用.exact修饰符可台精确控制触发事件所需的系统修饰符的组合。

<!-- 当按下 Ctrl 时,即使同时按下 Alt 或 Shift 也会触发 -->
<button @click.ctrl="onClick">A</button>

<!-- 仅当按下 Ctrl 且未按任何其他键时才会触发 -->
<button @click.ctrl.exact="onCtrlClick">A</button>

<!-- 仅当没有按下任何系统按键时触发 -->
<button @click.exact="onClick">A</button>
  • 鼠标按键修饰符

.left.right.middle这些修饰符将处理程序限定为由特定鼠标按键触发的事件。

9、表单输入绑定

  • v-model

    使用v-model指令可以简化将表单输入框的内容同步到JavaScript中相应的变量过程,即在表单<input><textarea><select>元素上创建双向数据绑定,它会根据控件类型自动使用正确的属性和事件。例如:

      <input
        :value="text"
        @input="event => text = event.target.value">
    

    使用v-model可以简化为:

      <input v-model="text">
    
    • 文本/多行文本

    绑定value并监听input事件。

      <p>Message is: </p>
      <input v-model="message" placeholder="edit me" />
    

    <textarea>中不支持插值表达式,需要使用v-model来替代:

      <!-- 错误 -->
      <textarea></textarea>
    
      <!-- 正确 -->
      <textarea v-model="text"></textarea>
    
    • 复选框/单选按钮

    绑定checked并监听change事件。

      <input type="checkbox" id="checkbox" v-model="checked" />
      <label for="checkbox"></label>
    
      <div>Picked: </div>
      <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>
    
    • 选择器

    绑定value并监听change事件。

      <div>Selected: </div>
    
      <select v-model="selected">
        <option disabled value="">Please select one</option>
        <option>A</option>
        <option>B</option>
        <option>C</option>
      </select>
    
  • 值绑定

    对于单选按钮、复选框和选择器选项,v-model绑定的值通常是静态的字符串或布尔值,可以通过v-bind将复选框、单选按钮等的值绑定到动态数据,支持非字符串值。

    • 复选框

    true-valuefalse-value是Vue特有的属性,仅支持和v-model配套使用。

      <!--这里 toggle 属性的值会在选中时被设为 'yes',取消选择时设为 'no'。-->
      <input
        type="checkbox"
        v-model="toggle"
        true-value="yes"
        false-value="no" />
    

    可以通过v-bind将其绑定为其他动态值:

      <input
        type="checkbox"
        v-model="toggle"
        :true-value="dynamicTrueValue"
        :false-value="dynamicFalseValue" />
    
    • 单选按钮
      <!--pick 会在第一个按钮选中时被设为 first,在第二个按钮选中时被设为 second。-->
      <input type="radio" v-model="pick" :value="first" />
      <input type="radio" v-model="pick" :value="second" />
    
    • 选择器选项
      <select v-model="selected">
          <!-- 内联对象字面量,当选项被选中,selected 会被设为该对象字面量值 { number: 123 } -->
          <option :value="{ number: 123 }">123</option>
      </select>
    
  • 修饰符

    • .lazy

    默认情况下,v-model会在每次input事件后更新数据,使用.lazy将其改为在每次change事件后更新数据:

      <!-- 在 "change" 事件后同步更新而不是 "input" -->
      <input v-model.lazy="msg" />
    
    • .number

    自动将用户输入转为数字:

      <input v-model.number="age" />
    

    如果该值无法被parseFloat()处理,那么将返回原始值。特别是当输入为空时 (例如用户清空输入字段之后),会返回一个空字符串。

    • .trim

    自动去除输入内容两端的空格:

      <input v-model.trim="msg" />
    

10、侦听器

在Vue中,侦听器用于在响应式数据变化时执行特定逻辑,例如:异步请求、操作DOM、或根据一个值的变化去修改另一个值等。

  • watch

在组合式API中,可以使用watch函数在每次响应式状态发生变化时触发回调函数,watch的第一个参数为数据源,可以是:ref、reactive对象、getter函数或由以上类型组成的数组。

<script setup>
import { ref, watch } from 'vue'

const question = ref('')
const answer = ref('')

watch(question, async (newQuestion, oldQuestion) => {
  if (newQuestion.includes('?')) {
    // 执行异步操作
    const res = await fetch('https://yesno.wtf/api')
    answer.value = (await res.json()).answer
  }
})
</script>

在选项式API中可以通过watch选项实现:

export default {
  data() {
    return {
      question: '',
      answer: ''
    }
  },
  watch: {
    // 监听 question 的变化,参数为新值和旧值
    question(newQuestion, oldQuestion) {
      if (newQuestion.includes('?')) {
        this.getAnswer()
      }
    }
  },
  methods: {
    async getAnswer() { /* ... */ }
  }
}
  • 深层侦听器

    默认情况下,watch是浅层的:即只有数据源本身的引用发生变化才会触发,嵌套属性的变化不会触发。

    可以使用reactive()或deep选项实现:

    • reactive
      // 组合式:直接传入 reactive 对象
      const state = reactive({ user: { name: '张三' } })
      watch(state, () => {
        console.log('state 中任何属性变化都会触发')
      })
      state.user.name = '李四' // 触发侦听器
    
      // 如果需要侦听某个嵌套属性,必须使用 getter
      watch(() => state.user.name, (newName) => {
        console.log('name 变化了', newName)
      })
    
    • deep

    组合式:

      watch(() => state.someObject, (newVal, oldVal) => {
        // 仅当 someObject 被替换时触发
      }, { deep: true }) // 加上 deep 后,内部属性变化也会触发
    

    选项式:

      export default {
        watch: {
          someObject: {
            handler(newVal, oldVal) { /* ... */ },
            deep: true
          }
        }
      }
    

    深层侦听会递归遍历对象的所有属性,当数据庞大时开销较大,请谨慎使用。

  • 即时回调

如果希望侦听器在创建时立即执行一次,而不必等待数据第一次变化时,可以使用immediate。例如:进入页面就拉取数据,之后每次参数变化再重新拉取。

组合式:

watch(source, (newVal) => {
  // 立即执行一次,之后每次 source 变化再执行
}, { immediate: true })

选项式:

export default {
  watch: {
    question: {
      handler(newQuestion) { /* ... */ },
      immediate: true
    }
  }
}
  • 一次性侦听器

如果只希望侦听器在数据变化时仅触发一次(不包括立即执行的那次),可以使用once。

watch(source, () => {
  // 仅第一次变化时执行
}, { once: true })
  • watchEffect

watchEffect会自动追踪其回调函数中使用到的所有响应式数据,当其中任何一个变化时,回调会重新执行。它相当于一个更简洁的watch + immediate: true,但无法获取旧值。

import { watchEffect } from 'vue'

const count = ref(0)
const name = ref('张三')

watchEffect(() => {
  // 会自动追踪 count.value 和 name.value
  console.log(`count: ${count.value}, name: ${name.value}`)
})
// 立即输出:count: 0, name: 张三

count.value++ // 输出:count: 1, name: 张三
name.value = '李四' // 输出:count: 1, name: 李四
  • 副作用清理

    Vue提供了onWatcherCleanup或回调参数onCleanup来实现副作用的清理。例如:当侦听器中执行异步操作(如fetch),在上一次请求完成前,数据又发生了变化,此时应当取消过时的请求,避免状态冲突。

    • onWatcherCleanup
      <script setup>
      import { watch, onWatcherCleanup } from 'vue'
    
      const id = ref(1)
    
      watch(id, (newId) => {
        const controller = new AbortController()
    
        fetch(`/api/${newId}`, { signal: controller.signal })
          .then(res => res.json())
          .then(data => { /* 处理数据 */ })
    
        // 注册清理函数,当 id 再次变化时,会先调用此函数
        onWatcherCleanup(() => {
          controller.abort() // 取消过时的请求
        })
      })
      </script>
    
    • onCleanup
      watch(id, (newId, oldId, onCleanup) => {
        const controller = new AbortController()
        fetch(`/api/${newId}`, { signal: controller.signal })
        onCleanup(() => controller.abort())
      })
    
  • 回调触发时机

Vue默认会在父组件更新之后、当前组件的DOM更新之前调用侦听器回调。如果你需要访问更新后的DOM,可以指定flush: 'post'

// 组合式
watch(source, callback, { flush: 'post' })
watchEffect(callback, { flush: 'post' })

// 快捷写法
import { watchPostEffect } from 'vue'
watchPostEffect(() => {
  // 在 DOM 更新后执行
})

如果需要在Vue进行任何更新之前触发,可以使用flush: 'sync'

watch(source, callback, {
  flush: 'sync'
})

watchEffect(callback, {
  flush: 'sync'
})

// 快捷写法
import { watchSyncEffect } from 'vue'

watchSyncEffect(() => {
  /* 在响应式数据变化时同步执行 */
})
  • 停止侦听器

setup()<script setup>中用同步语句创建的侦听器,会自动绑定到宿主组件实例上,并且会在宿主组件卸载时自动停止。如果用异步回调创建一个侦听器,那么它不会绑定到当前组件上,必须手动停止以防内存泄漏。

<script setup>
import { watchEffect } from 'vue'

// 它会自动停止
watchEffect(() => {})

// ...这个则不会!
setTimeout(() => {
  watchEffect(() => {})
}, 100)
</script>

要手动停止一个侦听器,需要调用watch或watchEffect返回的函数:

const unwatch = watchEffect(() => {})

// ...当该侦听器不再需要时
unwatch()

一般情况很少需要异步创建侦听器,尽可能选择同步创建。如果需要等待一些异步数据,可以使用以下方式:

// 需要异步请求得到的数据
const data = ref(null)

watchEffect(() => {
  if (data.value) {
    // 数据加载后执行某些操作...
  }
})

11、模板引用

Vue的声明性渲染模型抽象了大部分对DOM的直接操作,但在某些情况下,如果仍然需要直接访问底层DOM元素,可以使用ref属性。

<input ref="input">

ref允许在一个特定的DOM元素或子组件实例被挂载后,获得对它的直接引用。使用场景:例如在组件挂载时将焦点设置到一个input元素上,或者在一个元素上初始化一个第三方库。

<script setup>
// 在组合式 API 中获取引用,可以使用辅助函数 useTemplateRef()
import { useTemplateRef, onMounted } from 'vue'

// 第一个参数必须与模板中的 ref 值匹配
const input = useTemplateRef('my-input')

onMounted(() => {
	// 只可在组件挂载后才能访问模板引用
  input.value.focus()
})
</script>

<template>
  <input ref="my-input" />
</template>

模板引用也可以被用在一个子组件上。这种情况下引用中获得的值是组件实例:

<script setup>
import { useTemplateRef, onMounted } from 'vue'
import Child from './Child.vue'

const childRef = useTemplateRef('child')

onMounted(() => {
  // childRef.value 将持有 <Child /> 的实例
})
</script>

<template>
  <Child ref="child" />
</template>

12、组件

  • 定义组件

通常使用单文件组件(.vue文件),它在一个文件中封装了模板、脚本和样式。

<script setup>
import { ref } from 'vue'

const count = ref(0)
</script>

<template>
  <button @click="count++">You clicked me 8 times.</button>
</template>

如果不使用构建步骤时,一个Vue组件以一个包含Vue特定选项的JavaScript对象来定义:

import { ref } from 'vue'

export default {
  setup() {
    const count = ref(0)
    return { count }
  },
  template: `
    <button @click="count++">
      You clicked me 8 times.
    </button>`
}
  • 组件注册

    一个Vue组件在使用前需要先被“注册”,有两种方式:全局注册和局部注册。

    • 全局注册

    通过使用Vue应用实例的.component()方法,可以让组件在当前Vue应用中全局可用。

      import { createApp } from 'vue'
    
      const app = createApp({})
    
      app.component(
        // 注册的名字
        'MyComponent',
        // 组件的实现
        {
          /* ... */
        }
      )
    

    如果使用单文件组件,可以注册被导入的.vue文件:

      import MyComponent from './App.vue'
    
      app.component('MyComponent', MyComponent)
    

    .component()方法支持链式调用。

      app
        .component('ComponentA', ComponentA)
        .component('ComponentB', ComponentB)
        .component('ComponentC', ComponentC)
    
    • 局部注册

    局部注册的组件需要在使用它的父组件中显式导入,并且只能在该父组件中使用。它的优点是使组件之间的依赖关系更加明确,并且对tree-shaking更加友好。

    在使用<script setup>的单文件组件中,导入的组件可以直接在模板中使用,无需注册:

      <script setup>
      import ComponentA from './ComponentA.vue'
      </script>
    
      <template>
        <ComponentA />
      </template>
    

    如果没有使用<script setup>,则需要使用components选项来显式注册:

      import ComponentA from './ComponentA.js'
    
      export default {
        components: {
          ComponentA
        },
        setup() {
          // ...
        }
      }
    
  • 使用组件

在父组件中导入并注册(全局或局部)后,即可作为自定义标签使用。例如:把计数器组件放在了一个叫做ButtonCounter.vue的文件中,这个组件将会以默认导出的形式被暴露给外部。

<!--通过 <script setup>,导入的组件都在模板中直接可用。-->
<script setup>
import ButtonCounter from './ButtonCounter.vue'
</script>

<template>
  <h1>Here is a child component!</h1>
  <ButtonCounter />
</template>
  • 传递props

父组件通过props属性向子组件传递数据,子组件通过defineProps(组合式)或props选项(选项式)声明接收。props是一种特别的属性,可以在组件上声明注册:

<script setup>
defineProps(['title'])
</script>

<template>
  <h4></h4>
</template>

如果没有使用<script setup>,props必须以props选项的方式声明,props对象会作为setup()函数的第一个参数被传入:

export default {
  props: ['title'],
  setup(props) {
    console.log(props.title)
  }
}

defineProps是一个仅<script setup>中可用的编译宏命令,并不需要显式地导入,声明的props会自动暴露给模板。defineProps会返回一个对象,其中包含了可以传递给组件的所有props:

const props = defineProps(['title'])
console.log(props.title)

例如,在父组件中会有如下的一个博客文章数组:

const posts = ref([
  { id: 1, title: 'My journey with Vue' },
  { id: 2, title: 'Blogging with Vue' },
  { id: 3, title: 'Why Vue is so fun' }
])

之后可以使用v-for来渲染:

<BlogPost
  v-for="post in posts"
  :key="post.id"
  :title="post.title"
 />
  • 监听事件

在Vue中,通过自定义事件实现子组件向父组件传递消息或数据,父组件通过v-on@监听子组件触发的事件,子组件则通过$emit(选项式 API)或defineEmits(组合式 API)来触发事件。

  • $emit
$emit('事件名', 参数1, 参数2, ...)

子组件Child.vue

<template>
  <button @click="sendMessage">点击向父组件发送消息</button>
</template>

<script>
export default {
  methods: {
    sendMessage() {
      // 触发名为 'greet' 的自定义事件,并携带一个字符串参数
      this.$emit('greet', 'Hello from child component!')
    }
  }
}
</script>

父组件:

使用@greet="handleGreet"监听子组件触发的greet事件。

<template>
  <div>
    <Child @greet="handleGreet" />
    <p>收到子组件的消息:</p>
  </div>
</template>

<script>
import Child from './Child.vue'

export default {
  components: { Child },
  data() {
    return {
      messageFromChild: ''
    }
  },
  methods: {
    handleGreet(msg) {
      this.messageFromChild = msg
    }
  }
}
</script>
  • defineEmits

defineEmits()方法声明可触发的事件,它返回的emit函数用于触发事件。

子组件Child.vue

<template>
  <button @click="sendMessage">点击向父组件发送消息</button>
</template>

<script setup>
// 声明该组件可以触发的自定义事件
const emit = defineEmits(['greet'])

const sendMessage = () => {
  // 触发 'greet' 事件,携带参数
  emit('greet', 'Hello from child component!')
}
</script>

父组件:

<template>
  <div>
    <Child @greet="handleGreet" />
    <p>收到子组件的消息:</p>
  </div>
</template>

<script setup>
import { ref } from 'vue'
import Child from './Child.vue'

const messageFromChild = ref('')

const handleGreet = (msg) => {
  messageFromChild.value = msg
}
</script>

使用defineEmits不仅能让代码更清晰,还能在TypeScript中提供类型检查。

  • 插槽

如果想实现和HTML元素一样向组件中传递内容,可以通过<slot>元素实现。

AlertBox.vue

<template>
  <div class="alert-box">
    <strong>This is an Error for Demo Purposes</strong>
    <slot />
  </div>
</template>

<style scoped>
.alert-box {
  /* ... */
}
</style>

父组件:

<AlertBox>
  Something bad happened.
</AlertBox>

最终渲染出来的内容如下:

This is an Error for Demo Purposes

Something bad happened.
  • 动态组件

如果需要在两个组件间来回切换,例如Tab界面,可以通过<component>元素和特殊的is属性来实现:

<!-- currentTab 改变时组件也改变 -->
<component :is="tabs[currentTab]"></component>

被传给:is的值可以是被注册的组件名或导入的组件对象。当使用<component :is="...">来在多个组件间作切换时,被切换掉的组件会被卸载。

13、生命周期

每个Vue组件实例在创建时都需要经历一系列的初始化步骤,比如设置好数据侦听、编译模板、挂载实例到DOM,以及在数据改变时更新DOM等。在此过程中,可以通过生命周期钩子函数在特定阶段运行指定的代码。

  • 注册生命周期钩子函数

    例如,使用onMounted可以在组件完成初始渲染并创建DOM节点后运行代码:

      <script setup>
      import { onMounted } from 'vue'
    
      onMounted(() => {
        console.log(`the component is now mounted.`)
      })
      </script>
    

    还有其他一些钩子,会在实例生命周期的不同阶段被调用,常用的如下:

    • beforeCreate

    实例初始化之后被调用。

    • created

    实例创建完成后被立即调用。此时,组件实例已处理完响应式状态,但尚未挂载。可以在此进行数据请求、设置事件监听等。

    • beforeMount

    在挂载开始之前被调用。

    • mounted

    实例被挂载后调用,此时DOM已生成。常用于操作DOM、初始化第三方库。

    • beforeUpdate

    响应式数据变化后,DOM重新渲染前调用。可用于在更新前访问现有DOM。

    • updated

    由于数据变化导致的DOM重新渲染完成后调用。执行时请避免修改状态,以免触发无限更新循环。

    • beforeUnmount

    实例卸载之前调用。此阶段实例仍然完全可用。

    • unmounted

    实例卸载后调用。可以在此实现清理逻辑、移除事件监听器等。

参考资料