学习vue3, 主要是看着文档来一点点敲,给我的感触最大的就是借鉴hooks的思路, 给我们提供一种写代码的模式, 比如状态复用, 代码复用的思路,当初在学习react-hooks
就看到过这个项目react-use, 在vue3中也是值得推荐用这种模式来写代码
# composition API
# setup
创建组件实例,然后初始化 props
,紧接着就调用setup
函数。也就是说setup
会在beforeCreate
之前调用, 传递props
及context
# 使用
<template>
<div class="hello">
<p>{{readersNumber}}---{{book}}</p>
</div>
</template>
<script>
import { ref, reactive } from 'vue'
export default {
setup() {
const readersNumber = ref(0)
const book = reactive({ title: 'Vue 3 Guide' })
return {
readersNumber,
book
}
}
}
</script>
setup
返回的 ref
在模板中会自动解开,不需要写 .value
# props
export default {
props: {
title: String
},
setup(props) {
console.log(props.title)
}
}
props
是响应式的,不要使用结构赋值,去获取props
里面的值
export default {
props: {
msg: String,
},
// 这样写, msg就不是响应式数据了
setup({ msg }) {
watchEffect(() => {
console.log(`message is: ` + msg) // Destructuring the `props` will cause the value to lose reactivity
})
},
}
我们可以使用toRefs
/toRef
来赋值
import { toRefs } from 'vue'
setup(props) {
const { msg } = toRefs(props)
console.log(msg.value)
}
// or
import { toRef } from 'vue'
setup(props) {
const msg = toRef(props, 'msg')
console.log(msg.value)
}
# context
提供了一个上下文对象,从原来 2.x
中 this
选择性地暴露了一些 property
export default {
setup(props, context) {
// Attributes (Non-reactive object)
console.log(context.attrs)
// Slots (Non-reactive object)
console.log(context.slots)
// Emit Events (Method)
console.log(context.emit)
}
}
context
不是响应式数据,也就意味着可以使用结构赋值
export default {
setup(props, { attrs, slots, emit }) {
...
}
}
attrs
和 slots
都是内部组件实例上对应项的代理,可以确保在更新后仍然是最新值。所以可以解构,无需担心后面访问到过期的值:
setup(props, { attrs }) {
// 一个可能之后回调用的签名
function onClick() {
const { foo } = attrs // 这种情况会丢失响应性
console.log(attrs.foo) // 一定是最新的引用,没有丢失响应性
}
},
# 返回函数
setup
也可以返回一个函数,函数中也能使用当前 setup
函数作用域中的响应式数据:
import { h, ref, reactive } from 'vue'
export default {
setup() {
const readersNumber = ref(0)
const book = reactive({ title: 'Vue 3 Guide' })
// Please note that we need to explicitly expose ref value here
return () => h('div', [readersNumber.value, book.title])
}
}
# this
this 在 setup() 中不可用。由于 setup()
在解析 2.x
选项前被调用,setup()
中的 this
将与 2.x
选项中的 this
完全不同。
# Lifecycle Hooks
Options API | Hook inside |
---|---|
beforeCreate | setup |
created | setup |
beforeMount | onBeforeMount |
mounted | onMounted |
beforeUpdate | onBeforeUpdate |
updated | onUpdated |
beforeUnmount | onBeforeUnmount |
unmounted | onUnmounted |
errorCaptured | onErrorCaptured |
renderTracked | onRenderTracked |
renderTriggered | onRenderTriggered |
# provide && inject
provide 和 inject 提供依赖注入,功能类似 2.x 的 provide/inject。两者都只能在当前活动组件实例的 setup() 中调用。
# provide
属性名
属性值
<template>
<MyMarker />
</template>
<script>
import { provide } from 'vue'
import MyMarker from './MyMarker.vue
export default {
components: {
MyMarker
},
setup() {
provide('location', 'North Pole')
provide('geolocation', {
longitude: 90,
latitude: 135
})
}
}
</script>
# Inject
- 接受属性
— 默认值
inject
接受一个可选的的默认值作为第二个参数。如果未提供默认值,并且在 provide
上下文中未找到该属性,则 inject
返回 undefined
。
<script>
import { inject } from 'vue'
export default {
setup() {
const userLocation = inject('location', 'The Universe')
const userGeolocation = inject('geolocation')
return {
userLocation,
userGeolocation
}
}
}
</script>
# 注入响应性
可以使用 ref
来保证 provided
和 injected
之间值的响应
<template>
<MyMarker />
</template>
<script>
import { provide, reactive, ref } from 'vue'
import MyMarker from './MyMarker.vue
export default {
components: {
MyMarker
},
setup() {
const location = ref('North Pole')
const geolocation = reactive({
longitude: 90,
latitude: 135
})
provide('location', location)
provide('geolocation', geolocation)
}
}
</script>
如果注入一个响应式对象,则它的状态变化也可以被侦听。
有时我们需要更新注入组件内部的数据。在这种情况下,我们建议提供一种方法来改变反应性属性
<template>
<MyMarker />
</template>
<script>
import { provide, reactive, ref } from 'vue'
import MyMarker from './MyMarker.vue
export default {
components: {
MyMarker
},
setup() {
const location = ref('North Pole')
const geolocation = reactive({
longitude: 90,
latitude: 135
})
const updateLocation = () => {
location.value = 'South Pole'
}
provide('location', location)
provide('geolocation', geolocation)
provide('updateLocation', updateLocation)
}
}
</script>
<script>
import { inject } from 'vue'
export default {
setup() {
const userLocation = inject('location', 'The Universe')
const userGeolocation = inject('geolocation')
const updateUserLocation = inject('updateLocation')
return {
userLocation,
userGeolocation,
updateUserLocation
}
}
}
</script>
最后,如果要确保通过provide
传递的数据不会被注入的组件更改,我们建议对provided
属性使用readonly
。
<template>
<MyMarker />
</template>
<script>
import { provide, reactive, readonly, ref } from 'vue'
import MyMarker from './MyMarker.vue
export default {
components: {
MyMarker
},
setup() {
const location = ref('North Pole')
const geolocation = reactive({
longitude: 90,
latitude: 135
})
const updateLocation = () => {
location.value = 'South Pole'
}
provide('location', readonly(location))
provide('geolocation', readonly(geolocation))
provide('updateLocation', updateLocation)
}
}
</script>
# template refs
在使用Composition API
时,reactive refs
和 template refs
是统一的。为了获得对模板内元素或组件实例的引用,我们可以像往常一样声明一个ref
并从setup()
返回它
<template>
<div ref="root">This is a root element</div>
</template>
<script>
import { ref, onMounted } from 'vue'
export default {
setup() {
const root = ref(null)
onMounted(() => {
// the DOM element will be assigned to the ref after initial render
console.log(root.value) // <div>This is a root element</div>
})
return {
root
}
}
}
</script>
这里我们将 root
暴露在渲染上下文中,并通过 ref="root"
绑定到 div
作为其 ref
。 在 Virtual DOM patch
算法中,如果一个 VNode
的 ref
对应一个渲染上下文中的 ref
,则该 VNode
对应的元素或组件实例将被分配给该 ref
。 这是在 Virtual DOM
的 mount
/ patch
过程中执行的,因此模板 ref
仅在渲染初始化后才能访问。
# v-for 中使用ref
<template>
<div v-for="(item, i) in list" :ref="el => { if (el) divs[i] = el }">
{{ item }}
</div>
</template>
<script>
import { ref, reactive, onBeforeUpdate } from 'vue'
export default {
setup() {
const list = reactive([1, 2, 3])
const divs = ref([])
// make sure to reset the refs before each update
onBeforeUpdate(() => {
divs.value = []
})
return {
list,
divs
}
}
}
</script>
# Reactivity API
使用new Proxy(target, handler)
来实现数据的响应
const dinner = {
meal: 'tacos'
}
const handler = {
get(target, prop, receiver) {
track(target, prop)
return Reflect.get(...arguments)
},
set(target, key, value, receiver) {
trigger(target, key)
return Reflect.set(...arguments)
}
}
const proxy = new Proxy(dinner, handler)
console.log(proxy.meal)
// tacos
# reactive
接收一个普通对象然后返回该普通对象的响应式代理。等同于 2.x
的 Vue.observable()
import { reactive } from 'vue'
// reactive state
const state = reactive({
count: 0
})
reactive
响应式转换是“深层的”:会影响对象内部所有嵌套的属性。基于 ES2015
的 Proxy
实现,返回的代理对象不等于原始对象。建议仅使用代理对象而避免依赖原始对象。
# readonly
传入一个对象(响应式或普通)或 ref
,返回一个原始对象的只读代理。一个只读的代理是“深层的”,对象内部任何嵌套的属性也都是只读的。
const original = reactive({ count: 0 })
const copy = readonly(original)
watchEffect(() => {
// works for reactivity tracking
console.log(copy.count)
})
// mutating original will trigger watchers relying on the copy
original.count++
// mutating the copy will fail and result in a warning
copy.count++ // warning!
# isProxy
检查对象是不是由reactive
or readonly
生成代理对象。
# isReactive
检查对象是不是通过reactive
生成代理对象
import { reactive, isReactive } from 'vue'
export default {
setup() {
const state = reactive({
name: 'John'
})
console.log(isReactive(state)) // -> true
}
}
如果readonly
代理的对象是由reactive
生成的, 也是回返回true
import { reactive, isReactive, readonly } from 'vue'
export default {
setup() {
const state = reactive({
name: 'John'
})
// readonly proxy created from plain object
const plain = readonly({
name: 'Mary'
})
console.log(isReactive(plain)) // -> false
// readonly proxy created from reactive proxy
const stateCopy = readonly(state)
console.log(isReactive(stateCopy)) // -> true
}
}
# isReadonly
检查对象是不是通过readonly
生成代理对象
# shallowReadonly
创建一个自己的属性可读的代理对象,嵌套对象的深层属性不起作用
const state = shallowReadonly({
foo: 1,
nested: {
bar: 2
}
})
// mutating state's own properties will fail
state.foo++
// ...but works on nested objects
isReadonly(state.nested) // false
state.nested.bar++ // works
# shallowReactive
和shallowReadonly
类似
创建一个自己的属性响应的代理对象,嵌套对象的深层属性不起作用
const state = shallowReactive({
foo: 1,
nested: {
bar: 2
}
})
// mutating state's own properties is reactive
state.foo++
// ...but does not convert nested objects
isReactive(state.nested) // false
state.nested.bar++ // non-reactive
# ref
接受一个值并返回一个反应式和可变的ref
对象。ref
对象有一个指向内部值的.value
。
const count = ref(0)
console.log(count.value) // 0
count.value++
console.log(count.value) // 1
interface Ref<T> {
value: T
}
function ref<T>(value: T): Ref<T>
有时我们可能需要为ref
的内部值指定复杂类型。我们可以通过在调用ref
重写默认推理时传递泛型参数来简洁地实现这一点
const foo = ref<string | number>('foo') // foo's type: Ref<string | number>
foo.value = 123 // ok!
如果泛型的类型未知,建议将ref
强制转换为ref<T>
function useState<State extends string>(initial: State) {
const state = ref(initial) as Ref<State> // state.value -> State extends string
return state
}
# toRef
响应式对象上的属性创建引用ref
const state = reactive({
foo: 1,
bar: 2
})
const fooRef = toRef(state, 'foo')
fooRef.value++
console.log(state.foo) // 2
state.foo++
console.log(fooRef.value) // 3
如果要将prop
的ref
传递给合成函数,则toRef
非常有用
export default {
setup(props) {
useSomeFeature(toRef(props, 'foo'))
}
}
# toRefs
将响应式对象转化为普通对象,转化后的对象的属性指向原来响应式对象属性
const state = reactive({
foo: 1,
bar: 2
})
const stateAsRefs = toRefs(state)
/*
Type of stateAsRefs:
{
foo: Ref<number>,
bar: Ref<number>
}
*/
// The ref and the original property is "linked"
state.foo++
console.log(stateAsRefs.foo.value) // 2
stateAsRefs.foo.value++
console.log(state.foo) // 3
当从组合函数返回响应式对象时,toRefs
很有用,自定义组件在不损失返回对象响应式的情况下进行结构/扩展
function useFeatureX() {
const state = reactive({
foo: 1,
bar: 2
})
// logic operating on state
// convert to refs when returning
return toRefs(state)
}
export default {
setup() {
// can destructure without losing reactivity
const { foo, bar } = useFeatureX()
return {
foo,
bar
}
}
}
# customRef
创建自定义的ref
,对其依赖项跟踪和更新进行显式控制。它需要一个工厂函数,该函数接收track
和trigger
函数作为参数,并应返回一个带有get
和set
的对象。
<input v-model="text" />
function useDebouncedRef(value, delay = 200) {
let timeout
return customRef((track, trigger) => {
return {
get() {
track()
return value
},
set(newValue) {
clearTimeout(timeout)
timeout = setTimeout(() => {
value = newValue
trigger()
}, delay)
}
}
})
}
export default {
setup() {
return {
text: useDebouncedRef('hello')
}
}
}
function customRef<T>(factory: CustomRefFactory<T>): Ref<T>
type CustomRefFactory<T> = (
track: () => void,
trigger: () => void
) => {
get: () => T
set: (value: T) => void
}
# Computed and watch
# computed
获取一个getter
函数,并为getter
返回的值返回一个不可变的reactive ref
对象
const count = ref(1)
const plusOne = computed(() => count.value + 1)
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
// read-only
function computed<T>(getter: () => T): Readonly<Ref<Readonly<T>>>
// writable
function computed<T>(options: { get: () => T; set: (value: T) => void }): Ref<T>
# watchEffect
当依赖项改变时,立即运行
const count = ref(0)
watchEffect(() => console.log(count.value))
// -> logs 0
setTimeout(() => {
count.value++
// -> logs 1
}, 100)
当 watchEffect
在组件的 setup()
函数或生命周期钩子被调用时, 侦听器会被链接到该组件的生命周期,并在组件卸载时自动停止。
在一些情况下,也可以显式调用返回值以停止侦听:
const stop = watchEffect(() => {
/* ... */
})
// 之后
stop()
# 清除副作用
有时副作用函数会执行一些异步的副作用, 这些响应需要在其失效时清除(即完成之前状态已改变了)。所以侦听副作用传入的函数可以接收一个 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()
})
})
我们之所以是通过传入一个函数去注册失效回调,而不是从回调返回它(如 React useEffect
中的方式),是因为返回值对于异步错误处理很重要。
在执行数据请求时,副作用函数往往是一个异步函数:
const data = ref(null)
watchEffect(async (onInvalidate) => {
onInvalidate(() => { /* ... */ }) // we register cleanup function before Promise resolves
data.value = await fetchData(props.id)
})
# 副作用刷新时机
Vue
的响应式系统会缓存副作用函数,并异步地刷新它们,这样可以避免同一个 tick
中多个状态改变导致的不必要的重复调用。
如果副作用需要同步或在组件更新之前重新运行,我们可以传递一个拥有 flush
属性的对象作为选项(默认为 'post'):
// 同步运行
watchEffect(
() => {
/* ... */
},
{
flush: 'sync',
}
)
// 组件更新前执行
watchEffect(
() => {
/* ... */
},
{
flush: 'pre',
}
)
# watch
watch API
完全等效于 2.x
this.$watch
(以及 watch
中相应的选项)。watch
需要侦听特定的数据源,并在回调函数中执行副作用。默认情况是懒执行的,也就是说仅在侦听的源变更时才执行回调。
对比 watchEffect
,watch
允许我们:
懒执行副作用;
更明确哪些状态的改变会触发侦听器重新运行副作用;
访问侦听状态变化前后的值。
# 侦听单个数据源
// 侦听一个 getter
const state = reactive({ count: 0 })
watch(
() => state.count,
(count, prevCount) => {
/* ... */
}
)
// 直接侦听一个 ref
const count = ref(0)
watch(count, (count, prevCount) => {
/* ... */
})
# 侦听多个数据源
watch([fooRef, barRef], ([foo, bar], [prevFoo, prevBar]) => {
/* ... */
})