vue3笔记

177 阅读17分钟

初始化vue项目

vue create <项目名>

报错处理

未识别到vue命令:npm install -g @vue/cli
执行npm install报错/安装卡住:npm config set registry https://registry.npmmirror.com

应用实例API(app.xxx)

官方文档:cn.vuejs.org/api/applica…

app.mount()

将应用实例挂载在一个容器元素中

import { createApp } from 'vue'
const app = createApp(/* ... */)

app.mount('#app')

app.component():注册全局组件

cn.vuejs.org/guide/compo…

app.directive():注册全局指令

cn.vuejs.org/guide/reusa…

app.use():安装插件

createApp(App).use(store).use(router).mount('#app')

app.provide():全局注入key-value

注册:

import { createApp } from 'vue'
const app = createApp(/* ... */)
app.provide('message', 'hello')

使用:

import { inject } from 'vue'
export default {
  setup() {
    console.log(inject('message')) // 'hello'
  }
}

app.version:获取当前应用所使用的 Vue 版本号

使用场景:根据不同的 Vue 版本执行不同的逻辑

// 法一
export default {
  install(app) {
    const version = Number(app.version.split('.')[0])
    if (version < 3) {
      console.warn('This plugin requires Vue 3')
    }
  }
}

// 法二
import { version } from 'vue'
console.log(version)

app.config.errorHandler:为未捕获错误指定全局处理函数

// 三个参数:错误对象、触发该错误的组件实例和一个指出错误来源类型信息的字符串
app.config.errorHandler = (err, instance, info) => {
  // 处理错误,例如:报告给一个服务
}

app.config.globalProperties:定义全变量/方法

Vue 2 中 Vue.prototype 使用方式的一种替代

定义

image.png

调用

image.png

Attribute绑定

同名简写(v3.4+)

<!-- 与 :id="id" 相同 -->
<div :id=""></div>

<!-- 这也同样有效 -->

<div v-bind:id></div>

布尔型Attribute

<button :disabled="isButtonDisabled">Button</button>

当 isButtonDisabled 为真值或一个空字符串 (即 <button disabled="">) 时,元素会包含这个 disabled attribute

动态绑定多个值

<div v-bind="objectOfAttrs"></div>

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

    ## 动态参数
    动态参数中表达式的值应当是一个字符串,或者是 `null`。特殊值 `null` 意为显式移除该绑定  
    如果你需要传入一个复杂的动态参数,我们推荐使用**计算属性**替换复杂的表达式
    ```html
    <a :[attributeName]="url"> ... </a>
    <a @[eventName]="doSomething"> ... </a>

# 响应式基础

<https://cn.vuejs.org/guide/essentials/reactivity-fundamentals.html>

## ref

取值要`.value`;当在模板中使用时,ref会自动解包,不用`.value`

```ts
import { ref } from 'vue'
 
// 基本数据类型
const count = ref(0)
count.value = 1
console.log(count.value) // 1
 
// 数组
const list = ref([])
list.value = [...]

为ref标注类型

const year = ref<string | number>('2020')

// 推导得到的类型:Ref<number | undefined>
const n = ref<number>()

api

shadowRef:退出深层响应式,只监控.value层变化
const state = shallowRef({ count: 1 })

// 不会触发更改
state.value.count = 2

// 会触发更改
state.value = { count: 2 }
unref

如果参数是 ref,则返回内部值,否则返回参数本身。这是 val = isRef(val) ? val.value : val 计算的一个语法糖

【toValue】:unref的升级,能处理getter

如果参数是 ref,它会返回 ref 的值;如果参数是函数,它会调用函数并返回其返回值。否则,它会原样返回参数。它的工作方式类似于 unref(),但对函数有特殊处理

import type { MaybeRefOrGetter } from 'vue'

function useFeature(id: MaybeRefOrGetter<number>) {
  watch(() => toValue(id), id => {
    // 处理 id 变更
  })
}

// 这个组合式函数支持以下的任意形式:
useFeature(1)
useFeature(ref(1))
useFeature(() => 1)
toRef
const state = reactive({
  foo: 1,
  bar: 2
})

// 双向 ref,会与源属性同步
const fooRef = toRef(state, 'foo')

// 更改该 ref 会更新源属性
fooRef.value++
console.log(state.foo) // 2

// 更改源属性也会更新该 ref
state.foo++
console.log(fooRef.value) // 3

toRef() 这个函数在你想把一个 prop 的 ref 传递给一个组合式函数时会很有用(关于禁止对 props 做出更改的限制依然有效):

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

const props = defineProps(/* ... */)

// 将 `props.foo` 转换为 ref,然后传入
// 一个组合式函数
useSomeFeature(toRef(props, 'foo'))

// getter 语法——推荐在 3.3+ 版本使用
useSomeFeature(toRef(() => props.foo))
</script>
【toRefs】

将一个响应式对象转换为一个普通对象,这个普通对象的每个属性都是指向源对象相应属性的 ref。每个单独的 ref 都是使用toRef()创建的

const state = reactive({
  foo: 1,
  bar: 2
})

const stateAsRefs = toRefs(state)
/*
stateAsRefs 的类型:{
  foo: Ref<number>,
  bar: Ref<number>
}
*/

// 这个 ref 和源属性已经“链接上了”
state.foo++
console.log(stateAsRefs.foo.value) // 2

stateAsRefs.foo.value++
console.log(state.foo) // 3

当从组合式函数中返回响应式对象时,toRefs 相当有用。使用它,消费者组件可以解构/展开返回的对象而不会失去响应性:

function useFeatureX() {
  const state = reactive({
    foo: 1,
    bar: 2
  })

  // ...基于状态的操作逻辑

  // 在返回时都转为 ref
  return toRefs(state)
}

// 可以解构而不会失去响应性
const { foo, bar } = useFeatureX()

只有顶级的 ref 属性才会自动解包

image.png

reactive

数组不要直接用reactive包,因为我们修改数组一般都是把新的数组整体赋值给变量,这样会失去响应式。
解决方法:1)用.push()修改数组;2)把数组包在对象里声明;3)直接用ref([])定义数组,.value重新赋值

import { reactive } from 'vue'
 
// 对象
let person = reactive({
   name:"小满"
})
person.name = "大满"
 
// 数组
let list = reactive({
   data: []
})

ref 作为reactive对象的属性:

const count = ref(0)
const state = reactive({
  count
})

console.log(state.count) // 0

state.count = 1
console.log(count.value) // 1

reactive可能存在的问题

1、reactive整体重新赋值会失去响应式

即使重新赋值的数据外面包了reactive,仍然会失去响应式:

// 错误写法
let obj = reactive({});
obj = reactive({ id: 123 });

// 法一:对象通过Object.assign赋值
let obj = reactive({}); // 重新赋值,所以要用let
obj = Object.assign(obj, {id: 123});

// 法二:数组通过响应式api修改(会改变原数组)
// push()
// pop()
// shift()
// unshift()
// splice()
// sort()
// reverse()

// 法三:ref定义
const obj = ref({});
obj.value = { id: 123 };

filter()concat() 和 slice(),这些都不会更改原数组,而是返回一个新数组

2、用ref/reactive定义对象(数组)类型数据时,它们和原始数据是双向影响的
// 数组同理
const raw = { count: 1 };
const obj = ref(raw);
obj.value.count++;
console.log(obj.value.count, raw.count); // 2 2
raw.count++;
console.log(obj.value.count, raw.count); // 3 3

解决方法:深拷贝原始值 ref(JSON.parse(JSON.stringify(raw)))

const raw = { count: 1 };
const obj = ref(JSON.parse(JSON.stringify(raw)));
obj.value.count++;
console.log(obj.value.count, raw.count); // 2 1
raw.count++;
console.log(obj.value.count, raw.count); // 2 2
3、对解构操作不友好

解决方法:使用ref

const state = reactive({ count: 0 })
 
// 当解构时,count 已经与 state.count 断开连接
let { count } = state
// 不会影响原始的 state
count++
 
// 该函数接收到的是一个普通的数字
// 并且无法追踪 state.count 的变化
// 我们必须传入整个对象以保持响应性
callSomeFunction(state.count)

为reactive标注类型

import { reactive } from 'vue'

interface Book {
  title: string
  year?: number
}

const book: Book = reactive({ title: 'Vue 3 指引' })

shadowReactive:退出深层响应式

const state = shallowReactive({
  foo: 1,
  nested: {
    bar: 2
  }
})

// 更改状态自身的属性是响应式的
state.foo++

// ...但下层嵌套对象不会被转为响应式
isReactive(state.nested) // false

// 不是响应式的
state.nested.bar++

代理对象 vs 原始对象

reactive() 返回的是一个原始对象的Proxy(代理对象),它和原始对象是不相等的。我们应该操作的是代理对象。
对同一个原始对象调用 reactive() 会总是返回同样的代理对象,而对一个已存在的代理对象调用 reactive() 会返回其本身

const raw = { count: 1 } // 原始对象
const proxy = reactive(raw) // proxy代理对象,具有响应式
 
// 代理对象和原始对象是不同的
console.log(proxy === raw) // false
 
// 在同一个对象上调用 reactive() 会返回相同的代理
console.log(reactive(raw) === proxy) // true
 
// 在一个代理上调用 reactive() 会返回它自己
console.log(reactive(proxy) === proxy) // true

响应式原理

cn.vuejs.org/guide/extra…

副作用、订阅者

let A2

function update() {
  A2 = A0 + A1
}

update() 函数会产生一个副作用
update() 第一次调用之后成为 A0 和 A1 的订阅者。当给 A0 赋新值后,会通知其所有订阅了的副作用重新执行

Vue 中的响应性是如何工作的

我们可以追踪对象属性的读写,在 JavaScript 中有两种劫持方式:Object.defineProperty()的get/set 和 Proxies

vue2响应式原理

Vue2 使用 Object.defineProperty()的get/set 完全是出于支持旧版本浏览器的限制\ image.png

vue3响应式原理

ref通过Object.defineProperty()的get/set实现响应式
reactive通过 Proxy拦截对象属性变化(增删改查),通过Reflect操作源对象
注:ref也可定义对象(数组)类型数据,它内部自动通过reactive转为Proxy对象

function reactive(obj) {
  return new Proxy(obj, {
    get(obj, prop) {
      return Reflect.get(obj, prop)
    },
    set(obj, prop, value) {
      return Reflect.set(obj, prop, value)
    },
    deleteProperty(obj, prop) {
      return Reflect.deleteProperty(obj, prop)
    },
  })
}

DOM 更新时机

DOM 更新不是同步的,Vue 会在“next tick”更新周期中缓冲所有状态的修改

import { nextTick } from 'vue'
 
//  写法1
function increment() {
  state.count++
  nextTick(() => {
    // 访问更新后的 DOM
  })
}
 
// 写法2
async function increment() {
  count.value++
  await nextTick()
  // 现在 DOM 已经更新了
}

计算属性

计算属性vs方法:计算属性有缓存,只要响应式依赖的值不变,计算属性就不会重复执行
计算属性默认是只读的,要修改则需要自己写get、set

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

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

const fullName = computed({
  get() {
    // 不要改变其他状态、在 getter 中做异步请求或者更改 DOM
    return firstName.value + ' ' + lastName.value
  },
  set(newValue) {
    [firstName.value, lastName.value] = newValue.split(' ')
  }
})
</script>

ts标注类型

import { ref, computed } from 'vue'
 
const count = ref(0)
 
// 1. 隐式推导得到的类型:ComputedRef<number>
const double = computed(() => count.value * 2)
 
// 2. 泛型参数显式指定类型
const double = computed<number>(() => {
  // 若返回值不是 number 类型则会报错
})

watch

const x = ref(0)
const y = ref(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}`)
  }
)

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

 
// 监听响应式对象
const obj = reactive({count: 0})
watch(obj, (newValue, oldValue) => {
  // 强制开启深度监视,obj内部嵌套的属性变化就会触发(即使显示写deep:false也无效)
  // newValue和oldValue是相等的,因为它们是同一个对象
}, {immediate: true, deep: false}) // deep配置不生效
 
// 监听响应式对象的某个属性
watch(() => state.someObject, (newValue, oldValue) => {
  // 仅当state.someObject被替换时触发,因为不是深度监听,newValue和oldValue是不同的
})
watch(() => state.someObject, (newValue, oldValue) => {
  // 深度监听,newValue和oldValue是相等的,除非state.someObject被整个替换了
}, {deep: true})
 
// 监听响应式对象的多个属性
watch([() => person.job, () => person.name], (newValue, oldValue) => {
  // ...
})

watchEffect

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

上例回调会立即执行,不需要指定 immediate: true。在执行期间,它会自动追踪 todoId.value 作为依赖(和计算属性类似)。每当 todoId.value 变化时,回调会再次执行

一次性侦听器

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

onWatcherCleanup副作用清理(3.5+)

有时我们可能会在侦听器中执行副作用,例如异步请求:

watch(id, (newId) => {
  fetch(`/api/${newId}`).then(() => {
    // 回调逻辑
  })
})

如果在请求完成之前 id 发生了变化怎么办。理想情况下,我们希望能够在 id 变为新值时取消过时的请求
我们可以使用 onWatcherCleanup()来注册一个清理函数,当侦听器失效并准备重新运行时会被调用:

import { watch, onWatcherCleanup } from 'vue'

watch(id, (newId) => {
  const controller = new AbortController()

  fetch(`/api/${newId}`, { signal: controller.signal }).then(() => {
    // 回调逻辑
  })

  onWatcherCleanup(() => {
    // 终止过期请求
    controller.abort()
  })
})

回调触发时机

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

// 法一:指明 flush: 'post'
watch(source, callback, {
  flush: 'post'
})
watchEffect(callback, {
  flush: 'post'
})

// 法二:
import { watchPostEffect } from 'vue'
watchPostEffect(() => {
  /* 在 Vue 更新后执行 */
})

父传子props

所有 prop 默认都是可选的,除非声明了 required: true
数组default:() => []
对象default:() => ({})
除 Boolean 外的未传递的可选 prop 将会有一个默认值 undefined
Boolean 类型的未传递 prop 将被转换为 false。这可以通过为它设置 default 来更改

<script setup lang="ts">
  // 写法1:字符串数组
  const props = defineProps(['title', 'likes']); // 必须赋给一个变量
  console.log(props.title);
 
 
  // 写法2:对象
  const props = defineProps({
    title: String,
    likes: Number
  })
 
  
  // 写法3:各种props校验
  const props = defineProps({
    // 基础类型检查(给出 `null` 和 `undefined` 值则会跳过任何类型检查)
    propA: Number,
 
    // 多种可能的类型
    propB: [String, Number],
 
    // 必传
    propC: {
      type: [String, Number],
      required: true // 所有 prop 默认都是可选的,除非声明了 required: true
    },
 
    // Array 类型的默认值
    propD: {
      type: Array,
      // 对象或数组的默认值必须从一个工厂函数返回
      default: () => []
    },
    // 对象类型的默认值
    propE: {
      type: Object,
      default: () => ({})
    },
 
    // 自定义类型校验函数
    propF: {
      validator(value) {
        // The value must match one of these strings
        return ['success', 'warning', 'danger'].includes(value)
      }
    },
 
    // 函数类型的默认值
    propG: {
      type: Function,
      // 不像对象或数组的默认,这不是一个工厂函数。这会是一个用来作为默认值的函数
      default() {
        return 'Default function'
      }
    }
  })
</script>

ts

interface Props {
  msg?: string
  labels?: string[]
}

const props = withDefaults(defineProps<Props>(), {
  msg: 'hello',
  labels: () => ['one', 'two']
})

// 3.5+解构写法(不需要withDefaults)
const { msg = 'hello', labels = ['one', 'two'] } = defineProps<Props>()

props解构(3.5+)

const { foo } = defineProps(['foo'])

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

props单向绑定:子组件不应该修改props值

1、prop作为子组件变量的初始值

const props = defineProps(['initialCounter', 'initialObj'])
 
// 错误写法
// props.initialCounter++
// const obj = ref(props.initialObj) // 还是会相互影响
 
// 正确写法:
const counter = ref(props.initialCounter) // 简单数据类型
const obj = ref(JSON.parse(JSON.stringify(props.initialObj))) // 对象或数组

2、需要对传入的 prop 值做进一步转换(计算属性)

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

3、父通过props传给子,子通过emit进行修改(或用defineModel)

defineModel双向数据绑定(3.4+)

cn.vuejs.org/api/sfc-scr…

// 父组件
<Child v-model="data" v-model:title="title">
 
 
// 子组件 -----------------------------------
const data = defineModel();
const title = defineModel('title');
 
// 配置项(不要写default,会产生问题)
const title = defineModel('title', { type: 'String', required: true });

console.log(data.value, title.value)

v-model修饰符

<UserName
  v-model:first-name.capitalize="first"
  v-model:last-name.uppercase="last"
/>
<script setup>
const [firstName, firstNameModifiers] = defineModel('firstName', {
  // 可以给 defineModel() 传入 get 和 set 这两个选项
  set(value) {
    if (modifiers.capitalize) {
      return value.charAt(0).toUpperCase() + value.slice(1)
    }
    return value
  }
})
const [lastName, lastNameModifiers] = defineModel('lastName')

console.log(firstNameModifiers) // { capitalize: true }
console.log(lastNameModifiers) // { uppercase: true }
</script>

子传父emits

// 子组件
<template>
  <div @click="clickThis">点我</div>
  <div @click="$emit('click')">直接调</button>
</template>
 
<script setup lang="ts">
  // 写法1:ts
  const emit= defineEmits<{
    (e: 'click', n1: number, n2: number): void
  }>()
 
  // 写法2:非ts
  const emit= defineEmits(['click'])
  const emit = defineEmits({
    click: (n1: number, n2: number) => {
      // 返回 `true` 或 `false`,表明验证通过或失败
    }
  })
 
  const clickThis = () => {
    emit('click', 1, 2) // 可以传多个参数
  }
</script>

父传所有后代 provide、inject

cn.vuejs.org/guide/compo… image.png

祖先组件provide变量及其set方法:

  • provide的值可以是响应式变量,inject方可以对它响应式修改,但不建议直接修改,应provide对应的set方法进行修改
  • 如果不允许修改,provide时应用readonly包装
  • provide的key应该是唯一的,可以使用Symbol()生成
image.png 后代组件inject变量,并可使用祖先组件的set方法进行修改(**不要直接修改**): image.png 如果想确保提供的数据不被注入方更改,可以使用 `readonly()` 来包装:
<script setup>
import { ref, provide, readonly } from 'vue'

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

注入默认值

// 如果没有祖先组件提供 "message"
// `value` 会是 "这是默认值"
const value = inject('message', '这是默认值')

app.provide

在main.js里app.provide定义全局变量,在任意vue组件里获取其值(js/ts文件不行,因为inject必须在setup里使用

import { createApp } from 'vue'
const app = createApp({})
app.provide(/* 注入名 */ 'message', /* 值 */ 'hello!')

透传$attrs:一般透传class、style、id、v-on

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

// 父组件
<Child class="fatherClass" @click="onClick" />
 
// 子组件
<template>
  <div class="childClass"></div>
</template>
 
// 最终效果
<div class="childClass fatherClass" @click="onClick">

自定义透传位置

// 子组件
// 1、禁用透传继承
defineOptions({
  inheritAttrs: false
})
 
// 2、通过v-bind="$attrs"控制透传位置
<div class="btn-wrapper">
  ...
  <button class="btn" v-bind="$attrs">Click Me</button>
  ...
</div>

在 JavaScript 中访问透传 Attributes

<script setup>
import { useAttrs } from 'vue'
 
const attrs = useAttrs()
</script>

插槽

具名插槽

vue2写法是:<template slot="header"> vue3写法是:<template v-slot:header>或简写为<template #header>

// 子组件
<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>
    <h1>Here might be a page title</h1>
  </template>
 
  <template #default>
    <p>A paragraph for the main content.</p>
  </template>
 
  <template #footer>
    <p>Here's some contact info</p>
  </template>
 
  <!-- 动态插槽名(了解) -->
  <template #[dynamicSlotName]>
    ...
  </template>
</BaseLayout>

作用域插槽

vue2写法是:<template slot-scope="slotProps">

// 子组件
<div>
  <slot :text="greetingMessage" :count="1"></slot>
</div>
 
 
// 父组件
<MyComponent v-slot="slotProps">
  {{ slotProps.text }} {{ slotProps.count }}
</MyComponent>
<MyComponent #default="slotProps">
  {{ slotProps.text }} {{ slotProps.count }}
</MyComponent>

具名作用域插槽

v-slot:header="slotProps" 可简写为 #header="slotProps"

// 子组件
<slot name="header" message="hello"></slot>
 
 
// 父组件
<MyComponent>
  <template #header="{ message }">
    {{ message }}
  </template>
 
  <template #default="defaultProps">
    {{ defaultProps }}
  </template>
 
  <template #footer="footerProps">
    {{ footerProps }}
  </template>
</MyComponent>

条件插槽(插槽结合v-if):根据插槽是否存在来渲染某些内容

当 header、footer 或 default 插槽存在时,提供额外的样式:

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

模版引用ref

cn.vuejs.org/guide/essen…
3.5+:

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

const childRef = useTemplateRef('child')

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

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

3.5版本前:

<template>
  // 1. 在子组件上写ref
  <Child ref="child" />
</template>
 
<script setup>
  import { ref, onMounted } from 'vue'
  import Child from './Child.vue'
 
  // 2. setup里定义同名的ref变量
  const child = ref(null)
 
  onMounted(() => {
    // child.value 是 <Child /> 组件的实例
    // child.value?.x 获取子组件的变量
    // child.value?.fn() 调用子组件的方法
  })
</script>

如果子组件没有使用<script setup>,这意味着父组件对子组件的每一个属性和方法都有完全的访问权。大多数情况下,你应该首先使用标准的 propsemit 接口来实现父子组件交互。

如果子组件使用了 <script setup>,那么子组件是默认私有的,子组件需要通过 defineExpose 显式暴露:

// 使用setup语法糖的子组件,需要defineExpose显式暴露给父组件
<script setup>
  import { ref } from 'vue'
 
  const data = ref(1)
  const getData = () => {
    return data
  }
  const setData = (val) => {
    data.value = val
  }
 
  // defineExpose不需要导入
  defineExpose({
    getData,
    setData
  })
</script>

组件注册

cn.vuejs.org/guide/compo…

全局组件

注册:

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

调用:

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

缺点

1、全局组件如果没有被实际使用,它仍然会出现在打包后的 JS 文件中
2、组件之间的依赖关系不明确

局部组件

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

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

<template>
  <ComponentA />
</template>

异步组件

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

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

<template>
  <AdminPage />
</template>

全局注册:

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

加载与错误状态

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

  // 加载异步组件时使用的组件
  loadingComponent: LoadingComponent,
  // 展示加载组件前的延迟时间,默认为 200ms
  // 在网络状况较好时,加载完成得很快,加载组件和最终组件之间的替换太快可能产生闪烁
  delay: 200,

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

生命周期

image.png
<script setup>
import { onMounted } from 'vue'

onMounted(() => {
  console.log(`the component is now mounted.`)
})
</script>

内置组件

KeepAlive

cn.vuejs.org/guide/built…

<KeepAlive>
  <component :is="activeComponent" />
</KeepAlive>

include、exclude

include 和 exclude会根据组件的 name 选项进行匹配
在 3.2.34 或以上的版本中,使用 <script setup> 的单文件组件会自动根据文件名生成对应的 name 选项,无需再手动声明

<!-- 以英文逗号分隔的字符串 -->
<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>

最大缓存实例数

如果缓存的实例数量即将超过指定的那个最大数量,则最久没有被访问的缓存实例将被销毁,以便为新的实例腾出空间

<KeepAlive :max="10">
  <component :is="activeComponent" />
</KeepAlive>

缓存实例的生命周期

  • onActivated 在组件挂载时也会调用,并且 onDeactivated 在组件卸载时也会调用。
  • 这两个钩子不仅适用于 <KeepAlive> 缓存的根组件,也适用于缓存树中的后代组件。
<script setup>
import { onActivated, onDeactivated } from 'vue'

onActivated(() => {
  // 调用时机为首次挂载
  // 以及每次从缓存中被重新插入时
})

onDeactivated(() => {
  // 在从 DOM 上移除、进入缓存
  // 以及组件卸载时调用
})
</script>

Teleport

<Teleport> 接收一个 to prop 来指定传送的目标。to 的值可以是一个 CSS 选择器字符串,也可以是一个 DOM 元素对象

<button @click="open = true">Open Modal</button>

<Teleport to="body">
  <div v-if="open" class="modal">
    <p>Hello from the modal!</p>
    <button @click="open = false">Close</button>
  </div>
</Teleport>

多个 <Teleport> 组件可以将其内容挂载在同一个目标元素上,而顺序就是简单的顺次追加

<Teleport to="#modals">
  <div>A</div>
</Teleport>
<Teleport to="#modals">
  <div>B</div>
</Teleport>


<div id="modals">
  <div>A</div>
  <div>B</div>
</div>

禁用Teleport

<Teleport :disabled="isMobile">
  ...
</Teleport>

defer(3.5+)

<Teleport defer to="#late-div">...</Teleport>

<!-- 稍后出现于模板中的某处 -->
<div id="late-div"></div>

hooks逻辑复用

一般在/hooks下新建useXXX.js
每一个调用 useXXX() 的组件实例会创建其独有的 状态拷贝,因此他们不会互相影响。

hooks返回值一般为ref,而不是reactive对象,因为ref返回值解构后仍然具备响应式。如果一定要返回reactive对象,在接收时需要在外层包reactive()

输入参数

如果输入参数是 ref 或 getter 而非原始值,且需要创建响应式 effect:
法一:watchEffect()+toValue()(注意 toValue(url) 是在 watchEffect 回调函数的内部调用的。这确保了在 toValue() 规范化期间访问的任何响应式依赖项都会被侦听器跟踪)
法二:watch() 显式地监视 ref 或 getter

返回值

推荐返回一个包含多个 ref 的普通的非响应式对象,这样该对象在组件中被解构为 ref 之后仍可以保持响应性:

// x 和 y 是两个 ref
const { x, y } = useMouse()

如果你更希望以对象属性的形式来使用组合式函数中返回的状态,你可以将返回的对象用 reactive() 包装一次,这样其中的 ref 会被自动解包:

const mouse = reactive(useMouse())
// mouse.x 链接到了原来的 x ref
console.log(mouse.x)

示例

鼠标跟踪器示例(无输入参数)

// src/hooks/useMouse.js
import { ref, onMounted, onUnmounted } from 'vue'
 
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))
 
  // 通过返回值暴露所管理的状态(ref)
  return { x, y }
}
 
 
// hook函数在组件中的使用--------------------------------------------
<script setup>
import { useMouse } from '@/hooks/useMouse.js'
 
const { x, y } = useMouse() // x,y是响应式的
</script>
 
<template>Mouse position is at: {{ x }}, {{ y }}</template>

异步状态示例(有输入参数)

watchEffect()+toValue() 实现 URL 改变时重新 fetch:

// fetch.js
import { ref, watchEffect, toValue } from 'vue'

export function useFetch(url) {
  const data = ref(null)
  const error = ref(null)

  const fetchData = () => {
    // reset state before fetching..
    data.value = null
    error.value = null

    fetch(toValue(url))
      .then((res) => res.json())
      .then((json) => (data.value = json))
      .catch((err) => (error.value = err))
  }

  watchEffect(() => {
    fetchData()
  })

  return { data, error }
}

自定义指令

cn.vuejs.org/guide/reusa…
在 <script setup> 中,任何以 v 开头的驼峰式命名的变量都可以当作自定义指令使用
只有当所需功能只能通过直接的 DOM 操作来实现时,才应该使用自定义指令
不推荐在组件上使用自定义指令。当组件具有多个根节点时可能会出现预期外的行为

示例

常规写法

<script setup>
// 在模板中启用 v-highlight
const vHighlight = {
  mounted: (el) => {
    el.classList.add('is-highlight')
  }
}
</script>

<template>
  <p v-highlight>This sentence is important!</p>
</template>

全局:

const app = createApp({})

// 使 v-highlight 在所有组件中都可用
app.directive('highlight', {
  /* ... */
})

简化写法(不写钩子)

image.png

image.png

钩子

const myDirective = {
  // 在绑定元素的 attribute 前
  // 或事件监听器应用前调用
  created(el, binding, vnode) {
    // 下面会介绍各个参数的细节
  },
  // 在元素被插入到 DOM 前调用
  beforeMount(el, binding, vnode) {},
  // 在绑定元素的父组件
  // 及他自己的所有子节点都挂载完成后调用
  mounted(el, binding, vnode) {},
  // 绑定元素的父组件更新前调用
  beforeUpdate(el, binding, vnode, prevVnode) {},
  // 在绑定元素的父组件
  // 及他自己的所有子节点都更新后调用
  updated(el, binding, vnode, prevVnode) {},
  // 绑定元素的父组件卸载前调用
  beforeUnmount(el, binding, vnode) {},
  // 绑定元素的父组件卸载后调用
  unmounted(el, binding, vnode) {}
}

钩子参数

指令的钩子会传递以下几种参数:

  • el:指令绑定到的元素。这可以用于直接操作 DOM。

  • binding:一个对象,包含以下属性。

    • value:传递给指令的值。例如在 v-my-directive="1 + 1" 中,值是 2
    • oldValue:之前的值,仅在 beforeUpdate 和 updated 中可用。无论值是否更改,它都可用。
    • arg:传递给指令的参数 (如果有的话)。例如在 v-my-directive:foo 中,参数是 "foo"
    • modifiers:一个包含修饰符的对象 (如果有的话)。例如在 v-my-directive.foo.bar 中,修饰符对象是 { foo: true, bar: true }
    • instance:使用该指令的组件实例。
    • dir:指令的定义对象。
  • vnode:代表绑定元素的底层 VNode。

  • prevVnode:代表之前的渲染中指令所绑定元素的 VNode。仅在 beforeUpdate 和 updated 钩子中可用。

插件

插件实现(以国际化插件为例):

// plugins/i18n.js
export default {
  install: (app, options) => {
    // 注入一个全局可用的 $translate() 方法
    app.config.globalProperties.$translate = (key) => {
      // 获取 `options` 对象的深层属性
      // 使用 `key` 作为索引
      return key.split('.').reduce((o, i) => {
        if (o) return o[i]
      }, options)
    }
  }
}

注册插件:

import i18nPlugin from './plugins/i18n'

app.use(i18nPlugin, {
  greetings: {
    hello: 'Bonjour!'
  }
})

使用插件:

<h1>{{ $translate('greetings.hello') }}</h1>

全局事件总线mitt(vue2中eventBus.on/on/emit/$off已被移除)

1、安装:npm install mitt
2、新建src/utils/eventBus.js文件

import mitt from 'mitt';
const bus = mitt();
export default bus;

3、触发事件bus.emit(如果是多个参数,必须打包成一个对象传)

// One.vue
<template>
  <button @click="handleClick">发送消息</button>
</template>
 
<script setup lang="ts">
import bus from '../utils/eventBus';
const handleClick = () => {
  bus.emit('sendMsg', { // 多个参数必须写成一个对象传
    id: 123,
    name: 'zhangsan'
  });
}
</script>

4、接收事件bus.on

// Two.vue
<script setup lang="ts">
import bus from '../utils/eventBus';
import {onMounted} from 'vue';
 
onMounted(() => {
  bus.on('sendMsg', (msg) => {
    console.log(msg.id, msg.name); // 123 hello
  })
})
</script>

状态管理

vuex

vuex.vuejs.org/zh/

pinia(推荐)

pinia.vuejs.org/zh/introduc…

路由

router.vuejs.org/zh/guide/

Class 与 Style 绑定

cn.vuejs.org/guide/essen…

条件渲染

v-if可以在<template> 元素上使用,但v-show不行
v-if条件区块内的事件监听器和子组件会被销毁与重建
当 v-if 和 v-for 同时存在于一个元素上的时候,v-if 会首先被执行(不推荐一起使用)

v-for

可以在 <template> 标签上使用 v-for 来渲染一个包含多个元素的块

解构

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

<!-- 有 index 索引时 -->
<li v-for="({ message }, index) in items">
  {{ message }} {{ index }}
</li>

在 v-for 里使用范围值

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

n 的初值是从 1 开始而非 0

v-for 与 v-if

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

<!-- 错误写法 -->
<li v-for="todo in todos" v-if="!todo.isComplete">
  {{ todo.name }}
</li>

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

组件上使用 v-for

<MyComponent
  v-for="(item, index) in items"
  :item="item"
  :index="index"
  :key="item.id"
/>

v-for不会自动将任何数据传递给组件,因为组件有自己独立的作用域。为了将迭代后的数据传递到组件中,我们还需要传递 props