vue3知识整理

vue3知识整理

1. Vue简介

Vue是什么

一种渐进式JavaScript框架,易学易用,性能出色,适用场景丰富的web前端框架。是一个框架,也是一个生态

  • 无需构建步骤,渐进式增强静态的HTML
  • 在任何页面中作为Web Components嵌入
  • 单页应用(SPA)
  • 全栈/服务端渲染(SSR)
  • Jamstack/静态站点生成(SSG)
  • 开发桌面端、移动端、WebGL,甚至是命令行终端中的界面

Vue版本

官方文档-> vueJs官方文档
vue2 -> vue3
vue3涵盖了vue2的知识体系,但增加了很多新特性

Vue API风格

  • 选项式:使用选项式 API,我们可以用包含多个选项的对象来描述组件的逻辑,例如 data、methods 和 mounted。选项所定义的属性都会暴露在函数内部的 this 上,它会指向当前的组件实例。(vue2写法)
  • 组合式:通过组合式 API,我们可以使用导入的 API 函数来描述组件逻辑。在单文件组件中,组合式 API 通常会与 <script setup> 搭配使用。这个 setup attribute 是一个标识,告诉vue需要在编译时进行一些处理,让我们可以更简洁地使用组合式 API。比如, <script setup> 中的导入和顶层变量/函数都能够在模板中直接使用。

创建vue项目

npm init vue@latest

vue目录结构

在这里插入图片描述

  • .vscode:VSCode工具的配置文件夹
  • node_modules:vue项目的运行依赖文件夹
  • public:资源文件夹(浏览器图标)
  • src:源码文件夹
  • .gitignore:git忽略文件
  • index.html:入口html文件
  • package.json:信息描述文件
  • README.md:注释文件
  • vite.config.js:vue配置文件

2. 模版语法 及 属性绑定

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

文本插值

{{}} 支持变量、字面量、单一表达式

<p>{{ msg }}</p>
<p>{{ 123 }}</p>
<p>Num:{{ num + 1 }}</p>
<p>{{ flag ? 'ok' : 'no' }}</p>

原始HTML

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

<!-- rawHtml: "<a href='https://www.baidu.com'>百度</a>" -->
<p v-html="rawHtml"></p>

Attribute 绑定

双大括号不能在 HTML attributes 中使用。想要响应式地绑定一个 attribute,应该使用 v-bind 指令

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

<!-- 简写 -->
<div :id="dynamicId"></div>

<!-- 同名简写 -->
<!-- 与 :id="id" 相同 -->
<div :id></div>
<!-- 这也同样有效 -->
<div v-bind:id></div>

<!-- 布尔型 attribute 依据 true / false 值来决定 attribute 是否应该存在于该元素上。 
当 isButtonDisabled 为真值或一个空字符串 (即 <button disabled="">) 时,元素会包含这个 disabled attribute。
而当其为其他假值时 attribute 将被忽略。-->
<button :disabled="isButtonDisabled">Button</button><!-- 动态绑定多个值 --><!-- const objectOfAttrs = {
  id: 'container',
  class: 'wrapper'
} -->
<div v-bind="objectOfAttrs"></div>

使用 JavaScript 表达式

vue 实际上在所有的数据绑定中都支持完整的 JavaScript 表达式(可以return出来的表达式):

{{ number + 1 }}

{{ ok ? 'YES' : 'NO' }}

{{ message.split('').reverse().join('') }}

<div :id="`list-${id}`"></div>

指令 Directives​

指令是带有 v- 前缀的特殊 attribute。Vue 提供了许多内置指令,包括上面我们所介绍的 v-bindv-html。还有 v-onv-ifv-forv-slot

  • 参数 Arguments
<!-- 某些指令会需要一个“参数”,在指令名后通过一个冒号隔开做标识。
例如用 v-bind 指令来响应式地更新一个 HTML attribute: -->
<a v-bind:href="url"> ... </a>
<!-- 简写 -->
<a :href="url"> ... </a>
  • 动态参数
    • 值的限制:动态参数中表达式的值应当是一个字符串,或者是 null。特殊值 null 意为显式移除该绑定。其他非字符串的值会触发警告。
    • 语法限制:动态参数表达式因为某些字符的缘故有一些语法限制,比如空格和引号,在 HTML attribute 名称中都是不合法的。
<a v-bind:[attributeName]="url"> ... </a>
<!-- 简写 -->
<a :[attributeName]="url"> ... </a>

3. 条件渲染

  • v-if
  • v-else
  • v-else-if
  • v-show(通过display属性实现)
<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>
<div v-show="false">
  虽然看不见我,但我真实存在
</div>

v-if 与 v-show

  • v-if 是“真实的”按条件渲染,因为它确保了在切换时,条件区块内的事件监听器和子组件都会被销毁与重建
  • v-if 也是惰性的:如果在初次渲染时条件值为 false,则不会做任何事。条件区块只有当条件首次变为 true 时才被渲染。
  • 相比之下,v-show 简单许多,元素无论初始条件如何,始终会被渲染,只有 CSS display 属性会被切换
  • 总的来说,v-if 有更高的切换开销,而 v-show 有更高的初始渲染开销。因此,如果需要频繁切换,则使用 v-show 较好;如果在运行时绑定条件很少改变,则 v-if 会更合适。

4. 列表渲染

v-for

<li v-for="item in items">
  {{ item.message }}
</li>

<span v-for="n in 10">{{ n }}</span>

<!-- 第二个参数:索引 -->
<li v-for="(item, index) in items">
  {{ parentMessage }} - {{ index }} - {{ item.message }}
</li>

<!-- 遍历对象:值、键、索引 -->
<li v-for="(value, key, index) in myObject">
  {{ index }}. {{ key }}: {{ value }}
</li>

通过key管理状态

Vue 默认按照“就地更新”的策略来更新通过 v-for 渲染的元素列表。当数据项的顺序改变时,Vue 不会随之移动 DOM 元素的顺序,而是就地更新每个元素,确保它们在原本指定的索引位置上渲染。
为了给 Vue 一个提示,以便它可以跟踪每个节点的标识,从而重用和重新排序现有的元素,你需要为每个元素对应的块提供一个唯一的 key attribute

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

5. 事件

事件处理 及 事件传参

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

事件处理器 (handler) 的值可以是:

  • 内联事件处理器:事件被触发时执行的内联 JavaScript 语句 (与 onclick 类似)。
  • 方法事件处理器:一个指向组件上定义的方法的属性名或是路径(不进行额外传参)。
  • 方法与内联事件判断​:模板编译器会通过检查 v-on 的值是否是合法的 JavaScript 标识符或属性访问路径来断定是何种形式的事件处理器。举例来说,foo、foo.bar 和 foo[‘bar’] 会被视为方法事件处理器,而 foo() 和 count++ 会被视为内联事件处理器。
<!-- API风格:组合式 -->
<template>
  <p>Count is: {{ count }}</p>
  <!-- 内联事件处理器 -->
  <button @click="count ++">+1</button>
  <button @click="addStep(2, $event)">+2</button>
  <button @click="addThree()">+3</button>
  <!-- 方法事件处理器 -->
  <button @click="addThree">+3</button>
</template>

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

const count = ref(0);

function addStep(step = 1, e) {
  console.log(e);
  count.value += 2;
}

function addThree(e) {
  console.log(e);
  count.value += 3;
}
</script>
<!-- API风格:选项式 -->
<template>
  <p>Count is: {{ count }}</p>
  <!-- 内联事件处理器 -->
  <button @click="count ++">+1</button>
  <button @click="addStep(2, $event)">+2</button>
  <button @click="addThree()">+3</button>
  <!-- 方法事件处理器 -->
  <button @click="addThree">+3</button>
</template>

<script>
export default {
  data() {
    return {
      count: 0,
    }
  },
  methods: {
    addStep(step = 2, e) {
      console.log(e);
      this.count += step;
    },
    addThree(e){
      console.log(e);
      this.count +3;
    },
  }
}
</script>

事件修饰符

  • .stop:阻止事件冒泡,同原生event.stopPropagation()
  • .prevent:阻止默认事件,同原生event.preventDefault()
  • .self:只当在 event.target 是当前元素自身时触发处理函数
  • .capture:使用事件的捕捉模式
  • .once:只触发一次
  • .passive:当我们在监听元素滚动事件的时候,会一直触发onscroll事件,在pc端是没啥问题的,但是在移动端,会让我们的网页变卡。因此我们使用这个修饰符的时候,相当于给onscroll事件整了一个.lazy修饰符,因此我们使用这个修饰符的时候,相当于给onscroll事件整了一个.lazy修饰符(不要把 .passive 和 .prevent 一起使用,因为 .prevent 将会被忽略,同时浏览器可能会向你展示一个警告。passive 会告诉浏览器你不想阻止事件的默认行为)
  • .native:加在自定义组件的事件上,保证事件能执行。让组件变成像html内置标签那样监听根元素的原生事件,否则组件上使用 v-on 只会监听自定义事。(使用.native修饰符来操作普通HTML标签是会令事件失效的)

拓展:表单修饰符

  • .lazy:在我们填完信息,光标离开标签的时候,才会将值赋予给value,也就是在change事件之后再进行信息同步
  • .trim:自动过滤用户输入的首尾空格字符,而中间的空格不会过滤
  • .number:将值转成数字,但是先输入字符串和先输入数字,是两种情况。(先输入数字的话,只取前面数字部分,先输入字母的话,number修饰符无效)

拓展:v-bind修饰符

  • .sync:能对props进行一个双向绑定
<!-- 父组件 -->
<comp :myMessage.sync="bar"></comp> 
// 子组件
this.$emit('update:myMessage',params);
  • .prop:设置自定义标签属性,避免暴露数据,防止污染HTML结构
  • .camel:将命名变为驼峰命名法,如将 view-Box属性名转换为 viewBox

拓展:按键修饰符

可用来修饰键盘事件(如:onkeyup,onkeydown)
在这里插入图片描述

  • 按键别名: .enter.tab.delete (捕获“Delete”和“Backspace”两个按键)、.esc.up.down.left.right
<!-- 仅在 `key` 为 `Enter` 时调用 `submit` -->
<input @keyup.enter="submit" />
  • 系统按键修饰符:.ctrl.alt.shift.meta
<!-- Alt + Enter -->
<input @keyup.alt.enter="clear" />
<!-- Ctrl + 点击 -->
<div @click.ctrl="doSomething">Do something</div>
  • .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:中键点击

6. 拓展:数组变化侦测

  • 变更方法:原数组被改变
    • push():向数组的末尾添加一个或多个元素(逗号隔开),并返回新的长度
    • pop():删除并返回数组的最后一个元素。如果数组为空,则 pop() 不改变数组,并返回 undefined
    • shift():删除并返回数组的第一个元素。如果数组为空,则 shift() 不改变数组,并返回 undefined
    • unshift():用于在数组的开头添加一个或多个元素(逗号隔开),并返回新的长度
    • splice():在任意的位置给数组添加/删除任意数量的项,然后返回被删除的元素
    • sort():对数组的元素进行排序,并返回数组本身
    • reverse():颠倒数组中元素的顺序,并返回该数组
  • 替换方法:不改变原数据,返回新数组
    • filter():用于创建一个新数组,其包含通过所提供函数实现的测试的所有元素
    • concat():用于合并两个或多个数组或字符串,创建一个新的数组或字符串,而不会改变原有的数组或字符串
    • slice():用于提取某个部分的浅拷贝,返回一个新数组或字符串,而不会改变原始数组或字符串

7. vue3响应式基础(组合式)

建议使用 ref() 作为声明响应式状态的主要 API

ref()

// 引入
import { ref } from 'vue'
// 声明
const count = ref(0)
// 运用
console.log(count) // { value: 0 }
console.log(count.value) // 0
count.value++
console.log(count.value) // 1
  • ref() 接收参数,并将其包裹在一个带有 .value 属性的 ref 对象中返回
  • 在模板中使用 ref 时,会自动解包,不需要加.value
  • Ref 可以持有任何类型的值,包括深层嵌套的对象、数组或者 JavaScript 内置的数据结构,比如 Map。Ref 会使它的值具有深层响应性。这意味着即使改变嵌套对象或数组时,变化也会被检测到

reactive()

// 引入
import { reactive } from 'vue'
// 声明
const state = reactive({ count: 0 })
// 运用
console.log(state) // { count: 0 }
console.log(state.count) // 0
  • reactive() 将使对象本身具有响应性
  • reactive() 返回的是一个原始对象的 Proxy,它和原始对象是不相等的。为保证访问代理的一致性,对同一个原始对象调用 reactive() 会总是返回同样的代理对象,而对一个已存在的代理对象调用 reactive() 会返回其本身
const raw = {}
const proxy = reactive(raw)

// 代理对象和原始对象不是全等的
console.log(proxy === raw) // false
// 在同一个对象上调用 reactive() 会返回相同的代理
console.log(reactive(raw) === proxy) // true
// 在一个代理上调用 reactive() 会返回它自己
console.log(reactive(proxy) === proxy) // true

局限性:

  • 有限的值类型:它只能用于对象类型 (对象、数组和如 Map、Set 这样的集合类型)。它不能持有如 string、number 或 boolean 这样的原始类型。不适用于简单数据类型
  • 不能替换整个对象:由于 Vue 的响应式跟踪是通过属性访问实现的,因此我们必须始终保持对响应式对象的相同引用。这意味着我们不能轻易地“替换”响应式对象,因为这样的话与第一个引用的响应性连接将丢失
  • 对解构操作不友好:当我们将响应式对象的原始类型属性解构为本地变量时,或者将该属性传递给函数时,我们将丢失响应性连接

8. 计算属性

<template>
  <div>
    <h3>{{obj.name}}</h3>
    <p>{{obj.content.length ? 'yes' : 'no'}}</p>
    <p>{{objContent}}</p>
  </div>
</template>

<!-- 选项式 -->
<!-- <script>
export default {
  data() {
    return {
      obj: {
        name: 'hsy',
        content: ['vue2', 'vue3', 'vuex', 'vue-router'],
      }
    }
  },
  computed: {
    objContent() {
      return this.obj.content.length ? 'yes' : 'no'
    }
  }
}
</script> -->
<!-- 组合式 -->
<script setup>
import { reactive, computed } from 'vue'
const obj = reactive({
  name: 'hsy',
  content: ['vue2', 'vue3', 'vuex', 'vue-router'],
})
const objContent = computed(() => {
  return obj.content.length ? 'yes' : 'no'
})
</script>

9. class绑定

  • 绑定对象: :class (v-bind:class 的缩写)

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

<div :class="classObject"></div>
const classObject = reactive({
  active: true,
  'text-danger': false
})
const isActive = ref(true)
const error = ref(null)

const classObject = computed(() => ({
  active: isActive.value && !error.value,
  'text-danger': error.value && error.value.type === 'fatal'
}))
  • 绑定数组
<div :class="[isActive ? activeClass : '', errorClass]"></div>
<div :class="[{ active: isActive }, errorClass]"></div>
  • 在组件上使用

<!-- 子组件模板 -->
<p class="foo bar">Hi!</p>
<!-- 在使用组件时 -->
<MyComponent class="baz boo" :class="{ active: isActive }"/>
<!-- 当 isActive 为真时,被渲染的 HTML 会是 -->
<p class="foo bar baz boo active">Hi!</p>

②当组件有多个根元素时(vue3支持)

<!-- MyComponent 模板使用 $attrs 时 -->
<p :class="$attrs.class">Hi!</p>
<span>This is a child component</span>
<MyComponent class="baz" />
<!-- 渲染结果 -->
<p class="baz">Hi!</p>
<span>This is a child component</span>

10. style绑定

  • 绑定对象

<div :style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>

const styleObject = reactive({
  color: 'red',
  fontSize: '13px'
})
<div :style="styleObject"></div>
  • 绑定数组
<div :style="[{color: activeColor}, overridingStyles]"></div>
  • 自动前缀:当你在 :style 中使用了需要浏览器特殊前缀的 CSS 属性时,Vue 会自动为他们加上相应的前缀。Vue 是在运行时检查该属性是否支持在当前浏览器中使用。如果浏览器不支持某个属性,那么将尝试加上各个浏览器特殊前缀,以找到哪一个是被支持的。
  • 样式多值
<div :style="{ display: ['-webkit-box', '-ms-flexbox', 'flex'] }"></div>

11. 侦听器watch

<template>
  <div>
    <h1>侦听器</h1>
    <p>count:{{count}}</p>
    <button @click="count ++">+1</button>
    <p>这个数是:{{tip}}</p>
  </div>
</template>

<!-- 选项式 -->
<!-- <script>
export default {
  data() {
    return {
      count: 0,
      tip: '???',
    }
  },
  watch: {
    // count(newVal, oldVal) {
    //   this.tip = newVal % 2 ? '这个数是单数' : '这个数是双数'
    // },
    count: {
      handler(newVal, oldVal) {
        this.tip = newVal % 2 ? '这个数是单数' : '这个数是双数'
      },
      immediate: true,
    }
  }
}
</script> -->
<!-- 组合式 -->
<script setup>
import { ref, watch } from 'vue'
const count = ref(0);
const tip = ref('???');

watch(
  count,
  (newVal, oldVal) => {
    tip.value = newVal % 2 ? '这个数是单数' : '这个数是双数';
  },
  { immediate: true })
</script>

侦听数据源类型

watch 的第一个参数可以是不同形式的“数据源”:它可以是一个 ref (包括计算属性)、一个响应式对象、一个 getter 函数、或多个数据源组成的数组:

const x = ref(0)
const y = ref(0)
const obj = reactive({ count: 0 })

// 单个 ref
watch(x, (newX) => {
  console.log(`x is ${newX}`)
})

// getter 函数
watch(
  () => x.value + y.value,
  (sum) => {
    console.log(`sum of x + y is: ${sum}`)
  }
)
// 侦听响应式对象的属性值时需要提供一个getter函数
watch(
  () => obj.count,
  (count) => {
    console.log(`count is: ${count}`)
  }
)

// 多个来源组成的数组
watch([x, () => y.value], ([newX, newY]) => {
  console.log(`x is ${newX} and y is ${newY}`)
})

深层监听器

watch(
  () => state.someObject,
  (newValue, oldValue) => {
    // 注意:`newValue` 此处和 `oldValue` 是相等的
    // *除非* state.someObject 被整个替换了
  },
  { deep: true }
)

即时回调的侦听器

watch 默认是懒执行的:仅当数据源变化时,才会执行回调。但在某些场景中,我们希望在创建侦听器时,立即执行一遍回调

watch(
  source,
  (newValue, oldValue) => {
    // 立即执行,且当 `source` 改变时再次执行
  },
  { immediate: true }
)

一次性侦听器

回调只在源变化时触发一次,只触发一次

watch(
  source,
  (newValue, oldValue) => {
    // 当 `source` 变化时,仅触发一次
  },
  { once: true }
)

watchEffect()

侦听器的回调使用与源完全相同的响应式状态是很常见的。例如下面的代码,在每当 todoId 的引用发生变化时使用侦听器来加载一个远程资源:

const todoId = ref(1)
const data = ref(null)

watch(
  todoId,
  async () => {
    const response = await fetch(
      `https://jsonplaceholder.typicode.com/todos/${todoId.value}`
    )
    data.value = await response.json()
  },
  { immediate: true }
)

用 watchEffect 函数 来简化上面的代码:

watchEffect(async () => {
  const response = await fetch(
    `https://jsonplaceholder.typicode.com/todos/${todoId.value}`
  )
  data.value = await response.json()
})

watch vs. watchEffect

  • watch追踪明确侦听的数据源。它不会追踪任何在回调中访问到的东西。另外,仅在数据源确实改变时才会触发回调。watch 会避免在发生副作用时追踪依赖,因此,我们能更加精确地控制回调函数的触发时机。
  • watchEffect,则会在副作用发生期间追踪依赖。它会在同步执行过程中,自动追踪所有能访问到的响应式属性。这更方便,而且代码往往更简洁,但有时其响应性依赖关系会不那么明确

回调的触发机制

默认情况下,侦听器回调会在父组件更新 (如有) 之后、所属组件的 DOM 更新之前被调用。这意味着如果你尝试在侦听器回调中访问所属组件的 DOM,那么 DOM 将处于更新前的状态。


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

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

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

后置刷新的 watchEffect() 有个更方便的别名 watchPostEffect()

import { watchPostEffect } from 'vue'

watchPostEffect(() => {
  /* 在 Vue 更新后执行 */
})


同步侦听器:同步触发的侦听器,它会在 Vue 进行任何更新之前触发

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

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

同步触发的 watchEffect() 有个更方便的别名 watchSyncEffect()

import { watchSyncEffect } from 'vue'

watchSyncEffect(() => {
  /* 在响应式数据变化时同步执行 */
})

停止侦听器

侦听器必须用同步语句创建:如果用异步回调创建一个侦听器,那么它不会绑定到当前组件上,你必须手动停止它,以防内存泄漏。异步创建的侦听器不会自动停止

<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) {
    // 数据加载后执行某些操作...
  }
})

12. 表单输入绑定

基本用法 与 值绑定

<template>
  <div>
    <h1>表单输入绑定</h1>
    <!-- 文本 -->
    <h2>文本</h2>
    <input type="text" v-model.lazy="message">
    <p>输入文本为:{{message}}</p>
    <!-- 多行文本 -->
    <h2>多行文本</h2>
    <textarea v-model="message" placeholder="add multiple lines"></textarea>
    <p style="white-space: pre-line;">{{message}}</p>
    <!-- 复选框 -->
    <h2>复选框</h2>
    <input type="checkbox" id="checkbox" v-model="checked" />
    <label for="checkbox">{{ checked }}</label>
    <!-- 复选框-多选 -->
    <h2>复选框-多选</h2>
    <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>
    <input type="checkbox" id="mike" value="Mike" v-model="checkedNames">
    <label for="mike">Mike</label>
    <p>Checked names: {{ checkedNames }}</p>
    <!-- 单选按钮 -->
    <h2>单选按钮</h2>
    <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: {{ picked }}</p>
    <!-- 选择器 -->
    <h2>选择器</h2>
    <select v-model="selected">
      <option disabled value="">Please select one</option>
      <option>A</option>
      <option>B</option>
      <option>C</option>
    </select>
    <p>Selected: {{ selected }}</p>
    <!-- 选择器-多选 -->
    <h2>选择器-多选</h2>
    <select v-model="selected2" multiple>
      <option disabled value="">Please select one</option>
      <option>A</option>
      <option>B</option>
      <option>C</option>
    </select>
    <p>Selected: {{ selected2 }}</p>
  </div>
</template>

<!-- 选项式 -->
<!-- <script>
export default {
  data() {
    return {
      message: '',
      checked: false,
      picked: 'One',
      checkedNames: ['Jack'],
      selected: 'A',
      selected2: ['A'],
    }
  }
}
</script> -->

<!-- 组合式 -->
<script setup>
import { ref } from 'vue';
const message = ref('');
const checked = ref(false);
const picked = ref('One');
const checkedNames = ref(['Jack']);
const selected = ref('A');
const selected2 = ref(['A']);
</script>

表单修饰符

详情见前面 5.事件拓展 表单修饰符

13. 模板引用ref

ref:直接访问底层 DOM 元素

<template>
  <div ref="content">容器</div>
</template>

<!-- 选项式 -->
<!-- <script>
export default {
  mounted() {
    this.$refs.content.innerHTML = '这是一个容器'
  }
}
</script> -->

<!-- 组合式 -->
<script setup>
import { ref, onMounted } from 'vue'
// 声明一个 ref 来存放该元素的引用
// 必须和模板里的 ref 同名
const content = ref(null)

onMounted(() => {
  content.value.innerHTML = '这是一个容器呀'
})

</script>

v-for中的模板引用

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

const list = ref([
  /* ... */
])

const itemRefs = ref([])

onMounted(() => console.log(itemRefs.value))
</script>

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

函数模板引用

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

<input :ref="(el) => { /* 将 el 赋值给一个数据属性或 ref 变量 */ }">

注意我们这里需要使用动态的 :ref 绑定才能够传入一个函数。当绑定的元素被卸载时,函数也会被调用一次,此时的 el 参数会是 null。你当然也可以绑定一个组件方法而不是内联函数。

组件上的ref

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

const child = ref(null)

onMounted(() => {
  // child.value 是 <Child /> 组件的实例
})
</script>

<template>
  <Child ref="child" />
</template>
  • 如果一个子组件使用的是选项式 API 或没有使用 <script setup>,被引用的组件实例和该子组件的 this 完全一致,这意味着父组件对子组件的每一个属性和方法都有完全的访问权。
  • 使用了 <script setup> 的组件是默认私有的:一个父组件无法访问到一个使用了
<script setup>
import { ref } from 'vue'

const a = 1
const b = ref(2)

// 像 defineExpose 这样的编译器宏不需要导入
defineExpose({
  a,
  b
})
</script>
//当父组件通过模板引用获取到了该组件的实例时,得到的实例类型为 { a: number, b: number } (ref 都会自动解包,和一般的实例一样)。

14. 组件

14.1 组件基础

使用组件(局部注册)

<template>
  <Component ></Component >
</template>

<!-- 选项式 -->
<!-- <script>
import Component from "./component.vue";

export default {
  components:{
    Component 
  }
}
</script> -->

<!-- 组合式 -->
<script setup>
import Component from "./component.vue";
</script>

传递props

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

<template>
  <div>{{ title }}</div>
</template>

<!-- 选项式 -->
<!-- <script>
export default {
  props: {
    title: {
      type: String,
      default: '这是一个组件'
    }
  }
}
</script> -->

<!-- 组合式 -->
<script setup>
const props = defineProps({
  title: {
    type: String,
      default: '这是一个组件啊'
  }
})
</script>

监听事件

父组件:

<template>
  <p>count: {{ count }}</p>
  <Component2></Component2>
</template>

<!-- 组合式 -->
<script setup>
import { ref } from 'vue';
import Component2 from "./Component2.vue";
const count = ref(0);
</script>

子组件:

<template>
  <div>
    <button @click="$emit('add')">+1</button>
    <button @click="clickHandler">+1</button>
  </div>
</template>

<!-- 选项式 -->
<!--<script>
// export default {
//   methods: {
//     clickHandler(){
//       this.$emit('add');
//     }
//   }
// }
</script> -->

<!-- 组合式 -->
<script setup>
const emit = defineEmits(['add']);

function clickHandler() {
  emit('add');
}
</script>

通过插槽来分配内容

父组件:

<template>
  <Component3>我是组件1</Component3>
  <Component3>我是组件2</Component3>
  <Component3>我是组件3</Component3>
</template>

<!-- 组合式 -->
<script setup>
import Component3 from "./component3.vue";
</script>

子组件:

<template>
  <div>
    <p>这是一个有插槽的组件,以下是插槽内容</p>
    <slot/>
  </div>
</template>

在这里插入图片描述

动态组件

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

<template>
  <button
       v-for="(_, tab) in tabs"
       :key="tab"
       :class="['tab-button', { active: currentTab === tab }]"
       @click="currentTab = tab"
     >
      {{ tab }}
    </button>
  <component :is="tabs[currentTab]"></component>
</template>

<!-- 组合式 -->
<script setup>
import { ref } from 'vue'
import Component1 from "./component1.vue";
import Component2 from "./component2.vue";
import Component3 from "./component3.vue";
const currentTab = ref('Component1')
const tabs = {
  Component1,
  Component2,
  Component3,
}
</script>

选项式:

<template>
  <button
       v-for="(tab) in tabs"
       :key="tab"
       :class="['tab-button', { active: currentTab === tab }]"
       @click="currentTab = tab"
     >
      {{ tab }}
    </button>
  <component :is="currentTab"></component>
</template>

<!-- 选项式 -->
<script>
import Component1 from "./component1.vue";
import Component2 from "./component2.vue";
import Component3 from "./component3.vue";
export default {
  components: {
    Component1,
    Component2,
    Component3,
  },
  data(){
    return {
      tabs: ['Component1', 'Component2', 'Component3'],
      currentTab:'Component1'
    }
  }
}
</script>

14.2 组件注册

全局注册

main.js

import { createApp } from 'vue'
import App from './App.vue'
import Component4 from './components/component4.vue'

const app = createApp(App)
// 组件注册要写在createApp之后,.mount('#app')之前
app.component('MyComponent', Component4)
app.mount('#app')

使用

<template>
  <MyComponent></MyComponent>
</template>

<script setup>
</script>

局部注册

详情见 14.1 组件基础 使用组件(局部注册)

14.3 Props

Props声明

详情见 14.1 组件基础 传递props

传递props的细节

  • prop名字格式:应使用 camelCase 形式(驼峰形式),因为它们是合法的 JavaScript 标识符,可以直接在模板的表达式中使用
defineProps({
  greetingMessage: String
})
  • 静态 vs. 动态 Prop
<!-- 静态 -->
<BlogPost title="My journey with Vue" />

<!-- 动态: 使用 v-bind -->
<!-- 根据一个变量的值动态传入 -->
<BlogPost :title="post.title" />
<!-- 根据一个更复杂表达式的值动态传入 -->
<BlogPost :title="post.title + ' by ' + post.author.name" />
  • 传递不同的值类型:可以是String、Number、Boolean、Array、Object
  • 使用一个对象绑定多个prop
const post = {
  id: 1,
  title: 'My Journey with Vue'
}
<BlogPost v-bind="post" />
<!-- 等价于-->
<BlogPost :id="post.id" :title="post.title" />

单向数据流

所有的 props 都遵循着单向绑定原则,props 因父组件的更新而变化,自然地将新的状态向下流往子组件,而不会逆向传递。这也意味着props是只读的,子组件内不能变更prop的值,会抛出警告。

如果需要在子组件里变更prop,请使用以下两种方法:

  • 新定义一个局部数据属性,从 props 上获取初始值
const props = defineProps(['initialCounter'])

// 计数器只是将 props.initialCounter 作为初始值
// 像下面这样做就使 prop 和后续更新无关了
const counter = ref(props.initialCounter)
  • 基于该 prop 值定义一个计算属性
const props = defineProps(['size'])

// 该 prop 变更时计算属性也会自动更新
const normalizedSize = computed(() => props.size.trim().toLowerCase())

注意:更改对象 / 数组类型的 props:当对象或数组作为 props 被传入时,虽然子组件无法更改 props 绑定,但仍然可以更改对象或数组内部的值。这是因为 JavaScript 的对象和数组是按引用传递,而对 Vue 来说,禁止这样的改动,虽然可能生效,但有很大的性能损耗,比较得不偿失。这种更改的主要缺陷是它允许了子组件以某种不明显的方式影响父组件的状态,可能会使数据流在将来变得更难以理解。在最佳实践中,你应该尽可能避免这样的更改,除非父子组件在设计上本来就需要紧密耦合。在大多数场景下,子组件应该抛出一个事件来通知父组件做出改变

Prop校验

子组件:

<template>
  <h1>子组件</h1>
  <p v-for="(val, key) in props" :key="key">
    属性名:{{key}},属性值:{{val}},值类型:{{typeof val}}
  </p>
</template>

<script setup>
const props = defineProps({
  // 基础类型检查
  // (给出 `null` 和 `undefined` 值则会跳过任何类型检查)
  propA: Number,
  // 多种可能的类型
  propB: [String, Number],
  // 必传,且为 String 类型
  propC: {
    type: String,
    required: true
  },
  // Number 类型的默认值
  propD: {
    type: Number,
    default: 100
  },
  // 对象类型的默认值
  propE: {
    type: Object,
    // 对象或数组的默认值
    // 必须从一个工厂函数返回。
    // 该函数接收组件所接收到的原始 prop 作为参数。
    default(rawProps) {
      return { message: 'hello' }
    }
  },
  // 自定义类型校验函数
  // 在 3.4+ 中完整的 props 作为第二个参数传入
  propF: {
    validator(value, props) {
      // The value must match one of these strings
      return ['success', 'warning', 'danger'].includes(value)
    }
  },
  // 函数类型的默认值
  propG: {
    type: Function,
    // 不像对象或数组的默认,这不是一个
    // 工厂函数。这会是一个用来作为默认值的函数
    default() {
      return 'Default function'
    }
  },
  // 数组类型默认值
  propH: {
    type: Array,
    default() {
      return [1, 2, 3, 4, 5]
    }
  },
})
</script>

父组件:

<template>
  <h1>父组件</h1>
  <Child v-bind="propObj"></Child>
</template>

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

const propObj = ref({
  propA: 1,
  propB: '2',
  propC: 'propCVal',
  // propE: {
  //   title: 'hsy'
  // },
  propF: 'success',
})
</script>
  • 所有 prop 默认都是可选的,除非声明了 required: true
  • 除 Boolean 外的未传递的可选 prop 将会有一个默认值 undefined
  • Boolean 类型的未传递 prop 将被转换为 false。即默认值为 false
  • 如果声明了 default 值,那么在 prop 的值被解析为 undefined 时,无论 prop 是未被传递还是显式指明的 undefined,都会改为 default 值
  • 类型检查可以是这些原生构造函数:StringNumberBooleanArrayObjectDateFunctionSymbolError

Boolean类型转换

为了更贴近原生 boolean attributes 的行为,声明为 Boolean 类型的 props 有特别的类型转换规则。

defineProps({
  disabled: Boolean
})
<!-- 等同于传入 :disabled="true" -->
<MyComponent disabled />

<!-- 等同于传入 :disabled="false" -->
<MyComponent />

当一个 prop 被声明为允许多种类型时,Boolean 的转换规则也将被应用。然而,当同时允许 String 和 Boolean 时,有一种边缘情况——只有当 Boolean 出现在 String 之前时,Boolean 转换规则才适用

// disabled 将被转换为 true
defineProps({
  disabled: [Boolean, Number]
})
  
// disabled 将被转换为 true
defineProps({
  disabled: [Boolean, String]
})
  
// disabled 将被转换为 true
defineProps({
  disabled: [Number, Boolean]
})
  
// disabled 将被解析为空字符串 (disabled="")
defineProps({
  disabled: [String, Boolean]
})

// disabled 将被转换为 true
defineProps({
  disabled: [Boolean, String]
})

14.4 事件

触发与监听事件

详情见 14.1 组件基础 监听事件
组件的事件监听器也支持 .once 修饰符

<MyComponent @some-event.once="callback" />

事件参数

子组件:

<template>
  <div>
    <button @click="clickHandler(1)">+1</button>
    <button @click="clickHandler(2)">+2</button>
  </div>
</template>

<!-- 选项式 -->
<!--<script>
// export default {
//   methods: {
//     clickHandler(step){
//       this.$emit('add', step);
//     }
//   }
// }
</script> -->

<!-- 组合式 -->
<script setup>
const emit = defineEmits(['add']);

function clickHandler(step) {
  emit('add', step);
}
</script>

父组件:

<template>
  <p>count: {{ count }}</p>
  <Component2 @add="add"></Component2>
</template>

<!-- 组合式 -->
<script setup>
import { ref } from 'vue'
import Component2 from "./component2.vue";

const count = ref(0);
function add(step) {
  count.value += step
}
</script>

声明触发的事件

组件可以显式地通过 defineEmits() 宏来声明它要触发的事件:

<script setup>
const emit = defineEmits(['inFocus', 'submit'])

function buttonClick() {
  emit('submit')
}
</script>

事件校验

和对 props 添加类型校验的方式类似,所有触发的事件也可以使用对象形式来描述。

要为事件添加校验,那么事件可以被赋值为一个函数,接受的参数就是抛出事件时传入 emit 的内容,返回一个布尔值来表明事件是否合法。

<script setup>
const emit = defineEmits({
  // 没有校验
  click: null,

  // 校验 submit 事件
  submit: ({ email, password }) => {
    if (email && password) {
      return true
    } else {
      console.warn('Invalid submit event payload!')
      return false
    }
  }
})

function submitForm(email, password) {
  emit('submit', { email, password })
}
</script>

14.5 组件 v-model

基本用法

父组件

<template>
  <p>text:{{ text }}</p>
  <Child2 v-model="text"></Child2>
</template>

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

const text = ref('');
</script>

子组件

<template>
  <!-- 组件v-model -->
  <!-- 选项式 -->
  <!-- <input type="text" :value="modelValue"  @input="$emit('update:modelValue', $event.target.value)"> -->
  <!-- 组合式 -->
  <input type="text" v-model="modelValue">
</template>

<!-- 选项式 -->
<!-- <script>
// export default {
//   props: ['modelValue']
// }
</script> -->

<!-- 组合式 -->
<script setup>
  const modelValue = defineModel()
</script>

v-model 的参数

组件上的 v-model 也可以接受一个参数:

<MyComponent v-model:title="bookTitle" />

在子组件中,我们可以通过将字符串作为第一个参数传递给 defineModel() 来支持相应的参数:

<!-- MyComponent.vue -->
<script setup>
const title = defineModel('title')
</script>

<template>
  <input type="text" v-model="title" />
</template>

如果需要额外的 prop 选项,应该在 model 名称之后传递:

const title = defineModel('title', { required: true })

多个 v-model 绑定

<UserName
  v-model:first-name="first"
  v-model:last-name="last"
/>
<script setup>
const firstName = defineModel('firstName')
const lastName = defineModel('lastName')
</script>

<template>
  <input type="text" v-model="firstName" />
  <input type="text" v-model="lastName" />
</template>

处理 v-model 修饰符

在学习输入绑定时,我们知道了 v-model 有一些内置的修饰符,例如 .trim,.number 和 .lazy。在某些场景下,你可能想要一个自定义组件的 v-model 支持自定义的修饰符。

我们来创建一个自定义的修饰符 capitalize,它会自动将 v-model 绑定输入的字符串值第一个字母转为大写:

<MyComponent v-model.capitalize="myText" />

通过像这样解构 defineModel() 的返回值,可以在子组件中访问添加到组件 v-model 的修饰符:

<script setup>
const [model, modifiers] = defineModel()

console.log(modifiers) // { capitalize: true }
</script>

<template>
  <input type="text" v-model="model" />
</template>

为了能够基于修饰符选择性地调节值的读取和写入方式,我们可以给 defineModel() 传入 getset 这两个选项。这两个选项在从模型引用中读取或设置值时会接收到当前的值,并且它们都应该返回一个经过处理的新值。下面是一个例子,展示了如何利用 set 选项来应用 capitalize (首字母大写) 修饰符:

<script setup>
const [model, modifiers] = defineModel({
  set(value) {
    if (modifiers.capitalize) {
      return value.charAt(0).toUpperCase() + value.slice(1)
    }
    return value
  }
})
</script>

<template>
  <input type="text" v-model="model" />
</template>

14.6 拓展:用props子传父

通过函数类型的prop来实现
父组件:

<template>
  <p>text:{{ text }}</p>
  <Child3 :onEvent="onEvent"></Child3>
</template>

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

const text = ref('我是父组件的数据');
function onEvent(data) {
  text.value = data;
}
</script>

子组件:

<template>
  {{ onEvent('我是子组件里的数据') }}
</template>

<!-- 组合式 -->
<script setup>
  const props = defineProps({
    onEvent:{
      type: Function,
    }
  })
</script>

14.7 透传 Attributes

Attributes 继承

“透传 attribute”指的是传递给一个组件,却没有被该组件声明为 propsemitsattribute 或者 v-on 事件监听器。最常见的例子就是 classstyleid

<!-- <MyButton> 的模板 -->
<button class="btn">click me</button>
<MyButton class="large" />
<!-- 最后渲染出的 DOM 结果 -->
<button class="btn large">click me</button>

对 class 和 style 的合并
如果一个子组件的根元素已经有了 class 或 style attribute,它会和从父组件上继承的值合并

v-on 监听器继承
click 监听器会被添加到 的根元素,即那个原生的 元素之上。当原生的 被点击,会触发父组件的 onClick 方法。同样的,如果原生 button 元素自身也通过 v-on 绑定了一个事件监听器,则这个监听器和从父组件继承的监听器都会被触发

深层组件继承
有些情况下一个组件会在根节点上渲染另一个组件。例如,我们重构一下 ,让它在根节点上渲染 :

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

此时 接收的透传 attribute 会直接继续传给 往里透传

  • 透传的 attribute 不会包含 上声明过的 props 或是针对 emits 声明事件的 v-on 侦听函数,换句话说,声明过的 props 和侦听函数被 “消费”了。
  • 透传的 attribute 若符合声明,也可以作为 props 传入 。

禁用 Attributes 继承

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

<!-- 组合式 -->
<script setup>
defineOptions({
  inheritAttrs: false
})
// ...setup 逻辑
</script>
<!-- 选项式 -->
<script>
	export default {
	  name: 'Child',
	  props: {},
	  inheritAttrs: false
	}
</script>

最常见的需要禁用 attribute 继承的场景就是 attribute 需要应用在根节点以外的其他元素上。通过设置 inheritAttrs 选项为 false,你可以完全控制透传进来的 attribute 被如何使用。这些透传进来的 attribute 可以在模板的表达式中直接用 $attrs 访问到。这个 $attrs 对象包含了除组件所声明的 props 和 emits 之外的所有其他 attribute,例如 class,style,v-on 监听器等等。

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

多根节点的 Attributes 继承

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

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

在 JavaScript 中访问透传 Attributes

<!-- 组合式 -->
<script setup>
import { useAttrs } from 'vue'

const attrs = useAttrs()
</script>
<!-- 选项式 -->
<script>
	export default {
	  created() {
	    console.log(this.$attrs)
	  }
	}
</script>

14.8 插槽 Slots

插槽内容与出口

详情见14.1 组件基础 通过插槽来分配内容

<slot> 元素是一个插槽出口 (slot outlet),标示了父元素提供的插槽内容 (slot content) 将在哪里被渲染。
在这里插入图片描述

渲染作用域

插槽内容可以访问到父组件的数据作用域,因为插槽内容本身是在父组件模板中定义的。插槽内容无法访问子组件的数据

<span>{{ message }}</span>
<FancyButton>{{ message }}</FancyButton>

默认内容

在外部没有提供任何内容的情况下,可以为插槽指定默认内容。

父组件提供插槽内容时,默认的插槽内容会被覆盖。

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

具名插槽

在一个组件中包含多个插槽出口:

这类带 name 的插槽被称为具名插槽 (named slots)。没有提供 name 的 <slot> 出口会隐式地命名为“default”。
子组件:

<template>
  <div>
    <!-- 这类带 name 的插槽被称为具名插槽 (named slots)。没有提供 name 的 <slot> 出口会隐式地命名为“default”。 -->
    <p>以下是header插槽内容</p>
    <slot name="header"></slot>
    <p>以下是default插槽内容</p>
    <slot></slot>
    <p>以下是footer插槽内容</p>
    <slot name="footer"></slot>
  </div>
</template>

父组件:

<template>
  <Component3>
    <template #header>
      <p>this is header</p>
    </template>

    <template #default>
      <p>A paragraph for the main content.</p>
      <p>And another one.</p>
    </template>

    <template #footer>
      <p>this is footer</p>
    </template>
  </Component3>
</template>

<!-- 组合式 -->
<script setup>
import Component3 from "./component3.vue";
</script>

在这里插入图片描述
当一个组件同时接收默认插槽和具名插槽时,所有位于顶级的非 节点都被隐式地视为默认插槽的内容。如:

<Component3>
    <template #header>
      <p>this is header</p>
    </template>
    <!-- 隐式的默认插槽 -->
    <p>A paragraph for the main content.</p>
    <p>And another one.</p>
      

    <template #footer>
      <p>this is footer</p>
    </template>
  </Component3>

动态插槽名

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

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

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

作用域插槽 及 具名作用域插槽

在某些场景下插槽的内容可能想要同时使用父组件域内和子组件域内的数据。要做到这一点,我们需要一种方法来让子组件在渲染时将一部分数据提供给插槽。

我们可以像对组件传递 props 那样,向一个插槽的出口上传递 attributes:
在这里插入图片描述

子组件:

<template>
  <div>
    <!-- default作用域插槽 -->
    <slot :data="obj" :count="1"></slot>
    <!-- 具名作用域插槽 -->
    <slot name="title" title='我是title'></slot>
  </div>
</template>

<script setup>
import { ref } from 'vue'
const obj = ref({
  label: 'this is label',
  value: 'this is value',
})
</script>

父组件:

<template>
  <Component3>
    <template v-slot="{data, count}">
      我是default插槽的内容
      <p>data:{{data}}</p>
      <p>count:{{count}}</p>
    </template>
    <template #title="scope">
      我是title插槽的内容
      <p>子组件传来的数据:{{scope}}</p>
    </template>
  </Component3>
</template>

<!-- 组合式 -->
<script setup>
import Component3 from "./component3.vue";
</script>

在这里插入图片描述

14.9 依赖注入

prop逐级透传问题

有一些多层级嵌套的组件,形成了一颗巨大的组件树,而某个深层的子组件需要一个较远的祖先组件中的部分数据。在这种情况下,如果仅使用 props 则必须将其沿着组件链逐级传递下去,这会非常麻烦:
在这里插入图片描述
provideinject 可以帮助我们解决这一问题。一个父组件相对于其所有的后代组件,会作为依赖提供者。任何后代的组件树,无论层级有多深,都可以注入由父组件提供给整条链路的依赖。
在这里插入图片描述

Provide(提供)

<!-- 组合式 -->
<script setup>
import { ref, provide } from 'vue'

const count = ref(0)
// provide(/* 注入名 */ 'message', /* 值 */ 'hello!')
provide('key', count)
</script>
<!-- 选项式 -->
<script>
export default {
  data() {
    return {
      message: 'hello!'
    }
  },
  provide() {
    // 使用函数的形式,可以访问到 `this`
    return {
      message: this.message
    }
  }
}
</script>

应用层 Provide

main.js:

import { createApp } from 'vue'

const app = createApp({})

app.provide(/* 注入名 */ 'message', /* 值 */ 'hello!')

在应用级别提供的数据在该应用内的所有组件中都可以注入。

Inject(注入)

<!-- 组合式 -->
<script setup>
import { inject } from 'vue'

const message = inject('key''这里放默认值')
// 一些场景中,默认值可能需要通过调用一个函数或初始化一个类来取得。为了避免在用不到默认值的情况下进行不必要的计算或产生副作用,我们可以使用工厂函数来创建默认值(第三个参数表示默认值应该被当作一个工厂函数。):
const value = inject('key', () => new ExpensiveClass(), true)
</script>
<!-- 选项式 -->
<script>
export default {
  inject: {
  	msg: {
      from: 'message', // 当与原注入名同名时,这个属性是可选的
      default: 'default value'
    },
    user: {
      // 对于非基础类型数据,如果创建开销比较大,或是需要确保每个组件实例
      // 需要独立数据的,请使用工厂函数
      default: () => ({ name: 'John' })
    }
  },
  data() {
    return {
      // 基于注入值的初始数据
      fullMessage: this.msg
    }
  }
}
</script>

和响应式数据配合使用

建议尽可能将任何对响应式状态的变更都保持在供给方组件中。
① 组合式:

<!-- 在供给方组件内 -->
<script setup>
import { provide, ref } from 'vue'

const location = ref('North Pole')

function updateLocation() {
  location.value = 'South Pole'
}

provide('location', {
  location,
  updateLocation
})
</script>
<!-- 在注入方组件 -->
<script setup>
import { inject } from 'vue'

const { location, updateLocation } = inject('location')
</script>

<template>
  <button @click="updateLocation">{{ location }}</button>
</template>

如果你想确保提供的数据不能被注入方的组件更改,你可以使用 readonly() 来包装提供的值。

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

const count = ref(0)
provide('read-only-count', readonly(count))
</script>

② 选项式:
为保证注入方和供给方之间的响应性链接,我们需要使用 computed() 函数提供一个计算属性:

import { computed } from 'vue'

export default {
  data() {
    return {
      message: 'hello!'
    }
  },
  provide() {
    return {
      // 显式提供一个计算属性
      message: computed(() => this.message)
    }
  }
}

使用 Symbol 作注入名

// keys.js
export const myInjectionKey = Symbol()
// 在供给方组件中(组合式)
import { provide } from 'vue'
import { myInjectionKey } from './keys.js'

provide(myInjectionKey, { /*
  要提供的数据
*/ });

// 在供给方组件中(选项式)
// import { myInjectionKey } from './keys.js'

// export default {
//   provide() {
//     return {
//       [myInjectionKey]: {
//         /* 要提供的数据 */
//       }
//     }
//   }
// }
// 注入方组件(组合式)
import { inject } from 'vue'
import { myInjectionKey } from './keys.js'

const injected = inject(myInjectionKey)

// 注入方组件(选项式)
// import { myInjectionKey } from './keys.js'

// export default {
//   inject: {
//     injected: { from: myInjectionKey }
//   }
// }

14.10 异步组件

基本用法

在大型项目中,我们可能需要拆分应用为更小的块,并仅在需要时再从服务器加载相关组件。Vue 提供了 defineAsyncComponent 方法来实现此功能:

import { defineAsyncComponent } from 'vue'

const AsyncComp = defineAsyncComponent(() => {
  return new Promise((resolve, reject) => {
    // ...从服务器获取组件
    resolve(/* 获取到的组件 */)
  })
})
// ... 像使用其他一般组件一样使用 `AsyncComp`

如你所见,defineAsyncComponent 方法接收一个返回 Promise 的加载函数。这个 Promise 的 resolve 回调方法应该在从服务器获得组件定义时调用。你也可以调用 reject(reason) 表明加载失败。

ES 模块动态导入也会返回一个 Promise,所以多数情况下我们会将它和 defineAsyncComponent 搭配使用。类似 Vite 和 Webpack 这样的构建工具也支持此语法 (并且会将它们作为打包时的代码分割点),因此我们也可以用它来导入 Vue 单文件组件:

import { defineAsyncComponent } from 'vue'

const AsyncComp = defineAsyncComponent(() =>
  import('./components/MyComponent.vue')
)

最后得到的 AsyncComp 是一个外层包装过的组件,仅在页面需要它渲染时才会调用加载内部实际组件的函数。它会将接收到的 props 和插槽传给内部组件,所以你可以使用这个异步的包装组件无缝地替换原始组件,同时实现延迟加载。

与普通组件一样,异步组件可以使用 app.component() 全局注册:

app.component('MyComponent', defineAsyncComponent(() =>
  import('./components/MyComponent.vue')
))

也可以直接在父组件中直接定义它们:

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

const AdminPage = defineAsyncComponent(() =>
  import('./components/AdminPageComponent.vue')
)
</script>

<template>
  <AdminPage />
</template>

加载与错误状态

const AsyncComp = defineAsyncComponent({
  // 加载函数
  loader: () => import('./Foo.vue'),

  // 加载异步组件时使用的组件
  loadingComponent: LoadingComponent,
  // 展示加载组件前的延迟时间,默认为 200ms
  delay: 200,

  // 加载失败后展示的组件
  errorComponent: ErrorComponent,
  // 如果提供了一个 timeout 时间限制,并超时了
  // 也会显示这里配置的报错组件,默认值是:Infinity
  timeout: 3000
})

如果提供了一个加载组件,它将在内部组件加载时先行显示。在加载组件显示之前有一个默认的 200ms 延迟——这是因为在网络状况较好时,加载完成得很快,加载组件和最终组件之间的替换太快可能产生闪烁,反而影响用户感受。

如果提供了一个报错组件,则它会在加载器函数返回的 Promise 抛错时被渲染。你还可以指定一个超时时间,在请求耗时超过指定时间时也会渲染报错组件。

15. 生命周期

生命周期图示

在这里插入图片描述

生命周期钩子

选项式见文档-> 生命周期选项

组合式:

  • onBeforeMount():注册一个钩子,在组件被挂载之前被调用。当这个钩子被调用时,组件已经完成了其响应式状态的设置,但还没有创建 DOM 节点。它即将首次执行 DOM 渲染过程。这个钩子在服务器端渲染期间不会被调用。
  • onMounted():注册一个回调函数,在组件挂载完成后执行。组件在以下情况下被视为已挂载:其所有同步子组件都已经被挂载 (不包含异步组件或 <Suspense> 树内的组件)。其自身的 DOM 树已经创建完成并插入了父容器中。注意仅当根容器在文档中时,才可以保证组件 DOM 树也在文档中。这个钩子通常用于执行需要访问组件所渲染的 DOM 树相关的副作用,或是在服务端渲染应用中用于确保 DOM 相关代码仅在客户端执行这个钩子在服务器端渲染期间不会被调用。
  • onBeforeUpdate():注册一个钩子,在组件即将因为响应式状态变更而更新其 DOM 树之前调用。这个钩子可以用来在 Vue 更新 DOM 之前访问 DOM 状态。在这个钩子中更改状态也是安全的。这个钩子在服务器端渲染期间不会被调用。
  • onUpdated():注册一个回调函数,在组件因为响应式状态变更而更新其 DOM 树之后调用。父组件的更新钩子将在其子组件的更新钩子之后调用。这个钩子会在组件的任意 DOM 更新后被调用,这些更新可能是由不同的状态变更导致的,因为多个状态变更可以在同一个渲染周期中批量执行 (考虑到性能因素)。如果你需要在某个特定的状态更改后访问更新后的 DOM,请使用 nextTick() 作为替代。这个钩子在服务器端渲染期间不会被调用。
  • onBeforeUnmount():注册一个钩子,在组件实例被卸载之前调用。当这个钩子被调用时,组件实例依然还保有全部的功能。这个钩子在服务器端渲染期间不会被调用。
  • onUnmounted():注册一个回调函数,在组件实例被卸载之后调用。一个组件在以下情况下被视为已卸载:其所有子组件都已经被卸载。所有相关的响应式作用 (渲染作用以及 setup() 时创建的计算属性和侦听器) 都已经停止。可以在这个钩子中手动清理一些副作用,例如计时器、DOM 事件监听器或者与服务器的连接。这个钩子在服务器端渲染期间不会被调用。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值