在 Vue3 测试版刚刚发布的时候,我就学习了下 Composition API,但没想到正式版时隔一年多才出来,看了一下发现还是增加了不少新特性的,在这里我就将它们一一梳理一遍。

本文章只详细阐述 Vue3 中重要或常用的新特性,如果想了解全部的特性请转:Vue3 响应性基础 API

Composition API

这是一个非常重要的改变,我认为 Composition API 最大的用处就是将响应式数据和相关的业务逻辑结合到一起,便于维护(这样做的优点在处理庞大组件的时候显得尤为重要)。

之所以叫做 Composition API(或组合式 API) 是因为所有的响应式数据和业务逻辑代码都可以放在 setup 方法中进行处理,我们通过代码看一下 Vue2 的 Options API 和 Composition API 的区别:

/* Options API */
export default {
props: {},
data(){},
computed: {},
watch: {},
methods: {},
created(),
components:{}
// ...other options
} /* Composition API */
export default {
props: {},
setup(),
components:{}
}

这就是两种 API 在大致结构上的不同,虽然 Composition API 提倡使用 setup 来暴露组件的 datacomputedwatch、生命周期钩子... 但并不意味着强制使用,在 Vue3 中同样可以选择 Options API 或者两种写法混用。

接下来我们看看在 setup 的使用。

setup

执行时机

setupbeforeCreate 之前执行,因此访问不到组件实例,换句话说 setup 内无法使用 this 访问组件实例

参数

setup 方法接受两个参数 setup(props, context)props 是父组件传给组件的数据,context(上下文) 中包含了一些常用属性:

attrs

attrs 表示由上级传向该组件,但并不包含在 props 内的属性:

<!-- parent.vue -->
<Child msg="hello world" :name="'child'"></Child>
/* child.vue */
export default {
props: { name: String },
setup(props, context) {
console.log(props) // {name: 'child'}
console.log(context.attrs) // {msg: 'hello world'}
},
}
emit

用于在子组件内触发父组件的方法

<!-- parent.vue -->
<Child @sayWhat="sayWhat"></Child>
/* child.vue */
export default {
setup(_, context) {
context.emit('sayWhat')
},
}
slots

用来访问被插槽分发的内容,相当于 vm.$slots

<!-- parent.vue -->
<Child>
<template v-slot:header>
<div>header</div>
</template>
<template v-slot:content>
<div>content</div>
</template>
<template v-slot:footer>
<div>footer</div>
</template>
</Child>
/* child.vue */
import { h } from 'vue'
export default {
setup(_, context) {
const { header, content, footer } = context.slots
return () => h('div', [h('header', header()), h('div', content()), h('footer', footer())])
},
}

生命周期

Vue3 的生命周期除了可以使用传统的 Options API 形式外,也可以在 setup 中进行定义,只不过要在前面加上 on

export default {
setup() {
onBeforeMount(() => {
console.log('实例创建完成,即将挂载')
})
onMounted(() => {
console.log('实例挂载完成')
})
onBeforeUpdate(() => {
console.log('组件dom即将更新')
})
onUpdated(() => {
console.log('组件dom已经更新完毕')
})
// 对应vue2 beforeDestroy
onBeforeUnmount(() => {
console.log('实例即将解除挂载')
})
// 对应vue2 destroyed
onUnmounted(() => {
console.log('实例已经解除挂载')
})
onErrorCaptured(() => {
console.log('捕获到一个子孙组件的错误')
})
onActivated(() => {
console.log('被keep-alive缓存的组件激活')
})
onDeactivated(() => {
console.log('被keep-alive缓存的组件停用')
})
// 两个新钩子,可以精确地追踪到一个组件发生重渲染的触发时机和完成时机及其原因
onRenderTracked(() => {
console.log('跟踪虚拟dom重新渲染时')
})
onRenderTriggered(() => {
console.log('当虚拟dom被触发重新渲染时')
})
},
}

Vue3 没有提供单独的 onBeforeCreateonCreated 方法,因为 setup 本身是在这两个生命周期之前执行的,Vue3 建议我们直接在 setup 中编写这两个生命周期中的代码

Reactive API

ref

ref 方法用来为一个指定的值(可以是任意类型)创建一个响应式的数据对象,该对象包含一个 value 属性,值为响应式数据本身。

对于 ref 定义的响应式数据,无论获取其值还是做运算,都要用 value 属性。

import { ref } from 'vue'
export default {
setup() {
const count = ref(0)
console.log(count.value) // 0
count.value++
console.log(count.value) // 1
const obj = ref({ a: 2 })
console.log(obj.value.a) // 2
return {
count,
obj,
}
},
}

但是在 template 中访问 ref 响应式数据,是不需要追加 .value 的:

<template>
<div>
<ul>
<li>count: {{count}}</li>
<li>obj.a: {{obj.a}}</li>
</ul>
</div>
</template>

reactive

ref 方法一样,reactive 也负责将目标数据转换成响应式数据,但该数据只能是引用类型

<template>
<div>{{obj.a}}</div>
</template>
<script>
export default {
setup() {
const obj = reactive({ a: 2 })
obj.a++
console.log(obj.a) // 3
return { obj }
},
}
</script>

可以看出 reactive 类型的响应式数据不需要在后面追加 .value 来调用或使用。

reactive 和 ref 的区别

看上去 reactiveref 十分相似,那么这两个方法有什么不同呢?

实际上 ref 本质上与 reactive 并无区别,来看看 Vue3 的部分源码(来自于 @vue/reactivity/dist/reactivity.cjs.js):

function ref(value) {
return createRef(value)
}
function createRef(rawValue, shallow = false) {
/**
* rawValue表示调用ref函数时传入的值
* shallow表示是否浅监听,默认false表示进行深度监听,也就是递归地将对象/数组内所有属性都转换成响应式
*/
if (isRef(rawValue)) {
// 判断传入ref函数的数据是否已经是一个ref类型的响应式数据了
return rawValue
}
return new RefImpl(rawValue, shallow)
}
class RefImpl {
constructor(_rawValue, _shallow = false) {
// 用于保存未转换前的原生数据
this._rawValue = _rawValue
// 是否深度监听
this._shallow = _shallow
// 是否为ref类型
this.__v_isRef = true
// 如果为深度监听,则使用convert递归将所有嵌套属性转换为响应式数据
this._value = _shallow ? _rawValue : convert(_rawValue)
}
get value() {
track(toRaw(this), 'get' /* GET */, 'value')
return this._value
}
set value(newVal) {
if (shared.hasChanged(toRaw(newVal), this._rawValue)) {
this._rawValue = newVal
this._value = this._shallow ? newVal : convert(newVal)
trigger(toRaw(this), 'set' /* SET */, 'value', newVal)
}
}
}
// 如果val满足:val !== null && typeof val === 'object',则使用reactive方法转换数据
const convert = (val) => (shared.isObject(val) ? reactive(val) : val)

如果你不明白上面的代码做了什么,假设我现在执行这行代码:

const count = ref(0)

那么实际上 ref 函数返回的是一个 RefImpl 实例,里面包含如下属性:

{
_rawValue: 0,
_shallow: false,
__v_isRef: true,
_value: 0
}

通过 RefImpl 类的 get value() 方法可以看出,调用 value 属性返回的其实就是 _value 属性。

Vue3 建议在定义基本类型的响应式数据时使用 ref 是因为基本类型不存在引用效果,这样一来在其他地方改变该值便不会触发响应,因此 ref 将数据包裹在对象中以实现引用效果。

Vue3 会判断 template 中的响应式数据是否为 ref 类型,如果为 ref 类型则会在尾部自动追加 .value,判断方式很简单:

function isRef(r) {
return Boolean(r && r.__v_isRef === true)
}

那么其实我们是可以用 reactive 来伪装成 ref 的:

<template>
<div>{{count}}</div>
</template>
<script>
export default {
setup() {
const count = reactive({
value: 0,
__v_isRef: true,
})
return { count }
},
}
</script>

虽然这样做毫无意义,不过证明了 Vue3 确实是通过 __v_isRef 属性判断数据是否为 ref 定义的。

我们再看看 reactive 的实现:

function reactive(target) {
if (target && target['__v_isReadonly' /* IS_READONLY */]) {
return target
}
return createReactiveObject(target, false, mutableHandlers, mutableCollectionHandlers)
}
function createReactiveObject(target, isReadonly, baseHandlers, collectionHandlers) {
if (!shared.isObject(target)) {
{
console.warn(`value cannot be made reactive: ${String(target)}`)
}
return target
}
// 如果target已经被代理,直接返回target
if (target['__v_raw' /* RAW */] && !(isReadonly && target['__v_isReactive' /* IS_REACTIVE */])) {
return target
}
const proxyMap = isReadonly ? readonlyMap : reactiveMap
const existingProxy = proxyMap.get(target)
if (existingProxy) {
return existingProxy
}
const targetType = getTargetType(target)
if (targetType === 0 /* INVALID */) {
return target
}
const proxy = new Proxy(target, targetType === 2 /* COLLECTION */ ? collectionHandlers : baseHandlers)
proxyMap.set(target, proxy)
return proxy
}

reactive 方法会调用 createReactiveObject 代理对象中的各个属性来实现响应式,在使用 ref 定义引用类型数据的时候同样会用到这个方法:

export default {
setup() {
console.log(ref({ a: 123 }))
console.log(reactive({ a: 123 }))
},
}

可以看到 ref 对象的 _value 属性和 reactive 一样都被代理了。

综上所述,我们可以简单将 ref 看作是 reactive 的二次包装,只不过多了几个属性罢了。

明白了 refreactive 的大致实现和关系,我们再来看其他的响应式 API。

isRef & isReactive

判断一个值是否是 ref 或 reactive 类型:

const count = ref(0)
const obj = reactive({ a: 123 })
console.log(isRef(count)) // true
console.log(isRef(obj)) // false
console.log(isReactive(count)) // false
console.log(isReactive(obj)) // true

customRef

自定义 ref,常用来定义需要异步获取的响应式数据,举个搜索框防抖的例子:

function useDebouncedRef(value, delay = 1000) {
let timeout
return customRef((track, trigger) => {
/**
* customRef回调接受两个参数
* track用于追踪依赖
* trigger用于出发响应
* 回调需返回一个包含get和set方法的对象
*/
return {
get() {
track() // 追踪该数据
return value
},
set(newValue) {
clearTimeout(timeout)
timeout = setTimeout(() => {
value = newValue
trigger() // 数据被修改,更新ui界面
}, delay)
},
}
})
}
export default {
setup() {
const text = useDebouncedRef('')
const searchResult = reactive({})
watch(text, async (newText) => {
if (!newText) return void 0
const result = await new Promise((resolve) => {
console.log(`搜索${newText}中...`)
resolve(`${newText}的搜索结果在这里`)
})
searchResult.data = result
})
return {
text,
searchResult,
}
},
}
<template>
<input v-model="text" />
<div>{{searchResult.data}}</div>
</template>

在这个例子中我们使用 customRef 和防抖函数,延迟改变 text.value 值,当 watch 监听到 text 的改变再进行搜索以实现防抖搜索。

toRef & toRefs

toRef 可以将一个 reactive 形式的对象的属性转换成 ref 形式,并且 ref 对象会保持对源 reactive 对象的引用:

const obj1 = reactive({ a: 1 })
const attrA = toRef(obj1, 'a')
console.log(obj1.a) // 1
console.log(attrA.value) // 1
console.log(obj1 === attrA._object) // true
attrA.value++
console.log(obj1.a) // 2

如果使用 ref,那么由于 obj1.a 本身是一个基本类型值,最后会生成一个与原对象 obj1 毫无关系的新的响应式数据。

我们来看一下 toRef 的源码:

function toRef(object, key) {
return isRef(object[key]) ? object[key] : new ObjectRefImpl(object, key)
}
class ObjectRefImpl {
constructor(_object, _key) {
this._object = _object
this._key = _key
this.__v_isRef = true
}
get value() {
return this._object[this._key]
}
set value(newVal) {
this._object[this._key] = newVal
}
}

可以看到其涉及的代码非常简单,ObjectRefImpl 类就是为了保持数据与源对象之间的引用关系(设置新 value 值同时会改变原对象对应属性的值)。

可能你已经注意到 ObjectRefImpl并没有ref 方法用到的 RefImpl 类一样在 getset 时使用 track 追踪改变和用 trigger 触发 ui 更新。

因此可以得出一个结论,toRef 方法所生成的数据仅仅是保存了对源对象属性的引用,但该数据的改变可能不会直接触发 ui 更新!,举个例子:

<template>
<ul>
<li>obj1: {{obj1}}</li>
<li>attrA: {{attrA}}</li>
<li>obj2: {{obj2}}</li>
<li>attrB: {{attrB}}</li>
</ul>
<button @click="func1">addA</button>
<button @click="func2">addB</button>
</template>
<script>
import { reactive, toRef } from 'vue'
export default {
setup() {
const obj1 = { a: 1 }
const attrA = toRef(obj1, 'a')
const obj2 = reactive({ b: 1 })
const attrB = toRef(obj2, 'b')
function func1() {
attrA.value++
}
function func2() {
attrB.value++
}
return { obj1, obj2, attrA, attrB, func1, func2 }
},
}
</script>

可以看到,点击 addA 按钮不会触发界面渲染,而点击 addB 会更新界面。虽然 attrB.value 的改变确实会触发 ui 更新,但这是因为 attrB.value 的改变触发了 obj2.b 的改变,而 obj2 本身就是响应式数据,所以 attrB.value 的改变是间接触发了 ui 更新,而不是直接原因

再来看看 toRefstoRefs 可以将整个对象转换成响应式对象,而 toRef 只能转换对象的某个属性。但是 toRefs 生成的响应式对象和 ref 生成的响应式对象在用法上是有区别的:

const obj = { a: 1 }
const refObj = ref(obj)
const toRefsObj = toRefs(obj)
console.log(refObj.value.a) // 1
console.log(toRefsObj.a.value) // 1

toRefs 是将对象中的每个属性都转换成 ref 响应式对象,而 reactive 是代理整个对象。

shallowRef & shallowReactive

refreactive 在默认情况下会递归地将对象内所有的属性无论嵌套与否都转化为响应式,而 shallowRefshallowReactive 则只将第一层属性转化为响应式。

const dynamicObj2 = shallowReactive({ a: 1, b: { c: 2 } })
console.log(isReactive(dynamicObj2)) // true
console.log(isReactive(dynamicObj2.b)) // false
dynamicObj2.a++ // 触发ui更新
dynamicObj2.b.c++ // 不触发ui更新
const dynamicObj3 = shallowRef({ a: 1, b: { c: 2 } })
console.log(isRef(dynamicObj3)) // true
// ref函数在处理对象的时候会交给reactive处理,因此使用isReactive判断
console.log(isReactive(dynamicObj3.value)) // false

我们可以发现,shallowRefshallowReactive 类型的响应式数据,在改变其深层次属性时候是不会触发 ui 更新的。

注意shallowRef 的第一层是 value 属性所在的那一层,而 a 是在第二层,因此只有当 value 改变的时候,才会触发 ui 更新

triggerRef

如果 shallowRef 只有在 value 改变的时候,才会触发 ui 更新,有没有办法在其他情况下手动触发更新呢?有的:

const dynamicObj3 = shallowRef({ a: 1, b: { c: 2 } })
function func() {
dynamicObj3.value.b.c++
triggerRef(dynamicObj3) // 手动触发ui更新
}

readonly & isReadonly

readonly 可将整个对象(包含其内部属性)变成只读的,并且是深层次的。

isReadonly 通过对象中的 __v_isReadonly 属性判断对象是否只读。

const obj3 = readonly({ a: 0 })
obj3.a++ // warning: Set operation on key "a" failed: target is readonly
obj3.b.c++ // Set operation on key "c" failed: target is readonly
console.log(obj3.a) // 0
console.log(isReadonly(obj3)) // true
console.log(isReadonly(obj3.b)) // true

toRaw

toRaw 可以返回 reactivereadonly 所代理的对象。

const obj3 = { a: 123 }
const readonlyObj = readonly(obj3)
const reactiveObj = reactive(obj3)
const refObj = ref(obj3)
console.log(toRaw(readonlyObj) === obj3) // true
console.log(toRaw(reactiveObj) === obj3) // true
console.log(refObj._rawValue === obj3) // true
function toRaw(observed) {
return (observed && toRaw(observed['__v_raw' /* RAW */])) || observed
}

事实上,无论是 reactive 还是 readonly,都会将源对象保存一份在属性 __v_raw 中,而 ref 会将源对象或值保存在 _rawValue 属性中。

computed

Vue3 将 computed 也包装成了一个方法,我们看看 computed 的源码:

function computed(getterOrOptions) {
let getter
let setter
// 判断getterOrOptions是否为函数
if (shared.isFunction(getterOrOptions)) {
// 如果是函数,就作为getter,这种情况下只能获取值,更改值则会弹出警告
getter = getterOrOptions
setter = () => {
console.warn('Write operation failed: computed value is readonly')
}
} else {
// 如果不是函数,将getterOrOptions中的get和set方法赋给getter和setter
getter = getterOrOptions.get
setter = getterOrOptions.set
}
return new ComputedRefImpl(getter, setter, shared.isFunction(getterOrOptions) || !getterOrOptions.set)
}

我们可以发现,computed 接收两种不同的参数:

computed(() => {}) // only getter
computed({ get: () => {}, set: () => {} }) // getter and setter

和 Vue2 一样,computed 既可以单纯的用 getter 计算并返回数据,也可以设置 setter 使其变得可写。

const count = ref(1)
const countCpy = computed(() => count.value * 2)
// 由于computed返回的是ref对象,因此使用value获取值
console.log(countCpy.value) // 2
const countCpy2 = computed({
get: () => count.value,
set: (newVal) => {
count.value = newVal
},
})
countCpy2.value = 10
console.log(countCpy2.value) // 10

watch & watchEffect

Vue3 的 watch 和 Vue2 的 vm.$watch 效果是相同的。

watch 可以对一个 getter 发起监听:

const count = ref(2)
watch(
() => Math.abs(count.value),
(newVal, oldVal) => {
console.log(`count的绝对值发生了变化!count=${newVal}`)
}
)
count.value = -2 // 没有触发watch
count.value = 1 // count的绝对值发生了变化!count=1

也可以侦听一个 ref

const count = ref(2)
watch(count, (newVal, oldVal) => {
console.log(`count值发生了变化!count=${newVal}`)
})
count.value = -1 // count的绝对值发生了变化!count=-1

watch 不仅可以监听单一数据,也可以监听多个数据:

const preNum = ref('')
const aftNum = ref('')
watch([preNum, aftNum], ([newPre, newAft], [oldPre, oldAft]) => {
console.log('数据改变了')
})
preNum.value = '123' // 数据改变了
aftNum.value = '123' // 数据改变了

watchEffect 会在其任何一个依赖项发生变化的时候重新运行,其返回一个函数用于取消监听。

const count = ref(0)
const obj = reactive({ a: 0 })
const stop = watchEffect(() => {
console.log(`count或obj发生了变化,count=${count.value},obj.a=${obj.a}`)
})
// count或obj发生了变化,count=0,obj.a=0
count.value++ // count或obj发生了变化,count=1,obj.a=0
obj.a++ // count或obj发生了变化,count=1,obj.a=1
stop()
count.value++ // no log

可以看出:与 watch 不同,watchEffect 会在创建的时候立即运行,依赖项改变时再次运行;而 watch 只在监听对象改变时才运行。

watchwatchEffect 都用到了 doWatch 方法处理,来看看源码(删除了部份次要代码):

function watchEffect(effect, options) {
return doWatch(effect, null, options)
}
const INITIAL_WATCHER_VALUE = {}
function watch(source, cb, options) {
// 省略部分代码...
return doWatch(source, cb, options)
}
function doWatch(
source,
cb,
{ immediate, deep, flush, onTrack, onTrigger } = shared.EMPTY_OBJ, // 默认为Object.freeze({})
instance = currentInstance // 默认为null
) {
if (!cb) {
if (immediate !== undefined) {
warn(
`watch() "immediate" option is only respected when using the ` + `watch(source, callback, options?) signature.`
)
}
if (deep !== undefined) {
warn(`watch() "deep" option is only respected when using the ` + `watch(source, callback, options?) signature.`)
}
}
// 省略部分代码...
let getter
let forceTrigger = false
if (reactivity.isRef(source)) {
// 如果监听的是响应式ref数据
getter = () => source.value
forceTrigger = !!source._shallow
} else if (reactivity.isReactive(source)) {
// 如果监听的是响应式reactive对象
getter = () => source
deep = true
} else if (shared.isArray(source)) {
// 如果监听由响应式数据组成的数组
getter = () =>
source.map((s) => {
// 遍历数组再对各个值进行类型判断
if (reactivity.isRef(s)) {
return s.value
} else if (reactivity.isReactive(s)) {
// 如果是监听一个reactive类型数据,使用traverse递归监听属性
return traverse(s)
} else if (shared.isFunction(s)) {
return callWithErrorHandling(s, instance, 2)
} else {
warnInvalidSource(s)
}
})
} else if (shared.isFunction(source)) {
// 如果source是一个getter函数
if (cb) {
getter = () => callWithErrorHandling(source, instance, 2)
} else {
// 如果没有传递cb函数,说明使用的是watchEffect方法
getter = () => {
if (instance && instance.isUnmounted) {
return
}
if (cleanup) {
cleanup()
}
return callWithErrorHandling(source, instance, 3, [onInvalidate])
}
}
} else {
getter = shared.NOOP
warnInvalidSource(source)
}
if (cb && deep) {
// 如果传递了cb函数,并且为深层次监听,则使用traverse递归监听属性
const baseGetter = getter
getter = () => traverse(baseGetter())
}
let cleanup
const onInvalidate = (fn) => {
cleanup = runner.options.onStop = () => {
callWithErrorHandling(fn, instance, 4)
}
}
// 省略部分代码...
let oldValue = shared.isArray(source) ? [] : INITIAL_WATCHER_VALUE
// 观察者回调函数job
const job = () => {
if (!runner.active) {
return
}
if (cb) {
const newValue = runner()
if (deep || forceTrigger || shared.hasChanged(newValue, oldValue)) {
if (cleanup) {
cleanup()
}
callWithAsyncErrorHandling(cb, instance, 3, [
newValue,
// 在监听数据首次发生更改时将undefined置为旧值
oldValue === INITIAL_WATCHER_VALUE ? undefined : oldValue,
onInvalidate,
])
oldValue = newValue
}
} else {
// watchEffect
runner()
}
}
// 是否允许自动触发
job.allowRecurse = !!cb
let scheduler
if (flush === 'sync') {
scheduler = job
} else if (flush === 'post') {
scheduler = () => queuePostRenderEffect(job, instance && instance.suspense)
} else {
// default: 'pre'
scheduler = () => {
if (!instance || instance.isMounted) {
queuePreFlushCb(job)
} else {
job()
}
}
}
const runner = reactivity.effect(getter, {
lazy: true,
onTrack,
onTrigger,
scheduler,
})
recordInstanceBoundEffect(runner, instance)
// initial run
if (cb) {
if (immediate) {
// 如果immediate为true,则可以一开始就执行监听回调函数
job()
} else {
oldValue = runner()
}
} else if (flush === 'post') {
queuePostRenderEffect(runner, instance && instance.suspense)
} else {
runner()
}
return () => {
// 返回取消监听的函数
reactivity.stop(runner)
if (instance) {
shared.remove(instance.effects, runner)
}
}
}

通过上面的代码,我们可以发现 watchwatchEffect 函数还接收一个 options 参数,这个参数默认为 Object.freeze({}) 也就是一个被冻结的,无法添加任何属性的空对象。如果 options 不为空,那么它可以包含五个有效属性:immediatedeepflushonTrackonTrigger,我们来看看这五个属性的作用。

immediate 表示立即执行,我们之前说过,watch 是惰性监听,仅在侦听源发生更改时调用,但 watch 也可以主动监听,即在 options 参数中添加 immediate 属性为 true:

const count = ref(2)
watch(
() => count.value,
(newVal, oldVal) => {
console.log(`count发生了变化!count=${newVal}`)
},
{ immediate: true }
)
// log: count发生了变化!count=2

这样,watch 在一开始就会立即执行回调。

再说说第二个属性 deep,我们通过上面的源码可得知,deep 在监听 reactive 响应式数据的时候会置为 true,即递归地监听对象及其所有嵌套属性的变化。如果想要深度侦听 ref 类型的响应式数据,则需要手动将 deep 置为 true

const obj = ref({ a: 1, b: { c: 2 } })
watch(
obj,
(newVal, oldVal) => {
console.log(`obj发生了变化`)
},
{
deep: true,
}
)
obj.value.b.c++ // obj发生了变化

第三个属性 flush 有三个有效值:presyncpost

pre 为默认值,表示在组件更新前执行侦听回调;post 表示在更新后调用;而 sync 则强制同步执行回调,因为一些数据往往会在短时间内改变多次,这样的强制同步是效率低下的,不推荐使用。

<template>
<div>
{{count}}
<button @click="count++">add</button>
</div>
</template>
<script>
import { ref, watch, onBeforeUpdate } from 'vue'
export default {
setup() {
const count = ref(0)
watch(
count,
() => {
console.log('count发生了变化')
},
{
flush: 'post',
}
)
onBeforeUpdate(() => {
console.log('组件更新前')
})
},
}
</script>

在点击 add 按钮时,先输出 组件更新前 再输出 count发生了变化,如果 flushpre,则输出顺序相反。

再来看看 onTrackonTrigger,这两个一看就是回调函数。onTrack 将在响应式 propertyref 作为依赖项被追踪的时候调用;onTrigger 将在依赖项变更导致 watchEffect 回调触发时被调用。

const count = ref(0)
watchEffect(
() => {
console.log(count.value)
},
{
onTrack(e) {
console.log('onTrack')
},
onTrigger(e) {
console.log('onTrigger')
},
}
)
count.value++

注意:onTrackonTrigger 只能在开发模式下进行调试时使用,不能再生产模式下使用。

Fragments

在 Vue2 中,组件只允许一个根元素的存在:

<template>
<div>
<header>...</header>
<main>...</main>
<footer>...</footer>
</div>
</template>

在 Vue3 中,允许多个根元素的存在:

<template>
<header>...</header>
<main>...</main>
<footer>...</footer>
</template>

这不仅简化了嵌套,而且暴露出去的多个元素可以受父组件样式的影响,一定程度上也减少了 css 代码。

早在以前,React 就允许 Fragments 组件,该组件用来返回多个元素,而不用在其上面添加一个额外的父节点:

render() {
return (
<React.Fragment>
<ChildA />
<ChildB />
<ChildC />
</React.Fragment>
);
}

如果想在 Vue2 实现 Fragments,需要安装 Vue-fragment 包,如今 Vue3 整合了 Vue-fragment,我们可以直接使用这个功能了。

Teleport

Teleport 用来解决逻辑属于该组件,但从技术角度(如 css 样式)上看却应该属于 app 外部的其他位置。

一个简单的栗子让你理解:

<!-- child.vue -->
<template>
<teleport to="#messageBox">
<p>Here are some messages</p>
</teleport>
</template>
<!-- index.html -->
<body>
<div id="app"></div>
<div id="messageBox"></div>
</body>

teleport 接受一个 to 属性,值为一个 css 选择器,可以是 id,可以是标签名(如 body)等等。

teleport 内的元素将会插入到 to 所指向的目标父元素中进行显示,而内部的逻辑是和当前组件相关联的,除去逻辑外,上面的代码相当于这样:

<!-- index.html -->
<body>
<div id="app"></div>
<div id="messageBox">
<p>Here are some messages</p>
</div>
</body>

因此 teleport 中的元素样式,是会受到目标父元素样式的影响的,这在创建全屏组件的时候非常好用,全屏组件需要写 css 做定位,很容易受到父元素定位的影响,因此将其插入到 app 外部显示是非常好的解决方法。

Suspense

Suspense 提供两个 template,当要加载的组件不满足状态时,显示 default template,满足条件时才会开始渲染 fallback template

<!-- AsyncComponent.vue -->
<template>
<div>{{msg}}</div>
</template>
<script>
export default {
setup() {
return new Promise((resolve) => {
setTimeout(() => {
return resolve({ msg: '加载成功' })
}, 1000)
})
},
}
</script>
<!-- parent.vue -->
<template>
<div>
<Suspense>
<template #default>
<AsyncComponent></AsyncComponent>
</template>
<template #fallback>
<h1>Loading...</h1>
</template>
</Suspense>
</div>
</template>
<script>
import AsyncComponent from './AsyncComponent.vue'
export default {
components: { AsyncComponent },
setup() {},
}
</script>

这样在一开始会显示 1 秒的 Loading...,然后才会显示 AsyncComponent,因此在做加载动画的时候可以用 Suspense 来处理。

其他新特性

更多比较细小的新特性官网说的很详细,请看:其他新特性

简单梳理下 Vue3 的新特性的更多相关文章

  1. Vue3.0新特性

    Vue3.0新特性 Vue3.0的设计目标可以概括为体积更小.速度更快.加强TypeScript支持.加强API设计一致性.提高自身可维护性.开放更多底层功能. 描述 从Vue2到Vue3在一些比较重 ...

  2. 如何使用开源库,吐在VS2013发布之前,顺便介绍下V2013的新特性"Bootstrap"

    如何使用开源库,吐在VS2013发布之前,顺便介绍下VS2013的新特性"Bootstrap" 刚看到Visual Studio 2013 Preview - ASP.NET, M ...

  3. vue3.0新特性以及进阶路线

    Vue3.0新特性/改动 新手学习路线  ===> 起步 1. 扎实的 JavaScript / HTML / CSS 基本功.这是前置条件. 2. 通读官方教程 (guide) 的基础篇.不要 ...

  4. 如何使用开源库,吐在VS2013发布之前,顺便介绍下VS2013的新特性"Bootstrap"

    刚看到Visual Studio 2013 Preview - ASP.NET, MVC 5, Web API 2新功能搶先看 看了下VS2013带来的"新特性",直觉上看,除了引 ...

  5. 尝鲜 vue3.x 新特性 - CompositionAPI

    0. 基础要求 了解常见的 ES6 新特性 ES6 的导入导出语法 解构赋值 箭头函数 etc... 了解 vue 2.x 的基本使用 组件 常用的指令 生命周期函数 computed.watch.r ...

  6. Vue3的新特性及相关的Composition API使用

    首先 创建项目 Vue3 Vue3 相较于Vue2 的6大亮点: 1 性能快. 2 按需编译 体积更小 3 提供了组合API 类似于react 的React Hooks 4 更好的Ts支持 5 暴露了 ...

  7. vue3.x新特性之setup函数,看完就会用了

    最近有小伙伴跟我聊起setup函数,因为习惯了vue2.x的写法导致了,setup用起来觉得奇奇怪怪的,在一些api混编的情况下,代码变得更加混乱了,个人觉得在工程化思想比较强的团队中使用setup确 ...

  8. Vue3的新特性

    总概 1) 性能提升 打包大小减少 41% 初次渲染快 55%,更新渲染快 133% 内存减少 54% 使用 Proxy 代替 defineProperty 实现数据响应式 重写虚拟 DOM 的实现和 ...

  9. 《Java7中 下划线的新特性》

    //Java7引入了一个新功能:程序员可以在数值中使用下画线,不管是 //整形数值,还是浮点型数值,都可以自由地使用下划线.通过下划线 //分隔,可以更直观的分辨数值中到底有多少位. public c ...

随机推荐

  1. 【STM32】时钟

    1. 在STM32中,有五个时钟源,为HSI.HSE.LSI.LSE.PLL: ① HSI是高速内部时钟,RC振荡器,频率为8MHz: ② HSE是高速外部时钟,可接石英/陶瓷谐振器,或者接外部时钟源 ...

  2. JavaHomeWorkList

    3.17 关键词:剪刀石头布:随机数 1 import java.util.Scanner; 2 public class JSB { 3 public static void main(String ...

  3. Inceptor Parse error [Error 1110] line 102,24 SQL问题

    今天遇到一个SQL跑不通的问题: 去掉cast as 去掉round 最初以为是Inceptor不兼容ORACLE语句Cast as 导致的,做的以下测试 发现都能跑通,说明Cast as语句在Inc ...

  4. Educational Codeforces Round 85 (Div. 2)

    题目链接:https://codeforces.com/contest/1334 A. Level Statistics 题意 一个关卡有玩家的尝试次数和通关次数,按时间顺序给出一个玩家 $n$ 个时 ...

  5. LInux 终端命令

    删除目录: 绝对路径开头以"/"开始之后跟着根目录或家目录 删除后不会在垃圾站中 树的顶部那个'.'代表当前目录 用mv命令对一个存在文件重命名 这个more关键字一次性显示不完的 ...

  6. 牛客编程巅峰赛S1第6场 - 黄金&钻石&王者 A.牛牛爱奇数 (模拟)

    题意:有一组数,每次将所有相等的偶数/2,求最少操作多少次使得所有数变成奇数. 题解:用桶标记,将所有不同的偶数取出来,然后写个while模拟统计一下次数就行. 代码: class Solution ...

  7. ElasticSearch 搜索引擎概念简介

    公号:码农充电站pro 主页:https://codeshellme.github.io 1,倒排索引 倒排索引是一种数据结构,经常用在搜索引擎的实现中,用于快速找到某个单词所在的文档. 倒排索引会记 ...

  8. virtualBox 设置增强功能粘贴和拖放

    virtualBox 5.2.8 (在运行的虚拟里中) 设备 -> 安装增强功能 virtualBox 管理器中设置(要在虚拟机关机的情况下配置) 常规 -> 高级里设置双向粘贴和拖放

  9. 九种姿势运行Mimikatz

    https://www.freebuf.com/articles/web/176796.html

  10. JWT实现登录认证实例

    JWT全称JSON Web Token,是一个紧凑的,自包含的,安全的信息交换协议.JWT有很多方面的应用,例如权限认证,信息交换等.本文将简单介绍JWT登录权限认证的一个实例操作. JWT组成 JW ...