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>