一、计算属性
在模板中使用表达式只能进行简单的操作,假如在模板中也有复杂的逻辑代码就会变得臃肿、不易阅读且难以维护,所以可以使用vue计算属性简化,简单来说就是我把一个响应式的数据直接写在模板中就会很复杂,所以可以把它写在一个函数中,然后返回一个响应式的变量,然后在模板中使用这个变量就会简单。下面是一个计算属性的例子:
<script setup>
import { reactive, computed } from 'vue'
const author = reactive({
name: 'John Doe',
books: [
'Vue 2 - Advanced Guide',
'Vue 3 - Basic Guide',
'Vue 4 - The Mystery'
]
})
// 一个计算属性 ref
const publishedBooksMessage = computed(() => {
return author.books.length > 0 ? 'Yes' : 'No'
})
</script>
<template>
<p>Has published books:</p>
<span>{{ publishedBooksMessage }}</span>
</template>
1.1、只读计算属性和可写计算属性
上边的代码中定义了一个计算属性publishedBooksMessage ,通过computedApi实现,其期望我们传入一个getter函数返回一个响应式的ref对象,可以通过.value得到getter函数的返回值。同时他还可以接收get和set函数,创建可写的ref对象。通过typescript我们可以知道他的结构:
// 只读
function computed<T>(
getter: () => T,
// 查看下方的 "计算属性调试" 链接
debuggerOptions?: DebuggerOptions
): Readonly<Ref<Readonly<T>>>
// 可写的
function computed<T>(
options: {
get: () => T
set: (value: T) => void
},
debuggerOptions?: DebuggerOptions
): Ref<T>
第一个函数返回Readonly<Ref<Readonly<T>>>只读的ref对象,第二个可以给computed函数传入get和set函数返回可写的ref对象。第三个参数为debuggerOptions是可选项,返回一个只读的ref对象就如上边例子所示传入一个get函数即可
// 返回一个可写的computed的响应式对象。
const count = ref(1)
cosnt refComputed = conputed({
get:() => count.val + 1
set:(val) => {
count.val = val - 1
}
})
1.2、计算属性调试
computed()函数可以传入第二个参数类型为DebuggerOptions,这是一个包含onTrack和onTrigger两个回调函数的对象,一个回调在访问属性值的时候触发一个再更改value值的时候触发,代码如下所示:
const plusOne = computed(() => count.value + 1, {
onTrack(e) {
// 当 count.value 被追踪为依赖时触发
debugger
},
onTrigger(e) {
// 当 count.value 被更改时触发
debugger
}
})
// 访问 plusOne,会触发 onTrack
console.log(plusOne.value)
// 更改 count.value,应该会触发 onTrigger
count.value++
调试仅仅再开发模式下生效
1.3、计算属性标注类型
其实通过上边的代码我们就可以知道如何给计算属性标注类型了,像代中写的computed<T>()我们使用的时候显式的传入一个泛型即可,get函数的返回值为我们定义的泛型类型T,同时computed还会自动的推导返回类型。
// 推导类型的例子
import { ref, computed } from 'vue'
const count = ref(0)
// 推导得到的类型:ComputedRef<number>
const double = computed(() => count.value * 2)
// => TS Error: Property 'split' does not exist on type 'number'
const result = double.value.split('')
通过泛型参数显式的指定类型:
const double = computed<number>(() => {
// 若返回值不是 number 类型则会报错
})
计算属性中使用reverse()和sort()的时候小心,因为这些方法会改变原数组,调用这些方法之前需要创建一个原来数组的备份。
二、事件处理
记录一下vue中对事件处理的一些特性。
2.1、事件监听
事件监听方面可以使用v-on指令,指令的简写为@监听原生的DOM事件,事件触发的时候执行定义的js语法。事件处理器可以分为两类:
- 内联事件处理器:事件被触发执行的
JacvaScript语句,常见的如鼠标事件click - 方法事件处理器:对于组件上定义的方法的属性名或者是路径。
当需要访问原生的DOM事件的时候可以向方法处理器传入一个$event参数,还有一个方法是使用内联箭头函数,不常用就不记录了。
- 内联事件处理器举例
const count = ref(0)
// js代码写在方法体内部
<button @click="count++">Add 1</button>
<p>Count is: {{ count }}</p>
- 方法事件处理器
const name = ref('Vue.js')
function greet(event) {
alert(`Hello ${name.value}!`)
// `event` 是 DOM 原生事件
if (event) {
alert(event.target.tagName)
}
}
<!-- `greet` 是上面定义过的方法名 -->
<button @click="greet">Greet</button>
当click事件的逻辑比较复杂的时候就需要单独写一个函数进行处理,这就是方法事件处理器。
2.2、事件修饰符
Vue为事件提供了时间修饰符使用.表示指令的后缀,主要的事件修饰包含以下这些:
- .stop 单击事件停止传播
- .prevent 提交时间不再重新加载页面
- .self event.target 是元素本身时才会触发事件处理器
- .capture 捕获添加修饰符的元素,先触发添加修饰符的事件。
- .once 绑定once的监听器会触发一次,第一次触发后该监听器被remove
- .passivepassive 表示listener函数不会调用preventDefault()与prevent相对应。
关于capture的介绍:
- 冒泡是从里往外冒,捕获是从外往里捕。
- 当捕获存在时,先从外到里的捕获,剩下的从里到外的冒泡输出。
<!-- 单击事件将停止传递 -->
<a @click.stop="doThis"></a>
<!-- 提交事件将不再重新加载页面 -->
<form @submit.prevent="onSubmit"></form>
<!-- 修饰语可以使用链式书写 -->
<a @click.stop.prevent="doThat"></a>
<!-- 也可以只有修饰符 -->
<form @submit.prevent></form>
<!-- 仅当 event.target 是元素本身时才会触发事件处理器 -->
<!-- 例如:事件处理器不来自子元素 -->
<div @click.self="doThat">...</div>
这些修饰符与addenentLinstener相对应:
<!-- 添加事件监听器时,使用 `capture` 捕获模式 -->
<!-- 例如:指向内部元素的事件,在被内部元素处理前,先被外部处理 -->
<div @click.capture="doThis">...</div>
<!-- 点击事件最多被触发一次 -->
<a @click.once="doThis"></a>
<!-- 滚动事件的默认行为 (scrolling) 将立即发生而非等待 `onScroll` 完成 -->
<!-- 以防其中包含 `event.preventDefault()` -->
<div @scroll.passive="onScroll">...</div>
2.3、按键修饰符
按键修饰符主要为了监听键盘事件,因为需要监听键盘事件所以给@增加了案件修饰符
按键别名:
.enter.tab.delete(捕获“Delete”和“Backspace”两个按键).esc.space.up.down.left.right
系统按键修饰符
.ctrl.alt.shift.meta
鼠标按键修饰符
.left.right.middle
三、数据绑定v-model
3.1、表单输入绑定
基本用法不再记录,记录一下表单输入绑定中的值绑定规则与举例:
const selected = ref('A')
const options = ref([
{ text: 'One', value: 'A' },
{ text: 'Two', value: 'B' },
{ text: 'Three', value: 'C' }
])
<select v-model="selected">
<option v-for="option in options" :value="option.value">
{{ option.text }}
</option>
</select>
<div>Selected: {{ selected }}</div>
这个例子使用v-for指令动态渲染options中的可选项,同时将可选项的value值动态绑定到selected中,selected与option的value动态绑定,:value="option.value"表示将option.value这个值动态绑定到selected中
针对v-model数据绑定的修饰符
.lazy.number.trim
3.1、组件绑定v-model
假设我们有一个组件CustomInput再其使用v-model指令之前的例子为:
<CustomInput v-model="searchText"/>
对组件使用v-model指令会展开成如下的形式,相当于对子组件CustomInput传入了一个props属性名为modelValue值为searchText。同时定义了子组件给父组件传入数据的自定义事件update:modelValue,需要在子组件进行一定的处理才可以实现数据绑定。
<CustomInput
:modelValue="searchText"
@update:modelValue="newValue => searchText = newValue"
/>
在子组件当中
<script setup>
defineProps(['modelValue'])
defineEmits(['update:modelValue'])
</script>
<template>
<input
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)"
/>
</template>
- 将内部原生
input元素的valueattribute 绑定到modelValueprop - 输入新的值时在
input元素上触发update:modelValue事件
另一种在组件实现v-model的方法
另一种在组件内实现 v-model 的方式是使用一个可写的,同时具有 getter 和 setter 的计算属性。get 方法需返回 modelValue prop,而 set 方法需触发相应的事件:
<!-- 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作为props的默认参数,对应的自定义事件为update:modelValue,其实可以自定义model参数,语法为v-model:[propName]:
//Parent.vue
<MyComponent v-model:title="bookTitle" />
MyComponent.vue
defineProps(['title'])
defineEmits(['update:title'])
同时还可以给组件绑定多个v-model数据双向绑定,同时每个指令都会对应不同的prop例如:
// UserName的某个父组件中
<UserName
v-model:first-name="first"
v-model:last-name="last"
/>
//UserName.vue 中
defineProps({
firstName: String,
lastName: String
})
defineEmits(['update:firstName', 'update:lastName'])
v-model修饰符的处理
前文中数据绑定有一些内置的修饰符比如.trim,.number,.lazy,在对自定义的组件使用修饰符的时候,我们通常情况下内置的修饰符是不够的需要我们自定义修饰符,假如我们需要自定义一个修饰符(capitalize)将v-model绑定的字符串第一个转为大写写法如下:
// 在某一个父组件当中
<MyComponent v-model.capitalize="myText" />
在子组件当中我们可以通过prop中的modelModifiers访问的到。
<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.charAt(0).toUpperCase() + value.slice(1)
}
emit('update:modelValue', value)
}
</script>
<template>
<input type="text" :value="modelValue" @input="emitValue" />
</template>
有的时候会收到带有参数和带有修饰符的v-model绑定,得到的自定义修饰符的prop名称会是arg + "Modifiers"。
<MyComponent v-model:title.capitalize="myText">
相应的声明应该是:
const props = defineProps(['title', 'titleModifiers'])
defineEmits(['update:title'])
console.log(props.titleModifiers) // { capitalize: true }
四、侦听器
假如说需要在数据状态发生变化的时候执行一些副作用函数,如更改DOM或者是异步操作更改另一处的状态,我们可以使用组合式API中的watch函数来监听一个响应式的状态并触发回调函数。
4.1、示例
<script setup>
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
}
}
})
</script>
<template>
<p>
Ask a yes/no question:
<input v-model="question" />
</p>
<p>{{ answer }}</p>
</template>
watch函数的第一个参数值为监听的数据源,且数据源的格式不限(包括计算属性)。可监听的数据源的种类如下:
- ref✅
- reactive✅
- computed✅
- getter函数✅
- 多个数据源组成数组✅
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}`)
})
需要注意的是不能直接监听一个响应式对象的属性值,需要给watch传入一个该属性的getter函数。
4.2、深层监听器
直接给watch传入响应式对象,会隐地创建一个深层的监听器,这个响应式i对象的所有嵌套变更的时候都会触发回调,且监听的新值和旧值是一样的。
const obj = reactive({ count: 0 })
watch(obj, (newValue, oldValue) => {
// 在嵌套的属性变更时触发
// 注意:`newValue` 此处和 `oldValue` 是相等的
// 因为它们是同一个对象!
})
obj.count++
而传入的是响应式对象的getter函数,只有返回不同对象才会触发回调函数。也可以给这个代码假如{deep:true}选项,强制的转为深层的侦听器。
watch(
() => state.someObject,
() => {
// 仅当 state.someObject 被替换时触发
},
{deep:true}
)
4.3、watchEffect()
watch函数的监听是懒式的监听,当数据源发生变化的时候才触发回调,但是有的时候我们需要在创建侦听器的时候就立即执行回调。比如说我们想请求一个数据,在数据源发生变化的时候再重新请求数据:
watchEffect(async () => {
const response = await fetch(url.value)
data.value = await response.json()
})
这个例子中,回调会立即执行。在执行期间,它会自动追踪 url.value 作为依赖(和计算属性的行为类似)。每当 url.value 变化时,回调会再次执行。
五、模板引用
模板引用主要包括两方面,一个是假如需要直接操作DOM,我们引用DOM,还有一种引用包括组件的引用,这种引用会得到组件的示例,这些都可以通过ref这个属性来完成。需要注意的是下边两个例子的ref和模板的ref必须同名。
<script setup>
import { ref, onMounted } from 'vue'
// 声明一个 ref 来存放该元素的引用
// 必须和模板里的 ref 同名
const input = ref(null)
onMounted(() => {
input.value.focus()
})
</script>
<template>
<input ref="input" />
</template>
上边的例子使用了响应性语法糖,当不使用响应式语法糖的情况不在介绍,可以参考文档。
为模板引用标注类型TS
<script setup lang="ts">
import { ref, onMounted } from 'vue'
// 显式的指定一个泛型参数,和初始值
const el = ref<HTMLInputElement | null>(null)
onMounted(() => {
// 为了类型安全必须使用可选链或者类型守卫。
el.value?.focus()
})
</script>
<template>
<input ref="el" />
</template>
v-for中的模板引用对应的ref是一个数组。
组件上的引用
这种情况下会得到引用组件的实例,前提是没有使用<script setup>响应式语法糖,使用了的话需要使用defineExpose()宏暴漏,且默认的情况相应语法糖的组件是私有的。
为模板引用标注类型
在子组件中
<!-- MyModal.vue -->
<script setup lang="ts">
import { ref } from 'vue'
const isContentShown = ref(false)
const open = () => (isContentShown.value = true)
defineExpose({
open
})
</script>
父组件中
<!-- App.vue -->
<script setup lang="ts">
import MyModal from './MyModal.vue'
const modal = ref<InstanceType<typeof MyModal> | null>(null)
const openModal = () => {
modal.value?.open()
}
</script>
六、props传值与$emit传值
prop传值一般用于父组件给子组件进行值传递,在script setup语法中可以使用defineProps编译宏命令,这个命令不需要显式的导入
<!-- BlogPost.vue -->
<script setup>
defineProps(['title'])
</script>
<template>
<h4>{{ title }}</h4>
</template>
const props = defineProps(['title'])
console.log(props.title)
为props传递值类型:
<script setup lang="ts">
interface Props {
foo: string
bar?: number
}
const props = defineProps<Props>()
</script>
如何给props结构默认值呢?(还在实验中的功能需要手动开启)
<script setup lang="ts">
interface Props {
foo: string
bar?: number
}
// 对 defineProps() 的响应性解构
// 默认值会被编译为等价的运行时选项
const { foo, bar = 100 } = defineProps<Props>()
</script>
下边介绍一下vite显式的启用语法糖的语法,版本要求:vue@^3.2.25、@vitejs/plugin-vue@>=2.0.0
// vite.config.js
export default {
plugins: [
vue({
reactivityTransform: true
})
]
}
defineEmits声明自定义事件
自定义事件的语法为
<script setup>
// 可通过$emits('时间名',参数)在templete中特别的调用
defineEmits(['inFocus','submit'])
// 可以在script setip中通过函数调用 如下
const emit = defineEmits(['inFocus', 'submit'])
function buttonClick() {
emit('submit')
}
</script>
那么如何为emits标注类型呢?
<script setup lang="ts">
// 运行时
const emit = defineEmits(['change', 'update'])
// 基于类型
const emit = defineEmits<{
(e: 'change', id: number): void
(e: 'update', value: string): void
}>()
</script>
那么如何像props那样校验自定义时间呢?
<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>
七、透传Attributes
透传Attributes指的是传递给一个组件却没有被该组件声明为 props 或 emits 的 attribute 或者 v-on 事件监听器。假如给子组件传递的属性为style和class,且子组件也有了这些属性他就会和父组件中的style和class合并,同时监听器也会继承,即使是深层组件也会透传。
那么都什么会透传呢?
- class和style✅
- v-on事件✅
禁用透传的方法
如果你不想要一个组件自动地继承 attribute,你可以在组件选项中设置 inheritAttrs: false。如果你使用了 <script setup>,你需要一个额外的 <script> 块来书写这个选项声明。可以在组件中直接使用$attrs访问除了组件所声明的 props 和 emits 之外的所有其他 attribute,例如 class,style, 监听器等等。
<script>
// 使用普通的 <script> 来声明选项
export default {
inheritAttrs: false
}
</script>
<script setup>
// ...setup 部分逻辑
</script>
多节点的Attrtibutes继承
假如子节点下边有许多根节点模板,那么透传的属性需要哪个子模板继承呢?如下所示
<header>...</header>
<main>...</main>
<footer>...</footer>
可以显式用v-bind指定需要绑定的元素
<header>...</header>
<main v-bind="$attrs">...</main>
<footer>...</footer>
js访问透传的属性值
<script setup>
import { useAttrs } from 'vue'
const attrs = useAttrs()
</script>
八、依赖注入
假如从父组件像子组件中传递值我们可以使用props,但是假如层级比较多呢,有多层嵌套的组件需要从父组件像深层次的子组件传入数值,如果一层一层使用props就会变得不现实,我们可以使用依赖注入的方式。
8.1 、provide
provide 和 inject 可以帮助我们解决这一问题。一个父组件相对于其所有的后代组件,会作为依赖提供者。任何后代的组件树,无论层级有多深,都可以注入由父组件提供给整条链路的依赖。
provide()支持传入两个参数,一个参数为字符串String或者一个Symbol称为注入名,后面的参数为对应的注入值,后代组件会用注入名来查找期望注入的值。一个组件可以多次调用 provide(),使用不同的注入名,注入不同的依赖值。
<script setup>
import { provide } from 'vue'
provide(/* 注入名 */ 'message', /* 值 */ 'hello!')
</script>
应用层provide
就是在main.js/ts中进行provide
import { createApp } from 'vue'
const app = createApp({})
app.provide(/* 注入名 */ 'message', /* 值 */ 'hello!')
8.2 、inject
语法很简单
<script setup>
import { inject } from 'vue'
const message = inject('message')
</script>
为jnject提供默认数值
// 如果没有祖先组件提供 "message"
// `value` 会是 "这是默认值"
const value = inject('message', '这是默认值')
在一些场景中,默认值可能需要通过调用一个函数或初始化一个类来取得。为了避免在用不到默认值的情况下进行不必要的计算或产生副作用,我们可以使用工厂函数来创建默认值:
const value = inject('key', () => new ExpensiveClass())
使用symbol作为注入名
在大型应用中会有非常多的依赖项,或者在编写给其他人使用的组件的时候最好使用Symbol作为注入值,避免一些可能的冲突。
- 在单文件组件中导出这些注入的Symbol
// keys.js
export const myInjectionKey = Symbol()
供给方进行使用
// 在供给方组件中
import { provide } from 'vue'
import { myInjectionKey } from './keys.js'
provide(myInjectionKey, { /*
要提供的数据
*/ });
注入方使用Symbol
// 注入方组件
import { inject } from 'vue'
import { myInjectionKey } from './keys.js'
const injected = inject(myInjectionKey)
九、插槽
插槽的适用场景有的时候需要为子组件传入一些模板片段,那么子组件如何接受这些模板片段呢?也就是说组组件接收的片段由父组件决定,这就用到了插槽,相当于在子组件放置一个slot,父组件传入template模板,渲染到子组件中。插槽还可以指定默认内容,也就是当父组件没有提供任何内容的时候,有一个默认渲染的内容。
<button type="submit">
<slot>
Submit <!-- 默认内容 -->
</slot>
</button>
9.1、具名插槽
有时在一个组件中包含多个插槽出口是很有用的。举例来说,在一个 <BaseLayout> 组件中,有如下模板:
<div class="container">
<header>
<!-- 标题内容放这里 -->
<slot name="header"></slot>
</header>
<main>
<!-- 主要内容放这里 -->
<slot name="main"></slot>
</main>
<footer>
<!-- 底部内容放这里 -->
<slot name="footer"></slot>
</footer>
</div>
这种情况有三个不容的位置需要父组件渲染模板内容,所以可以给不同的slot插槽取名字。在父组件中就需要使用带v-slot简写为#的指令指定名字,插槽的名字还可以动态指定语法为#[dynamicSlotName]还有v-slot:[dynamicSlotName]
<BaseLayout>
<template v-slot:header>
<!-- header 插槽的内容放这里 -->
</template>
</BaseLayout>
<BaseLayout>
<template #header>
<h1>Here might be a page title</h1>
</template>
<template #default>
<p>A paragraph for the main content.</p>
<p>And another one.</p>
</template>
<template #footer>
<p>Here's some contact info</p>
</template>
</BaseLayout>
9.2、作用域插槽
上面的例子中插槽内容无法访问到子组件的状态,那么我们想要访问子组件的状态该如何做呢?可以像对组件传递 props 那样,向一个插槽的出口上传递 attributes:
<!-- <MyComponent> 的模板 -->
<div>
<slot :text="greetingMessage" :count="1"></slot>
</div
那么父组件如何接收这些传过来的数据呢?默认插槽如下
<MyComponent v-slot="slotProps">
{{ slotProps.text }} {{ slotProps.count }}
</MyComponent>
<MyComponent v-slot="{ text, count }">
{{ text }} {{ count }}
</MyComponent>
9.3、具名作用域插槽
<MyComponent>
<template #header="headerProps">
{{ headerProps }}
</template>
<template #default="defaultProps">
{{ defaultProps }}
</template>
<template #footer="footerProps">
{{ footerProps }}
</template>
</MyComponent>
<slot name="header" message="hello"></slot>
name不会当作prop传递给插槽。