Docs
Vue (读音 /vjuː/,类似于view) 是一套用于构建用户界面的渐进式框架。与其它大型框架不同的是,Vue 被设计为可以自底向上逐层应用。Vue 的核心库只关注视图层,不仅易于上手,还便于与第三方库或既有项目整合。
Advanced Guides
Reactivity
- 深入响应性原理
- 什么是响应性?
- 响应性是一种允许我们以声明式的方式去适应变化的一种编程范例。
2. vue如何追踪变化?
- 当把一个普通的 JavaScript 对象作为
data
选项传给应用或组件实例的时候,Vue 会使用带有 getter 和 setter 的处理程序遍历其所有 property 并将其转换为Proxy。这是 ES6 仅有的特性,但是我们在 Vue 3 版本也使用了Object.defineProperty来支持 IE 浏览器。 - proxy对象
3. 侦听器
- 每个组件实例都有一个相应的侦听器实例,该实例将在组件渲染期间把“触碰”的所有 property 记录为依赖项。之后,当触发依赖项的 setter 时,它会通知侦听器,从而使得组件重新渲染。
- 响应式原理
声明响应式状态
import {
reactive } from 'vue'
// 响应式状态
const state = reactive({
count: 0
})
reactive相当于 Vue 2.x 中的Vue.observable()API,该 API 返回一个响应式的对象状态。该响应式转换是“深度转换”——它会影响嵌套对象传递的所有 property。
Vue 中响应式状态的基本用例是我们可以在渲染期间使用它。因为依赖跟踪的关系,当响应式状态改变时视图会自动更新。
这就是 Vue 响应式系统的本质。当从组件中的data()返回一个对象时,它在内部交由reactive()使其成为响应式对象。模板会被编译成能够使用这些响应式 property 的渲染函数。
创建独立的响应式值作为
refs
ref
会返回一个可变的响应式对象,该对象作为它的内部值——一个响应式的引用,这就是名称的来源。此对象只包含一个名为value
的 property:
import {
ref } from 'vue'
const count = ref(0)
console.log(count.value) // 0
count.value++
console.log(count.value) // 1
Ref 展开:
当 ref 作为渲染上下文 (从setup ()中返回的对象) 上的 property 返回并可以在模板中被访问时,它将自动展开为内部值。不需要在模板中追加.value
访问响应式对象:
- 当
ref
作为响应式对象的 property 被访问或更改时,为使其行为类似于普通 property,它会自动展开内部值: - 如果将新的 ref 赋值给现有 ref 的 property,将会替换旧的 ref:
- Ref 展开仅发生在被响应式
Object
嵌套的时候。当从Array
或原生集合类型如Map
访问 ref 时,不会进行展开:
响应式状态解构
当我们想使用大型响应式对象的一些 property 时,可能很想使用ES6 解构来获取我们想要的 property:
import {
reactive } from 'vue'
const book = reactive({
author: 'Vue Team',
year: '2020',
title: 'Vue 3 Guide',
description: 'You are reading this book right now ;)',
price: 'free'
})
let {
author, title } = book
遗憾的是,使用解构的两个 property 的响应式都会丢失。对于这种情况,我们需要将我们的响应式对象转换为一组 ref。这些 ref 将保留与源对象的响应式关联:
import {
reactive, toRefs } from 'vue'
const book = reactive({
author: 'Vue Team',
year: '2020',
title: 'Vue 3 Guide',
description: 'You are reading this book right now ;)',
price: 'free'
})
let {
author, title } = toRefs(book)
title.value = 'Vue 3 Detailed Guide' // 我们需要使用 .value 作为标题,现在是 ref
console.log(book.title) // 'Vue 3 Detailed Guide'
使用
readonly
防止更改响应式对象
有时我们想跟踪响应式对象 (ref
或reactive
) 的变化,但我们也希望防止在应用程序的某个位置更改它。例如,当我们有一个被provide的响应式对象时,我们不想让它在注入的时候被改变。为此,我们可以基于原始对象创建一个只读的 Proxy 对象:
import {
reactive, readonly } from 'vue'
const original = reactive({
count: 0 })
const copy = readonly(original)
// 在copy上转换original 会触发侦听器依赖
original.count++
// 转换copy 将导失败并导致警告
copy.count++ // 警告: "Set operation on key 'count' failed: target is readonly."
- 响应式计算和侦听
计算值
有时我们需要依赖于其他状态的状态——在 Vue 中,这是用组件计算属性处理的,以直接创建计算值,我们可以使用computed
方法:它接受 getter 函数并为 getter 返回的值返回一个不可变的响应式ref对象。
const count = ref(1)
const plusOne = computed(() => count.value++)
console.log(plusOne.value) // 2
plusOne.value++ // error
或者,它可以使用一个带有get
和set
函数的对象来创建一个可写的 ref 对象。
const count = ref(1)
const plusOne = computed({
get: () => count.value + 1,
set: val => {
count.value = val - 1
}
})
plusOne.value = 1
console.log(count.value) // 0
watchEffect
为了根据反应状态自动应用和重新应用副作用,我们可以使用watchEffect
方法。它立即执行传入的一个函数,同时响应式追踪其依赖,并在其依赖变更时重新运行该函数。
const count = ref(0)
watchEffect(() => console.log(count.value))
// -> logs 0
setTimeout(() => {
count.value++
// -> logs 1
}, 100)
1.停止侦听
当watchEffect
在组件的setup()函数或生命周期钩子被调用时,侦听器会被链接到该组件的生命周期,并在组件卸载时自动停止。
在一些情况下,也可以显式调用返回值以停止侦听:
const stop = watchEffect(() => {
/* ... */
})
// later
stop()
2.清除副作用
有时副作用函数会执行一些异步的副作用,这些响应需要在其失效时清除 (即完成之前状态已改变了) 。所以侦听副作用传入的函数可以接收一个 onInvalidate
函数作入参,用来注册清理失效时的回调。当以下情况发生时,这个失效回调会被触发:
- 副作用即将重新执行时
- 侦听器被停止 (如果在
setup()
或生命周期钩子函数中使用了watchEffect
,则在组件卸载时)
watchEffect(onInvalidate => {
const token = performAsyncOperation(id.value)
onInvalidate(() => {
// id has changed or watcher is stopped.
// invalidate previously pending async operation
token.cancel()
})
})
我们之所以是通过传入一个函数去注册失效回调,而不是从回调返回它,是因为返回值对于异步错误处理很重要。
在执行数据请求时,副作用函数往往是一个异步函数:
const data = ref(null)
watchEffect(async onInvalidate => {
onInvalidate(() => {...}) // 我们在Promise解析之前注册清除函数
data.value = await fetchData(props.id)
})
我们知道异步函数都会隐式地返回一个 Promise,但是清理函数必须要在 Promise 被 resolve 之前被注册。另外,Vue 依赖这个返回的 Promise 来自动处理 Promise 链上的潜在错误。
3.副作用刷新时机
Vue 的响应式系统会缓存副作用函数,并异步地刷新它们,这样可以避免同一个“tick” 中多个状态改变导致的不必要的重复调用。在核心的具体实现中,组件的update
函数也是一个被侦听的副作用。当一个用户定义的副作用函数进入队列时,默认情况下,会在所有的组件update
前执行:
<template>
<div>{
{
count }}</div>
</template>
<script>
export default {
setup() {
const count = ref(0)
watchEffect(() => {
console.log(count.value)
})
return {
count
}
}
}
</script>
在这个例子中:
count
会在初始运行时同步打印出来- 更改
count
时,将在组件更新前执行副作用。
如果需要在组件更新后重新运行侦听器副作用,我们可以传递带有flush
选项的附加options
对象 (默认为'pre'
):
// fire before component updates
watchEffect(
() => {
/* ... */
},
{
flush: 'post'
}
)
flush
选项还接受 sync
,这将强制效果始终同步触发。然而,这是低效的,应该很少需要。
4.侦听器调试
onTrack
和onTrigger
选项可用于调试侦听器的行为。
- 当响应式 property 或 ref 作为依赖项被追踪时,将调用
onTrack
- 当依赖项变更导致副作用被触发时,将调用
onTrigger
这两个回调都将接收到一个包含有关所依赖项信息的调试器事件。建议在以下回调中编写debugger
语句来检查依赖关系:
watchEffect(
() => {
/* 副作用 */
},
{
onTrigger(e) {
debugger
}
}
)
onTrack
和 onTrigger
只能在开发模式下工作。
watch
watch
API 完全等同于组件侦听器property。watch
需要侦听特定的数据源,并在回调函数中执行副作用。默认情况下,它也是惰性的,即只有当被侦听的源发生变化时才执行回调。
- 与watchEffect比较,
watch
允许我们:- 懒执行副作用;
- 更具体地说明什么状态应该触发侦听器重新运行;
- 访问侦听状态变化前后的值。
1.侦听单个数据源
侦听器数据源可以是返回值的 getter 函数,也可以直接是ref
:
// 侦听一个 getter
const state = reactive({
count: 0 })
watch(
() => state.count,
(count, prevCount) => {
/* ... */
}
)
// 直接侦听ref
const count = ref(0)
watch(count, (count, prevCount) => {
/* ... */
})
2.侦听多个数据源
侦听器还可以使用数组同时侦听多个源:
watch([fooRef, barRef], ([foo, bar], [prevFoo, prevBar]) => {
/* ... */
})
3.与 watchEffect
共享的行为
watch
与watchEffect
共享停止侦听,清除副作用(相应地onInvalidate
会作为回调的第三个参数传入)、副作用刷新时机和侦听器调试行为。
Composition API
介绍
用组件的选项 (data
、computed
、methods
、watch
) 组织逻辑在大多数情况下都有效。然而,当我们的组件变得更大时,逻辑关注点的列表也会增长。这可能会导致组件难以阅读和理解,尤其是对于那些一开始就没有编写这些组件的人来说。
一个大型组件的示例,其中逻辑关注点是按颜色分组。
这种碎片化使得理解和维护复杂组件变得困难。选项的分离掩盖了潜在的逻辑问题。此外,在处理单个逻辑关注点时,我们必须不断地“跳转”相关代码的选项块。
如果我们能够将与同一个逻辑关注点相关的代码配置在一起会更好。而这正是 Composition API 使我们能够做到的。
Composition API 基础
既然我们知道了为什么,我们就可以知道怎么做。为了开始使用 Composition api,我们首先需要一个可以实际使用它的地方。在 Vue 组件中,我们将此位置称为 setup
。
setup 组件选项
新的setup
组件选项在创建组件之前执行,一旦props
被解析,并充当合成 API 的入口点。
由于在执行setup
时尚未创建组件实例,因此在setup
选项中没有this
。这意味着,除了props
之外,你将无法访问组件中声明的任何属性——本地状态、计算属性或方法。
setup
选项应该是一个接受props
和context
的函数,我们将在稍后讨论。此外,我们从setup
返回的所有内容都将暴露给组件的其余部分 (计算属性、方法、生命周期钩子等等) 以及组件的模板。
// src/components/UserRepositories.vue
export default {
components: {
RepositoriesFilters, RepositoriesSortBy, RepositoriesList },
props: {
user: {
type: String }
},
setup(props) {
console.log(props) // { user: '' }
return {} // 这里返回的任何内容都可以用于组件的其余部分
}
// 组件的“其余部分”
}
现在让我们从提取第一个逻辑关注点开始 (在原始代码段中标记为“1”)。
1.从假定的外部 API 获取该用户名的仓库,并在用户更改时刷新它
我们将从最明显的部分开始:
- 仓库列表
- 更新仓库列表的函数
- 返回列表和函数,以便其他组件选项可以访问它们
// src/components/UserRepositories.vue `setup` function
import {
fetchUserRepositories } from '@/api/repositories'
// 在我们的组件内
setup (props) {
let repositories = []
const getUserRepositories = async () => {
repositories = await fetchUserRepositories(props.user)
}
return {
repositories,
getUserRepositories // 返回的函数与方法的行为相同
}
}
这是我们的出发点,但它还不能工作,因为我们的 repositories
变量不是被动的。这意味着从用户的角度来看,仓库列表将保持为空。我们来解决这个问题!
带 ref 的响应式变量
在 Vue 3.0 中,我们可以通过一个新的ref
函数使任何响应式变量在任何地方起作用,如下所示:
import {
ref } from 'vue'
const counter = ref(0)
ref
接受参数并返回它包装在具有value
property 的对象中,然后可以使用该 property 访问或更改响应式变量的值:
import {
ref } from 'vue'
const counter = ref(0)
console.log(counter) // { value: 0 }
console.log(counter.value) // 0
counter.value++
console.log(counter.value) // 1
在对象中包装值似乎不必要,但在 JavaScript 中保持不同数据类型的行为统一是必需的。这是因为在 JavaScript 中,Number
或 String
等基本类型是通过值传递的,而不是通过引用传递的: