写在开头,学了哪里就填坑哪里。空着代表未涉及,或与vue2一致不必再提及。
文章目录
VUE(3.x)
起步
创建一个vue应用:
import { createApp } from 'vue'
// 从一个单文件组件中导入根组件
import App from './App.vue'
const app = createApp(App)
注册一个组件:
app.component('TodoDeleteButton', TodoDeleteButton)
全局API
通用
- version
- nextTick()
- defineComponent()
- defineAsyncComponent()
- defineCustomElement()
组合式API
注意:不是函数式编程,与React Hooks比较
setup()
- 基本使用
- 访问Props
- Setup上下文
- 与渲染函数一起使用
核心
- ref():创建任何值类型的响应式ref。
import { ref } from 'vue'
// 推导得到的类型:{ count: number }
const count = ref(0)
console.log(count) // { value: 0 }
console.log(count.value) // 0
count.value++
console.log(count.value) // 1
// 显式标注类型(TS)
// 方式1:调用ref()时传入泛型参数
const year = ref<string | number>('2020')
// 方式2:通过Ref这个类型
import type { Ref } from 'vue'
const year: Ref<string | number> = ref('2020')
将其属性赋值或解构至本地变量时,或是将该属性传入一个函数时,不会失去响应性
const objectRef = ref({ count: 0 })
// 这是响应式的替换
objectRef.value = { count: 1 }
// 该函数接收一个 ref
// 需要通过 .value 取值
// 但它会保持响应性
callSomeFunction(obj.foo)
// 仍然是响应式的
const { foo, bar } = obj
ref在模板和响应式对象中会自动解包,不需要使用value
,但当ref自身作为响应式数组或像Map
这种原生集合类型被访问时,不会进行解包。
// 自动解包
const count = ref(0)
const state = reactive({
count
})
console.log(state.count) // 0
state.count = 1
console.log(count.value) // 1
const otherCount = ref(2)
state.count = otherCount
console.log(state.count) // 2
// 原始 ref 现在已经和 state.count 失去联系
console.log(count.value) // 1
// 不会进行解包
const books = reactive([ref('Vue 3 Guide')])
// 这里需要 .value
console.log(books[0].value)
- reactive():创建一个响应式对象或数组(JavaScript Proxy),且Vue能跟踪其属性的访问和更改操作(对原始类型(String、number等)无效)
import { reactive } from 'vue'
// 推导得到的类型:{ count: number }
const state = reactive({ count: 0 })
// 显式标注类型(TS)
interface Book {
title: string
year?: number
}
const book: Book = reactive({ title: 'Vue 3 指引' })
注意:reactive
返回的是一个原始对象的Proxy,它和原始对象不相等:
const raw = {}
const proxy = reactive(raw)
console.log(proxy === raw) // false
响应式对象内的嵌套对象依然是代理:
const proxyObj = reactive({})
proxyObj.nested = raw
console.log(proxyObj.nested === raw) // false
将其属性赋值或解构至本地变量时,或是将该属性传入一个函数时,我们会失去响应性
// count 和 state.count 失去了响应性连接
let { count } = state
// 不会影响原始的 state
count++
// 该函数接收一个普通数字,并且
// 将无法跟踪 state.count 的变化
callSomeFunction(state.count)
- computed():接收一个getter函数,返回值为一个计算属性ref。可以通过.value访问计算结果。
import { reactive, computed } from 'vue'
const author = reactive({
name: 'John Doe',
books: [
'Vue 2 - Advanced Guide',
'Vue 3 - Basic Guide',
'Vue 4 - The Mystery'
]
})
// 推导得到的类型:ComputedRef<string>
const publishedBooksMessage = computed(() => {
return author.books.length > 0 ? 'Yes' : 'No'
})
// 显式标注类型(TS)
const double = computed<number>(() => {
// 若返回值不是 number 类型则会报错
})
尝试修改计算属性会收到一个运行时警告,但可以通过同时提供getter和setter来创建“可写”的属性。
const fullName = computed({
// getter
get() {
return firstName.value + ' ' + lastName.value
},
// setter
set(newValue) {
// 注意:我们这里使用的是解构赋值语法
[firstName.value, lastName.value] = newValue.split(' ')
}
})
注意,计算函数不应有副作用,例如,不要在计算函数中做异步请求或者更改DOM。后续会讨论如何使用监听器根据其他响应式状态的变更来创建副作用。
-
readonly()
-
watchEffect():是懒执行的:仅当数据源变化才会执行回调。它会立即执行一遍回调函数。
watchEffect
仅会在其同步执行期间,才追踪依赖。在使用异步回调时,只有在第一个 await 正常工作前访问到的属性才会被追踪。
默认情况下,用户创建的侦听器回调都会在VUE组件更新之前被调用。这意味着在侦听器中访问的DOM将是vue更新前的状态。如果需要访问更新后的DOM,需要指明flush: 'post'
选项——即watchPostEffect()
。
<script setup>
import { watchEffect } from 'vue'
// 它会自动停止
watchEffect(() => {})
// ...这个则不会!
setTimeout(() => {
watchEffect(() => {})
}, 100)
// 手动停止侦听器
const unwatch = watchEffect(() => {})
// ...当该侦听器不再需要时
unwatch()
</script>
-
watchPostEffect():在VUE更新后执行。
-
watchSyncEffect()
-
watch():声明性的计算衍生值。然而有时候我们需要在状态变化时执行一些“副函数”:例如更改DOM,或异步处理。(懒执行:仅当数据源变化才回调)
import { ref, watch } from 'vue'
const question = ref('')
const answer = ref('Questions usually contain a question mark. ;-)')
// 可以直接侦听一个 ref
watch(question, async (newQuestion, oldQuestion) => {
if (newQuestion.indexOf('?') > -1) {
answer.value = 'Thinking...'
try {
const res = await fetch('https://yesno.wtf/api')
answer.value = (await res.json()).answer
} catch (error) {
answer.value = 'Error! Could not reach the API. ' + error
}
}
})
watch
的第一个参数可以是:一个ref、一个响应式对象、一个getter函数、或多个数据源组成的数组。
const x = ref(0)
const y = ref(0)
// 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() 得到的参数是一个 number
watch(obj.count, (count) => {
console.log(`count is: ${count}`)
})
// 需要提供一个getter函数
watch(
() => obj.count,
(count) => {
// 仅当count改变时才会触发
console.log(`count is: ${count}`)
}
)
// 直接传入会隐式创建一个深层侦听器
const obj = reactive({ count: 0 })
watch(obj, (newValue, oldValue) => {
// 在嵌套的属性变更时触发
// 注意:`newValue` 此处和 `oldValue` 是相等的
// 因为它们是同一个对象!
})
生命周期钩子
- onMounted():组建完成初始渲染并创建DOM节点后运行
- onUpdated()
- onUnmounted()
- onBeforeMount()
- onBeforeUpdate()
- onBeforeUnmount()
- onErrorCaptured()
- onRenderTracked()
- onRenderTriggered()
- onActivated()
- onDeactivated()
- onServerPrefetch()
依赖注入
- provide()
- inject()
工具
- isRef()
- unref()
- toRef()
- toRefs()
- isProxy()
- isReactive()
- isReadonly()
进阶
- shallowRef()
- triggerRef()
- customRef()
- shallowReactive():
reactive
的浅层作用形式。只有根级别的属性式响应式。 - shallowReadonly()
- toRaw()
- markRaw()
- effectScope()
- getCurrentScope()
- onScopeDispose()
选项式API
状态
- expose
- emits
渲染
- template
- render
- compilerOptions
生命周期
- beforeUnmount
- unmounted
- renderTriggered
- renderTracked
- serverPrefetch
组合
- mixins
- extends
其他
- components
- inheritAttrs:
如果使用了script setup
,你需要额外的<script>
块来书写这个选项声明。
<script>
// 使用普通的 <script> 来声明选项
export default {
inheritAttrs: false
}
</script>
<script setup>
// ...setup 部分逻辑
</script>
最常见需要禁用的场景时attribute需要应用在根节点以外的其他元素上。
这些透传进来的attribute可以在模板的表达式中直接用$attrs
访问到。
<span>Fallthrough attribute: {{ $attrs }}</span>
这个$attrs
对象包含了除组件声明的props
和emits
之外的所有其他attribute,例如class,style,v-on监听器等。
- directives
组件实例
- $data
- $props
- $el
- $options
- $parent
- $root
- $slots
- $refs:相对于普通的js变量,我们不得不用相对繁琐的
.value
来获取ref的值。然而通过编译时转换可以省去麻烦。
<script setup>
let count = $ref(0)
function increment() {
// 无需 .value
count++
}
</script>
- $attrs:一个包含了组件所有透传attributes的对象。
它可以配合inheritAttrs:false
使用:
<MyButton class="large" />
<div class="btn-wrapper">
<button class="btn" v-bind="$attrs">click me</button>
</div>
<script>
// 使用普通的 <script> 来声明选项
export default {
inheritAttrs: false
}
</script>
上面的例子中,所有像class
和v-on
监听器这样的透传attribute都应用在内部的<button>
上而不是外层的<div>
上。
Tips:没有参数的v-bind
会将一个对象的所有属性都作为attribute应用到目标元素上。
和单根节点组件不同,有着多个根节点的组件没有自动attribute透传行为。如果$attrs
没有被显式绑定,将会抛出一个运行时警告。
<CustomLayout id="custom-layout" @click="changeValue" />
<header>...</header>
<main>...</main>
<footer>...</footer>
由于Vue不知道要将attribute透传到哪里,所以会抛出一个警告。如果$attrs
被显示绑定,则不会有警告。
<header>...</header>
<main v-bind="$attrs">...</main>
<footer>...</footer>
- $watch()
- $emit()
<button @click="$emit('someEvent')">click me</button>
- $forceUpdate()
- $nextTick()
内置内容
指令
- v-memo
- v-model:在组件上时的表现。
在组件上,v-model
会被展开为如下的形式:
<CustomInput
:modelValue="searchText"
@update:modelValue="newValue => searchText = newValue"
/>
因此,子组件需要:
<!-- CustomInput.vue -->
<script setup>
defineProps(['modelValue'])
defineEmits(['update:modelValue'])
</script>
<template>
<input
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)"
/>
</template>
现在v-model
可以在这个组件上正常工作了。
<CustomInput v-model="searchText" />
另一种在组件内实现v-model
的方式时使用一个可写的,同时据有getter和setter的计算属性。
<!-- CustomInput.vue -->
<script setup>
import { computed } from 'vue'
const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])
const value = computed({
get() {
return props.modelValue
},
set(value) {
emit('update:modelValue', value)
}
})
</script>
<template>
<input v-model="value" />
</template>
v-model
默认情况下都是使用modelValue
作为prop,并以update:modelValue
作为对应的事件。我们可以通过给v-model
指定一个参数来更改这些名字。
<MyComponent v-model:title="bookTitle" />
<!-- MyComponent.vue -->
<script setup>
defineProps(['title'])
defineEmits(['update:title'])
</script>
<template>
<input
type="text"
:value="title"
@input="$emit('update:title', $event.target.value)"
/>
</template>
利用上面这个技巧,我们可以在一个组件上创建多个v-model
双向绑定,每一个v-model
都会同步不同的prop,这里就不赘述了。
我们都知道v-model
有一些内置的修饰符,如.trim
,.number
等。
接下来我们来创建一个自定义的修饰符capitalize
,它会自动将v-model
绑定输入的字符串值第一个字母转为大写:
<MyComponent v-model.capitalize="myText" />
组件的v-model
上添加的修饰符,可以通过modelModifiers
prop在组件内访问到。
<script setup>
const props = defineProps({
modelValue: String,
modelModifiers: { default: () => ({}) }
})
defineEmits(['update:modelValue'])
console.log(props.modelModifiers) // { capitalize: true }
function emitValue(e) {
let value = e.target.value
if (props.modelModifiers.capitalize) {
value = value.charAt(0).toUpperCase() + value.slice(1)
}
emit('update:modelValue', value)
}
</script>
<template>
<input
type="text"
:value="modelValue"
@input="emitValue"
/>
</template>
对于有参数又有修饰符的,生成的prop名将是arg + "Modifiers"
。举个例子:
<MyComponent v-model:title.capitalize="myText">
const props = defineProps(['title', 'titleModifiers'])
defineEmits(['update:title'])
console.log(props.titleModifiers) // { capitalize: true }
组件
- teleport
- suspense
特殊元素
<component>
有些场景需要在两个组件间来回切换,比如Tab页面
<!-- currentTab 改变时组件也改变 -->
<component :is="tabs[currentTab]"></component>
被传给:is
的值可以是:被注册的组件名、导入的组件对象。
当使用<component :is="...">
来在多个组件间切换时,被切换掉的组件会被卸载。我们可以用<KeepAlive>
组件强制被切换掉的组件保持“存活”。
<slot>
特殊Attributes
- key
- ref:获取对DOM元素或子组件实例的直接引用。注意,只能在组件挂载后才能访问模板引用。
<script setup>
import { ref, onMounted } from 'vue'
// 声明一个 ref 来存放该元素的引用
// 必须和模板里的 ref 同名
const input = ref(null)
</script>
<template>
<input ref="input" />
</template>
在v-for
中使用模板引用时,对应的ref包含的是一个数组(并不保证与数组相同的顺序)。
<script setup>
import { ref, onMounted } from 'vue'
const list = ref([
/* ... */
])
const itemRefs = ref([])
</script>
<template>
<ul>
<li v-for="item in list" ref="itemRefs">
{{ item }}
</li>
</ul>
</template>
ref
还可以绑定为一个函数,会在每次组件更新时都被调用,注意这里需要使用动态的:ref
才能传入函数。当元素被卸载时,函数也会被调用一次。
<input :ref="(el) => { /* 将 el 赋值给一个数据属性或 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
完全一致,也就是父组件对子组件的每个属性和方法都有完全的访问权。因此,应该只在绝对需要时才使用组件引用,大多数情况,应首先使用props和emit。
使用了<script setup>
的组件是默认私有的:父组件无法访问到一个使用了 <script setup>
的子组件中的任何东西,除非子组件在其中通过defineExpose
宏显式暴露:
<script setup>
import { ref } from 'vue'
const a = 1
const b = ref(2)
defineExpose({
a,
b
})
</script>
当父组件通过模板引用获取到该组件实例时,得到实例为{ a: number, b: number }
(ref都会自动解包)。
- is
前面已经说过可以使用is
来动态切换<component>
。
但某些HTML元素对放在其中的元素类型有限制,例如<ul>
、<ol>
、<table>
和<select>
,相应的,某些元素仅放在特定元素中才会显示,例如<li>
、<tr>
和<option>
。
这将导致在使用带有此类限制元素的组件时出现问题。例如:
<table>
<blog-post-row></blog-post-row>
</table>
自定义组件<blog-post-row>
将作为无效内容被忽略,因而在最终呈现的输出中造成错误。我们可以使用is
作为一种解决方案:
<table>
<tr is="vue:blog-post-row"></tr>
</table>
当使用原生HTML元素时,
is
的值必须加上前缀vue:
才可以被解析为一个Vue组件,为了避免和原生的自定义内置元素相混淆。
单文件组件
语法定义
- 总览
- 相应语言块
- 自动名称推导
- 预处理器
- Src导入
- 注释
<script setup>
- 基本语法
- 响应式
- 使用组件
- 使用自定义指令
- defineProps():仅
<script setup>
中可用的编译宏命令,并不需要显式导入。defineProps
会返回一个对象,其中包含所有props
。
const props = defineProps(['title'])
console.log(props.title)
// 或者使用对象的形式
defineProps({
title: String,
propB: [String, Number],
// 自定义类型校验函数
propC: {
type: String,
required: true, // 必选
default: 'success',
validator(value) {
// The value must match one of these strings
return ['success', 'warning', 'danger'].includes(value)
}
},
// 对象类型的默认值
propE: {
type: Object,
// 对象或数组的默认值
// 必须从一个工厂函数返回。
// 该函数接收组件所接收到的原始 prop 作为参数。
default(rawProps) {
return { message: 'hello' }
}
},
// 函数类型的默认值
propG: {
type: Function,
// 不像对象或数组的默认,这不是一个工厂函数。这会是一个用来作为默认值的函数
default() {
return 'Default function'
}
}
})
// 类型标注(TS)
<script setup lang="ts">
defineProps<{
title?: string
likes?: number
}>()
// 使用类型声明时的默认props值(TS)
interface Props {
msg?: string
labels?: string[]
}
const props = withDefaults(defineProps<Props>(), {
msg: 'hello',
labels: () => ['one', 'two']
})
</script>
另外,type
也可是自定义的类或构造函数,Vue将会通过instanceof
来检查类型是否匹配。例如:
class Person {
constructor(firstName, lastName) {
this.firstName = firstName
this.lastName = ?lastName
}
}
可以将其作为一个prop的类型:
defineProps({
author: Person
})
如果没有使用<script setup>
,props必须以props
选项的方式声明。
export default {
props: ['title'],
setup(props) {
console.log(props.title)
}
}
- defineEmits():声明需要抛出的事件,它返回一个等同于
$emit
方法的emit
函数。
<script setup>
const emit = defineEmits(['enlarge-text'])
emit('enlarge-text')
</script>
如果没有使用<script setup>
,可以通过emits
选项定义组件会抛出的事件,并从setup()
函数的第二个参数,即setup上下文对象上访问到emit
函数。
export default {
emits: ['enlarge-text'],
setup(props, ctx) {
ctx.emit('enlarge-text')
}
}
这个emits
选项还支持对象语法,它允许我们对触发事件的参数进行验证:
<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>
搭配TS使用,也可以使用纯类型标注来声明触发的事件:
<script setup lang="ts">
const emit = defineEmits<{
(e: 'change', id: number): void
(e: 'update', value: string): void
}>()
</script>
- defineExpose
- useSlots()
- useAttrs():可以在JavaScript中访问透传Attributes。
<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)
}
}
虽然这里的attrs
对象总是反映为最新的透传attribute,但它并不是响应式的。你不能通过侦听器去监听它的变化。如果你需要响应性,可以使用prop。或者使用onUpdated()
使得在每次更新时结合最新的attrs
执行副作用。
- 与普通的
<script>
一起使用 - 顶层await
- 针对TypeScript的功能
- 限制
CSS功能
- 组件作用域CSS
- CSS Modules
- CSS中的 v-bind()