Skip to main content

Command Palette

Search for a command to run...

[Vue] Vue3 官方文档阅读

Published
•6 min read•View as Markdown
[Vue] Vue3 官方文档阅读

记录一下自己阅读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,那么并不推荐使用自定义指令

image-20240118153541102

组合式函数(自定义 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

More from this blog

[Vite] 处理静态资源路径

静态资源路径的种类 从路径的格式上来说,vite 的静态资源路径有两种:相对路径 和 绝对路径 从路径的类型上来说,vite 的静态资源路径有两种:基于 http URL 的 和 基于文件系统的 相对路径 在 vite 文档的表述中,相对路径常用于在 “嵌入式” 的页面中使用: 这里的 “嵌入式” 指的是例如浏览器插件这种,将一个页面嵌入到一个网页中的形式 绝对路径 绝对路径指的是 “以根路径为起点” 而表示的路径 绝对路径在 “基于 http URL” 和 “基于文件系统” 的情况下,最终表...

Oct 2, 20251 min read

[Node] Mac m 芯片使用 node 14

最近在 m 芯片的 mac 电脑上使用 node 14 版本,遇到了芯片架构不兼容的错误,所以记录一下解决问题的过程 遇到的问题 原因是 node 14 版本只支持 x86_64 架构的芯片,所以使用 arm 架构的 m 芯片就会报错 解决方案 前置知识 在 m 芯片的 mac 上,node 16 及以上的版本全面支持了 arm 芯片架构;如果要安装 16 以下的版本,需要通过 Rosetta 2 转译执行 步骤 按照 nvm 官方的步骤进行:https://github.com/nvm-sh/...

Jul 11, 20251 min read
[Node] Mac m 芯片使用 node 14

[其他] WSL Interop

最近在 wsl 下使用 code <dir> 命令拉起 windows 下的 vscode 客户端时,遇到了错误,搜了一下发现是 wsl 和 windows 的互操作出现了问题 这篇文章就来记录一下遇到的问题和相关的知识点 WSLInterop.conf 文件 WSLInterop.conf 文件是 WSL(Windows Subsystem for Linux)中的一个关键配置文件,它定义了 Linux 系统如何识别和处理 Windows 可执行文件 文件位置和基本作用 这个文件通常位于 /u...

Jul 11, 20252 min read
[其他] WSL Interop

lordmoon's blog

25 posts