[Vue] Vue3 官方文档阅读
![[Vue] Vue3 官方文档阅读](https://cdn.hashnode.com/res/hashnode/image/upload/v1707709148830/41509d54-5a84-48d8-8f20-ac5d00e12228.jpeg)
记录一下自己阅读vue3官方文档学习的理解,以防看了一遍就忘记
ref 和 reactive
在引用数据时,对ref和reactive的引用都是对它们所包装的对象的引用
ref使用.value的方式来修改值,这就保证了,在修改值时,对ref对象的引用保持不变,这样你就可以放心你替换掉ref所包装的整个值,包括对象
而reactive如果要替换掉它包装的对象,那么就必须使用如下方式:
// reactive 所包装的值必须是一个对象,包括数组、Map、Set等
const state = reactive({
name: "lordmoon"
})
state = reactive({
name: "fzx"
})
那么这样的改变就会使得reactive对象的引用发生了改变,从而丢失响应式,所以reactive是不能改变整个对象的
我们可以改变reactive对象中的属性,无论层级结构多深,reactive都会作出响应式更新
当ref包装的值是一个对象时,它会自动被包装成reactive对象,从而实现了深层级的响应式
watch 和 watchEffect
这两个api都是用来侦听在响应式数据变化后,进行一些操作
watch的第一个参数是数据源,它可以是以下的值:
一个
ref对象或者reactive对象一个
getter函数,其返回值就是要侦听的值一个由以上的数据组成的数组
watch的第二个参数是在响应式数据发生改变后执行的副作用函数,它的第一个参数是侦听的数据的新值,第二个参数是旧值,但是这里需要搞清楚有些情况下新值可能与旧值相等:
- 监听一个
ref对象
/**
监听一个 ref 对象时,使用 .value 来修改这个 ref 对象的值,会触发 watch 的回调
此时新值和旧值是不相等的
*/
<template>
<button @click="set">点击修改</button>
</template>
<script setup lang="ts">
import { ref, reactive, watch } from "vue"
const state = ref("111")
const set = () => {
state.value = "222"
}
watch(state, (newVal, oldVal) => {
console.log(newVal, oldVal) // false
})
</script>
- 监听一个
reactive对象
/**
监听一个 reactive 对象时,修改 reactive 对象的属性值,会触发 watch 的回调
此时新值和旧值是相等的,因为 reactive 的引用并没有改变
*/
<template>
<button @click="set">点击修改</button>
</template>
<script setup lang="ts">
import { ref, reactive, watch } from "vue"
const state = reactive({
name: "lordmoon"
})
const set = () => {
state.name = "fzx"
}
// 这里直接侦听 reactive 对象是可以侦听到的
watch(state, (newVal, oldVal) => {
console.log(newVal === oldVal) // true
})
</script>
如果第一个参数是一个getter函数,那么它的返回值也必须是ref或者reactive对象:
<template>
<button @click="set">点击修改</button>
</template>
<script setup lang="ts">
import { ref, reactive, watch } from "vue"
let state = ref(1)
const set = () => {
state.value = 2
}
/**
要注意,这里的返回值必须是 .value,否则 watch 就侦听不到这个响应式数据的变化
因为这个响应式对象是没有改变的,只是其中的 .value 改变
同时,getter 函数的返回值必须是一个响应式的数据,如果只是一个普通变量,也无法侦听到
*/
watch(() => state.value, (newVal, oldVal) => {
console.log(newVal === oldVal) // false
})
</script>
如果getter函数返回一个reactive对象,那么这种情况watch是侦听不到的,因为这个reactive对象的引用并没有改变:
<template>
<button @click="set">点击修改</button>
</template>
<script setup lang="ts">
import { ref, reactive, watch } from "vue"
let state = reactive({
name: "lordmoon"
})
const set = () => {
state.name = "fzx"
}
// 这里的副作用函数是不会触发的
watch(() => state, (newVal, oldVal) => {
console.log(newVal, oldVal)
})
</script>
另外,默认情况下,副作用回调函数会在组件重新渲染前执行,如果要修改,可以加上一个option参数,详情请参考官方文档的描述:侦听器 | Vue.js (vuejs.org)
组件更新(重新渲染)机制
个人认为,vue的重新渲染 (状态更新) 机制,相比react,是更符合直觉的
在vue中,响应式数据ref、reactive更新时,它会追溯到使用了这些响应式数据的地方进行局部更新,而不是整个组件从头到尾重新渲染一遍。从vue的生命周期hook流程图来看,setup和omMounted这两个钩子在组件re-render的时候都不会再次触发,经实践确实如此:
// 下面的打印输出在组件 re-render 之后都不会再输出
<template>
<button @click="set">点击修改</button>
</template>
<script setup lang="ts">
import { ref, reactive, watch, onMounted } from "vue"
console.log("setup")
let state = reactive({
name: "lordmoon"
})
const set = () => {
state.name = "fzx"
}
onMounted(() => {
console.log("mounted")
})
watch(state, (newVal, oldVal) => {
console.log(newVal === oldVal)
})
</script>
这就说明了vue的组件更新不会导致组件的二次挂载
props 和 emits
props和emits是父子组件通信的重要机制
在<script setup>中,需要用defineProps来声明由父组件传来的props,传入的props是一个用reactive包装的对象
// 需要注意,传递的 props 是只读属性,如果直接修改的话会报错
<script setup lang="ts">
import { defineProps, withDefaults, withDefaults, } from "vue"
// 如果使用 ts 则需要 defineProps 声明类型
const props = defineProps<{
text: string
}>()
// 定义 props 类型接口
interface Props {
text: string
}
// 可以使用 withDefaults 来为 props 声明默认值
const props = withDefaults(defineProps<Props>(), {
text: "111"
})
</script>
用defineEmits来声明父组件传过来的自定义事件:
// 父组件
<script setup lang="ts">
import RelationGraph from './components/relationGraph/RelationGraph.vue'
const change = (a: string, b: string, c: string) => {
console.log(a, b, c)
}
</script>
<template>
<!-- 传递自定义事件(监听) -->
<RelationGraph text="666" @my-change="change"/>
</template>
// 子组件
<template>
<button @click="myChange">点击修改</button>
</template>
<script setup lang="ts">
import { ref, reactive, defineProps, withDefaults, defineEmits } from "vue"
interface Props {
text: string
}
// 通过 emit 触发父组件的自定义事件
const myChange = () => {
emit("myChange", "111", "222", "333")
}
const props = withDefaults(defineProps<Props>(), {
text: "111"
})
// 如果使用 ts,则需要为 emit 指定类型
const emit = defineEmits<{
myChange: [a: string, b: string, c: string] // 自定义事件myChange:参数为 a, b, c,参数类型为 string
}>()
</script>
子组件中调用emit()传递的所有参数,父组件中都可以一一对应,当然你也可以传一个对象,一次性全部传回去
provide 和 inject
provide和inject的作用大致与react的useContext和createContext相同,根据官方文档的描述,都是用于解决在组件间传递值的时候层级太多导致的代码冗余情况,当然你也可以选用一些全局状态管理库
使用provide时,既可以使用string类型的参数作为key,也可以使用Symbol类型作为key,官方文档是推荐使用Symbol的,因为在使用ts时,能够为对应的key声明要provide的值的类型,下面是一个例子
- 首先把
Symbol类型的key放到一个单独的ts文件中:
import { type InjectionKey } from "vue"
// 使用 InjectionKey 将这个 key 对应的要 provide 的值的类型声明为 string
export const key = Symbol() as InjectionKey<string>
- 在父组件中
provide:
<script setup lang="ts">
import RelationGraph from './components/relationGraph/RelationGraph.vue'
import { key } from "./components/symbol-key"
import { provide } from "vue"
provide(key, "111")
</script>
- 在子组件中
inject:
<script setup lang="ts">
import { ref, reactive, inject } from "vue"
import { key } from "../symbol-key"
const foo = inject(key)
console.log(foo) // "111"
</script>
如果不使用Symbol作为key的类型而使用string类型,也可以在inject时声明值的类型
// 父组件
<script setup lang="ts">
import RelationGraph from './components/relationGraph/RelationGraph.vue'
import { provide } from "vue"
// 使用字符串作为 key 来 provide
provide("key", 111)
</script>
// 子组件
<script setup lang="ts">
import { inject } from "vue"
// 在 inject 时声明值得类型为 number
const foo = inject<number>("key")
</script>
v-model 讲解
v-model常用于原生input组件上绑定响应式的输入值,但实际上它是个语法糖,先来看看它用在input上的例子,下面这两种写法是等价的,由此可以看出v-model实际上是:value和@input组合的语法糖
// 使用 v-model
<template>
<input v-model="value" @input="() => { console.log(value) }">
</template>
<script setup lang="ts">
import { ref, reactive } from "vue"
const value = ref("输入点什么")
</script>
// 将 v-model 拆成 :value 和 @input
<template>
<input :value="value" @input="setValue">
</template>
<script setup lang="ts">
import { ref, reactive } from "vue"
const setValue = (e: Event) => {
value.value = (e.target as HTMLInputElement).value
console.log(value.value)
}
const value = ref("输入点什么")
</script>
如果v-model用在组件上,那么同理也会被拆成类似于:value和@input这样的,但是情况稍有不同:
// 父组件
<script setup lang="ts">
import RelationGraph from './components/relationGraph/RelationGraph.vue'
import { ref } from "vue"
const text = ref("111")
</script>
<template>
{{ text }}
// 使用 v-model 来给子组件传 prop 和 emit
<RelationGraph v-model="text"/>
</template>
// 子组件
<template>
<button @click="set">点击修改</button>
</template>
<script setup lang="ts">
import { ref, reactive, defineEmits, defineProps } from "vue"
// 接收父组件的 prop,默认名称为 "modelValue"
const props = defineProps<{
modelValue: string
}>()
console.log(props.modelValue) // "111"
// 接收父组件的 emit,默认名称为 "update:modelValue"
const emit = defineEmits<{
"update:modelValue": [val: string]
}>()
const set = () => {
emit("update:modelValue", "222")
}
</script>
除此之外,还可以用defineModel来接收v-model的参数:
// 子组件
<script setup lang="ts">
import { ref, reactive, defineModel } from "vue"
// 这里还可以指定是否必传和默认值
const model = defineModel({
required: true,
default: "222"
})
const set = () => {
// 直接用 .value 语法修改 v-model 的值就会触发 emit 事件
model.value = "333"
}
</script>
另外,v-model还有绑定参数和修饰符的用法,这里不多赘述,详情参考官方文档:组件 v-model | Vue.js (vuejs.org)
组件实例与 defineExpose
ref除了可以引用dom元素,还可以引用组件实例,在引用了某一个组件的实例后,我们可能想使用这个组件内的变量与函数,但是在<script setup>下组件是“封闭”的,所以可以用defineExpose来进行暴露
// 父组件
<script setup lang="ts">
import RelationGraph from './components/relationGraph/RelationGraph.vue'
import { provide, ref, onMounted } from "vue"
// 这里还可以使用 InstanceType 来声明子组件模板引用的实例类型
const child = ref<InstanceType<typeof RelationGraph> | null>(null)
onMounted(() => {
// child.value 就是组件实例
console.log(child.value.text) // "111"
})
</script>
<template>
<!-- ref 引用子组件实例 -->
<RelationGraph ref="child"/>
</template>
// 子组件
<template>
<button @click="">点击修改</button>
</template>
<script setup lang="ts">
import { ref, reactive, defineExpose } from "vue"
const text = ref("111")
// 暴露自身的变量
defineExpose({
text
})
</script>
自定义指令
自定义指令是一个包含了hook函数的对象,每个hook函数都有4个参数,分别是el、binding、vnode、prevVnode,最常用的就是前面两个,el是使用该指令的dom元素,binding与传递给指令的参数、修饰符和值有关,其结构如下:
<div v-example:foo.bar="baz"></div>
{
arg: 'foo', /* 参数 */
modifiers: { bar: true }, /* 修饰符 */
value: /* `baz` 的值 */,
oldValue: /* 上一次更新时 `baz` 的值 */
}
自定义指令还可以在全局注册:
const app = createApp({})
// 使 v-focus 在所有组件中都可用
app.directive('focus', {
// ...
})
官方文档有提示,如果不是必须操作dom,那么并不推荐使用自定义指令
组合式函数(自定义 hooks)
组合式函数与react中的自定义hooks非常相似,将需要复用的逻辑封装成一个函数,然后把需要用到的从函数中return出去
import { ref, onMounted } from "vue"
export funtion useHook() {
const a = ref(0)
const func = () => {
a.value = 1
}
// 钩子函数也是可以复用的
onMounted(() => {
// 进行一些操作...
})
return {
a, func
}
}
这是不接收参数的hook,如果要接收参数,比如请求的加载中、失败、成功等效果,我们可以传入一个url,在hook内部进行请求,然后再将请求的结果返回,根据请求结果渲染对应组件
App.vue:
<script setup>
import { ref, computed } from 'vue'
import { useFetch } from './useFetch.js'
const baseUrl = 'https://jsonplaceholder.typicode.com/todos/'
const id = ref('1')
// 这里的 url 是一个 ref 对象
const url = computed(() => baseUrl + id.value)
// 如果传入的 url 不是一个响应式对象,那么自定义 hook 中的 watchEffect 将不会侦听到变化
// const url = baseUrl + id.value
const { data, error } = useFetch(url)
const retry = () => {
id.value = ''
id.value = '1'
}
</script>
<template>
Load post id:
<button v-for="i in 5" @click="() => { id = i + '' }">{{ i }}</button>
<div v-if="error">
<p>Oops! Error encountered: {{ error.message }}</p>
<button @click="retry">Retry</button>
</div>
<div v-else-if="data">Data loaded: <pre>{{ data }}</pre></div>
<div v-else>Loading...</div>
</template>
useFetch.js:
import { ref, watchEffect, toValue } from 'vue'
export function useFetch(url) {
const data = ref(null)
const error = ref(null)
watchEffect(async (onInvalidate) => {
let ignore = false
// 在每次请求前先重置之前发出的请求的值
data.value = null
error.value = null
const urlValue = toValue(url)
try {
const res = await fetch(urlValue)
if(!ignore) data.value = await res.json()
} catch (e) {
error.value = e
}
/**
这里可以做一个竞态优化
很奇怪的是 vue3 的官方文档居然没有提这个
详情可参考 react 的文档:https://zh-hans.react.dev/learn/you-might-not-need-an-effect#fetching-data
*/
onInvalidate(() => {
ignore = true
})
})
return { data, error }
}
最佳实践详情参考官方文档:组合式函数 | Vue.js (vuejs.org)
结合 Typescript
官方有提供一些ts工具类,详情请参考api:TypeScript 工具类型 | Vue.js (vuejs.org)
除此之外,我想要提两个插件(npm包 ):unplugin-vue-components和unplugin-autp-import,这两个插件可以让我们省去一些手动引入的过程,例如我们在使用第三方组件库的时候就不需要再import组件了,著名的Element Plus就使用了这样的方式
具体的方式可以参考他们官网上的示例,这里我讲一下components.d.ts和auto-import.d.ts这两个文件,这是这两个插件自动为我们生成的类型声明文件,以下是components.d.ts的示例:
declare module 'vue' {
export interface GlobalComponents {
BaseFilter: typeof import('./components/Common/BaseFilter.vue')['default']
BaseHeader: typeof import('./components/Common/BaseHeader.vue')['default']
BasePagination: typeof import('./components/Common/BasePagination.vue')['default']
BaseSidebar: typeof import('./components/Common/BaseSidebar.vue')['default']
BaseTags: typeof import('./components/Common/BaseTags.vue')['default']
BaseTitle: typeof import('./components/Common/BaseTitle.vue')['default']
}
}
export { }
这里的import()语法是.d.ts文件的一个特点,可用于引入一个模块对象,看上面的用法就大致清楚了,它与一般的模块中使用的import()函数不同,一般模块中使用import()动态导入返回的是一个Promise,而在.d.ts文件中是引入一个模块对象,详细的讨论可以参考stackoverflow上的这篇文章:typescript - Import class in definition file (*d.ts) - Stack Overflow
![[JS] fetch 不捕获 4xx](https://cdn.hashnode.com/res/hashnode/image/upload/v1752292253648/9db0ffc4-101a-41a9-9484-b3891c2d81b3.png)
![[Node] Mac m 芯片使用 node 14](https://cdn.hashnode.com/res/hashnode/image/upload/v1752243750001/cf2a586a-c5f8-488c-84a7-a3f7068f34b2.jpeg)
![[其他] WSL Interop](https://cdn.hashnode.com/res/hashnode/image/upload/v1752241086475/51441fc0-e5d9-4423-b009-ac9af9a51c04.jpeg)