响应式基础
在组合式 API 中,推荐使用 ref()函数来声明响应式状态,
ref()
接收参数,并将其包裹在一个带有 .value
属性的 ref 对象中返回
该 .value
属性给予了 Vue 一个机会来检测 ref 何时被访问或修改(使用.value解包)
Vue 会在“next tick”更新周期中缓冲所有状态的修改,要等待 DOM 更新完成后再执行额外的代码,可以使用 nextTick() 全局 API
reactive()
的局限性:有限的值类型(只有对象类型 ) 不能替换整个对象 (不能“替换”响应式对象) 对解构操作不友好(解构为本地变量时,将丢失响应性连接)
额外的 ref 解包细节(作为 reactive 对象的属性):
一个 ref 会在作为响应式对象的属性被访问或修改时自动解包
与 reactive 对象不同的是,当 ref 作为响应式数组或原生集合类型(如 Map
) 中的元素被访问时,它不会被解包
在模板渲染上下文中,只有顶级的 ref 属性才会被解包。
计算属性
推荐使用计算属性来描述依赖响应式状态的复杂逻辑
计算属性值会基于其响应式依赖被缓存。一个计算属性仅会在其响应式依赖更新时才重新计算。相比之下,方法调用总是会在重渲染发生时再次执行函数。
生命周期钩子
最常用的是 onMounted、onUpdated 和 onUnmounted。
侦听器
watch 函数在每次响应式状态发生变化时触发回调函数
watch
vs. watchEffect
watch
和 watchEffect
都能响应式地执行有副作用的回调。它们之间的主要区别是追踪响应式依赖的方式:
-
watch
只追踪明确侦听的数据源。它不会追踪任何在回调中访问到的东西。另外,仅在数据源确实改变时才会触发回调。watch
会避免在发生副作用时追踪依赖,因此,我们能更加精确地控制回调函数的触发时机。 -
watchEffect
,则会在副作用发生期间追踪依赖。它会在同步执行过程中,自动追踪所有能访问到的响应式属性。这更方便,而且代码往往更简洁,但有时其响应性依赖关系会不那么明确watchEffect(async () => { const response = await fetch( `https://jsonplaceholder.typicode.com/todos/${todoId.value}` ) data.value = await response.json() })
这个例子中,回调会立即执行,不需要指定
immediate: true
。在执行期间,它会自动追踪todoId.value
作为依赖(和计算属性类似)。每当todoId.value
变化时,回调会再次执行。有了watchEffect()
,我们不再需要明确传递todoId
作为源值。
默认情况下,用户创建的侦听器回调,都会在 Vue 组件更新之前被调用。这意味着你在侦听器回调中访问的 DOM 将是被 Vue 更新之前的状态。
如果想在侦听器回调中能访问被 Vue 更新之后的 DOM,你需要指明 flush: 'post'
选项:
watch(source, callback, {
flush: 'post'
})
watchEffect(callback, {
flush: 'post'
})
后置刷新的 watchEffect()
有个更方便的别名 watchPostEffect()
:
注意,需要异步创建侦听器的情况很少,请尽可能选择同步创建。如果需要等待一些异步数据,你可以使用条件式的侦听逻辑:
// 需要异步请求得到的数据
const data = ref(null)
watchEffect(() => {
if (data.value) {
// 数据加载后执行某些操作...
}
})
模板引用
直接访问底层 DOM 元素,使用 ref
attribute,在组件挂载后才能访问模板引用
<input ref="input">
模板引用也可以被用在一个子组件上。这种情况下引用中获得的值是组件实例:
<script setup>
import { ref, onMounted } from 'vue'
import Child from './Child.vue'
const child = ref(null)
onMounted(() => {
// child.value 是 <Child /> 组件的实例
})
</script>
<template>
<Child ref="child" />
</template>
如果一个子组件使用的是选项式 API 或没有使用 <script setup>
,被引用的组件实例和该子组件的 this
完全一致,这意味着父组件对子组件的每一个属性和方法都有完全的访问权。这使得在父组件和子组件之间创建紧密耦合的实现细节变得很容易,当然也因此,应该只在绝对需要时才使用组件引用。大多数情况下,你应该首先使用标准的 props 和 emit 接口来实现父子组件交互。
有一个例外的情况,使用了 <script setup>
的组件是默认私有的:一个父组件无法访问到一个使用了 <script setup>
的子组件中的任何东西,除非子组件在其中通过 defineExpose
宏显式暴露
组件基础
传递 props 与 监听事件
App.vue
<script setup>
import { ref } from 'vue'
import BlogPost from './BlogPost.vue'
const posts = ref([
{ id: 1, title: 'My journey with Vue' },
{ id: 2, title: 'Blogging with Vue' },
{ id: 3, title: 'Why Vue is so fun' }
])
const postFontSize = ref(1)
</script>
<template>
<div :style="{ fontSize: postFontSize + 'em' }">
<BlogPost
v-for="post in posts"
:key="post.id"
:title="post.title"
@enlarge-text="postFontSize += 0.1"
></BlogPost>
</div>
</template>
BlogPost.vue
<script setup>
defineProps(['title'])
defineEmits(['enlarge-text'])
</script>
<template>
<div class="blog-post">
<h4>{{ title }}</h4>
<button @click="$emit('enlarge-text')">Enlarge text</button>
</div>
</template>
defineProps
和defineEmits
仅可用于 <script setup>
之中,并且不需要导入,
defineProps
会返回一个对象(props.title),其中包含了可以传递给组件的所有 props:
const props = defineProps(['title'])
console.log(props.title)
通过插槽来分配内容
一些情况下我们会希望能和 HTML 元素一样向组件中传递内容:
App.vue
<script setup>
import AlertBox from './AlertBox.vue'
</script>
<template>
<AlertBox>
Something bad happened.
</AlertBox>
</template>
这可以通过 Vue 的自定义 <slot>
元素来实现:
AlertBox.vue
<template>
<div class="alert-box">
<strong>Error!</strong>
<br/>
<slot />
</div>
</template>
<style scoped>
.alert-box {
color: #666;
border: 1px solid red;
border-radius: 4px;
padding: 20px;
background-color: #f8f8f8;
}
strong {
color: red;
}
</style>
动态组件
有些场景会需要在两个组件间来回切换,比如 Tab 界面:
上面的例子是通过 Vue 的 <component>
元素和特殊的 is
attribute 实现的:
<!-- currentTab 改变时组件也改变 -->
<component :is="tabs[currentTab]"></component>
在上面的例子中,被传给 :is
的值可以是以下几种:
- 被注册的组件名
- 导入的组件对象
你也可以使用 is
attribute 来创建一般的 HTML 元素。
当使用 <component :is="...">
来在多个组件间作切换时,被切换掉的组件会被卸载。我们可以通过 <KeepAlive> 组件强制被切换掉的组件仍然保持“存活”的状态。
组件注册
全局注册
我们可以使用 Vue 应用实例的 .component()
方法,让组件在当前 Vue 应用中全局可用。
import { createApp } from 'vue'
import MyComponent from './App.vue'
const app = createApp({})
app.component('MyComponent', MyComponent)
.component()
方法可以被链式调用:
app
.component('ComponentA', ComponentA)
.component('ComponentB', ComponentB)
.component('ComponentC', ComponentC)
全局注册的组件可以在此应用的任意组件的模板中使用:
<!-- 这在当前应用的任意组件中都可用 -->
<ComponentA/>
<ComponentB/>
<ComponentC/>
所有的子组件也可以使用全局注册的组件,这意味着这三个组件也都可以在彼此内部使用。
局部注册
全局注册虽然很方便,但有以下几个问题:
-
全局注册,但并没有被使用的组件无法在生产打包时被自动移除 (也叫“tree-shaking”)。如果你全局注册了一个组件,即使它并没有被实际使用,它仍然会出现在打包后的 JS 文件中。
-
全局注册在大型项目中使项目的依赖关系变得不那么明确。在父组件中使用子组件时,不太容易定位子组件的实现。和使用过多的全局变量一样,这可能会影响应用长期的可维护性。
相比之下,局部注册的组件需要在使用它的父组件中显式导入,并且只能在该父组件中使用。它的优点是使组件之间的依赖关系更加明确,并且对 tree-shaking 更加友好。
在使用 <script setup>
的单文件组件中,导入的组件可以直接在模板中使用,无需注册:
<script setup>
import ComponentA from './ComponentA.vue'
</script>
<template>
<ComponentA />
</template>
请注意:局部注册的组件在后代组件中并不可用。在这个例子中,ComponentA
注册后仅在当前组件可用,而在任何的子组件或更深层的子组件中都不可用。
组件名格式
使用 PascalCase 作为组件名的注册格式
为了方便,Vue 支持将模板中使用 kebab-case 的标签解析为使用 PascalCase 注册的组件。这意味着一个以 MyComponent
为名注册的组件,在模板中可以通过 <MyComponent>
或 <my-component>
引用。这让我们能够使用同样的 JavaScript 组件注册代码来配合不同来源的模板。
组件 v-model
基本用法
v-model
可以在组件上使用以实现双向绑定,使用 defineModel() 宏。
多个 v-model
绑定
//父组件
<UserName
v-model:first-name="first"
v-model:last-name="last"
/>
//子组件
<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>
透传 Attributes
“透传 attribute”指的是传递给一个组件,却没有被该组件声明为 props 或 emits 的 attribute 或者 v-on
事件监听器。最常见的例子就是 class
、style
和 id
插槽 Slots
为子组件传递一些模板片段
依赖注入
父组件向其所有的后代组件,传递数据
异步组件
从服务器加载相关组件 ,emits 方法来实现此功能
状态管理
<script setup>
import { ref } from 'vue'
// 状态
const count = ref(0)
// 动作
function increment() {
count.value++
}
</script>
<!-- 视图 -->
<template>{{ count }}</template>
它是一个独立的单元,由以下几个部分组成:
- 状态:驱动整个应用的数据源;
- 视图:对状态的一种声明式映射;
- 交互:状态根据用户在视图中的输入而作出相应变更的可能方式。
下面是“单向数据流”这一概念的简单图示:
用响应式 API 做简单状态管理
状态在多个组件实例间共享,你可以使用 reactive() (或者ref(),
computed()
,组合式函数来返回一个全局状态)来创建一个响应式对象,并将它导入到多个组件中