vue3学习笔记

一、基础语法

1、创建一个Vue应用

1)应用实例:每个 Vue 应用都是通过 createApp 函数创建一个新的 应用实例
import { createApp } from 'vue'

const app = createApp({
  /* 根组件选项 */
})
2)根组件:我们传入 createApp 的对象实际上是一个组件,每个应用都需要一个“根组件”,其他组件将作为其子组件。

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

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

const app = createApp(App)
3)挂载应用:

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

4)应用配置:

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

app.config.errorHandler = (err) => {
  /* 处理错误 */
}
5)多个应用实例:【基本不用】

2、模板语法

1)文本插值:
2)原始HTML:

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

3)Attribute 绑定:
  • 同名简写:如果 attribute 的名称与绑定的 JavaScript 值的名称相同,那么可以进一步简化语法,省略attribute 值:

    <!-- 与 :id="id" 相同 -->
    <div :id></div>
    
    <!-- 这也同样有效 -->
    <div v-bind:id></div>
    
  • 动态绑定多个值:
    果你有像这样的一个包含多个 attribute 的 JavaScript 对象:

    const objectOfAttrs = {
      id: 'container',
      class: 'wrapper',
      style: 'background-color:green'
    }
    

    通过不带参数的 v-bind,你可以将它们绑定到单个元素上:

    <div v-bind="objectOfAttrs"></div>
    
4)使用JavaScript表达式:
  • 仅支持表达式
  • 调用函数
  • 受限的全局访问
    模板中的表达式将被沙盒化,仅能够访问到有限的全局对象列表。该列表中会暴露常用的内置全局对象,比如 MathDate
    没有显式包含在列表中的全局对象将不能在模板内表达式中访问,例如用户附加在 window 上的属性。然而,你也可以自行在 app.config.globalProperties 上显式地添加它们,供所有的 Vue 表达式使用。
5)指令 Directives
  • 参数 Arguments:某些指令会需要一个“参数”,在指令名后通过一个冒号隔开做标识。
    例如:v-bind:href="url",v-bind冒号后面的href就是v-bind指令的参数。

    <a v-bind:href="url"> ... </a>
    
  • 动态参数:
    同样在指令参数上也可以使用一个 JavaScript 表达式,需要包含在一对方括号内:

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

    注意:指令中动态参数值是有限制的,值应当是一个字符串。语法上也有一些限制。

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

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

3、响应式基础: 【响应式进阶

1)ref()

ref()函数,返回一个RefImpl类型的响应式对象。它的参数值包裹在value属性中。

const count = ref(0) // RefImpl { value: 0 }
  • 深层响应式:
    • 非原始值将通过 reactive() 转换为响应式代理,该函数将在后面讨论。
    • 也可以通过 shallow ref 来放弃深层响应性。对于浅层 ref,只有 .value 的访问会被追踪。浅层 ref 可以用于避免对大型数据的响应性开销来优化性能。
2)reactive()
3)DOM更新时机:

当你修改了响应式状态时,DOM 会被自动更新。但是需要注意的是,DOM 更新不是同步的。Vue 会在“next tick”更新周期中缓冲所有状态的修改,以确保不管你进行了多少次状态修改,每个组件都只会被更新一次。

import { nextTick } from 'vue'

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

4、计算属性:

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

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

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

1)为计算属性标注类型
2)可写计算属性

计算属性创建的变量 默认是只读的。当你尝试修改一个计算属性时,你会收到一个运行时警告。只在某些特殊场景中你可能才需要用到“可写”的属性。

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

注意:set函数中不一定要设置当前值的,也可以做其它的事。如组件中v-model绑定值,和父组件有交互时。值改变,需要触发父组件的自定义事件,这时就是在set中触发的。

// 监听 el-dialog 的关闭事件,同步状态到父组件
const dialogVisible = computed({
  get: () => props.modelValue,
  set: (value) => emit('update:modelValue', value)
});
3)获取上一个值:

如果需要,可以通过访问计算属性的 getter 的第一个参数来获取计算属性返回的上一个值。

5、侦听器:watch数据监听

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

const question = ref('')

// 可以直接侦听一个 ref
watch(question, async (newQuestion, oldQuestion) => {

})
</script>
1)侦听数据源类型:

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

2)深层侦听器:

直接给 watch() 传入一个响应式对象,会隐式地创建一个深层侦听器——该回调函数在所有嵌套的变更时都会被触发:

const obj = reactive({ count: 0 })

watch(obj, (newValue, oldValue) => {
  // 在嵌套的属性变更时触发
})

obj.count++
3)即时回调的侦听器:(立刻执行)

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

watch(
  source,
  (newValue, oldValue) => {
    // 立即执行,且当 `source` 改变时再次执行
  },
  { immediate: true } // 设置 立即调用
)
4)一次性侦听器:

每当被侦听源发生变化时,侦听器的回调就会执行。如果希望回调只在源变化时触发一次,请使用 once: true 选项。

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

watchEffect() 允许我们自动跟踪回调的响应式依赖。

即,watchEffect是watch的简化。watchEffect不需要明确写出依赖的响应式源,自动把回调中使用到的响应式源作为依赖源。默认就是立即执行的,所以不需要指定 immediate: true

watchEffect(async () => {
  const response = await fetch(
    `https://jsonplaceholder.typicode.com/todos/${todoId.value}`
  )
  data.value = await response.json()
})
6)副作用清理
7)回调的触发时机:
  • Post Watchers
  • 同步侦听器:
8)停止侦听器:

setup()<script setup> 中用同步语句创建的侦听器,会自动绑定到宿主组件实例上,并且会在宿主组件卸载时自动停止。因此,在大多数情况下,你无需关心怎么停止一个侦听器。

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

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

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

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

6、模板引用:DOM引用或组件引用

<input ref="input">
1)访问模板引用:

要在组合式 API 中获取引用,我们可以使用辅助函数 useTemplateRef()

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

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

onMounted(() => {
  input.value.focus()
})
</script>

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

vue3.5版本以前是没有useTemplateRef函数的。所以3.5以前的用法是,声明一个与模板里 ref attribute 匹配的引用:

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

// 声明一个 ref 来存放该元素的引用
// 必须和模板里的 ref 同名
const input = ref(null)

onMounted(() => {
  input.value.focus()
})
</script>

<template>
  <input ref="input" />
</template>
2)v-for 中的模板引用

当在 v-for 中使用模板引用时,对应的 ref 中包含的值是一个数组,它将在元素被挂载后包含对应整个列表的所有元素:

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

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

const itemRefs = useTemplateRef('items')

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

<template>
  <ul>
    <li v-for="item in list" ref="items">
      {{ item }}
    </li>
  </ul>
</template>
3)函数模板引用
<input :ref="(el) => { /* 将 el 赋值给一个数据属性或 ref 变量 */ }">
  • ref还可以绑定为一个函数,会在每次组件更新时都被调用。该函数会收到元素引用作为其第一个参数。
  • 当绑定的元素被卸载时,函数也会被调用一次,此时的 el 参数会是 null。你当然也可以绑定一个组件方法而不是内联函数。
4)组件上的ref:

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

7、组件基础

1)定义一个组件:

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

2)使用组件:
  • 全局注册
  • 局部注册

注意:

  1. 在单文件组件中,推荐为子组件使用 PascalCase 的标签名,以此来和原生的 HTML 元素作区分。虽然原生 HTML 标签名是不区分大小写的,但 Vue 单文件组件是可以在编译中区分大小写的。我们也可以使用 /> 来关闭一个标签。
  2. 如果你是直接在 DOM 中书写模板 (例如原生 <template> 元素的内容),模板的编译需要遵从浏览器中 HTML 的解析行为。在这种情况下,你应该需要使用 kebab-case 形式并显式地关闭这些组件的标签。(即直接在html文件上的标签上写vue模板)
3)传递 props
<script setup>
const props = defineProps(['title'])
console.log(props.title)
</script>

<template>
  <h4>{{ title }}</h4> <!-- 这里直接用props里面的值,不用带上props对象 -->
</template>
  • defineProps 声明的 props 会自动暴露给模板 。且defineProps 会返回一个对象,其中包含了可以传递给组件的所有 props。
    即,在模板中使用props,不需要带上props对象,直接用里面的属性就可以。
4)监听事件
<script setup>
const emit = defineEmits(['enlarge-text'])

emit('enlarge-text')
</script>

在模板中直接使用$emit

<button @click="$emit('increaseBy')">Increase</button>

注意:虽然显示声明自定义事件,不是必须的。但是强烈推荐声明。有很多eslint创建就对这个做强烈要求的。

特点:如果使用显示声明,Vue 会知道这些是自定义事件,不会尝试将其作为原生 DOM 事件处理。

5)通过插槽来分配内容

使用 <slot> 作为一个占位符,父组件传递进来的内容就会渲染在这里。

6)动态组件<component>

有些场景会需要在两个组件间来回切换,比如 Tab 界面:

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

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

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

感悟:针对这种来回切换的组件,使用动态组件比v-if来实现的更好。

7)DOM 内模板解析注意事项:

即html文件中创建DOM模板,而不是单文件.vue中创建模板。

8、生命周期

二、深入组件

2.1、组件注册

1)全局注册:

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

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

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

import MyComponent from './App.vue'

app.component('MyComponent', MyComponent)

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

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

在使用 <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() {
    // ...
  }
}
3)组件名格式:PascalCase 和 kebab-case 格式

引入肯定都是PascalCase格式,使用可以选上面其中一种。

  • PascalCase:推荐这种方式。原因就是使用kebab-case碰到一个坑。
  • kebab-case:Vue 支持将模板中使用 kebab-case 的标签解析为使用 PascalCase 注册的组件。所以在通过PascalCase 格式导入的组件,可以用kebab-case格式的标签表示。

遇坑经验:在使用script-setup 的组件里引入子组件UpdateLog,使用kebab-case格式的标签,结果子组件就是没有加载到。反复确认了没有书写的问题,把标签改成PascalCase 格式就好了。后面突然发现,setup中另外声明了一个updateLog函数。即:

<update-log></update-log>
<script lang="ts" setup>
import { defineComponent, h, reactive, ref } from 'vue';
import UpdateLog from "./UpdateLog.vue";

const updateLog = () => {}
</script>

问题分析:从JS上来说,UpdateLog 和 updateLog是两个不同名字的变量。但是模板上的kebab-case标签名解析成驼峰格式(大/小驼峰同时符合的),解析后和updateLog函数匹配上。所以没有找到正确的组件变量;而PascalCase之所以不会,是因为单文件组件是可以在编译中区分大小写的,不需要解析成其它变量去匹配组件,而是直接去找同名的变量。
亲测:kebab-case标签,导入的组件名,使用大/小驼峰格式,都可以正确匹配到。

经验之谈:在单文件组件中,统一使用PascalCase格式的组件标签,可以避免上面出现的问题。而且PascalCase格式和原生的 HTML 元素很好作区分。
很多组件库使用kebab-case格式案例,可能是考虑到在DOM上书写模板。

2.2、Props

1)Props声明:

在使用 <script setup> 的单文件组件中,props 可以使用 defineProps() 宏来声明:

<script setup>
const props = defineProps(['foo'])

console.log(props.foo)
</script>

在没有使用 <script setup> 的组件中,props 可以使用 props 选项来声明:

export default {
  props: ['foo'],
  setup(props) {
    // setup() 接收 props 作为第一个参数
    console.log(props.foo)
  }
}

注意传递给 defineProps() 的参数和提供给 props 选项的值是相同的,两种声明方式背后其实使用的都是 props 选项。

2)响应式 Props 解构:
const { foo } = defineProps(['foo'])

watchEffect(() => {
  // 在 3.5 之前只运行一次
  // 在 3.5+ 中在 "foo" prop 变化时重新执行
  console.log(foo)
})

可以使用 JavaScript 原生的默认值语法声明 props 默认值。这在使用基于类型的 props 声明时特别有用。

const { foo = 'hello' } = defineProps<{ foo?: string }>()

难点:这块了解有点 费劲,下次再补充下

3)传递 prop 的细节
4)单向数据流
5)Prop 校验

值不满足类型要求,Vue 会在控制台抛出警告来提醒使用者。这在开发给其他开发者使用的组件时非常有用。

要声明对 props 的校验,你可以向 defineProps() 宏提供一个带有 props 校验选项的对象:

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

2.3、事件

4)触发与监听事件:同vue2的
<button @click="$emit('someEvent')">Click Me</button>
5)事件参数:同vue2的
6)声明触发的事件:

我们在 <template> 中使用的 $emit 方法不能在组件的 <script setup> 部分中使用,但 defineEmits() 会返回一个相同作用的函数供我们使用:

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

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

显式地使用了 setup 函数而不是 <script setup>,则事件需要通过 emits 选项来定义,emit 函数也被暴露在 setup() 的上下文对象上:

export default {
  emits: ['inFocus', 'submit'],
  setup(props, ctx) {
    ctx.emit('submit')
  }
}

说明:$emit可以在模板中直接使用,但在setup中不能直接使用,需要先声明。

注意:如果一个原生事件的名字 (例如 click) 被定义在 emits 选项中,则监听器只会监听组件触发的 click 事件而不会再响应原生的 click 事件。

即,子组件中显示声明了触发的事件,则对应的事件一定是组件的自定义事件。不会作为根标签的原生事件。

7)事件校验:

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

8)vue3中没有.native事件修饰符:如何实现原生事件绑定在组件根元素上。
  • 单根节点组件:默认就是绑定在根元素上,所以用不着.native修饰符。
    组件的原生事件会自动透传到根元素上。 【查看文档的 "透传 Attributes"章节】
  • 多根节点组件:多根节点的组件没有自动 attribute 透传行为。vue由于不知道透传哪里,还会发出警告。

2.4、组件 v-model

v-model 可以在组件上使用以实现双向绑定。

1)基本用法:
  • 底层机制:

    • 编译器会把v-model编译为 modelValue 的 prop;
    • 并且父组件中的 v-model="foo" 会被编译为带自定义事件的标签:
    <Child v-model="foo" />
    

    编译后:

    <Child
      :modelValue="foo"
      @update:modelValue="$event => (foo = $event)"
    />
    

    所以父组件中不需要对这个绑定数据,做监听事件绑定处理。底层已经做了。

    子组件要接收modelValue这个prop,并且通过emit( @update:modelValue)事件触发父组件更新。

  • 从 Vue 3.4 开始,推荐的实现方式是使用 defineModel() 宏:这种方式,连子组件手动触发的方式不用了,也直接给你弄好了。

<!-- Child.vue -->
<script setup>
const model = defineModel()

function update() {
  model.value++
}
</script>

<template>
  <div>Parent bound v-model is: {{ model }}</div>
  <button @click="update">Increment</button>
</template>

父组件可以用 v-model 绑定一个值:

<!-- Parent.vue -->
<Child v-model="countModel" />
2)v-model 的参数:
3)多个 v-model 绑定:
4)处理 v-model 修饰符:

2.5、透传 Attributes

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

1)Attributes 继承:

当一个组件以单个元素为根作渲染时,透传的 attribute 会自动被添加到根元素上。

  • classstyle 的合并:
    如果一个子组件的根元素已经有了 classstyle attribute,它会和从父组件上继承的值合并。

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

    <MyButton @click="onClick" />
    

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

    说明:从这里就能看出,vue3组件绑定原生事件,根本不需要.native修饰符了。默认就透传到根节点上了。

  • 深层组件继承
    有些情况下一个组件会在根节点上渲染另一个组件。

2)禁用 Attributes 继承:

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

<script setup>
defineOptions({
  inheritAttrs: false
})
// ...setup 逻辑
</script>
3)多根节点的 Attributes 继承:

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

4)在 JavaScript 中访问透传 Attributes:

如果需要,你可以在 <script setup> 中使用 useAttrs() API 来访问一个组件的所有透传 attribute:

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

const attrs = useAttrs()
</script>

如果没有使用 <script setup>attrs 会作为 setup() 上下文对象的一个属性暴露:

export default {
  setup(props, ctx) {
    // 透传 attribute 被暴露为 ctx.attrs
    console.log(ctx.attrs)
  }
}

2.6、插槽

1)插槽内容与出口:

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

2)渲染作用域:
  • 插槽内容可以访问到父组件的数据作用域,因为插槽内容本身是在父组件模板中定义的。
  • 插槽内容无法访问子组件的数据。除非通过作用域插槽提供数据。
3)默认内容:
4)具名插槽:
<div class="container">
  <header>
    <slot name="header"></slot>
  </header>
  <main>
    <slot></slot>
  </main>
  <footer>
    <slot name="footer"></slot>
  </footer>
</div>

使用时,父组件

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

v-slot 对应的简写 #,因此 可以简写为 <template #header>

5)条件插槽:根据是否有插槽内容决定是否渲染某些内容。

结合使用 $slots 属性与 v-if 来实现。

<template>
  <div class="card">
    <div v-if="$slots.header" class="card-header">
      <slot name="header" />
    </div>
  </div>
</template>

插槽内容本来就是可选的,那为什么还要条件插槽呢?实现以下复杂情况:

  • 通过 v-if 控制插槽内容的渲染,避免不必要的 DOM 节点。如上,没有插槽,插槽内容的父节点是不需要的,通过条件插槽实现判断。

  • 不一定是$slots决定渲染情况,通过组件的props也可以实现决定哪些插槽是否渲染。

    <!-- 子组件 Card.vue -->
    <template>
      <div class="card">
        <header v-if="showHeader">
          <slot name="header"></slot> <!-- 仅在 showHeader 为 true 时渲染 -->
        </header>
      </div>
    </template>
    <script>
    export default {
      props: {
        showHeader: Boolean // 控制是否渲染 header 插槽
      }
    };
    </script>
    
    <!-- 父组件 Parent.vue -->
    <template>
      <Card :showHeader="false"> <!-- 即使父组件传递了 header 内容,也不会渲染 -->
        <template #header>This content won't be rendered.</template>
        Default content
      </Card>
    </template>
    
6)动态插槽名:使用时,v-slot指定插槽名是动态的。【不常用】
7)作用域插槽:

在上面的渲染作用域中我们讨论到,插槽的内容无法访问到子组件的状态。

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

  • 默认作用域插槽:

    <MyComponent v-slot="{ text, count }">
      {{ text }} {{ count }}
    </MyComponent>
    
  • 具名作用域插槽:

    <MyComponent>
      <template #header="headerProps">
        {{ headerProps }}
      </template>
    
      <template #default="defaultProps">
        {{ defaultProps }}
      </template>
    
      <template #footer="footerProps">
        {{ footerProps }}
      </template>
    </MyComponent>
    
  • 如果你同时使用了具名插槽与默认插槽,则需要为默认插槽使用显式的 <template> 标签。

    <MyComponent>
      <!-- 使用显式的默认插槽 -->
      <template #default="{ message }">
        <p>{{ message }}</p>
      </template>
    
      <template #footer>
        <p>Here's some contact info</p>
      </template>
    </MyComponent>
    

总结:

  1. 默认作用域插槽,获取作用域是在组件标签上。具名作用域插槽获取作用域是在具名的template上,如 <template #header="headerProps">

  2. 组件内部提供作用域数据的方式是通过在slot标签上加属性,如下,给header插槽提供l了message数据。

    <!-- <MyComponent> template -->
    <div>
      <slot name="header" message="hello"></slot>
    </div>
    
  3. 使用组件时,获取作用域插槽,通过v-solt等号右边的变量接收。

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

2.7、依赖注入

1)Prop 逐级透传问题:如果仅使用 props 则必须将其沿着组件链逐级传递下去,这会非常麻烦。
2)Provide (提供):

要为组件后代提供数据,需要使用到 provide() 函数:

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

provide(/* 注入名 */ 'message', /* 值 */ 'hello!')
</script>
3)应用层 Provide:即从整个app实例上提供 Provide
import { createApp } from 'vue'

const app = createApp({})

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

要注入上层组件提供的数据,需使用 inject() 函数:

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

const message = inject('message')
</script>
5)注入默认值:

如果在注入一个值时不要求必须有提供者,那么我们应该声明一个默认值,和 props 类似

const value = inject('message', '这是默认值')
6)和响应式数据配合使用

当提供 / 注入响应式的数据时,建议尽可能将任何对响应式状态的变更都保持在供给方组件中。这样可以确保所提供状态的声明和变更操作都内聚在同一个组件内,使其更容易维护。

7)使用 Symbol 作注入名:

如果你正在构建大型的应用,包含非常多的依赖提供,或者你正在编写提供给其他开发者使用的组件库,建议最好使用 Symbol 来作为注入名以避免潜在的冲突。

2.8、异步组件

三、逻辑复用

3.1、组合式函数

“组合式函数”(Composables) 是一个利用 Vue 的组合式 API 来封装和复用有状态逻辑的函数。

也有称 hooks,而“Hooks”这一名称更多是社区和开发者借鉴 React Hooks 的命名习惯。

1)鼠标跟踪器示例:
// mouse.js
import { ref, onMounted, onUnmounted } from 'vue'

// 按照惯例,组合式函数名以“use”开头
export function useMouse() {
  // 被组合式函数封装和管理的状态
  const x = ref(0)
  const y = ref(0)

  // 组合式函数可以随时更改其状态。
  function update(event) {
    x.value = event.pageX
    y.value = event.pageY
  }

  // 一个组合式函数也可以挂靠在所属组件的生命周期上
  // 来启动和卸载副作用
  onMounted(() => window.addEventListener('mousemove', update))
  onUnmounted(() => window.removeEventListener('mousemove', update))

  // 通过返回值暴露所管理的状态
  return { x, y }
}

组件中使用它:

<script setup>
import { useMouse } from './mouse.js'

const { x, y } = useMouse()
</script>

<template>Mouse position is at: {{ x }}, {{ y }}</template>
2)在选项式 API 中使用组合式函数:

如果你正在使用选项式 API,组合式函数必须在 setup() 中调用。且其返回的绑定必须在 setup() 中返回,以便暴露给 this 及其模板:

import { useMouse } from './mouse.js'
import { useFetch } from './fetch.js'

export default {
  setup() {
    const { x, y } = useMouse()
    const { data, error } = useFetch('...')
    return { x, y, data, error }
  },
  mounted() {
    // setup() 暴露的属性可以在通过 `this` 访问到
    console.log(this.x)
  }
  // ...其他选项
}

3.2、自定义指令

3.3、插件

1)介绍:

一个插件可以是一个拥有 install() 方法的对象,也可以直接是一个安装函数本身。安装函数会接收到安装它的应用实例和传递给 app.use() 的额外选项作为参数:

const myPlugin = {
  install(app, options) {
    // 配置此应用
  }
}

插件发挥作用的常见场景主要包括以下几种:

  1. 通过 app.component()app.directive() 注册一到多个全局组件或自定义指令。

  2. 通过 app.provide() 使一个资源可被注入进整个应用。

  3. app.config.globalProperties 中添加一些全局实例属性或方法

  4. 一个可能上述三种都包含了的功能库 (例如 vue-router)。

  5. 对已经插件进行二次封装插件【主要方便代码管理】

    所有后,我们需要把一个插件相关的依赖和配置放在一个单独的文件中。main.js主要导入一个文件安装就可以了。这种方式通过插件来实现也是非常常用的。

    import ElementPlus from 'element-plus'
    import 'element-plus/dist/index.css'
    /* element其它相关的配置都可以在这里处理 */
    export default {
        install: (app) => {
            app.use(ElementPlus)
        }
    }
    

四、内置组件

4.1、Transition

1)<Transition> 组件
<button @click="show = !show">Toggle</button>
<Transition>
  <p v-if="show">hello</p>
</Transition>
.v-enter-active,
.v-leave-active {
  transition: opacity 0.5s ease;
}

.v-enter-from,
.v-leave-to {
  opacity: 0;
}

注意:<Transition> 仅支持单个元素或组件作为其插槽内容。如果内容是一个组件,这个组件必须仅有一个根元素。

2)基于 CSS 的过渡效果
  • CSS 过渡 class

  • 为过渡效果命名:我们可以给 <Transition> 组件传一个 name prop 来声明一个过渡效果名。

    <Transition name="fade">
      ...
    </Transition>
    
  • CSS 的 transition

  • CSS 的 animation

  • 自定义过渡 class

  • 同时使用 transition 和 animation

  • 深层级过渡与显式过渡时长

3)JavaScript 钩子
4)可复用过渡效果
5)出现时过渡
6)元素间过渡
7)过渡模式
8)组件间过渡
9)动态过渡
10)使用 Key Attribute 过渡

4.2、KeepAlive

1)基本使用:
<!-- 非活跃的组件将会被缓存! -->
<KeepAlive>
  <component :is="activeComponent" />
</KeepAlive>
2)包含/排除:

<KeepAlive> 默认会缓存内部的所有组件实例,但我们可以通过 includeexclude 属性来定制该行为。这两个 prop 的值都可以是一个以英文逗号分隔的字符串、一个正则表达式,或是包含这两种类型的一个数组:

<!-- 以英文逗号分隔的字符串 -->
<KeepAlive include="a,b">
  <component :is="view" />
</KeepAlive>

<!-- 正则表达式 (需使用 `v-bind`) -->
<KeepAlive :include="/a|b/">
  <component :is="view" />
</KeepAlive>

<!-- 数组 (需使用 `v-bind`) -->
<KeepAlive :include="['a', 'b']">
  <component :is="view" />
</KeepAlive>
3)最大缓存实例数:我们可以通过传入 max prop 来限制可被缓存的最大组件实例数。
4)缓存实例的生命周期:

请注意

  • onActivated 在组件挂载时也会调用,并且 onDeactivated 在组件卸载时也会调用。
  • 这两个钩子不仅适用于 <KeepAlive> 缓存的根组件,也适用于缓存树中的后代组件。
    【即后代组件中也会有这两个生命周期。但是注意,子组件是不会缓存状态的】

4.3、Teleport:常用于传送到body标签里

<Teleport> 是一个内置组件,它可以将一个组件内部的一部分模板“传送”到该组件的 DOM 结构外层的位置去。

<Teleport to="body">
  <div>Teleport里面的内容</div>
</Teleport>

使用场景:弹框组件上非常有用;还有一种是在组件内的组件。但父组件视图隐藏时,则它的子组件也会被动的隐藏掉。这种情况使用Teleport就非常好。(如果父组件完全销毁的话,子组件应该也会销毁吧)

五、应用规模化

5.1、单文件组件:查看第六章节

Vue 的单文件组件 (即 *.vue 文件,英文 Single-File Component,简称 SFC) 是一种特殊的文件格式,使我们能够将一个 Vue 组件的模板、逻辑与样式封装在单个文件中。

1)为什么要使用单文件组件

如果你的用例只需要给静态 HTML 添加一些简单的交互,你可以看看 petite-vue,它是一个 6 kB 左右、预优化过的 Vue 子集。

2)单文件组件是如何工作的
  • Vue 单文件组件是一个框架指定的文件格式,因此必须交由 @vue/compiler-sfc 编译为标准的 JavaScript 和 CSS,一个编译后的单文件组件是一个标准的 JavaScript(ES) 模块.
  • 单文件组件中的 <style> 标签一般会在开发时注入成原生的 <style> 标签以支持热更新,而生产环境下它们会被抽取、合并成单独的 CSS 文件。

5.2、工具链

1)项目脚手架
2)IDE支持:

推荐使用的 IDE 是 VS Code,配合 Vue - Official 扩展 (之前是 Volar)。

3)浏览器开发者插件
4)typescript
5)代码规范
6)格式化

5.3、路由:看另外文档

5.4、状态管理

5.5、服务端渲染

六、进阶主题

6.1、使用 Vue 的多种方式

6.2、渲染函数 & JSX

1)基本用法:
  • 创建Vnodes:
    Vue 提供了一个 h() 函数用于创建 vnodes。

  • 声明渲染函数:
    当组合式 API 与模板一起使用时,setup() 钩子的返回值是用于暴露数据给模板。然而当我们使用渲染函数时,可以直接把渲染函数返回:

    import { ref, h } from 'vue'
    
    export default {
      props: {
        /* ... */
      },
      setup(props) {
        const count = ref(1)
    
        // 返回渲染函数
        return () => h('div', props.msg + count.value)
      }
    }
    

    注意:请确保返回的是一个函数而不是一个值!

  • Vnodes 必须唯一

2)JSX / TSX: 【脚手架默认不支持JSX,需要额外安装依赖包的】

JSX 是 JavaScript 的一个类似 XML 的扩展,有了它,我们可以用以下的方式来书写代码:

const vnode = <div id={dynamicId}>hello, {userName}</div>

注意:在 JSX 表达式中,使用大括号来嵌入动态值


七、全局 API:---------------- 后面是API文档

八、组合式 API:

8.1、setup()

8.2、响应式:核心

1)ref()
2)computed()
3)reactive()
4)readonly()
5)watchEffect()
6)watchPostEffect()
7)watchSyncEffect()
8)watch()
9)onWatcherCleanup()[^3.5+]

8.3、响应式:工具

8.4、响应式:进阶

1)

7)markRaw():将一个对象标记为不可被转为代理。返回该对象本身。

const foo = markRaw({})
console.log(isReactive(reactive(foo))) // false

// 也适用于嵌套在其他响应性对象,即响应式对象,内部某个属性不可代理(即不是响应式的)。
const bar = reactive({ foo })
console.log(isReactive(bar.foo)) // false

一个响应式对象,里面有些属性值,我们不希望它是响应的。就可以用这个来处理。

8.5、生命周期钩子

8.6、依赖注入

8.7、辅助

九、内置内容:

十、指令:

十一、单文件组件:

11.1、语法定义

11.2、<script setup>

11.3、单文件组件 CSS 功能

1)组件作用域css
  • 子组件的根元素:

    使用 scoped 后,父组件的样式将不会渗透到子组件中。不过,子组件的根节点会同时被父组件的作用域样式和子组件的作用域样式影响。

  • 深度选择器:
    处于 scoped 样式中的选择器如果想要做更“深度”的选择,也即:影响到子组件,可以使用 :deep()

    <style scoped>
    .a :deep(.b) {
      /* ... */
    }
    </style>
    

    :deep可以前面没有选择器:

    <style scoped>
    :deep(.b) {
      /* ... */
    }
    </style>
    

    编译后:

    [data-v-f3f3eg9] .b {
      /* ... */
    }
    

    从这里可以看出,:deep开始处理的选择器还是保留了当前组件上的作用域,只是可以向内穿透了。
    底层分析:使用 :deep 后,就是把他里面的选择器不再添加上

  • 插槽选择器
    默认情况下,作用域样式不会影响到 <slot/> 渲染出来的内容,因为它们被认为是父组件所持有并传递进来的。使用 :slotted 伪类以明确地将插槽内容作为选择器的目标:

    <style scoped>
    :slotted(div) {
      color: red;
    }
    </style>
    
  • 全局选择器:
    如果想让其中一个样式规则应用到全局,比起另外创建一个 <style>,可以使用 :global 伪类来实现 (看下面的代码):

    <style scoped>
    :global(.red) {
      color: red;
    }
    </style>
    
  • 混合使用局部与全局样式:

    <style>
    /* 全局样式 */
    </style>
    
    <style scoped>
    /* 局部样式 */
    </style>
    

实践分析:使用scoped后,会在选择器的最后面添加上属性选择器。如果使用了深度选择器:deep,则把它前面的部分作为最后区域添加上属性选择器。

.box .a {}
.a :deep(.b){}
:deep(.b){}

编译后:

.box .a[data-v-f3f3eg9] {}
.a[data-v-f3f3eg9] .b {}
[data-v-f3f3eg9] .b {}
2)CSS Modules

一个 <style module> 标签会被编译为 CSS Modules 并且将生成的 CSS class 作为 $style 对象暴露给组件。(就是把css变成js对象,在模板像js一样使用)

3)CSS 中的 v-bind()

单文件组件,支持 css中使用组件状态的数据,通过v-bind函数引入。

<script setup>
import { ref } from 'vue'
const color = ref('red')
</script>

<style scoped>
p {
  color: v-bind('color'); /* 如果是对象值,可以v-bind('theme.color')获取 */
}
</style>

十二、进阶API

12.1、自定义元素:

12.2、渲染函数:

1)h()

创建虚拟 DOM 节点 (vnode)

  • 类型:

    // 完整参数签名
    function h(
      type: string | Component,
      props?: object | null,
      children?: Children | Slot | Slots
    ): VNode
    
    // 省略 props 时,可以设置为null,也可以没有这项。
    function h(type: string | Component, children?: Children | Slot): VNode
    
    • type: 第一个参数既可以是一个字符串 (用于原生元素) 也可以是一个 Vue 组件定义。

    • prop:可选。第二个参数是要传递的 prop

      • 普通prop:

      • 事件prop:事件prop,以onXxx开头等价于@xxx

        // 传递 prop
        h(Foo, {
          // 等价于 some-prop="hello"
          someProp: 'hello',
          // 等价于 @update="() => {}"
          onUpdate: () => {}
        })
        
    • children:第三个参数是子节点。如果有多个子节点,那children就是一个数组。

  • 详细信息:

    • 创建原生元素:

      import { h } from 'vue'
      
      // 除了 type 外,其他参数都是可选的
      h('div')
      h('div', { id: 'foo' })
      
      // attribute 和 property 都可以用于 prop
      // Vue 会自动选择正确的方式来分配它
      h('div', { class: 'bar', innerHTML: 'hello' })
      
      // class 与 style 可以像在模板中一样
      // 用数组或对象的形式书写
      h('div', { class: [foo, { bar }], style: { color: 'red' } })
      
      // 事件监听器应以 onXxx 的形式书写
      h('div', { onClick: () => {} })
      
      // children 可以是一个字符串
      h('div', { id: 'foo' }, 'hello')
      
      // 没有 prop 时可以省略不写
      h('div', 'hello')
      h('div', [h('span', 'hello')])
      
      // children 数组可以同时包含 vnode 和字符串
      h('div', ['hello', h('span', 'hello')])
      
    • 创建组件:

      import Foo from './Foo.vue'
      
      // 传递 prop
      h(Foo, {
        // 等价于 some-prop="hello"
        someProp: 'hello',
        // 等价于 @update="() => {}"
        onUpdate: () => {}
      })
      
      // 传递单个默认插槽
      h(Foo, () => 'default slot')
      
      // 传递具名插槽
      // 注意,需要使用 `null` 来避免
      // 插槽对象被当作是 prop
      h(MyComponent, null, {
        default: () => 'default slot',
        foo: () => h('div', 'foo'),
        bar: () => [h('span', 'one'), h('span', 'two')]
      })
      

      h()创建组件,传递自定义事件,还看到一种方式:放在on属性下:

      h(Foo, {
        // 等价于 @update="() => {}"
        on{
          update: () => {}
        }
      })
      
posted @ 2025-06-12 21:02  吴飞ff  阅读(57)  评论(0)    收藏  举报