vue3文档笔记

161 阅读26分钟

文章背景

只学了vue视频课总感觉还是啥也不会,现在看了文档才有些眉目,之前做的项目不理解的地方在里面都有解释。官方文档是源头,只有通读一遍才能对vue3有系统的认识。以下是我对文章通读过程中写下的我认为常用的,比较重要的知识点。

模板语法

Vue 使用一种基于 HTML 的模板语法,使我们能够声明式地将其组件实例的数据绑定到呈现的 DOM 上。所有的 Vue 模板都是语法层面合法的 HTML,可以被符合规范的浏览器和 HTML 解析器解析。

在底层机制中,Vue 会将模板编译成高度优化的 JavaScript 代码。结合响应式系统,当应用状态变更时,Vue 能够智能地推导出需要重新渲染的组件的最少数量,并应用最少的 DOM 操作。

如果你对虚拟 DOM 的概念比较熟悉,并且偏好直接使用 JavaScript,你也可以结合可选的 JSX 支持直接手写渲染函数而不采用模板。但请注意,这将不会享受到和模板同等级别的编译时优化。

原始 HTML

双大括号会将数据解释为纯文本,而不是 HTML。若想插入 HTML,你需要使用 v-html 指令

在网站上动态渲染任意 HTML 是非常危险的,因为这非常容易造成 XSS 漏洞。请仅在内容安全可信时再使用 v-html,并且永远不要使用用户提供的 HTML 内容。

*Attribute 绑定

*两种方法动态展示属性

  1. v-bind属性绑定:<div :id="dynamicId"></div>
  2. 双花括号插值语法:<div id="{{ dynamicId }}"></div>

同名简写 【Vue 3.4】

如果 attribute 的名称与绑定的 JavaScript 值的名称相同,那么可以进一步简化语法,省略 attribute 值:

<!-- 与 :id="id" 相同 -->
<div :id></div>

<!-- 这也同样有效 -->
<div v-bind:id></div>

这与在 JavaScript 中声明对象时使用的属性简写语法类似。请注意,这是一个只在 Vue 3.4 及以上版本中可用的特性。

布尔型 Attribute

布尔型 attribute 依据 true / false 值来决定 attribute 是否应该存在于该元素上。disabled 就是最常见的例子之一。

v-bind 在这种场景下的行为略有不同:

<button :disabled="isButtonDisabled">Button</button>

当 isButtonDisabled 为真值或一个空字符串 (即 <button disabled="">) 时,元素会包含这个 disabled attribute。而当其为其他假值时 attribute 将被忽略。

*动态绑定多个值

如果你有像这样的一个包含多个 attribute 的 JavaScript 对象:

const objectOfAttrs = {
  id: 'container',
  class: 'wrapper'
}

通过不带参数的 v-bind,你可以将它们绑定到单个元素上:

<div v-bind="objectOfAttrs"></div>

使用 JS 表达式:

uTools_1707811201209.png

指令 Directives

在Vue.js中,动态参数是指通过计算得到的属性名或事件名。使用动态参数能够使你的模板更加灵活,适用于一些特定的场景,其中一些典型的应用包括:

  1. 动态绑定属性名: 当你想要根据某个变量或表达式的值来动态设置属性名时,动态参数非常有用。例如,动态设置元素的 iddata-* 属性:

    htmlCopy code
    <div v-bind:[dynamicAttributeName]="value"></div>
    

    这里 dynamicAttributeName 是一个在Vue实例中的变量,它的值将作为属性名来动态绑定到元素上。

  2. 动态绑定事件名: 类似地,你可以使用动态参数来动态绑定事件名。这在处理多个相似事件时尤其有用:

    htmlCopy code
    <button v-on:[dynamicEventName]="handleClick">Click me</button>
    

    在这里,dynamicEventName 是一个变量,它的值将作为事件名来动态绑定到按钮元素上。

  3. 动态生成组件名称: 在使用动态组件时,你可能希望动态地生成组件的名称:

    htmlCopy code
    <component v-bind:is="dynamicComponentName"></component>
    

    其中 dynamicComponentName 是一个变量,它的值将作为组件的名称来动态指定要渲染的组件。

总体来说,动态参数适用于需要根据运行时的变量或条件来动态生成属性名、事件名或组件名的情况。这使得你可以更灵活地处理不同情境下的UI交互和组件渲染。

动态参数值的限制

动态参数中表达式的值应当是一个字符串,或者是 null。特殊值 null 意为显式移除该绑定。其他非字符串的值会触发警告。

动态参数语法的限制

动态参数表达式因为某些字符的缘故有一些语法限制,比如空格和引号,在 HTML attribute 名称中都是不合法的。例如下面的示例:

<!-- 这会触发一个编译器警告 -->
<a :['foo' + bar]="value"> ... </a>

uTools_1707914901236.png

响应式基础 ref / reactive

声明响应式

ref()

在模板中使用 ref 时,我们需要附加 .value。为了方便起见,当在模板中使用时,ref 会自动解包;要在组件模板中访问 ref 或函数方法,请从组件的 setup() 函数中声明并返回它们

<setup>

<script setup>
import { ref } from 'vue'
const count = ref(0)
function increment() {
  count.value++
}
</script>

<template>
  <button @click="increment"> {{ count }} </button>
</template>

为什么用 ref

当你在模板中使用了一个 ref,然后改变了这个 ref 的值时,Vue 会自动检测到这个变化,并且相应地更新 DOM。当一个组件首次渲染时,Vue 会追踪在渲染过程中使用的每一个 ref。然后,当一个 ref 被修改时,它会触发追踪它的组件的一次重新渲染。

在标准的 JavaScript 中,检测普通变量的访问或修改是行不通的。然而,我们可以通过 getter 和 setter 方法来拦截对象属性的 get 和 set 操作。

reactive 【推荐使用 ref 而非 reactive】

const obj = ref({ 
    nested: { count: 0 }, 
    arr: ['foo', 'bar'] 
})

reactive() API 有一些局限性:

  1. 有限的值类型:它只能用于对象类型 (对象、数组和如 MapSet 这样的集合类型)。它不能持有如 stringnumber 或 boolean 这样的原始类型

  2. 不能替换整个对象:由于 Vue 的响应式跟踪是通过属性访问实现的,因此我们必须始终保持对响应式对象的相同引用。这意味着我们不能轻易地“替换”响应式对象,因为这样的话与第一个引用的响应性连接将丢失:

    let state = reactive({ count: 0 })
    
    // 上面的 ({ count: 0 }) 引用将不再被追踪
    // (响应性连接已丢失!)
    state = reactive({ count: 1 })
    
  3. 对解构操作不友好:当我们将响应式对象的原始类型属性解构为本地变量时,或者将该属性传递给函数时,我们将丢失响应性连接:

    const state = reactive({ count: 0 })
    
    // 当解构时,count 已经与 state.count 断开连接
    let { count } = state
    // 不会影响原始的 state
    count++
    
    // 该函数接收到的是一个普通的数字
    // 并且无法追踪 state.count 的变化
    // 我们必须传入整个对象以保持响应性
    callSomeFunction(state.count)
    

深层响应式 / 浅层响应式

深层响应式 (Deep Reactive)

  • 特点:

    1. 递归监听: 对象的所有嵌套属性都会被递归地监听,无论嵌套有多深。
    2. 自动触发更新: 当任何嵌套属性发生变化时,Vue.js 会自动检测并触发响应式更新。
    3. 简便性: 在使用中更为方便,无需手动处理嵌套属性的更新。
  • 创建方式:

    javascriptCopy code
    import { reactive } from 'vue';
    
    const deepObj = reactive({
      nestedProp: 'value'
    });
    

浅层响应式 (Shallow Reactive)

  • 特点:

    1. 只监听顶层属性: 只有对象的顶层属性会被监听,不会递归监听嵌套属性。
    2. 手动更新嵌套属性: 直接修改嵌套属性不会触发响应式更新,需要使用 Vue.setthis.$set 来手动通知 Vue.js 进行更新。
    3. 性能优化: 在某些情况下可以提高性能,因为不需要递归监听嵌套属性。
  • 创建方式:

import { shallowReactive } from 'vue';
const shallowObj = shallowReactive({
  nestedProp: 'value'
});

// 修改顶层属性将触发响应式更新
shallowObj.nestedProp = 'new value'; // 触发响应式更新

// 修改嵌套属性将不会触发响应式更新
shallowObj.nestedProp = { newNested: 'nested value' }; // 不会触发响应式更新

// 使用 Vue.set 或 this.$set 可以手动通知 Vue.js 更新嵌套属性
import { set } from 'vue';
set(shallowObj, 'nestedProp', { newNested: 'nested value' }); // 手动触发响应式更新

如何选择

  • 性能考虑: 如果你有大量的嵌套属性但只关心顶层属性的变化,考虑使用浅层响应式以提高性能。
  • 方便性: 如果你希望能够自动处理对象的所有嵌套属性的变化,而不必手动通知 Vue.js 进行更新,选择深层响应式。
  • 嵌套深度: 如果你的数据结构嵌套深度不深,可能不会明显感受到深层响应式的性能开销,可以优先考虑使用深层响应式以方便操作。

额外的 ref 解包细节

在模板渲染上下文中,只有顶级的 ref 属性才会被解包。

在下面的例子中,count 和 object 是顶级属性,但 object.id 不是:

const count = ref(0)
const object = { id: ref(1) }

因此,这个表达式按预期工作:

{{ count + 1 }}

但这个不会

{{ object.id + 1 }}

渲染的结果将是 [object Object]1,因为在计算表达式时 object.id 没有被解包,仍然是一个 ref 对象。为了解决这个问题,我们可以将 id 解构为一个顶级属性:

const { id } = object
{{ id + 1 }}

现在渲染的结果将是 2

计算属性 computed【只读不写】

计算属性缓存 vs 方法

有这样一个对象:

const author = reactive({ name: 'John Doe',
    books: [ 
        'Vue 2 - Advanced Guide', 
        'Vue 3 - Basic Guide', 
        'Vue 4 - The Mystery' 
    ] 
})

计算属性:

<script setup>
import { reactive, computed } from 'vue'

// 一个计算属性 ref
const publishedBooksMessage = computed(() => {
  return author.books.length > 0 ? 'Yes' : 'No'
})
</script>

<template>
  <p>{{ publishedBooksMessage }}</p>
</template>

方法:

<p>{{ calculateBooksMessage() }}</p>
// 组件中
function calculateBooksMessage() {
  return author.books.length > 0 ? 'Yes' : 'No'
}

对比:

两种方式在结果上确实是完全相同的,然而,不同之处在于计算属性值会基于其响应式依赖被缓存。一个计算属性仅会在其响应式依赖更新时才重新计算。这意味着只要 author.books 不改变,无论多少次访问 publishedBooksMessage 都会立即返回先前的计算结果,而不用重复执行 getter 函数。

相比之下,方法调用总是会在重渲染发生时再次执行函数。

条件/列表渲染 v-xx

v-if

用于条件性地渲染一块内容。这块内容只会在指令的表达式返回真值时才被渲染。可以在<template>或元素上使用

<h1 v-if="awesome">Vue is awesome!</h1>

v-else

<div v-if="type === 'A'"> A </div>
<div v-else-if="type === 'B'"> B </div>
<div v-else-if="type === 'C'"> C </div>
<div v-else> Not A/B/C </div>

v-show

不能在<template>上使用

<h1 v-show="ok">Hello!</h1>

*v-if vs v-show

  1. 渲染方式:

    • v-if:真实地从 DOM 中添加或删除元素,即在条件为 true 时,元素被渲染到 DOM 中;条件为 false 时,元素被从 DOM 中移除。
    • v-show:通过 CSS 的 display 属性来控制元素的显示和隐藏。元素始终被渲染到 DOM 中,只是通过改变 display 属性来切换其可见性。
  2. 性能影响:

    • v-if:当条件变为 false 时,元素及其内部的事件监听器和子组件会被销毁,这可能会带来一些性能开销。但当条件再次变为 true 时,会重新创建元素及其状态。
    • v-show:无论条件是 true 还是 false,元素始终保持在 DOM 中,只是通过 display: none 控制可见性。不会销毁和重新创建元素,因此在切换时的性能开销较小。
  3. 适用场景:

    • v-if:适用于需要在条件满足时才渲染大量元素的情况,或者当切换条件时,希望释放资源。
    • v-show:适用于需要在同一组件之间频繁切换显示和隐藏的情况,例如通过简单的 CSS 过渡来实现动画效果。
  4. 编译时机:

    • v-if:在条件为 false 时,元素及其子元素在编译时会被完全忽略,不会被包括在最终的渲染结果中。
    • v-show:无论条件为 true 还是 false,元素及其子元素始终在编译时包含在渲染结果中。
  5. 初始渲染开销:

    • v-if:在条件首次变为 true 时,会执行元素的初始化和挂载,可能会带来一些初始渲染开销。
    • v-show:在初始渲染时,元素会被渲染到 DOM 中,然后根据条件决定是否隐藏。因此,初始渲染时的开销相对较小。

v-for

v-for="(item, key, index) in(或of)items" :key="item.xxx"

第一个参数item表示形参,第二个参数key表示,第三个参数index表示位置索引,第四个items表示源数据,key追踪节点标识来对元素进行排序【Vue 默认按照“就地更新”的策略来更新通过 v-for 渲染的元素列表。key 绑定的值期望是一个基础类型的值,例如字符串或 number 类型。不要用对象作为 v-for 的 key。】

const items = ref([{ message: 'Foo' }, { message: 'Bar' }])
<li v-for="(item, index) in items">
  {{ index }} - {{ item.message }}
</li>

/*
0 - Foo  
1 - Bar
*/
const myObject = reactive({
  title: 'How to do lists in Vue',
  author: 'Jane Doe',
  publishedAt: '2016-04-10'
})

<ul><li v-for="(value, key, index) in myObject">
    {{ index }}. {{ key }}: {{ value }}
</li></ul>

/*
0. title: How to do lists in Vue
1. author: Jane Doe
2. publishedAt: 2016-04-10
*/

还可以遍历元素/组件列表

事件处理

生命周期钩子

vuelifehook.png

侦听器

watch

watch用于在(响应式)状态变化时异步执行(回调)操作

watch 的第一个参数可以是不同形式的“数据源”:它可以是一个 ref (包括计算属性)、一个响应式对象、一个 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}`)
})

注意,你不能直接侦听响应式对象的属性值,例如:

const obj = reactive({ count: 0 })

// 错误,因为 watch() 得到的参数是一个 number
watch(obj.count, (count) => {
  console.log(`count is: ${count}`)
})

这里需要用一个返回该属性的 getter 函数:

// 提供一个 getter 函数
watch(
  () => obj.count,
  (count) => {
    console.log(`count is: ${count}`)
  }
)

watch第二个参数为被监视数据的变化

watch(myData, (newValue, oldValue) => { 
    console.log('myData 发生变化:', newValue, oldValue)
    // 在这里可以根据新值和旧值执行相应的逻辑 });

watch第三个参数:我们可以通过传入 immediate: true 选项来强制侦听器的回调立即执行:

watch(
  source,
  (newValue, oldValue) => {
    // 立即执行,且当 `source` 改变时再次执行
  },
  { immediate: true }
)

watchEffect

watch 和 watchEffect 都能响应式地执行有副作用的回调。它们之间的主要区别是追踪响应式依赖的方式:

  • watch 只追踪明确侦听的数据源。它不会追踪任何在回调中访问到的东西。另外,仅在数据源确实改变时才会触发回调。watch 会避免在发生副作用时追踪依赖,因此,我们能更加精确地控制回调函数的触发时机。
  • watchEffect,则会在副作用【更改 DOM,或根据异步操作的结果去修改另一处的状态。】发生期间追踪依赖。它会在同步执行过程中,自动追踪所有能访问到的响应式属性。这更方便,而且代码往往更简洁,但有时其响应性依赖关系会不那么明确。
//当todoId的引用发生变化时使用侦听器来加载一个远程资源:
const todoId = ref(1)
const data = ref(null)

// 使用watch
watch(
  todoId,
  async () => {
    const response = await fetch(
      `https://jsonplaceholder.typicode.com/todos/${todoId.value}`
    )
    data.value = await response.json()
  },
  { immediate: true }
)

//使用watchEffect
watchEffect(async () => {
  const response = await fetch(
    `https://jsonplaceholder.typicode.com/todos/${todoId.value}`
  )
  data.value = await response.json()
})

深入组件

组件注册

全局注册

全局注册后,在使用时无需导入直接使用

app 
    .component('ComponentA', ComponentA) 
    .component('ComponentB', ComponentB)

局部注册

使用了<script setup>导入就使用无需注册

<script setup>
import ComponentA from './ComponentA.vue'
</script>

<template>
  <ComponentA />
</template>

没有使用<script setup>则需要components显示注册

import ComponentA from './ComponentA.js'

export default {
  components: {
    ComponentA    // ComponentA: ComponentA
  },
  setup() {
    // ...
  }
}

动态组件

动态加载组件。如:Tab界面

<!-- currentTab 改变时组件也改变 -->
<component :is="tabs[currentTab]"></component>

当使用 <component :is="..."> 来在多个组件间作切换时,被切换掉的组件会被卸载。我们可以通过 <KeepAlive> 组件强制被切换掉的组件仍然保持“存活”的状态。

Props【只定义初始值不改】

  • 所有 prop 默认都是可选的,除非声明了 required: true

在使用 <script setup> 的单文件组件中,props 可以使用 defineProps() 宏来声明:

<script setup>
const props = defineProps(['foo'])

console.log(props.foo)
</script>

在没有使用 <script setup> 的组件中,prop 可以使用 props 选项来声明:

export default {
  props: ['foo'],
  setup(props) {
    // setup() 接收 props 作为第一个参数
    console.log(props.foo)
  }
}

使用一个对象绑定多个 props

const post = {
  id: 1,
  title: 'My Journey with Vue'
}

<BlogPost v-bind="post" />

而这实际上等价于:

<BlogPost :id="post.id" :title="post.title" />

*单向数据流

所有的 props 都遵循着单向绑定原则,props 因父组件的更新而变化,自然地将新的状态向下流往子组件,而不会逆向传递。这避免了子组件意外修改父组件的状态的情况。

另外,每次父组件更新后,所有的子组件中的 props 都会被更新到最新值,这意味着你不应该在子组件中去更改一个 prop

组件 v-model

v-model 可以在组件上使用以实现双向绑定。 从 Vue 3.4 开始,推荐的实现方式是使用宏:

绑定一个值

<script setup>
const msg = ref('Hello World!')
</script>

<template>
  <h1>{{ msg }}</h1>
  <Child v-model="msg" />
</template>
<script setup>
const model = defineModel()
</script>

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

uTools_1708344645914.png

绑定多个有参数的变量

<script setup>
import { ref } from 'vue'
import UserName from './UserName.vue'

const first = ref('John')
const last = ref('Doe')
</script>

<template>
  <h1>{{ first }} {{ last }}</h1>
  <UserName
    v-model:first-name="first"
    v-model:last-name="last"
  />
</template>
<script setup>
const firstName = defineModel('firstName')
const lastName = defineModel('lastName')
</script>

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

uTools_1708345250125.png

透传 Attributes

Attributes 继承

“透传 attribute”指的是传递给一个组件,却没有被该组件声明为 props 或 emits 的 attribute 或者 v-on 事件监听器。最常见的例子就是 classstyle 和 id

当一个组件以单个元素为根作渲染时,透传的 attribute 会自动被添加到根元素上。举例来说,假如我们有一个 <MyButton> 组件,它的模板长这样:

<!-- <MyButton> 的模板 -->
<button>click me</button>

一个父组件使用了这个组件,并且传入了 class

<MyButton class="large" />

最后渲染出的 DOM 结果是:

<button class="large">click me</button>

这里,<MyButton> 并没有将 class 声明为一个它所接受的 prop,所以 class 被视作透传 attribute,自动透传到了 <MyButton> 的根元素上。

禁用 Attributes 继承

如果你不想要一个组件自动地继承 attribute,你可以在组件选项中设置 inheritAttrs: false

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

插槽 slots

default插槽

插槽用于组件接受模板内容,<slot> 元素是一个插槽出口 (slot outlet),标示了父组件提供的插槽内容 (slot content) 将在子组件那里被渲染。

slot.png

<FancyButton>
  Click me! <!-- 插槽内容 -->
</FancyButton>
<button class="fancy-btn">
  <slot></slot> <!-- 插槽出口 -->
</button>

最终渲染出的 DOM 是这样:

<button class="fancy-btn">Click me!</button>

具名插槽

一个组件中包含多个插槽出口。v-slot 有对应的简写 #,因此 <template v-slot:header> 可以简写为 <template #header>

<div class="container">
  <header>
    <slot name="header"></slot>
  </header>
  <main>
    <slot></slot>
  </main>
  <footer>
    <slot name="footer"></slot>
  </footer>
</div>
<BaseLayout>
  <template v-slot:header>  <!--简写为<template #header>-->
    <!-- header 插槽的内容放这里 -->
  </template>
</BaseLayout>

name-slots.png

动态插槽名

动态指令参数在 v-slot 上也是有效的,即可以定义下面这样的动态插槽名:

<base-layout>
  <template v-slot:[dynamicSlotName]>
    ...
  </template>

  <!-- 缩写为 -->
  <template #[dynamicSlotName]>
    ...
  </template>
</base-layout>

注意这里的表达式和动态指令参数受相同的语法限制

依赖注入 Provide / Inject

解决多级嵌套组件数据传递的问题

provide-inject.png

provide 函数接收两个参数。第一个参数被称为注入名,可以是一个字符串或是一个 Symbol。后代组件会用注入名来查找期望注入的值。一个组件可以多次调用 provide(),使用不同的注入名,注入不同的依赖值。

第二个参数是提供的值,值可以是任意类型,包括响应式的状态,比如一个 ref:

import { ref, provide } from 'vue'

const count = ref(0)
provide('key', count)

要注入上层组件提供的数据,需使用 inject 函数:(注入进来的值不会解包仍是ref)

const key = inject('key')

?异步组件

逻辑复用

组合式函数 【命名为useXxx】

组合式函数是一个利用 Vue 的组合式 API 来封装和复用有状态逻辑的函数。相比之下,有状态逻辑负责管理会随时间而变化的状态。如跟踪当前鼠标在页面中的位置、触摸手势或与数据库的连接状态等。

例如:鼠标跟踪器:在多个组件中复用这个相同的逻辑

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>

效果:

uTools_1708341219530.png

使用限制

组合式函数只能在 <script setup> 或 setup() 钩子中被调用。在这些上下文中,它们也只能被同步调用。在某些情况下,你也可以在像 onMounted() 这样的生命周期钩子中调用它们。

这些限制很重要,因为这些是 Vue 用于确定当前活跃的组件实例的上下文。访问活跃的组件实例很有必要,这样才能:

  1. 将生命周期钩子注册到该组件实例上
  2. 将计算属性和监听器注册到该组件实例上,以便在该组件被卸载时停止监听,避免内存泄漏。

自定义指令

一个自定义指令由一个包含类似组件生命周期钩子的对象来定义。钩子函数会接收到指令所绑定元素作为其参数。下面是一个自定义指令的例子,当一个 input 元素被 Vue 插入到 DOM 中后,它会被自动聚焦:

使用<script setup>

<script setup>
// 在模板中启用 v-focus
const vFocus = {
  mounted: (el) => el.focus()
}
</script>

<template>
  <input v-focus />
</template>

假设你还未点击页面中的其他地方,那么上面这个 input 元素应该会被自动聚焦。该指令比 autofocus attribute 更有用,因为它不仅仅可以在页面加载完成后生效,还可以在 Vue 动态插入元素后生效。

不使用 <script setup>

自定义指令需要通过 directives 选项注册:

export default {
  setup() {
    /*...*/
  },
  directives: {
    // 在模板中启用 v-focus
    focus: {
      /* ... */
    }
  }
}

自定义指令全局注册到应用层级:

const app = createApp({})

// 使 v-focus 在所有组件中都可用
app.directive('focus', {
  /* ... */
})

插件

介绍

插件 (Plugins) 是一种能为 Vue 添加全局功能的工具代码。下面是如何安装一个插件的示例:

import { createApp } from 'vue'

const app = createApp({})

app.use(myPlugin, {
  /* 可选的选项 */
})

一个插件可以是一个拥有 install() 方法的对象,也可以直接是一个安装函数本身。安装函数会接收到安装它的应用实例和传递给 app.use() 的额外选项作为参数:

const myPlugin = {
  install(app, options) {
    // 配置此应用
  }
}

插件没有严格定义的使用范围,但是插件发挥作用的常见场景主要包括以下几种:

  1. 通过 app.component() 和 app.directive() 注册一到多个全局组件或自定义指令。
  2. 通过 app.provide() 使一个资源可被注入进整个应用。
  3. 向 app.config.globalProperties 中添加一些全局实例属性或方法
  4. 一个可能上述三种都包含了的功能库 (例如 vue-router)。

编写一个插件

我们希望有一个翻译函数,这个函数接收一个以 . 作为分隔符的 key 字符串,用来在用户提供的翻译字典中查找对应语言的文本。期望的使用方式如下:

<h1>{{ $translate('greetings.hello') }}</h1>

让这个函数在任意模板中被全局调用:通过在插件中将它添加到 app.config.globalProperties 上来实现:

// plugins/i18n.js
export default {
  install: (app, options) => {
    // 注入一个全局可用的 $translate() 方法
    app.config.globalProperties.$translate = (key) => {
      // 获取 `options` 对象的深层属性
      // 使用 `key` 作为索引
      return key.split('.').reduce((o, i) => {
        if (o) return o[i]
      }, options)
    }
  }
}

我们的 $translate 函数会接收一个例如 greetings.hello 的字符串,在用户提供的翻译字典中查找,并返回翻译得到的值。 用于查找的翻译字典对象则应当在插件被安装时作为 app.use() 的额外参数被传入:

import i18nPlugin from './plugins/i18n'

app.use(i18nPlugin, {
  greetings: {
    hello: 'Bonjour!'
  }
})

这样,我们一开始的表达式 $translate('greetings.hello') 就会在运行时被替换为 Bonjour! 了。

内置组件

Transition

用于在一个元素或组件进入和离开 DOM 时应用动画。

TransitionGroup

用于对 v-for 列表中的元素或组件的插入、移除和顺序改变添加动画效果。

KeepAlive

用于在多个组件间动态切换时缓存被移除的组件实例。

基本使用

默认情况下,一个组件实例在被替换掉后会被销毁。这会导致它丢失其中所有已变化的状态——当这个组件再一次被显示时,会创建一个只带有初始状态的新实例。

要组件能在被“切走”的时候保留它们的状态,使用<KeepAlive>(例子)

<!-- 非活跃的组件将会被缓存! -->
<KeepAlive>
  <component :is="activeComponent" />
</KeepAlive>

Teleport

用于将一个组件内部的一部分模板“传送”到该组件的 DOM 结构外层的位置去。

基本用法

举例:把以下模板片段传送到 body 标签下”。

<Teleport to="body">
  <div v-if="open" class="modal">
    <p>Hello from the modal!</p>
    <button @click="open = false">Close</button>
  </div>
</Teleport>

禁用 Teleport在某些场景下可能需要视情况禁用 <Teleport>。举例来说,我们想要在桌面端将一个组件当做浮层来渲染,但在移动端则当作行内组件。我们可以通过对 <Teleport> 动态地传入一个 disabled prop 来处理这两种不同情况。

// isMobile 状态可以根据 CSS media query 的不同结果动态地更新。
<Teleport :disabled="isMobile">
  ...
</Teleport>

多个 Teleport 共享目标

一个可重用的模态框组件可能同时存在多个实例。对于此类场景,多个 <Teleport> 组件可以将其内容挂载在同一个目标元素上,而顺序就是简单的顺次追加,后挂载的将排在目标元素下更后面的位置上。

<Teleport to="#modals">
  <div>A</div>
</Teleport>
<Teleport to="#modals">
  <div>B</div>
</Teleport>

渲染的结果为:

<div id="modals">
  <div>A</div>
  <div>B</div>
</div>

?Suspense 【目前不稳定】

用于在组件树中协调对异步依赖的处理。它让我们可以在组件树上层等待下层的多个嵌套异步依赖项解析完成,并可以在等待时渲染一个加载状态。

应用规模化(简单过)

工具链

浏览器内模板编译注意事项

为了减小打包出的客户端代码体积,Vue 提供了多种格式的“构建文件”以适配不同场景下的优化需求。

  • 前缀为 vue.runtime.* 的文件是只包含运行时的版本:不包含编译器,当使用这个版本时,所有的模板都必须由构建步骤预先编译。
  • 名称中不包含 .runtime 的文件则是完全版:即包含了编译器,并支持在浏览器中直接编译模板。然而,体积也会因此增长大约 14kb。

默认的工具链中都会使用仅含运行时的版本,因为所有 SFC 中的模板都已经被预编译了。如果因为某些原因,在有构建步骤时,你仍需要浏览器内的模板编译,你可以更改构建工具配置,将 vue 改为相应的版本 vue/dist/vue.esm-bundler.js

如果你需要一种更轻量级,不依赖构建步骤的替代方案,也可以看看 petite-vue

路由

状态管理

测试

服务端渲染 SSR

进阶主题

组合式API

与react hook相比:

  • Vue 的响应性系统运行时会自动收集计算属性和侦听器的依赖,因此无需手动声明依赖。
  • 无需手动缓存回调函数来避免不必要的组件更新。Vue 细粒度的响应性系统能够确保在绝大部分情况下组件仅执行必要的更新。对 Vue 开发者来说几乎不怎么需要对子组件更新进行手动优化。

渲染机制

1. 模板编译: Vue 应用开始时,Vue 会首先对模板进行编译。模板编译过程将模板转换为渲染函数。渲染函数是一个 JavaScript 函数,负责根据数据创建 Virtual DOM。

2. 创建 Virtual DOM: 通过执行渲染函数,Vue 创建了一个 Virtual DOM 树。Virtual DOM 是一个轻量的、内存中的虚拟副本,反映了真实 DOM 的结构。

3. 初始化: 首次渲染时,Vue 会将 Virtual DOM 转化成真实的 DOM,并挂载到页面上。

4. 响应式系统: Vue 的响应式系统会追踪数据的变化。当数据发生变化时,响应式系统会通知相关的 Watcher(观察者)。

5. Watcher: Watcher 是负责侦听数据变化的对象。当数据发生变化时,Watcher 接收通知,并执行更新操作。

6. Reactivity(响应式): Vue 使用 Object.defineProperty 或 Proxy 等技术来实现数据的响应式。这使得当数据发生变化时,能够触发相关的 Watcher。

7. 更新 Virtual DOM: 当数据变化时,Vue 会重新执行渲染函数,生成新的 Virtual DOM。

8. Diff 算法: Vue 使用 Diff 算法比较新旧 Virtual DOM 的差异。这个算法会找到最小的更新操作,以最优的方式更新真实 DOM。

9. Patch(打补丁): Vue 将根据 Diff 算法的结果,生成一系列 DOM 操作,然后将这些操作应用到真实 DOM 上,从而更新用户界面。

整个流程是一个自动的过程,Vue 在数据变化时,能够智能地更新只发生变化的部分,而不需要手动干预 DOM 操作。这种方式使得 Vue 具有高效的渲染性能和良好的开发体验。Vue 的渲染机制通过 Virtual DOM 和响应式系统的结合,实现了高效的数据更新和视图渲染。

渲染函数 & JSX

创建Vnodes

Vue 提供了一个 h() 函数用于创建 vnodes:(h() 是 hyperscript 的简称,意思是“能生成 HTML (超文本标记语言) 的 JavaScript”)

import { h } from 'vue'

const vnode = h(
  'div', // type
  { id: 'foo', class: 'bar' }, // props
  [
    /* children */
  ]
)

Vnodes 必须唯一

组件树中的 vnodes 必须是唯一的。下面是错误示范:

function render() {
  const p = h('p', 'hi')
  return h('div', [
    // 啊哦,重复的 vnodes 是无效的
    p,
    p
  ])
}

如果你真的非常想在页面上渲染多个重复的元素或者组件,你可以使用一个工厂函数来做这件事。比如下面的这个渲染函数就可以完美渲染出 20 个相同的段落:

function render() {
  return h(
    'div',
    Array.from({ length: 20 }).map(() => {
      return h('p', 'hi')
    })
  )
}

声明渲染函数【返回的只能是函数!】

当组合式 API 与模板一起使用时,setup() 钩子的返回值是用于暴露数据给模板。然而当我们使用渲染函数时,可以直接把渲染函数返回:

import { ref, h } from 'vue'

export default {
  props: {
    /* ... */
  },
  setup(props) {
    const count = ref(1)

    // 返回渲染函数
    return () => h('div', props.msg + count.value)
  }
}

在 setup() 内部声明的渲染函数天生能够访问在同一范围内声明的 props 和许多响应式状态。

除了返回一个 vnode,你还可以返回字符串或数组:

export default {
  setup() {
    return () => 'hello world!'
  }
}
import { h } from 'vue'

export default {
  setup() {
    // 使用数组返回多个根节点
    return () => [
      h('div'),
      h('div'),
      h('div')
    ]
  }
}

JSX

JSX 是 JavaScript 的一个类似 XML 的扩展,有了它,我们可以用以下的方式来书写代码:

const vnode = <div>hello</div>

在 JSX 表达式中,使用大括号来嵌入动态值:

const vnode = <div id={dynamicId}>hello, {userName}</div>

create-vue 和 Vue CLI 都有预置的 JSX 语法支持。如果你想手动配置 JSX,请参阅 @vue/babel-plugin-jsx 文档获取更多细节。

虽然最早是由 React 引入,但实际上 JSX 语法并没有定义运行时语义,并且能被编译成各种不同的输出形式。如果你之前使用过 JSX 语法,那么请注意 Vue 的 JSX 转换方式与 React 中 JSX 的转换方式不同,因此你不能在 Vue 应用中使用 React 的 JSX 转换。与 React JSX 语法的一些明显区别包括:

  • 可以使用 HTML attributes 比如 class 和 for 作为 props - 不需要使用 className 或 htmlFor
  • 传递子元素给组件 (比如 slots) 的方式不同

Vue 的类型定义也提供了 TSX 语法的类型推导支持。当使用 TSX 语法时,确保在 tsconfig.json 中配置了 "jsx": "preserve",这样的 TypeScript 就能保证 Vue JSX 语法转换过程中的完整性。

动画技巧