Vue3.0学习笔记

news2024/11/24 19:37:06

文章目录

  • 一. Vue开始
    • 1.1 渐进式框架
    • 1.2 单文件组件
    • 1.3 API 风格
      • 1.3.1 选项式 API
      • 1.3.2 组合式 API
  • 二. Vue基础
    • 2.1 创建一个 Vue 应用
      • 2.1.1 应用实例
      • 2.1.2 根组件
      • 2.1.3 挂载应用
      • 2.1.4 DOM 中的根组件模板
      • 2.1.5 应用配置
      • 2.1.6 多个应用实例
    • 2.2 模板语法
      • 2.2.1 文本插值
      • 2.2.2 原始 HTML
      • 2.2.3 Attribute 绑定
      • 2.2.4 修饰符
    • 2.3 响应式基础
      • 2.3.1 声明响应式状态
      • 2.3.2 响应式代理 vs. 原始值
      • 2.3.3 声明方法
      • 2.3.4 DOM 更新时机
      • 2.3.5 深层响应性
      • 2.3.6 有状态方法
    • 2.4 计算属性
      • 2.4.1 基本示例
      • 2.4.2 计算属性缓存 vs 方法
      • 2.4.3 可写计算属性
      • 2.4.4 计算函数不应有副作用
      • 2.4.5 避免直接修改计算属性值
    • 2.5 Class 与 Style 绑定
      • 2.5.1 绑定 class
      • 2.5.2 绑定内联样式
    • 2.6 条件渲染
      • 2.6.1 v-if
      • 2.6.2 v-else
      • 2.6.3 v-else-if
      • 2.6.4 template上的 v-if
      • 2.6.5 v-show
      • 2.6.6 v-if vs v-show
      • 2.6.7 v-if 和 v-for
    • 2.7 列表渲染
      • 2.7.1 v-for
      • 2.7.2 template上的 v-for
      • 2.7.3 v-for 与 v-if
      • 2.7.4 通过 key 管理状态
      • 2.7.5 数组变化侦测
    • 2.8 事件处理
      • 2.8.1 监听事件
      • 2.8.2 方法事件处理器
      • 2.8.3 事件修饰符
      • 2.8.4 按键修饰符
      • 2.8.5 系统按键修饰符
      • 2.8.6 `.exact` 修饰符
      • 2.8.7 鼠标按键修饰符
    • 2.9 表单输入绑定
      • 2.9.1 选择器选项
      • 2.9.2 `.lazy`修饰符
      • 2.9.3 `.number`
      • 2.9.4 `.trim`
    • 2.10 生命周期钩子
      • 2.10.1 beforeCreate
      • 2.10.2 created
      • 2.10.3 beforeMount
      • 2.10.4 mounted
      • 2.10.5 beforeUpdate
      • 2.10.6 updated
      • 2.10.7 beforeUnmount
      • 2.10.8 unmounted
      • 2.10.9 errorCaptured
    • 2.11 侦听器
      • 2.11.1 watch
      • 2.11.2 深层侦听器
      • 2.11.3 即时回调的侦听器
      • 2.11.4 回调的刷新时机
      • 2.11.5 this.$watch()
      • 2.11.6 停止侦听器
    • 2.12 模板 ref
      • 2.12.1 访问模板 ref
      • 2.12.2 v-for 中的 ref
      • 2.12.3 函数型 ref
      • 2.12.4 组件上的 ref
    • 2.13 组件基础
      • 2.13.1 定义一个组件
      • 2.13.2 使用组件
      • 2.13.3 传递 props
      • 2.13.4 监听事件
      • 2.13.5 通过插槽来分配内容
      • 2.13.6 动态组件
  • 三. 深入组件
    • 3.1 组件注册
      • 3.1.1 全局注册
      • 3.1.2 局部注册
      • 3.1.3 组件名格式
    • 3.2 Props
      • 3.2.1 Prop 声明
      • 3.2.2 传递 prop 的细节
      • 3.2.2 单向数据流
    • 3.3 透传
      • 3.3.1 对 class 和 style 的合并
      • 3.3.2 v-on 监听器继承
      • 3.3.3 深层组件继承
      • 3.3.4 禁用 Attribute 继承
      • 3.3.5 多根节点的 Attribute 继承
      • 3.3.6 在 JavaScript 中访问透传 Attribute
    • 3.4 插槽
      • 3.4.1 默认内容
      • 3.4.2 具名插槽
      • 3.4.3 动态插槽名
      • 3.4.4 作用域插槽
      • 3.4.5 具名作用域插槽
      • 3.4.6 一个漂亮的列表示例

一. Vue开始

Vue 是一款用于构建用户界面的 JavaScript 框架。

两个 Vue 的核心功能:

  • 声明式渲染:Vue 通过自己的模板语法扩展了标准 HTML,使得我们可以声明式地描述基于 JavaScript 状态输出的 HTML。
  • 响应性:Vue 会自动跟踪 JavaScript 状态变化并在改变发生时响应式地更新 DOM。

1.1 渐进式框架

  1. 增强静态的 HTML 而无需构建步骤
  2. 在任何页面中作为 Web Components 嵌入
  3. 单页应用 (SPA)
  4. 全栈 / 服务端渲染 (SSR)
  5. Jamstack / 静态站点生成 (SSG)
  6. 目标为桌面端、移动端、WebGL,甚至是命令行终端

Vue特点和Web开发常见高级功能

  1. 解耦视图和数据
  2. 可复用的组件
  3. 前端路由技术
  4. 状态管理
  5. 虚拟DOM

1.2 单文件组件

在大多数启用了构建工具的 Vue 项目中,我们可以使用一种类似 HTML 格式的文件来书写 Vue 组件,它被称为单文件组件 (也被称为 *.vue 文件,英文缩写为 SFC)。顾名思义,Vue 的单文件组件会将一个组件的逻辑 (JavaScript),模板 (HTML) 和样式 (CSS) 封装在同一个文件里。

<script>
export default {
  data() {
    return {
      count: 0
    }
  }
}
</script>

<template>
  <button @click="count++">Count is: {{ count }}</button>
</template>

<style scoped>
button {
  font-weight: bold;
}
</style>

1.3 API 风格

Vue 的组件可以按两种不同的风格书写:选项式 API组合式 API

1.3.1 选项式 API

使用选项式 API,我们可以用包含多个选项的对象来描述组件的逻辑,例如 datamethodsmounted。选项所定义的属性都会暴露在函数内部的 this 上,它会指向当前的组件实例。

<script>
export default {
  // data() 返回的属性将会成为响应式的状态
  // 并且暴露在 `this` 上
  data() {
    return {
      count: 0
    }
  },

  // methods 是一些用来更改状态与触发更新的函数
  // 它们可以在模板中作为事件监听器绑定
  methods: {
    increment() {
      this.count++
    }
  },

  // 生命周期钩子会在组件生命周期的各个不同阶段被调用
  // 例如这个函数就会在组件挂载完成后被调用
  mounted() {
    console.log(`The initial count is ${this.count}.`)
  }
}
</script>

<template>
  <button @click="increment">Count is: {{ count }}</button>
</template>

1.3.2 组合式 API

通过组合式 API,我们可以使用导入的 API 函数来描述组件逻辑。在单文件组件中,组合式 API 通常会与 <script setup> 搭配使用。这个 setup attribute 是一个标识,告诉 Vue 需要在编译时进行转换,来减少使用组合式 API 时的样板代码。例如,

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

// 响应式状态
const count = ref(0)

// 用来修改状态、触发更新的函数
function increment() {
  count.value++
}

// 生命周期钩子
onMounted(() => {
  console.log(`The initial count is ${count.value}.`)
})
</script>

<template>
  <button @click="increment">Count is: {{ count }}</button>
</template>

二. Vue基础

2.1 创建一个 Vue 应用

2.1.1 应用实例

每个 Vue 应用都是通过 createApp 函数创建一个新的 应用实例:

import { createApp } from 'vue'

const app = createApp({
  /* 根组件选项 */
})

2.1.2 根组件

我们传入 createApp 的对象实际上是一个组件,每个应用都需要一个“根组件”,其他组件将作为其子组件。

如果你使用的是单文件组件,我们可以直接从另一个文件中导入根组件。

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

const app = createApp(App)

2.1.3 挂载应用

应用实例必须在调用了 .mount() 方法后才会渲染出来。该方法接收一个“容器”参数,可以是一个实际的 DOM 元素或是一个 CSS 选择器字符串:

<div id="app"></div>

app.mount('#app')

应用根组件的内容将会被渲染在容器元素里面。容器元素自己将不会被视为应用的一部分。

.mount() 方法应该始终在整个应用配置和资源注册完成后被调用。同时请注意,不同于其他资源注册方法,它的返回值是根组件实例而非应用实例。

2.1.4 DOM 中的根组件模板

当在未采用构建流程的情况下使用 Vue 时,我们可以在挂载容器中直接书写根组件模板:

<div id="app">
  <button @click="count++">{{ count }}</button>
</div>
import { createApp } from 'vue'

const app = createApp({
  data() {
    return {
      count: 0
    }
  }
})

app.mount('#app')

当根组件没有设置 template 选项时,Vue 将自动使用容器的 innerHTML 作为模板。

2.1.5 应用配置

应用实例会暴露一个 .config 对象允许我们配置一些应用级的选项,例如定义一个应用级的错误处理器,它将捕获所有由子组件上抛而未被处理的错误:

app.config.errorHandler = (err) => {
  /* 处理错误 */
}

应用实例还提供了一些方法来注册应用范围内可用的资源,例如注册一个组件:

app.component('TodoDeleteButton', TodoDeleteButton)

这使得 TodoDeleteButton 在应用的任何地方都是可用的。

2.1.6 多个应用实例

你不必再受限于一个页面只能拥有一个应用实例。createApp API 允许多个 Vue 应用共存于同一个页面上,而且每个应用都拥有自己的用于配置和全局资源的作用域。

const app1 = createApp({
  /* ... */
})
app1.mount('#container-1')

const app2 = createApp({
  /* ... */
})
app2.mount('#container-2')

如果你正在使用 Vue 来增强服务端渲染 HTML,并且只想要 Vue 去控制一个大型页面中特殊的一小部分,应避免将一个单独的 Vue 应用实例挂载到整个页面上,而是应该创建多个小的应用实例,将它们分别挂载到所需的元素上去。

2.2 模板语法

Vue 使用一种基于 HTML 的模板语法,使我们能够声明式地将其组件实例的数据绑定到呈现的 DOM 上。所有的 Vue 模板都是语法上合法的 HTML,可以被符合规范的浏览器和 HTML 解析器解析。

在底层机制中,Vue 会将模板编译成高度优化的 JavaScript 代码。结合响应式系统,当应用状态变更时,Vue 能够智能地推导出需要重新渲染的组件的最少数量,并应用最少的 DOM 操作。

如果你对虚拟 DOM 的概念比较熟悉,并且偏向于 JavaScript 的原始力量,你也可以结合可选的 JSX 支持直接手写渲染函数而不采用模板。但请注意,这将不会享受到和模板同等级别的编译时优化

2.2.1 文本插值

最基本的数据绑定形式是文本插值,它使用的是{{}}语法 (即双大括号):

<span>Message: {{ msg }}</span>

双大括号标签会被替换为相应组件实例中 msg property 的值。同时每次 msg property 更改时它也会同步更新。

2.2.2 原始 HTML

双大括号将会将数据插值为纯文本,而不是 HTML。若想插入 HTML,你需要使用 v-html 指令:

<p>Using v-html directive: <span v-html="rawHtml"></span></p>

2.2.3 Attribute 绑定

双大括号不能在 HTML attributes 中使用。相应的,应该使用 v-bind 指令:

<div v-bind:id="dynamicId"></div>

v-bind 指令指示 Vue 将元素的 id attribute 与组件的 dynamicId property 保持一致。如果绑定的值是 null 或者 undefined,那么该 attribute 将会从渲染的元素上移除。

简写:

<div :id="dynamicId"></div>

2.2.4 修饰符

修饰符是以点开头的特殊后缀,表明指令需要以一些特殊的方式被绑定。例如 .prevent 修饰符会告知 v-on 指令对触发的事件调用 event.preventDefault()

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

在这里插入图片描述

2.3 响应式基础

2.3.1 声明响应式状态

选用选项式 API 时,会用 data 选项来声明组件的响应式状态。此选项的值应为返回一个对象的函数。Vue 将在创建新组件实例的时候调用此函数,并将函数返回的对象封装到其响应式系统中。此对象的任何顶层 property 都被代理到组件实例 (即方法和生命周期钩子中的 this) 上。

export default {
  data() {
    return {
      count: 1
    }
  },

  // `mounted` 是生命周期钩子,之后我们会讲到
  mounted() {
    // `this` 指向当前组件实例
    console.log(this.count) // => 1

    // 数据属性也可以被更改
    this.count = 2
  }
}

这些实例上的 property 仅在实例首次创建时被添加,因此你需要确保它们都出现在 data 函数返回的对象上。若所需的值还未准备好,在必要时也可以使用 nullundefined 或者其他一些值占位。

也可以不在 data 上定义,直接向组件实例添加新属性。但这个属性将无法触发响应式更新。

Vue 在组件实例上暴露的内置 API 使用 $ 作为前缀。它同时也为内部 property 保留 _ 前缀。你应该避免在顶层 data 上使用任何以这些字符作前缀的 property。

2.3.2 响应式代理 vs. 原始值

在 Vue 3 中,数据是基于 JavaScript Proxy(代理) 实现响应式的。使用过 Vue 2 的用户可能需要注意下面这样的边界情况:

export default {
  data() {
    return {
      someObject: {}
    }
  },
  mounted() {
    const newObject = {}
    this.someObject = newObject

    console.log(newObject === this.someObject) // false
  }
}

当你在赋值后再访问 this.someObject,此值已经是原来的 newObject 的一个响应式代理。这与 Vue 2 中原始的 newObject 不会变为响应式完全不同:请确保始终通过 this 来访问响应式状态。

2.3.3 声明方法

要为组件添加方法,我们需要用到 methods 选项。它应该是一个包含所有方法的对象:

export default {
  data() {
    return {
      count: 0
    }
  },
  methods: {
    increment() {
      this.count++
    }
  },
  mounted() {
    // 在其他方法或是生命周期中也可以调用方法
    this.increment()
  }
}

Vue 自动为 methods 中的方法绑定了永远指向组件实例的 this。这确保了方法在作为事件监听器或回调函数时始终保持正确的 this。你不应该在定义 methods 时使用箭头函数,因为这会阻止 Vue 的自动绑定。

export default {
  methods: {
    increment: () => {
      // 反例:无法访问此处的 `this`!
    }
  }
}

和组件实例上的其他 property 一样,方法也可以在模板上被访问。在模板中它们常常被用作事件监听器:

<button @click="increment">{{ count }}</button>

2.3.4 DOM 更新时机

当你更改响应式状态后,DOM 也会自动更新。然而,你得注意 DOM 的更新并不是同步的。相反,Vue 将缓冲它们直到更新周期的 “下个时机” 以确保无论你进行了多少次声明更改,每个组件都只需要更新一次。

若要等待一个状态改变后的 DOM 更新完成,你可以使用 nextTick() 这个全局 API:

import { nextTick } from 'vue'

export default {
  methods: {
    increment() {
      this.count++
      nextTick(() => {
        // 访问更新后的 DOM
      })
    }
  }
}

2.3.5 深层响应性

在 Vue 中,状态都是默认深层响应式的。这意味着即使在更改深层次的对象或数组,你的改动也能被检测到。

export default {
  data() {
    return {
      obj: {
        nested: { count: 0 },
        arr: ['foo', 'bar']
      }
    }
  },
  methods: {
    mutateDeeply() {
      // 以下都会按照期望工作
      this.obj.nested.count++
      this.obj.arr.push('baz')
    }
  }
}

2.3.6 有状态方法

在某些情况下,我们可能需要动态地创建一个方法函数,比如创建一个预置防抖的事件处理器:

import { debounce } from 'lodash-es'

export default {
  methods: {
    // 使用 Lodash 的防抖函数
    click: debounce(function () {
      // ... 对点击的响应 ...
    }, 500)
  }
}

不过这种方法对于被重用的组件来说是有问题的,因为这个预置防抖的函数是 有状态的:它在运行时维护着一个内部状态。如果多个组件实例都共享这同一个预置防抖的函数,那么它们之间将会互相影响。

要保持每个组件实例的防抖函数都彼此独立,我们可以改为在 created 生命周期钩子中创建这个预置防抖的函数:

export default {
  created() {
    // 每个实例都有了自己的预置防抖的处理函数
    this.debouncedClick = _.debounce(this.click, 500)
  },
  unmounted() {
    // 最好是在组件卸载时
    // 清除掉防抖计时器
    this.debouncedClick.cancel()
  },
  methods: {
    click() {
      // ... 对点击的响应 ...
    }
  }

2.4 计算属性

2.4.1 基本示例

export default {
  data() {
    return {
      author: {
        name: 'John Doe',
        books: [
          'Vue 2 - Advanced Guide',
          'Vue 3 - Basic Guide',
          'Vue 4 - The Mystery'
        ]
      }
    }
  },
  computed: {
    // 一个计算属性的 getter
    publishedBooksMessage() {
      // `this` 指向当前组件实例
      return this.author.books.length > 0 ? 'Yes' : 'No'
    }
  }
}

在模板中使用计算属性的方式和一般的 property 并无二致。Vue 会检测到 this.publishedBooksMessage 依赖于 this.author.books,所以当 this.author.books 改变时,任何依赖于 this.publishedBooksMessage 的绑定都将同时更新。

2.4.2 计算属性缓存 vs 方法

我们在表达式中像这样调用一个函数也会获得和计算属性相同的结果:

<p>{{ calculateBooksMessage() }}</p>

// 组件中
methods: {
  calculateBooksMessage() {
    return this.author.books.length > 0 ? 'Yes' : 'No'
  }
}

若我们将同样的函数定义为一个方法而不是计算属性,两种方式在结果上确实是完全相同的,然而,不同之处在于计算属性值会基于其响应式依赖被缓存。一个计算属性仅会在其响应式依赖更新时才重新计算。这意味着只要 author.books 不改变,无论多少次访问 publishedBooksMessage 都会立即返回先前的计算结果,而不用重复执行 getter 函数。

这也意味着下面的计算属性永远不会更新,因为 Date.now() 并不是一个响应式依赖:

computed: {
  now() {
    return Date.now()
  }
}

相比之下,方法调用总是会在重渲染发生时再次执行函数。

2.4.3 可写计算属性

计算属性默认仅能通过计算函数得出结果。当你尝试修改一个计算属性时,你会收到一个运行时警告。只在某些特殊场景中你可能才需要用到“可写”的属性,你可以通过同时提供 gettersetter 来创建:

export default {
  data() {
    return {
      firstName: 'John',
      lastName: 'Doe'
    }
  },
  computed: {
    fullName: {
      // getter
      get() {
        return this.firstName + ' ' + this.lastName
      },
      // setter
      set(newValue) {
        // 注意:我们这里使用的是解构赋值语法
        [this.firstName, this.lastName] = newValue.split(' ')
      }
    }
  }
}

现在当你再运行 this.fullName = 'John Doe' 时,setter 会被调用而 this.firstNamethis.lastName 会随之更新。

2.4.4 计算函数不应有副作用

计算属性的计算函数应只做计算而没有任何其他的副作用,这一点非常重要,请务必牢记。举个例子,**不要在计算函数中做异步请求或者更改 DOM!**一个计算属性的声明中描述的是如何根据其他值派生一个值。因此计算函数的职责应该仅为计算和返回该值。在之后的指引中我们会讨论如何使用监听器根据其他响应式状态的变更来创建副作用。

2.4.5 避免直接修改计算属性值

从计算属性返回的值是派生状态。可以把它看作是一个“临时快照”,每当源状态发生变化时,就会创建一个新的快照。更改快照是没有意义的,因此计算属性的返回值应该被视为只读的,并且永远不应该被更改——应该更新它所依赖的源状态以触发新的计算。

2.5 Class 与 Style 绑定

数据绑定的一个常见需求场景是操纵元素的 CSS class 列表和内联样式。因为 classstyle 都是 attribute,我们可以和其他 attribute 一样使用 v-bind 把一个动态的字符串赋值给它们。然而通过字符串拼接生成这些值是麻烦且易出错的。因此,Vue 专门为 classstylev-bind 用法提供了特殊的功能增强。除了字符串外,表达式的结果还可以是对象或数组。

2.5.1 绑定 class

我们可以给 :class (v-bind:class 的缩写) 传递一个对象来动态切换 class:

<div :class="{ active: isActive }"></div>

上面的语法表示 active 是否存在取决于数据属性 isActive 的真假值。

我们可以给 :class 绑定一个数组以应用一系列 CSS class:

data() {
  return {
    activeClass: 'active',
    errorClass: 'text-danger'
  }
}
<div :class="[activeClass, errorClass]"></div>

如果你也想在数组中按条件触发某个 class,你可以使用三元表达式:

<div :class="[isActive ? activeClass : '', errorClass]"></div>

errorClass 会一直存在,但 activeClass 只会在 isActive 为真时才存在。

然而,这可能在有多个依赖条件的 class 时会有些冗长。因此也可以在数组中使用对象语法:

<div :class="[{ active: isActive }, errorClass]"></div>

2.5.2 绑定内联样式

:style 支持绑定 JavaScript 对象值,对应的是 HTML 元素的 style 属性:

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

2.6 条件渲染

2.6.1 v-if

v-if 指令被用于按条件渲染一个区块。这个区块只会在指令的表达式为真时才被渲染。

<h1 v-if="awesome">Vue is awesome!</h1>

2.6.2 v-else

你也可以使用 v-elsev-if 添加一个“else 区块”。

<h1 v-if="awesome">Vue is awesome!</h1>
<h1 v-else>Oh no 😢</h1>

一个 v-else 元素必须跟在一个 v-if 或者 v-else-if 元素后面,否则将不会识别它。

2.6.3 v-else-if

顾名思义,v-else-if 提供的是相应于 v-if 的“else if 区块”。它可以连续多次重复使用:

<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>

2.6.4 template上的 v-if

因为 v-if 是一个指令,他必须依附于某个元素。但如果我们想要切换不止一个元素呢?在这种情况下我们可以在一个 <template> 元素上使用 v-if,这只是一个不可见的包装器元素,最后渲染的结果并不会包含这个 <template> 元素。

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

2.6.5 v-show

另一个可以用来按条件显示一个元素的指令是 v-show。其用法基本一样:

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

不同之处在于 v-show 会在 DOM 渲染中保留该元素;v-show 仅切换了该元素上名为 display 的 CSS 属性。

v-show 不支持在 <template> 元素上使用,也没有 v-else 来配合。

2.6.6 v-if vs v-show

v-if 是“真实的”按条件渲染,因为它确保了条件区块内的事件监听器和子组件都会在切换时被销毁与重建。

v-if 也是懒加载的:如果在初次渲染时条件值为 false,则不会做任何事。条件区块会直到条件首次变为 true 时才渲染。

相比之下,v-show 简单许多,元素无论初始条件如何,始终会被渲染,仅作 CSS class 的切换。

总的来说,v-if 在首次渲染时的切换成本比 v-show 更高。因此当你需要非常频繁切换时 v-show 会更好,而运行时不常改变的时候 v-if 会更合适。

2.6.7 v-if 和 v-for

v-ifv-for 同时存在于一个元素上的时候,v-if 会首先被执行。

2.7 列表渲染

2.7.1 v-for

我们可以使用 v-for 指令基于一个数组来渲染一个列表。v-for 指令需要一种特殊的语法形式 item in items,其中 items 是源数据的数组,而 item 是迭代项的别名:

data() {
  return {
    items: [{ message: 'Foo' }, { message: 'Bar' }]
  }
}
<li v-for="item in items">
  {{ item.message }}
</li>

v-for 块中可以完整地访问父作用域内的 propertyv-for 也支持使用可选的第二个参数表示当前项的位置索引。

data() {
  return {
    parentMessage: 'Parent',
    items: [{ message: 'Foo' }, { message: 'Bar' }]
  }
}
<li v-for="(item, index) in items">
  {{ parentMessage }} - {{ index }} - {{ item.message }}
</li>

你也可以使用 v-for 来遍历一个对象的所有属性。

data() {
  return {
    myObject: {
      title: 'How to do lists in Vue',
      author: 'Jane Doe',
      publishedAt: '2016-04-10'
    }
  }
}
<ul>
  <li v-for="value in myObject">
    {{ value }}
  </li>
</ul>

2.7.2 template上的 v-for

与模板上的 v-if 类似,你也可以在 <template> 标签上使用 v-for 来渲染一个包含多个元素的块。例如:

<ul>
  <template v-for="item in items">
    <li>{{ item.msg }}</li>
    <li class="divider" role="presentation"></li>
  </template>
</ul>

2.7.3 v-for 与 v-if

当它们同时存在于一个节点上时,v-ifv-for 的优先级更高。这意味着 v-if 的条件将无法访问到 v-for 作用域内定义的变量别名:

<!--
 这会抛出一个错误,因为属性 todo 此时
 没有在该实例上定义
-->
<li v-for="todo in todos" v-if="!todo.isComplete">
  {{ todo.name }}
</li>

在外新包装一层 <template> 再在其上使用 v-for 可以解决这个问题 (这也更加明显易读):

<template v-for="todo in todos">
  <li v-if="!todo.isComplete">
    {{ todo.name }}
  </li>
</template>

2.7.4 通过 key 管理状态

Vue 默认按照“就地更新”的策略来更新通过 v-for 渲染的元素列表。当数据项的顺序改变时,Vue 不会随之移动 DOM 元素的顺序,而是就地更新每个元素,确保它们在原本指定的索引位置上渲染。

默认模式是高效的,但只适用于列表渲染输出不依赖子组件状态或者临时 DOM 状态 (例如表单输入值)。

为了给 Vue 一个提示,以便它可以跟踪每个节点的标识,从而重用和重新排序现有的元素,你需要为每个项目提供一个唯一的 key attribute:

<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>{{ todo.name }}</li>
</template>

推荐在任何可行的时候为 v-for 提供一个 key attribute,除非所迭代的 DOM 内容非常简单 (例如:不包含组件或有状态的 DOM 元素),或者有意依赖默认行为来获得性能增益。

key 绑定的值期望是一个基础类型的值,例如字符串或 number 类型。不要用对象作为 v-for 的 key。

2.7.5 数组变化侦测

Vue 包装了一批侦听数组的变更方法,以至于这些方法可以触发视图更新。被包装的变更方法如下:

  • push()
  • pop()
  • shift()
  • unshift()
  • splice()
  • sort()
  • reverse()

2.8 事件处理

2.8.1 监听事件

你可以使用 v-on 指令 (简写为 @) 来监听 DOM 事件和运行 JavaScript 代码。用法:v-on:click="methodName"@click="handler"

2.8.2 方法事件处理器

data() {
  return {
    name: 'Vue.js'
  }
},
methods: {
  greet(event) {
    // 方法中的 `this` 指向当前活跃的组件实例
    alert(`Hello ${this.name}!`)
    // `event` 是 DOM 原生事件
    if (event) {
      alert(event.target.tagName)
    }
  }
}
<!-- `greet` 是上面定义过的方法名 -->
<button @click="greet">Greet</button>

方法事件处理器会自动接收原生 DOM 事件并触发执行。在上面的例子中,我们能够通过被触发事件的 event.target.tagName 访问到该 DOM 元素。

有时我们需要在内联事件处理器中访问原生 DOM 事件。你可以向该处理器方法传入一个特殊的 $event 变量,或者使用内联箭头函数:

<!-- 使用特殊的 $event 变量 -->
<button @click="warn('Form cannot be submitted yet.', $event)">
  Submit
</button>

<!-- 使用内联箭头函数 -->
<button @click="(event) => warn('Form cannot be submitted yet.', event)">
  Submit
</button>
methods: {
  warn(message, event) {
    // 这里可以访问 DOM 原生事件
    if (event) {
      event.preventDefault()
    }
    alert(message)
  }
}

2.8.3 事件修饰符

在处理事件时调用 event.preventDefault()event.stopPropagation() 是很常见的。尽管我们可以直接在方法内调用,但如果方法能更专注于数据逻辑而不用去处理 DOM 事件的细节会更好。

为解决这一问题,Vue 为 v-on 提供了事件修饰符。修饰符是用点表示的指令后缀。

  • .stop
  • .prevent
  • .self
  • .capture
  • .once
  • .passive
<!-- 单击事件将停止传递 -->
<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>

<!-- 添加事件监听器时,使用 `capture` 捕获模式 -->
<!-- 例如:指向内部元素的事件,在被内部元素处理前,先被外部处理 -->
<div @click.capture="doThis">...</div>

<!-- 点击事件最多被触发一次 -->
<a @click.once="doThis"></a>

<!-- 滚动事件的默认行为 (scrolling) 将立即发生而非等待 `onScroll` 完成 -->
<!-- 以防其中包含 `event.preventDefault()` -->
<div @scroll.passive="onScroll">...</div>

2.8.4 按键修饰符

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

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

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

<input @keyup.page-down="onPageDown" />

在上面的例子中,仅会在 $event.key 为 ‘PageDown’ 时调用事件处理。

Vue 为一些常用的按键提供了别名:

  • .enter
  • .tab
  • .delete (捕获“Delete”和“Backspace”两个按键)
  • .esc
  • .space
  • .up
  • .down
  • .left
  • .right

2.8.5 系统按键修饰符

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

  • .ctrl
  • .alt
  • .shift
  • .meta
<!-- Alt + Enter -->
<input @keyup.alt.enter="clear" />

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

2.8.6 .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>

2.8.7 鼠标按键修饰符

  • .left
  • .right
  • .middle

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

2.9 表单输入绑定

v-model连接值绑定和更改事件监听器

<input v-model="text">

2.9.1 选择器选项

可能希望将该值绑定到当前活动实例上的动态属性,那么可以使用 v-bind 来做到。此外使用 v-bind 还使我们可以将选项值绑定为非字符串类型。

<select v-model="selected">
  <!-- 内联对象字面量 -->
  <option :value="{ number: 123 }">123</option>
</select>

v-model 同样也支持非字符串类型的值绑定!在上面这个例子中,当某个选项被选中,selected 会被设为该对象字面量值 { number: 123 }

2.9.2 .lazy修饰符

默认情况下,v-model 会在每次 input 事件后更新数据 。你可以添加 lazy 修饰符来改为在每次 change 事件后更新数据:

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

2.9.3 .number

如果你想让用户输入自动转换为数字,你可以在 v-model 后添加 .number 修饰符来管理输入:

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

如果该值无法被 parseFloat() 处理,那么将返回原始值。

number 修饰符会在输入框有 type="number" 时自动应用。

2.9.4 .trim

如果你想要默认自动去除用户输入内容中两端的空格,你可以在 v-model 后添加 .trim 修饰符来管理输入:

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

2.10 生命周期钩子

在这里插入图片描述
所有生命周期钩子函数的 this 上下文都会自动指向当前调用它的组件实例。注意:避免用箭头函数来定义生命周期钩子,因为如果这样的话你将无法在函数中通过 this 获取组件实例。

2.10.1 beforeCreate

在组件实例初始化完成之后立即调用。

export default {
  beforeCreate() {
    console.log(`the component is now mounted.`)
  }
}

会在实例初始化完成、prop 解析之后、data() 和 computed 等选项处理之前立即调用。

2.10.2 created

在组件实例处理完所有与状态相关的选项后调用。

export default {
  created() {
    console.log(`the component is now mounted.`)
  }
}

当这个钩子被调用时,以下内容已经设置完成:响应式数据、计算属性、方法和侦听器。然而,此时挂载阶段还未开始,因此 $el property 仍不可用。

建议在此调用接口,避免闪屏和服务端不调用的情况。

2.10.3 beforeMount

在组件被挂载之前调用。

export default {
  beforeMount() {
    console.log(`the component is now mounted.`)
  }
}

当这个钩子被调用时,组件已经完成了其响应式状态的设置,但还没有创建 DOM 节点。它即将首次执行 DOM 渲染过程。

这个钩子在服务端渲染时不会被调用。

2.10.4 mounted

在组件被挂载之后调用。

export default {
  mounted() {
    console.log(`the component is now mounted.`)
  }
}

组件在以下情况下被视为已挂载:

  • 所有同步子组件都已经被挂载。(不包含异步组件或 树内的组件)

  • 其自身的 DOM 树已经创建完成并插入了父容器中。注意仅当根容器在文档中时,才可以保证组件 DOM 树也在文档中。

这个钩子通常用于执行需要访问组件所渲染的 DOM 树相关的副作用,或是在服务端渲染应用中用于约束给客户端的 DOM 相关代码。

这个钩子在服务端渲染时不会被调用。

2.10.5 beforeUpdate

在组件即将因为一个响应式状态变更而更新其 DOM 树之前调用。

export default {
  beforeUpdate() {
    console.log(`the component is now mounted.`)
  }
}

这个钩子可以用来在 Vue 更新 DOM 之前访问 DOM 状态。在这个钩子中更改状态也是安全的。

这个钩子在服务端渲染时不会被调用。

2.10.6 updated

在组件即将因为一个响应式状态变更而更新其 DOM 树之后调用。

export default {
  updated() {
    console.log(`the component is now mounted.`)
  }
}

父组件的更新钩子将在其子组件的更新钩子之后调用。

这个钩子会在组件的任意 DOM 更新后被调用,这些更新可能是由不同的状态变更导致的。如果你需要在某个特定的状态更改后访问更新后的 DOM,请使用 nextTick() 作为替代。

这个钩子在服务端渲染时不会被调用。

2.10.7 beforeUnmount

在一个组件实例被卸载之前调用。

export default {
  beforeUnmount() {
    console.log(`the component is now mounted.`)
  }
}

当这个钩子被调用时,组件实例依然还保有全部的功能。

这个钩子在服务端渲染时不会被调用。

2.10.8 unmounted

在一个组件实例被卸载之后调用。

export default {
  unmounted() {
    console.log(`the component is now mounted.`)
  }
}

一个组件在以下情况下被视为已卸载:

  • 其所有子组件都已经被卸载。

  • 所有相关的响应式作用 (渲染作用以及 setup() 时创建的计算属性和侦听器) 都已经停止。

可以在这个钩子中手动清理一些副作用,例如计时器、DOM 事件监听器或者与服务器的连接。

这个钩子在服务端渲染时不会被调用。

2.10.9 errorCaptured

在捕获了后代组件传递的错误时调用。

export default {
  errorCaptured( 
  	err: unknown,
    instance: ComponentPublicInstance | null,
    info: string) {
    console.log(`the component is now mounted.`)
  }
}

错误可以从以下几个来源中捕获:

  • 组件渲染
  • 事件处理器
  • 生命周期钩子
  • setup() 函数
  • 侦听器
  • 自定义指令钩子
  • 过渡钩子

这个钩子带有三个实参:错误对象、触发该错误的组件实例,以及一个说明错误来源类型的信息字符串。

你可以在 errorCaptured() 中更改组件状态来为用户显示一个错误状态。然而重要的是,不要让错误状态渲染为导致本次错误的内容,否则组件就会进入无限的渲染循环中。

这个钩子可以通过返回 false 来阻止错误继续传递。请看下方的传递细节介绍。

错误传递规则

  • 默认情况下,所有的错误都会被发送到应用级的 app.config.errorHandler (前提是这个函数已经定义),这样这些错误都能在一个统一的地方报告给分析服务。
  • 如果组件的继承链或组件链上存在多个 errorCaptured 钩子,对于同一个错误,这些钩子都会被调用。
  • 如果 errorCaptured 钩子本身抛出了一个错误,那么这个错误和原来捕获到的错误都将被发送到 app.config.errorHandler
  • errorCaptured 钩子可以通过返回 false 来阻止错误继续传递。即表示“这个错误已经被处理了,应当被忽略”,它将阻止其他的 errorCaptured 钩子或 app.config.errorHandler 因这个错误而被调用。

2.11 侦听器

2.11.1 watch

使用 watch 选项在每次响应式 property 发生变化时触发一个函数。

  watch: {
    // 每当 question 改变时,这个函数就会执行
    question(newQuestion, oldQuestion) {
      if (newQuestion.indexOf('?') > -1) {
        this.getAnswer()
      }
    }
  },

2.11.2 深层侦听器

watch 默认是浅层的:被侦听的 property,仅在被赋新值时,才会触发回调函数——而嵌套 property 的变化不会触发。如果想侦听所有嵌套的变更,你需要深层侦听器:

export default {
  watch: {
    someObject: {
      handler(newValue, oldValue) {
        // 注意:在嵌套的变更中,
        // 只要没有替换对象本身,
        // 那么这里的 `newValue` 和 `oldValue` 相同
      },
      deep: true
    }
  }
}

谨慎使用:深度侦听需要遍历被侦听对象中的所有嵌套的 property,当用于大型数据结构时,开销很大。因此请只在必要时才使用它,并且要留意性能。

2.11.3 即时回调的侦听器

watch 默认是懒侦听的:仅在侦听源变化时,才会执行回调。但在某些场景中,我们希望在创建侦听器时,立即执行一遍回调。举个例子,我们想请求一些初始数据,然后在相关状态更改时重新请求数据。

我们可以用一个对象来声明侦听器,这个对象有 handler 方法和 immediate: true 选项,这样便能强制回调函数立即执行:

export default {
  // ...
  watch: {
    question: {
      handler(newQuestion) {
        // 在组件实例创建时会立即调用
      },
      // 强制立即执行回调
      immediate: true
    }
  }
  // ...
}

2.11.4 回调的刷新时机

当你更改了响应式状态,它可能会同时触发 Vue 组件更新侦听器回调

默认情况下,用户创建的侦听器回调,都会在 Vue 组件更新之前被调用。这意味着你在侦听器回调中访问的 DOM 将是被 Vue 更新之前的状态。

如果想在侦听器回调中能访问被 Vue 更新之后的DOM,你需要指明 flush: 'post' 选项:

export default {
  // ...
  watch: {
    key: {
      handler() {},
      flush: 'post'
    }
  }
}

2.11.5 this.$watch()

可以使用组件实例的 $watch() 方法来命令式地创建一个侦听器:

export default {
  created() {
    this.$watch('question', (newQuestion) => {
      // ...
    })
  }
}

如果要在特定条件下设置一个侦听器,或者只侦听响应用户交互的内容,这方法很有用。它还允许你提前停止该侦听器。

2.11.6 停止侦听器

watch 选项或者 $watch() 实例方法声明的侦听器,会在宿主组件卸载时自动停止。因此,在大多数场景下,你无需关心怎么停止它。

在少数情况下,你的确需要在组件卸载之前就停止一个侦听器,这时可以调用 $watch() API 返回的函数:

const unwatch = this.$watch('foo', callback)

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

2.12 模板 ref

在某些情况下,我们仍然需要直接访问底层 DOM 元素。要实现这一点,我们可以使用特殊的 ref attribute:

<input ref="input">

ref 是一个特殊的 attribute,和 v-for 章节中提到的 key 类似。它允许我们在一个特定的 DOM 元素或子组件实例被挂载后,获得对它的直接引用。这可能很有用,比如说在组件挂载时编程式地聚焦到一个 input 元素上,或在一个元素上初始化一个第三方库。

2.12.1 访问模板 ref

挂载结束后 ref 都会被暴露在 this.$refs 之上:

<script>
export default {
  mounted() {
    this.$refs.input.focus()
  }
}
</script>

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

注意,你只可以在组件挂载后才能访问 ref。如果你想在模板中的表达式上访问 $refs.input,在初次渲染时会是 null。这是因为在初次渲染前这个元素还压根不存在呢!

2.12.2 v-for 中的 ref

当 ref 在 v-for 中使用时,相应的 ref 中包含的值是一个数组:

<script>
export default {
  data() {
    return {
      list: [
        /* ... */
      ]
    }
  },
  mounted() {
    console.log(this.$refs.items)
  }
}
</script>

<template>
  <ul>
    <li v-for="item in list" ref="items">
      {{ item }}
    </li>
  </ul>
</template>

ref 数组不能保证与源数组相同的顺序。

2.12.3 函数型 ref

除了使用字符串值作名字,ref attribute 还可以绑定为一个函数,会在每次组件更新时都被调用。函数接受该元素引用作为第一个参数:

<input :ref="(el) => { /* 将 el 分配给 property 或 ref */ }">

如果你正在使用一个动态的 :ref 绑定,我们也可以传一个函数。当元素卸载时,这个 el 参数会是 null。你当然也可以使用一个方法而不是内联函数。

2.12.4 组件上的 ref

ref 也可以被用在一个子组件上。此时 ref 中引用的是组件实例:

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

export default {
  components: {
    Child
  },
  mounted() {
    // this.$refs.child 是 <Child /> 组件的实例
  }
}
</script>

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

如果一个子组件使用的是选项式 API ,被引用的组件实例和该子组件的 this 完全一致,这意味着父组件对子组件的每一个属性和方法都有完全的访问权。这使得在父组件和子组件之间创建紧密耦合的实现细节变得很容易,当然也因此,应该只在绝对需要时才使用组件引用。大多数情况下,你应该首先使用标准的 propsemit 接口来实现父子组件交互。

expose 选项可以用于限制对子组件实例的访问:

export default {
  expose: ['publicData', 'publicMethod'],
  data() {
    return {
      publicData: 'foo',
      privateData: 'bar'
    }
  },
  methods: {
    publicMethod() {
      /* ... */
    },
    privateMethod() {
      /* ... */
    }
  }
}

在上面这个例子中,父组件通过模板 ref 访问到子组件实例后,仅能访问 publicDatapublicMethod

2.13 组件基础

组件允许我们将 UI 划分为独立的、可重用的部分来思考。组件在应用程序中常常被组织成层层嵌套的树状结构:
在这里插入图片描述

2.13.1 定义一个组件

当使用构建步骤时,我们一般会将 Vue 组件定义在一个单独的 .vue 文件中,这被叫做单文件组件 (简称 SFC):

<script>
export default {
  data() {
    return {
      count: 0
    }
  }
}
</script>

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

2.13.2 使用组件

要使用一个子组件,我们需要在父组件中导入它。假设我们把计数器组件放在了一个叫做 ButtonCounter.vue 的文件中,这个组件将会以默认导出的形式被暴露给外部。

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

export default {
  components: {
    ButtonCounter
  }
}
</script>

<template>
  <h1>Here is a child component!</h1>
  <ButtonCounter />
</template>

若要将导入的组件暴露给模板,我们需要在 components 选项上注册它。这个组件将会以其注册时的名字作为模板中的标签名。

当然,你也可以全局地注册一个组件,使得它在当前应用中的任何组件上都可以使用,而不需要额外再导入。

每一个组件都维护着自己的状态,是不同的。这是因为每当你使用一个组件,就创建了一个新的实例

2.13.3 传递 props

Props 是一种特别的 attributes,你可以在组件上声明注册。要传递给博客文章组件一个标题,我们必须在组件的 props 列表上声明它,使用 props 选项:

<!-- BlogPost.vue -->
<script>
export default {
  props: ['title']
}
</script>

<template>
  <h4>{{ title }}</h4>
</template>

当一个值被传递给 prop 时,它将成为该组件实例上的一个属性。该属性的值可以像其他组件属性一样,在模板和组件的 this 上下文中访问。

一个组件可以有任意多的 props,默认情况下,任何值都可以传递给任何 prop。

当一个 prop 被注册后,可以像这样以自定义 attribute 的形式传递数据给它:

<BlogPost title="My journey with Vue" />
<BlogPost title="Blogging with Vue" />
<BlogPost title="Why Vue is so fun" />

2.13.4 监听事件

$emit()在当前组件触发一个自定义事件。任何额外的参数都会传递给事件监听器的回调函数。常用于子组件调用父组件方法

父组件可以通过 v-on 或 @ 来选择性地监听子组件上抛的事件,就像监听原生 DOM 事件那样:

<BlogPost
  ...
  @enlarge-text="postFontSize += 0.1"
 />

子组件可以通过调用内置的 $emit 方法,通过传入事件名称来抛出一个事件:

<!-- BlogPost.vue, 省略了 <script> -->
<template>
  <div class="blog-post">
    <h4>{{ title }}</h4>
    <button @click="$emit('enlarge-text')">Enlarge text</button>
  </div>
</template>

因为有了 @enlarge-text="postFontSize += 0.1" 的监听,父组件会接收这一事件,从而更新 postFontSize 的值。

我们可以通过 emits 选项来选择性地声明需要抛出的事件:

<!-- BlogPost.vue -->
<script>
export default {
  props: ['title'],
  emits: ['enlarge-text']
}
</script>

$emit带有额外的参数

export default {
  created() {
    // 仅触发事件
    this.$emit('foo')
    // 带有额外的参数
    this.$emit('bar', 1, 2, 3)
  }
}

2.13.5 通过插槽来分配内容

和 HTML 元素一样,像这样能够向组件中传递内容是非常有用的:

<AlertBox>
  Something bad happened.
</AlertBox>

这会引起报错:This is an Error for Demo Purposes

这可以通过 Vue 的自定义 <slot> 元素来实现:

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

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

正如你上面所看到的,我们使用 <slot> 作为一个占位符,之后的内容就会放在这里。

2.13.6 动态组件

有的需求会想要在两个组件间来回切换,比如 Tab 界面

通过 Vue 的 <component> 元素和特殊的 is attribute 实现:

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

在上面的例子中,被传给 :is 的值可以是以下几种:

  • 被注册的组件名
  • 导入的组件对象

当使用 <component :is="..."> 来在多个组件间作切换时,组件会在被切换掉后卸载。我们可以通过 <KeepAlive> 组件强制不活跃的组件仍然保持“存活”的状态。

三. 深入组件

3.1 组件注册

一个 Vue 组件需要被“注册”使得 Vue 在渲染模板时能找到其实现。有两种方式来注册组件:全局注册局部注册

3.1.1 全局注册

我们可以使用 app.component() 方法,让组件在当前 Vue 应用中全局可用。

import { createApp } from 'vue'

const app = createApp({})

app.component(
  // 注册的名字
  'MyComponent',
  // 组件的实现
  {
    /* ... */
  }
)

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

import MyComponent from './App.vue'

app.component('MyComponent', MyComponent)

app.component() 方法可以被链式调用:

app
  .component('ComponentA', ComponentA)
  .component('ComponentB', ComponentB)
  .component('ComponentC', ComponentC)

全局注册的组件可以在此应用的任意组件的模板中使用:

<!-- 这在当前应用的任意组件中都可用 -->
<ComponentA/>
<ComponentB/>
<ComponentC/>

所有的子组件也可以使用全局注册的组件,这意味着这三个组件也都可以在彼此内部使用。

3.1.2 局部注册

全局注册虽然很方便,但有以下几个短板:

  1. 全局注册使构建系统无法移除未使用的组件 (也叫“tree-shaking”)。如果你全局注册了一个组件,却一次都没有使用,它仍然会出现在最终的构建产物中。
  2. 全局注册在大型项目中使项目的依赖关系变得不那么明确。在父组件中使用子组件时,很难定位子组件的实现。这可能会影响未来长期的可维护性,类似于使用过多的全局变量。

局部注册将注册组件的可用性限定在当前组件的范围内。它使依赖关系更加明确,并且对 tree-shaking 更加友好。

局部注册需要使用 components 选项:

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

export default {
  components: {
    ComponentA
  }
}
</script>

<template>
  <ComponentA />
</template>

局部注册组件在后代组件中并不可用

3.1.3 组件名格式

在整个指引中,我们都使用 PascalCase 作为组件名的注册格式,这是因为:

  1. PascalCase 是合法的 JavaScript 标识符。这使得在 JavaScript 中导入和注册组件都很容易,同时 IDE 也能提供较好的自动补全。
  2. <PascalCase /> 在模板中更明显地表明了这是一个 Vue 组件,而不是原生 HTML 元素。同时也能够将 Vue 组件和自定义元素 (web components) 区分开来。

3.2 Props

3.2.1 Prop 声明

除了使用字符串数组来声明 prop 外,还可以使用对象的形式:

export default {
  props: {
    title: String,
    likes: Number
  }
}

3.2.2 传递 prop 的细节

如果 prop 的名字很长,应使用 camelCase 形式

export default {
  props: {
    greetingMessage: String
  }
}

从技术上来讲,你也可以在子组件传递 prop 时使用 camelCase 形式。 而实际上为了和 HTML attribute 对齐,都会将其转为 kebab-case 形式:

<MyComponent greeting-message="hello" />

3.2.2 单向数据流

所有的 prop 都遵循着单向绑定原则,prop 因父组件的更新而变化,自然地将新的状态向下流往子组件,而不会逆向传递。这避免了子组件意外修改了父组件的状态,不然应用的数据流就会变得难以理解了。

另外,每次父组件更新后,所有的子组件中的 props 都会被更新到最新值,这意味着你不应该在子组件中去更改一个 prop。

3.3 透传

“透传 attribute”是传递给组件的 attribute 或者 v-on 事件监听器,但并没有显式地声明在所接收组件的 propsemits 上。最常见的例子就是 classstyleid

当一个组件以单个元素为根作渲染时,透传的 attribute 会自动添加到根元素的 attribute 中。举个例子,下面这个 <MyButton> 组件有这样的模板:

<!-- <MyButton> 的模板 -->
<button>click me</button>

一个父组件使用了这个组件:

<MyButton class="large" />

最后渲染出的 DOM 结果是:

<button class="large">click me</button>

3.3.1 对 class 和 style 的合并

如果一个子组件的根元素已经有了 classstyle attribute,它会和从父组件上继承的值合并。将之前的 <MyButton> 组件的模板改成这样:

<!-- <MyButton> 的模板 -->
<button class="btn">click me</button>

最后渲染出的 DOM 结果是:

<button class="btn large">click me</button>

3.3.2 v-on 监听器继承

同样的规则也适用于 v-on 事件监听器:

<MyButton @click="onClick" />

监听器 click 会被添加到 <MyButton> 的根元素,即那个原生的 <button> 元素之上。当原生的 <button> 被点击,会触发父组件的 onClick 方法。如果原生 button 元素已经通过 v-on 绑定了一个事件监听器,则这些监听器都会被触发。

3.3.3 深层组件继承

如果一个组件在根节点上渲染另一个组件,例如,我们重构一下 <MyButton>,让它在根节点上渲染 <BaseButton>

<!-- <MyButton/> 的模板,只是渲染另一个组件 -->
<BaseButton />

此时 <MyButton> 接收的透传 attribute 会直接传向 <BaseButton>

3.3.4 禁用 Attribute 继承

如果你不想要一个组件自动地继承 attribute,你可以在组件选项中设置 inheritAttrs: false

最常见的需要禁用 attribute 继承的场景就是 attribute 需要应用在根节点以外的其他元素上。通过设置 inheritAttrs 选项为 false,你可以完全控制透传进来的 attribute 如何应用。

这些透传进来的 attribute 可以在模板的表达式中直接用 $attrs 访问到。

<span>Fallthrough attribute: {{ $attrs }}</span>

这个 $attrs 对象包含了除组件的 propsemits 属性外的所有其他 attribute,例如 classstylev-on 监听器等等。

有几点需要注意:

  • 和 props 有所不同,透传 attributes 在 JavaScript 中保留了它们原始的大小写,所以像 foo-bar 这样的一个 attribute 需要通过 $attrs['foo-bar'] 来访问。
  • @click 这样的一个 v-on 事件监听器将在此对象下被暴露为一个函数 $attrs.onClick

我们想要所有像 class 和 v-on 监听器这样的透传 attribute 都应用在内部的 <button> 上而不是外层的 <div> 上。我们可以通过设定 inheritAttrs: false 和使用 v-bind="$attrs" 来实现:

<div class="btn-wrapper">
  <button class="btn" v-bind="$attrs">click me</button>
</div>

3.3.5 多根节点的 Attribute 继承

和单根节点组件有所不同,有着多个根节点的组件没有自动 attribute 透传行为。如果 $attrs 没有被显式绑定,将会抛出一个运行时警告。

<header>...</header>
<main v-bind="$attrs">...</main>
<footer>...</footer>

3.3.6 在 JavaScript 中访问透传 Attribute

如果需要,你可以通过 $attrs 这个实例属性来访问组件的所有透传 attribute

export default {
  created() {
    console.log(this.$attrs)
  }
}

3.4 插槽

3.4.1 默认内容

将“Submit”写在 <slot> 标签之间,使其成为默认内容:

<button type="submit">
  <slot>
    Submit <!-- 默认内容 -->
  </slot>
</button>

3.4.2 具名插槽

<slot> 元素可以有一个特殊的 attribute name,用来给各个插槽分配唯一的 ID,以确定每一处要渲染的内容:

<div class="container">
  <header>
    <slot name="header"></slot>
  </header>
  <main>
    <slot></slot>
  </main>
  <footer>
    <slot name="footer"></slot>
  </footer>
</div>

在父组件中使用时,我们需要一种方式将多个插槽内容传入到各自目标插槽的插口。此时就需要用到具名插槽了:

要为具名插槽传入内容,我们需要使用一个含 v-slot 指令的 <template> 元素,并将目标插槽的名字传给该指令:

<BaseLayout>
  <template v-slot:header>
    <!-- header 插槽的内容放这里 -->
  </template>
</BaseLayout>

v-slot 有对应的简写 #,因此 <template v-slot:header> 可以简写为 <template #header>。其意思就是“将这部分模板片段传入子组件的 header 插槽中”。

在这里插入图片描述

3.4.3 动态插槽名

动态指令参数v-slot 上也是有效的,即可以定义下面这样的动态插槽名:

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

  <!-- 缩写为 -->
  <template #[dynamicSlotName]>
    ...
  </template>
</base-layout>

3.4.4 作用域插槽

我们需要一种方法来让子组件在渲染时将一部分数据提供给插槽。

可以像对组件传递 prop 那样,向一个插槽的插口上传递 attribute:

<!-- <MyComponent> 的模板 -->
<div>
  <slot :text="greetingMessage" :count="1"></slot>
</div>

当需要接收插槽 prop 时,默认插槽和具名插槽的使用方式有一些小区别。下面我们将先展示默认插槽如何接受 prop,通过子组件标签上的 v-slot 指令,直接接收到了一个插槽 prop 对象:

<MyComponent v-slot="slotProps">
  {{ slotProps.text }} {{ slotProps.count }}
</MyComponent>
/*使用解构赋值*/
<MyComponent v-slot="{ text, count }">
  {{ text }} {{ count }}
</MyComponent>

3.4.5 具名作用域插槽

具名作用域插槽的工作方式也是类似的,插槽 props 可以作为 v-slot 指令的值被访问到:v-slot:name="slotProps"。当使用缩写时是这样:

<MyComponent>
  <template #header="headerProps">
    {{ headerProps }}
  </template>

  <template #default="defaultProps">
    {{ defaultProps }}
  </template>

  <template #footer="footerProps">
    {{ footerProps }}
  </template>
</MyComponent>

向具名插槽中传入 props:

<slot name="header" message="hello"></slot>

注意插槽上的 name 是由 Vue 保留的,不会作为 props 传递给插槽。因此最终 headerProps 的结果是 { message: 'hello' }

3.4.6 一个漂亮的列表示例

<FancyList :api-url="url" :per-page="10">
  <template #item="{ body, username, likes }">
    <div class="item">
      <p>{{ body }}</p>
      <p>by {{ username }} | {{ likes }} likes</p>
    </div>
  </template>
</FancyList>

<FancyList> 之中,我们可以多次渲染 <slot> 并每次都提供不同的数据 (注意我们这里使用了 v-bind 来传递插槽的 props):

<ul>
  <li v-for="item in items">
    <slot name="item" v-bind="item"></slot>
  </li>
</ul>

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/346460.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

4K、高清、无水印视频素材库

推荐5个可以免费下载视频素材的网站&#xff0c;建议收起来&#xff01; 1、菜鸟图库 视频素材下载_mp4视频大全 - 菜鸟图库 菜鸟图库主要提供设计素材为主&#xff0c;自媒体等相关素材也很多&#xff0c;像商用图片、背景图、视频素材、音频素材都很齐全。视频素材全部都是…

不同接口的LCD硬件操作原理

不同接口的LCD硬件操作原理 文章目录不同接口的LCD硬件操作原理参考资料&#xff1a;一、 应用工程师眼里看到的LCD1.1 像素的颜色怎么表示二、 驱动工程师眼里看到的LCD2.1 统一的LCD硬件模型2.2 MCU常用的8080接口LCD模组2.3 MPU常用的TFT RGB接口2.4 有一个MIPI标准参考资料…

大专生现在转行IT可行吗?

抛开其他具体情况不说&#xff0c;大专是行业基本要求&#xff0c;大专学习转IT完全没有问题&#xff01; 关于IT行业的学历要求 大专学历是满足基本入行要求的&#xff0c;并且大专可以选择的方向也很多&#xff0c;比如开发、云计算、大数据、数据分析等方向&#xff0c;这…

云计算ACP云服务器ECS实例题库(二)

&#x1f618;作者简介&#xff1a;一名99年软件运维应届毕业生&#xff0c;正在自学云计算课程。&#x1f44a;宣言&#xff1a;人生就是B&#xff08;birth&#xff09;和D&#xff08;death&#xff09;之间的C&#xff08;choise&#xff09;&#xff0c;做好每一个选择。&…

java面试题-容器

目录 1、Java 容器都有哪些&#xff1f; 2.、Collection 和 Collections 有什么区别&#xff1f; 3、 List、Set、Map 之间的区别是什么&#xff1f; 4、HashMap 和 Hashtable 有什么区别&#xff1f; 5、 如何决定使用 HashMap 还是 TreeMap&#xff1f; 6、 说一下 Has…

融资、量产和一栈式布局,这家Tier 1如此备战高阶智驾决赛圈

作者 | Bruce 编辑 | 于婷 从早期的ADAS&#xff0c;到高速/城市NOA&#xff0c;智能驾驶的竞争正逐渐升级&#xff0c;这对于车企和供应商的核心技术和产品布局都是一个重要的考验。 部分智驾供应商已经在囤积粮草&#xff0c;响应变化。 2023刚一开年&#xff0c;智能驾驶…

C语言消消乐游戏代码

C和C游戏趣味编程》一书各个章节的案例代码&#xff0c;每章案例逐步利用学到的语法知识。 本章我们将编写十字消除游戏&#xff0c;用户点击空白方块&#xff0c;沿其上下左右方向寻找第一个彩色方块&#xff0c;如果有两个或两个以上颜色一致&#xff0c;就将其消除。在进度…

Spark 3.1.1 shuffle fetch 导致shuffle错位的问题

背景 最近从数据仓库小组那边反馈了一个问题,一个SQL任务出来的结果不正确&#xff0c;重新运行一次之后就没问题了&#xff0c;具体的SQL如下&#xff1a; select col1,count(1) as cnt from table1 where dt 20230202 group by col1 having count(1) > 1这个问题是偶发…

SpringBoot 整合JWT实现基于自定义注解的-登录请求验证拦截(保姆级教学,附:源码)

学习目标&#xff1a; Spring Boot 整合JWT实现基于自定义注解的 登录请求接口拦截 例&#xff1a; 一篇掌握 JWT 入门知识1.1 在学习SpringBoot 整合JWT之前&#xff0c;我们先来说说JWT进行用户身份验证的流程 1:客户端使用用户名和密码请求登录 2:服务端收到请求&#xf…

spring中@Autowire和@Resource的区别在哪里?

介绍今天使用Idea写代码的时候&#xff0c;看到之前的项目中显示有warning的提示&#xff0c;去看了下&#xff0c;是如下代码?Autowire private JdbcTemplate jdbcTemplate;提示的警告信息Field injection is not recommended Inspection info: Spring Team recommends: &quo…

AntDB-M设计之内存结构

亚信科技专注通信行业多年&#xff0c;AntDB数据库从诞生开始&#xff0c;就面对通信级的大数据量应用场景挑战&#xff0c;在性能、稳定性、规模化等方面获得了超过10年的通信核心业务系统验证&#xff0c;性能峰值达到每秒百万的通信核心交易量。AntDB-M&#xff08;AntDB内存…

CMMI有哪几个级别,每个级别有哪些其特征

CMMI 一共分五个级别&#xff0c;一级低&#xff0c;五级高&#xff0c;一般初次认证CMMI从三级或者二级开始。 1、CMMI一级&#xff0c;完成级。 在完成级水平上&#xff0c;企业对项目的目标与要做的努力很清晰。项目的目标得以实现。一般来说&#xff0c;公司的初始阶段就是…

工作记录------@Accessors(chain = true)引起的BUG,Excel导入时获取不到值

工作记录------Accessors(chain true)引起的BUG&#xff0c;Excel导入时获取不到值 如题所示 背景&#xff1a;在进行文件excel文件导入时&#xff0c;发现实体类获取到的属性值都为null。 框架&#xff1a;com.alibaba.excel 2.2.0的版本。 结论&#xff1a;首先说下结论 如…

2021年职业院校技能大赛“网络安全”项目江西省A模块

目录 一、竞赛时间 三、竞赛任务书内容 &#xff08;一&#xff09;拓扑图 &#xff08;二&#xff09;A模块基础设施设置/安全加固&#xff08;200分&#xff09; A-1&#xff1a;登录安全加固 1.密码策略&#xff08;Web&#xff09; a.最小密码长度不少于8个字符&…

C++入门基础

本章内容&#xff1a; 一、C前言 1. 什么是C C语言是结构化和模块化的语言&#xff0c;适合处理较小规模的程序。对于复杂的问题&#xff0c;规模较大的程序&#xff0c;需要高度的抽象和建模时&#xff0c;C语言则不合适。为了解决软件危机&#xff0c; 20世纪80年代&#x…

笔记本连接wifi,浏览器访问页面,显示访问被拒绝

打开chrome、edge浏览器访问第1个第2个页面正常&#xff0c;后面再打开页面显示异常。 但手机连接正常&#xff0c;笔记本连接异常&#xff0c;起初完全没有怀疑是wifi问题 以为用了vpn软件问题&#xff0c;认为中了病毒。杀毒&#xff0c;并没有中毒。 1、关闭vpn代理&#…

基于Java+Spring+vue+element商城销售平台设计和实现

博主介绍&#xff1a;✌全网粉丝20W,csdn特邀作者、博客专家、CSDN新星计划导师、java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专…

【Call for papers】ICCV-2023(CCF-A/人工智能/2023年3月8日截稿)

ICCV is the premier international computer vision event comprising the main conference and several co-located workshops and tutorials. 文章目录1.会议信息2.时间节点1.会议信息 会议介绍&#xff1a; ICCV是主要的国际计算机视觉活动&#xff0c;包括主要会议和几个…

【LeetCode】剑指 Offer 03. 数组中重复的数字 -- Java Version

题目链接&#xff1a; https://leetcode.cn/problems/shu-zu-zhong-zhong-fu-de-shu-zi-lcof/ 1. 题目介绍&#xff08;03. 数组中重复的数字&#xff09; 找出数组中重复的数字。 在一个长度为 n 的数组 nums 里的所有数字都在 0&#xff5e;n-1 的范围内。数组中某些数字是重…

linux篇【15】:应用层-网络https协议

目录 一.HTTPS介绍 1.HTTPS 定义 2.HTTP与HTTPS &#xff08;1&#xff09;端口不同&#xff0c;是两套服务 &#xff08;2&#xff09;HTTP效率更高&#xff0c;HTTPS更安全 3.加密&#xff0c;解密&#xff0c;密钥 概念 4.为什么要加密&#xff1f; 5.常见的加密方式…