【Vue官方文档】记录一些比较在意的知识😗

5/21/2024 Vue官方文档学习浅记

# defineModel

v-model能够实现双向数据绑定就不用说啦

父组件给子组件绑定v-model时,比如这样:

<!-- Parent.vue -->
<Child v-model="countModel" />

子组件内部可以用defineModel()宏进行接收:

<script setup>
const model = defineModel()
</script>

<template>
  <input v-model="model" />
</template>

defineModel会返回一个ref,这样父组件的值就可以和子组件的某个元素进行双向数据绑定了

# 底层机制

当子组件使用defineModel时,编译器会做这些事

  • 定义一个prop,让本地的ref值与其同步
  • 定义一个update:modelValue事件,本地ref值变化时触发,修改父组件中的值

其实就是父子组件通信中,经常使用的父组件定义自定义事件、子组件触发自定义事件的方式的语法糖。

父组件v-model="modelValue"被编译为:

<Child
  :modelValue="foo"
  @update:modelValue="$event => (foo = $event)"
/>

而子组件中则被编译为:

<!-- Child.vue -->
<script setup>
const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])
</script>

<template>
  <input
    :value="props.modelValue"
    @input="emit('update:modelValue', $event.target.value)"
  />
</template>

# 其他配置

# 自定义修饰符

在父组件上添加这样的自定义修饰符:

<MyComponent v-model.capitalize="myText" />

子组件中可以这样把修饰符取出来:

<script setup>
const [model, modifiers] = defineModel()

console.log(modifiers) // { capitalize: true }
</script>

<template>
  <input type="text" v-model="model" />
</template>

为了更方便的使用修饰符,可以给defineModel传入get和set,读取和设置值:

<script setup>
const [model, modifiers] = defineModel({
  set(value) {
    if (modifiers.capitalize) {
      return value.charAt(0).toUpperCase() + value.slice(1)
    }
    return value
  }
})
</script>

<template>
  <input type="text" v-model="model" />
</template>

还可以这样取:

父组件

<UserName
  v-model:first-name.capitalize="first"
  v-model:last-name.uppercase="last"
/>

子组件:

<script setup>
const [firstName, firstNameModifiers] = defineModel('firstName')
const [lastName, lastNameModifiers] = defineModel('lastName')

console.log(firstNameModifiers) // { capitalize: true }
console.log(lastNameModifiers) // { uppercase: true }
</script>

defineModel()宏是Vue3.4后推荐的方法,这之前可以用常规方式

# Attribute透传

当一个组件以单个元素为根作渲染时,父组件中给他设置的attribute会被自动添加到他的根元素上,比如这样:

父组件:

<MyButton class="large" />

子组件:

<button>Click Me</button>

最终渲染结果:

<button class="large">Click Me</button>

同样,其他attribute也会这样透传。

如果子组件根元素已经有了class或着style,透传后还会进行自动合并,像这样:

父组件:

<MyButton class="large" />

子组件:

<button class="btn">Click Me</button>

结果:

<button class="btn large">Click Me</button>

v-on事件监听器也会这样。

# 禁用透传

有时候,我们可能不希望让子组件的根元素自动继承父组件给的attribute,而是挂到子组件中的其他元素上,这时候可以在子组件中禁用继承

<script setup>
defineOptions({
  inheritAttrs: false
})
// ...setup 逻辑
</script>

在子组件模板中可以用$attrs直接拿到

<span>Fallthrough attribute: {{ $attrs }}</span>

这样就可以挂到其他元素上了

# 多根节点的Attribute继承

当子元素有多个根节点时,如果进行透传,会被抛出运行时警告,因为无法确定要透传到哪个节点上,多根节点没有透传行为

但是可以用$attrs显示绑定

<header>...</header>
<main v-bind="$attrs">...</main>
<footer>...</footer>

这样也不会有警告

# useAttrs

在JS中要访问透传的Attributes,可以用useAttrs:

<script setup>
import { useAttrs } from 'vue'

const attrs = useAttrs()
</script>

# 异步组件

在大型项目中,应用可能要被拆分成更小的块,并在需要的时候再从服务器加载,这时候可以用异步组件

Vue中异步组件可以用defineAsyncComponent实现:

import { defineAsyncComponent } from 'vue'

const AsyncComp = defineAsyncComponent(() => {
  return new Promise((resolve, reject) => {
    // ...从服务器获取组件
    resolve(/* 获取到的组件 */)
  })
})
// ... 像使用其他一般组件一样使用 `AsyncComp`

defineAsyncComponent要传入一个方法,方法返回一个promise。而ES模块动态导入的方式也可以返回一个Promise,所以一般这样用就好:

import { defineAsyncComponent } from 'vue'

const AsyncComp = defineAsyncComponent(() =>
  import('./components/MyComponent.vue')
)

最后再用app.component全局注册即可:

app.component('MyComponent', defineAsyncComponent(() =>
  import('./components/MyComponent.vue')
))

父组件局部注册也可以:

<script setup>
import { defineAsyncComponent } from 'vue'

const AdminPage = defineAsyncComponent(() =>
  import('./components/AdminPageComponent.vue')
)
</script>

<template>
  <AdminPage />
</template>

# 加载与错误状态

异步操作不可避免的涉及到加载状态和错误状态,可以传入配置项进行处理

const AsyncComp = defineAsyncComponent({
  // 加载函数
  loader: () => import('./Foo.vue'),

  // 加载异步组件时使用的组件
  loadingComponent: LoadingComponent,
  // 展示加载组件前的延迟时间,默认为 200ms
  delay: 200,

  // 加载失败后展示的组件
  errorComponent: ErrorComponent,
  // 如果提供了一个 timeout 时间限制,并超时了
  // 也会显示这里配置的报错组件,默认值是:Infinity
  timeout: 3000
})

如果提供了一个加载组件,它将在内部组件加载时先行显示。在加载组件显示之前有一个默认的 200ms 延迟——这是因为在网络状况较好时,加载完成得很快,加载组件和最终组件之间的替换太快可能产生闪烁,反而影响用户感受。

如果提供了一个报错组件,则它会在加载器函数返回的 Promise 抛错时被渲染。你还可以指定一个超时时间,在请求耗时超过指定时间时也会渲染报错组件。

# 组合式函数

在 Vue 应用的概念中,“组合式函数”(Composables) 是一个利用 Vue 的组合式 API 来封装和复用有状态逻辑的函数。

当构建前端应用时,我们常常需要复用公共任务的逻辑。例如为了在不同地方格式化时间,我们可能会抽取一个可复用的日期格式化函数。这个函数封装了无状态的逻辑:它在接收一些输入后立刻返回所期望的输出。复用无状态逻辑的库有很多,比如你可能已经用过的 lodash 或是 date-fns。

相比之下,有状态逻辑负责管理会随时间而变化的状态。一个简单的例子是跟踪当前鼠标在页面中的位置。在实际应用中,也可能是像触摸手势或与数据库的连接状态这样的更复杂的逻辑。

就是说封装一些状态,和react的自定义hook功能类似。

官方例子,鼠标追踪器:

直接再组件里写是这样实现的:

<script setup>
import { ref, onMounted, onUnmounted } from 'vue'

const x = ref(0)
const y = ref(0)

function update(event) {
  x.value = event.pageX
  y.value = event.pageY
}

onMounted(() => window.addEventListener('mousemove', update))
onUnmounted(() => window.removeEventListener('mousemove', update))
</script>

<template>Mouse position is at: {{ x }}, {{ y }}</template>

script中写的逻辑比较多,如果其他组件也想用这段逻辑的话,就可以封装成一个组合式函数,提到外面进行复用。

// mouse.js
import { ref, onMounted, onUnmounted } from 'vue'

// 按照惯例,组合式函数名以“use”开头
export function useMouse() {
  // 被组合式函数封装和管理的状态
  const x = ref(0)
  const y = ref(0)

  // 组合式函数可以随时更改其状态。
  function update(event) {
    x.value = event.pageX
    y.value = event.pageY
  }

  // 一个组合式函数也可以挂靠在所属组件的生命周期上
  // 来启动和卸载副作用
  onMounted(() => window.addEventListener('mousemove', update))
  onUnmounted(() => window.removeEventListener('mousemove', update))

  // 通过返回值暴露所管理的状态
  return { x, y }
}

这样在组件中直接使用:

<script setup>
import { useMouse } from './mouse.js'

const { x, y } = useMouse()
</script>

<template>Mouse position is at: {{ x }}, {{ y }}</template>

# 响应式状态

比如先实现一个普通的组合式函数:

// fetch.js
import { ref } from 'vue'

export function useFetch(url) {
  const data = ref(null)
  const error = ref(null)

  fetch(url)
    .then((res) => res.json())
    .then((json) => (data.value = json))
    .catch((err) => (error.value = err))

  return { data, error }
}

组件中使用他:

<script setup>
import { useFetch } from './fetch.js'

const { data, error } = useFetch('...')
</script>

但是这样传入的url只是静态的,只会执行一次。可以给useFetch传入一个ref或者getter函数的响应式数据,这样就会被依赖收集,参数改变的时候自动重新执行函数:

const url = ref('/initial-url')

const { data, error } = useFetch(url)

// 这将会重新触发 fetch
url.value = '/new-url'

在组合式函数中,再用watchEffect和toValue的APi重构一下:

// fetch.js
import { ref, watchEffect, toValue } from 'vue'

export function useFetch(url) {
  const data = ref(null)
  const error = ref(null)

  const fetchData = () => {
    // reset state before fetching..
    data.value = null
    error.value = null

    fetch(toValue(url))
      .then((res) => res.json())
      .then((json) => (data.value = json))
      .catch((err) => (error.value = err))
  }

  watchEffect(() => {
    fetchData()
  })

  return { data, error }
}

toValue()是vue3.3新增的api,目的是规范化ref和getter的值。它会返回 ref 的值;如果参数是函数,它会调用函数并返回其返回值。否则,它会原样返回参数。它的工作方式类似于 unref(),但对函数有特殊处理。

注意 toValue(url) 是在 watchEffect 回调函数的内部调用的。这确保了在 toValue() 规范化期间访问的任何响应式依赖项都会被侦听器跟踪。

这个版本的 useFetch() 现在能接收静态 URL 字符串、ref 和 getter,使其更加灵活。watch effect 会立即运行,并且会跟踪 toValue(url) 期间访问的任何依赖项。如果没有跟踪到依赖项 (例如 url 已经是字符串),则 effect 只会运行一次;否则,它将在跟踪到的任何依赖项更改时重新运行。

这样的代码结构就很漂亮:

<script setup>
import { useFeatureA } from './featureA.js'
import { useFeatureB } from './featureB.js'
import { useFeatureC } from './featureC.js'

const { foo, bar } = useFeatureA()
const { baz } = useFeatureB(foo)
const { qux } = useFeatureC(baz)
</script>

# 与其他模块对比

# 和 Mixin 的对比​

Vue 2 的用户可能会对 mixins 选项比较熟悉。它也让我们能够把组件逻辑提取到可复用的单元里。然而 mixins 有三个主要的短板:

不清晰的数据来源:当使用了多个 mixin 时,实例上的数据属性来自哪个 mixin 变得不清晰,这使追溯实现和理解组件行为变得困难。这也是我们推荐在组合式函数中使用 ref + 解构模式的理由:让属性的来源在消费组件时一目了然。

命名空间冲突:多个来自不同作者的 mixin 可能会注册相同的属性名,造成命名冲突。若使用组合式函数,你可以通过在解构变量时对变量进行重命名来避免相同的键名。

隐式的跨 mixin 交流:多个 mixin 需要依赖共享的属性名来进行相互作用,这使得它们隐性地耦合在一起。而一个组合式函数的返回值可以作为另一个组合式函数的参数被传入,像普通函数那样。

基于上述理由,我们不再推荐在 Vue 3 中继续使用 mixin。保留该功能只是为了项目迁移的需求和照顾熟悉它的用户。

# 和无渲染组件的对比​

在组件插槽一章中,我们讨论过了基于作用域插槽的无渲染组件。我们甚至用它实现了一样的鼠标追踪器示例。

组合式函数相对于无渲染组件的主要优势是:组合式函数不会产生额外的组件实例开销。当在整个应用中使用时,由无渲染组件产生的额外组件实例会带来无法忽视的性能开销。

我们推荐在纯逻辑复用时使用组合式函数,在需要同时复用逻辑和视图布局时使用无渲染组件。

# 和 React Hooks 的对比​

如果你有 React 的开发经验,你可能注意到组合式函数和自定义 React hooks 非常相似。组合式 API 的一部分灵感正来自于 React hooks,Vue 的组合式函数也的确在逻辑组合能力上与 React hooks 相近。然而,Vue 的组合式函数是基于 Vue 细粒度的响应性系统,这和 React hooks 的执行模型有本质上的不同。这一话题在组合式 API 的常见问题中有更细致的讨论。