初始化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():注册全局组件
app.directive():注册全局指令
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 使用方式的一种替代
定义
调用
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 属性才会自动解包
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
响应式原理
副作用、订阅者
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 完全是出于支持旧版本浏览器的限制\
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+)
// 父组件
<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
祖先组件provide变量及其set方法:
- provide的值可以是响应式变量,inject方可以对它响应式修改,但不建议直接修改,应provide对应的set方法进行修改
- 如果不允许修改,provide时应用readonly包装
- provide的key应该是唯一的,可以使用Symbol()生成
<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>,这意味着父组件对子组件的每一个属性和方法都有完全的访问权。大多数情况下,你应该首先使用标准的 props 和 emit 接口来实现父子组件交互。
如果子组件使用了 <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>
组件注册
全局组件
注册:
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
})
生命周期
<script setup>
import { onMounted } from 'vue'
onMounted(() => {
console.log(`the component is now mounted.`)
})
</script>
内置组件
KeepAlive
<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', {
/* ... */
})
简化写法(不写钩子)
钩子
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.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
pinia(推荐)
路由
Class 与 Style 绑定
条件渲染
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