Skip to main content

Vue3是如何使用的

ref(适用于基本类型值)

在组合式 API 中,推荐使用 ref() 函数来声明响应式状态。

import { ref } from 'vue'

const count = ref(0)

ref() 接收参数,并将其包裹在一个带有 .value 属性的 ref 对象中返回。

const count = ref(0)

console.log(count) // { value: 0 }
console.log(count.value) // 0

count.value++
console.log(count.value) // 1

要在组件模板中访问 ref,需从组件的 setup() 函数中声明并返回它们。

import { ref } from 'vue'

export default {
// `setup` 是一个特殊的钩子,专门用于组合式 API。
setup() {
const count = ref(0)

function increment() {
// 在 JavaScript 中需要 .value
count.value++
}

// 暴露给模板
return {
count,
increment
}
}
}

在模板中使用 ref 时,不需要附加 .value,即 ref 会自动解包。

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

<div>{{ count }}</div>

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

在 setup() 函数中手动暴露大量的状态和方法非常繁琐。可以通过使用单文件组件 (SFC) 来避免这种情况。

可以使用 <script 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 被修改时,它会触发追踪它的组件的一次重新渲染。

原理是通过 getter 和 setter 方法来拦截对象属性的 get 和 set 操作。

可以将 ref 看作是一个类似这样的对象。

// 伪代码,不是真正的实现
const myRef = {
_value: 0,
get value() {
track()
return this._value
},
set value(newValue) {
this._value = newValue
trigger()
}
}

ref() 可以持有任何类型的值,包括深层嵌套的对象、数组或者 JavaScript 内置的数据结构。

ref() 会使它的值具有深层响应性,也可以通过 shallowRef() 来放弃深层响应性。对于浅层 ref,只有 .value 的访问会被追踪。

reactive(适用于对象类型值)

还有另一种声明响应式状态的方式,即使用 reactive() API。

reactive() 将深层地转换对象。也可以通过 shallowReactive() 来放弃深层响应性。

import { reactive } from 'vue'

const state = reactive({ count: 0 })

在模板中使用

<button @click="state.count++">
{{ state.count }}
</button>

reactive() 返回的是一个原始对象的 Proxy,它和原始对象是不相等的。

const raw = {}
const proxy = reactive(raw)

// 代理对象和原始对象不是全等的
console.log(proxy === raw) // false
// 在同一个对象上调用 reactive() 会返回相同的代理
console.log(reactive(raw) === proxy) // true
// 在一个代理上调用 reactive() 会返回它自己
console.log(reactive(proxy) === proxy) // true

reactive() API 有一些局限性

  • 它只能用于对象类型,它不能持有如 string、number 或 boolean 这样的原始类型。
  • 不能替换整个对象,必须始终保持对响应式对象的相同引用。
let state = reactive({ count: 0 })

// 上面的 ({ count: 0 }) 引用将不再被追踪
// (响应性连接已丢失!)
state = reactive({ count: 1 })
  • 对解构操作不友好
const state = reactive({ count: 0 })

// 当解构时,count 已经与 state.count 断开连接
let { count } = state
// 不会影响原始的 state
count++

计算属性

计算属性值会基于其响应式依赖被缓存,一个计算属性仅会在其响应式依赖更新时才重新计算。

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

只要 author.books 不改变,无论多少次访问 publishedBooksMessage 都会立即返回先前的计算结果,而不用重复执行 getter 函数。

可写计算属性

import { ref, computed } from 'vue'

const firstName = ref('John')
const lastName = ref('Doe')

const fullName = computed({
// getter
get() {
return firstName.value + ' ' + lastName.value
},
// setter
set(newValue) {
// 注意:我们这里使用的是解构赋值语法
[firstName.value, lastName.value] = newValue.split(' ')
}
})

侦听器

当需要在状态变化时执行一些副作用:例如更改 DOM,或是根据异步操作的结果去修改另一处的状态。

可以使用 watch 函数在每次响应式状态发生变化时触发回调函数。

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

const question = ref('')
const answer = ref('Questions usually contain a question mark. ;-)')

// 可以直接侦听一个 ref
watch(question, async (newQuestion, oldQuestion) => {
if (newQuestion.indexOf('?') > -1) {
answer.value = 'Thinking...'
try {
const res = await fetch('https://yesno.wtf/api')
answer.value = (await res.json()).answer
} catch (error) {
answer.value = 'Error! Could not reach the API. ' + error
}
}
})
</script>

<template>
<p>
Ask a yes/no question:
<input v-model="question" />
</p>
<p>{{ answer }}</p>
</template>
  • watch 的第一个参数可以是不同形式的数据源:它可以是一个 ref (包括计算属性)、一个响应式对象、一个 getter 函数、或多个数据源组成的数组

深层侦听器

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

即时回调侦听器

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

可以用 watchEffect 函数 来简化上面的代码,watchEffect() 允许我们自动跟踪回调的响应式依赖。

watchEffect(async (newValue, oldValue) => {
// 立即执行,且当依赖改变时再次执行
})
  • watch 只追踪明确侦听的数据源,它不会追踪任何在回调中访问到的东西
  • watchEffect,则会在副作用发生期间追踪依赖,它会在同步执行过程中,自动追踪所有能访问到的响应式属性

如果想在侦听器回调中能访问被 Vue 更新之后的 DOM,需要指明选项。

watch(source, callback, {
flush: 'post'
})

watchEffect(callback, {
flush: 'post'
})

停止侦听器

要手动停止一个侦听器,请调用 watch 或 watchEffect 返回的函数。

const unwatch = watchEffect(() => {})

// ...当该侦听器不再需要时
unwatch()

模板引用

当需要直接访问底层 DOM 元素时,可以使用特殊的 ref attribute。

只有在组件挂载后才能访问模板引用。

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

// 声明一个 ref 来存放该元素的引用
// 必须和模板里的 ref 同名
const input = ref(null)

onMounted(() => {
input.value.focus()
})
</script>

<template>
<input ref="input" />
</template>

如果不使用 <script setup>,需确保从 setup() 返回 ref。

export default {
setup() {
const input = ref(null)
// ...
return {
input
}
}
}

函数模板引用

需要使用动态的 :ref 绑定才能够传入一个函数。

当绑定的元素被卸载时,函数也会被调用一次,此时的 el 参数会是 null。

<input :ref="(el) => { /* 将 el 赋值给一个数据属性或 ref 变量 */ }">

组件上的ref

模板引用也可以被用在一个子组件上,这种情况下引用中获得的值是组件实例。

<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>

一个父组件无法访问到一个使用了 <script setup> 的子组件中的任何东西,除非子组件在其中通过 defineExpose 宏显式暴露。

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

const a = 1
const b = ref(2)

// 像 defineExpose 这样的编译器宏不需要导入
defineExpose({
a,
b
})
</script>