第一篇章
动态绑定多个值
如果有想这样的一个包含多个 attribute 的JavaScript 对象:
const obj = {id:'container', class:"wrap"}
可以通过不带参数的 v-bind 将这些 attribute 绑定到单个元素上:
<!-- 不带参数名的绑定 -->
<div v-bind="obj"></div>
动态参数
<a v-bind:[attributeName]="url"></a>
<!-- 简写 -->
<a :[attributeName]="url"></a>
<a v-on:[eventName]="doSomething"></a>
<!-- 简写 -->
<a @[eventName]="doSomething"></a>
- 动态参数值的限制:
- 动态参数中表达式的值应当是一个字符串,或者 null
- null 意味着显示移除该绑定,其他非字符串的值会发出警告
- 动态参数语法的限制:
- 在HTML attribute 名称中不能使用 空格和引号,不合法
<!-- 这会触发编译器警告 -->
<a :['foo' + bar]="value"></a>
- 模板中避免在名称中使用大写字母(浏览器会强制转换为小写)
<!-- someAttr 会转为 someattr , 这将与组件中的 someAttr 属性对应不上 -->
<a :[someAttr]="value"></a>
响应式基础 reactive() 的局限性
-
仅对对象类型有效(对象、数组、Map、Set)
-
必须始终保持对该响应式对象的相同引用(Vue 的响应式系统是通过属性访问进行追踪的):
- 不可随意“替换”一个响应式对象。
let state = reactive({count: 0})
// 上面的引用 ({count: 0}) 将不再被跟踪(响应性链接已丢失!)
state = reactive({count: 1})
- 将响应式对象的属性赋值或解构至本地变量时,或者是将该属性作为一个函数参数传入时,会失去响应性。
const state = reactive({count: 0})
// 1. 将响应式对象的属性赋值,失去响应性链接
let n = state.count
n++ // 不影响原始的 state
// 2. count 和 state.count 失去了响应性链接
let {count} = state
count++ // 不影响原始的 state
// 3. 该函数接收一个普通数字,且将无法跟踪 state.count 的变化
say(state.count)
用 ref() 定义响应式变量
- 一个包含对象类型值的 ref 可以响应式地替换整个对象:
const objRef = ref({count: 0})
objRef.value = {count: 1}
- ref 被传递给函数或者是从一般对象上解构,不会丢失响应性。
ref 在响应式对象中的解包
- 当一个 ref 被嵌套在一个响应式对象中,作为属性被访问或更改时,它会自动解包
const count = ref(0)
const state = reactive({
count
})
console.log(state.count) // 0
state.count = 1
console.log(state.count) // 1
- 如果将一个新的 ref 赋值给一个关联了已有 ref 的属性,那么它会替换就的 ref
const otherCount = ref(2)
state.count = otherCount
console.log(state.count) // 2
// 原始 ref 现在已经和 state.count 失去联系
console.log(count.value) // 1
数组和集合类型的 ref 解包
- 跟响应式对象不同,当 ref 作为响应式数组或像 Map 这种原生集合类型的元素被访问时,不会进行解包
const books = reactive([ref('Vue3')])
console.log(books[0].value)
v-if 和 v-show
-
v-if v-else v-else-if 可以在
<template>
上使用 -
v-show 会在DOM 渲染中保留改元素,不支持在
<template>
元素上使用
v-if 和 v-for
- 当 v-if 和 v-for 同时存在于同一元素上,v-if 会先被执行
这就意味着 v-if 的条件中无法使用 v-for 作用域内定义的变量别名 item
<!-- 会抛出错误,因为属性 item 此时没有在该实例上定义 -->
<li v-for="item in todoList" v-if="!item.done">{{ item.name }}</li>
v-for
-
v-for 也可用 of 作为分隔符来替代 in
-
v-for 的变量别名 item 可以使用解构
-
可以在
<template>
上使用 v-for
在内联事件处理器中访问事件参数
- 传入一个特殊的变量 $event
<button @click="write('OK', $event)">点击</button>
- 使用内联箭头函数
<button @click="(event) => write('OK', event)">>点击</button>
注册周期钩子
- onMounted() 也可以在一个外部函数中调用,只要调用栈是同步的,且最终起源自 setup() 就可以
watch 惰性执行的:仅当数据源发生变化时才会执行回调
- watch 的第一个参数可以是不同形式的数据源:一个 ref(包括计算属性)、一个响应式对象、一个 getter 函数、或者多个数据源组成的数组。
const x = ref(0)
const y = ref(0)
// 单个 ref
watch(x, (value) => console.log(value))
// getter 函数
watch(() =>x.value + y.value, (sum) => console.log(sum))
// 多个来源组成的数组
watch([x, () => y.value], ([newX, newY]) => console.log(newX, newY))
- 注意,不能直接侦听响应式对象的属性值,需要用到 getter 函数:
const obj = reactive({count: 0})
// 这是错的,因为 watch() 得到的参数是一个 number
// watch(obj.count, (value) => console.log(value))
// 使用 getter 函数进行监听
watch(() => obj.count, (value) => console.log(value))
- 直接给 watch() 传入一个响应式对象,会隐式开启深度监听
/* 在 watch() 直接传入一个响应式对象 */
const obj = reactive({count: 0})
// 在 obj.count 发生变化时触发(隐式开启深度监听),newVal 和 oldVal 是相等的(是同个对象)
watch(obj, (newVal, oldVal) => {})
obj.count++
/* 在 watch() 中传入一个 返回响应式对象的 getter 函数 */
const state = reactive({
obj: {count: 0}
})
// 相比,一个返回响应式对象的 getter 函数,只有在返回不同的对象时,才会触发回调(没有开始深度监听)
watch(() => state.obj, () => {})
// 开启深度监听,newVal 和 oldVal 是相等的
watch(() => state.obj, (newVal, oldVal) => {}, {deep: true})
watchEffect()
-
会立即执行一遍回调函数
-
仅会在同步执行期间追踪所有依赖。在使用异步回调时,只在第一个 await 正常工作前访问到的属性才会被追踪。
watchEffect(async () => {
const response = await fetch(url.value)
data.value = await response.join()
})
以上代码中,watchEffect() 仅追踪到 url.value
ref
- v-for 中的模板引用:对应的 ref 中包含的值是一个数组,它将在元素被挂载后包含对应整个列表的所有元素
注意:ref 数组并不保证与源数组相同的顺序。
- 组件上的 ref
组件
- DOM 模板解析注意事项(涉及直接在 [.html文件中] DOM 中编写模板的情况)
-
HTML 标签和属性名是不区分大小写的,需要转换为短横线形式
-
一定要显式闭合标签
-
有些 HTML 元素必须在特定的元素中才会显示,例如
<tr>
需要<table>
标签下才显示,这时候在<table>
标签下显示组件,就需要用到 is
<table>
<tr is="vue:组件名"></tr>
</table>
当使用在原生 HTML 元素上时,is 的值必须加上前缀 vue: 才可以被解析为一个 Vue 组件
- 当使用
<component :is="...">
来在多个组件间作切换时,被切换掉的组件会被卸载。
- 可以通过
<KeepAlive>
组件强制被切换掉的组件仍然保持“存活”的状态。
- 全局注册:可以在该应用的任意模板中直接使用,无需再引入
import { createApp } from 'vue'
const app = createApp({})
// 第一个参数是组件名称,第二个参数是组件的实现(如果是单文件组件,则是被导入的.vue文件)
app.component('componentName', {...})
// 可以链式调用
app.component('componentName', {...}).component('componentName', {...})
props
- 在使用
<script setup>
的单文件组件中,props 可以使用 defineProps() 宏来声明:
<script setup>
const props = defineProps(['foo'])
console.log(props.foo)
</script>
-
解构 defineProps 的返回值,得到的变量不是响应式的,也不会更新
-
在没用使用
<script setup>
的组件中,setup(props) 接收 props 第一个参数
事件
事件声明是可选的,建议声明出来
- 在
<script setup>
不能使用 $emit,但 defineEmits() 会返回一个相同作用的函数可供使用
<script setup>
const emit = defineEmits(['submit'])
function buttonClick() {
emit('submit')
}
</script>
-
defineEmits() 宏不能在子函数中使用,必须直接放置在
<script setup>
的顶级作用域下 -
如果是显式使用了 setup() ,则事件通过 emits 选项来定义,emit 函数也被暴露在 setup() 的上下文对象上(可以被解构出来的)
export default {
emits: ['submit'],
setup(props, { emit }) {
emit('submit')
}
}
这个 emits 选项还支持对象语法,允许我们对触发事件的参数进行验证(事件校验):
<script setup>
const emit = defineEmits({
// 没有校验
click: null,
// 校验 submit 事件
submit: ({ email, password }) => {
if(email && password) return true
console.log('校验 submit 事件')
return false
}
})
const submitForm = (email, password) => {
emit('submit', { email, password })
}
</script>
-
如果 emits 中声明的自定义的事件名与原生事件名一致(如 click),监听器只会监听组件触发的 click 事件
-
和原生 DOM 事件不同,组件触发的事件没有冒泡机制
组件配合 v-model 使用
- 在组件上使用 v-model (如
<MyComponent v-model="keyword">
)会被展开成这种形式:
<MyComponent :modelValue="keyword" @update:modelValue="newValue => keyword = newValue">
要让<MyComponent v-model="keyword">
实际工作起来,
- 方法一:在组件
<MyComponent>
中接收 modelValue 且抛出事件 update:modelValue
<script setup>
defineProps(['modelValue'])
defineEmits(['update:modelValue'])
</script>
<template>
<input
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)"
/>
</template>
- 方法二:在组件
<MyComponent>
中使用可写的,同时具有 getter 和 setter 的计算属性
<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 的自定义修饰符
自定义修饰符 capitalize 将 v-model 绑定输入的值转为大写 <MyComponent v-model.capitalize="myText" />
组件的 v-model 上的修饰符 capitalize 可以通过
modelModifiers
prop 在组件中访问到
在组件中接收 modelModifiers , 如果 v-model 有使用 capitalize 修饰符,modelModifiers 会返回 { capitalize: true }
<script setup>
const props = defineProps({
modelValue: String,
modelModifiers: { default: () => ({}) }
})
const emit = defineEmits(['update:modelValue'])
function emitValue(e) {
let value = e.target.value
if (props.modelModifiers.capitalize) value = value.toUpperCase()
emit('update:modelValue', value)
}
</script>
<template>
<input type="text" :value="modelValue" @input="emitValue" />
</template>
如果是 v-model:title 这种带参数的,生成的 prop 名字是 arg + 'Modifiers'
透传 attributes
指的是传递给一个组件,却没有被该组件声明为 props 或 emits 的 attribute 或者 v-on 事件监听器。
-
Attributes 继承 常见的:style class id
-
v-on 监听器继承
-
深层组件继承
- 声明过的 props 和侦听函数不再透传到下一层
- 禁用 Attributes 继承
- inheritAttrs: false
- 常见场景: attribute 需要应用在根节点以外的其他元素上,可以完全控制透传进来的 attribute 被如何使用
如果使用的是
<script setup>
则需要额外的<script>
来书写这个声明
<script>
export default {
inheritAttrs: false
}
</script>
- 透传进来的 attribute 可以在模板的表达式中直接用 $attrs 访问到
- 这个 $attrs 对象包含了除组件所声明的 props 和 emits 之外的所有其他 attribute,例如 class,style,v-on 监听器等等
- 透传 attributes 写法保持原样,如 foo-bar 这样子的 attribute 则需要通过 $attrs['foo-bar'] 来访问
- 像 @click 这种 v-on 事件监听器的,在 此对象下被暴露为一个函数 $attrs.onClick
- 没有参数的 v-bind 会将一个对象的所有属性都作为 attribute 应用到目标元素上 v-bind="$attrs"
- 单根节点 & 多根节点
- 单根节点:当一个组件以单个元素为根作渲染时,透传的 attribute 会自动被添加到根元素上
- 多根节点:和单根节点组件有所不同,有着多个根节点的组件没有自动 attribute 透传行为,需要显式绑定 $attrs
- 在 js 中访问透传 attribute:
- 使用 useAttrs()
- setup() 上下文对象的一个属性 attrs (非响应式的,不能监听到变化)
插槽
- 动态插槽名:参数使用[]
<template v-slot:[slotName]> </template>
<!-- 简写 -->
<template #[slotName]> </template>
- 作用域插槽
-
父组件模板中的表达式只能访问父组件的作用域;子组件模板中的表达式只能访问子组件的作用域。
-
插槽的内容需要同时使用到父组件域和子组件域中的数据,可以像组件传递 props 一样,向插槽的出口上传递 attributes
<slot :text="text"></slot>
<!-- 可以传入整个对象的所有属性 -->
<slot v-bind="item"></slot>
接收插槽 props 时,默认插槽通过子组件标签上的 v-slot 指令,直接接收到一个插槽 props 对象
<MyComponent v-slot="slotProps">
{{ slotProps.text }}
</MyComponent>
<!-- slotProps 也可以直接解构 -->
<MyComponent v-slot="{ text }">
{{ text }}
</MyComponent>
-
如果混用了具名插槽与默认插槽,则需要为默认插槽使用显式的
<template>
标签 -
无渲染组件:包含了逻辑而不需要自己渲染内容的组件,视图输出是通过作用域插槽交给消费者组件了。
依赖注入
- provide(提供)为组件后代提供数据
<script setup>
import { provide } from "vue"
provide(/* 注入名 */ 'message', /* 值 */ 'hello!')
</script>
如果不使用
<script setup>
,provide 和 inject 需要确保在 setup() 中是同步调用的:
setup() {
provide(/* 注入名 */ 'message', /* 值 */ 'hello!')
}
-
一个组件可以多次调用 provide(),使用不同的注入名,注入不同的依赖值。
-
可以在应用级提供依赖,所有组件中都可以注入:
app.provide('key', 'value')
- inject(注入)注入上层组件提供的数据
const message = inject('message')
-
如果提供的是一个 ref,注入进来的也会是该 ref 对象,而不会自动解包(保持响应性链接)
-
注入默认值,第二个参数声明默认值
const value = inject('message', '这是默认值')
-
建议尽可能将任何对响应式状态的变更都保持在供给方组件中,如果需要在注入方组件中更改数据,可以在提供方组件中国声明一个可以更改数据的方法
-
使用 readonly() 来包装提供(provide)的值,可以确保提供的数据不能被注入方的组件更改
第二篇章
组合式函数
自定义命令
-
命名规范:以 v 开头的驼峰式命名变量,在模板中使用短横线方式
-
在没有使用
<script setup>
的情况下,自定义指令需要通过 directives 选项注册 -
全局注册:app.directive('focus', {...})
-
当在组件上使用自定义指令时,它会始终应用于组件的根节点,不推荐在组件上使用自定义指令。
-
多根组件时,和 attribute 不同,自定义指令不能通过 v-bind="$attrs" 来传递给一个不同的元素。
插件
- 安装插件:app.use(myPlugin, { /* 可选的选项 */ })
** 第三篇章 **
SFC 单文件组件
项目脚手架
- Vite 轻量级、速度极快
安装:npm init vue@latest
路由
文档:Vue Router