vue3目前已经发布了alpha版本,除了服务端渲染,其它工作已经全部完成。尤大大也升级了vue-loader,提供了一个可以使用.vue组件的测试模板。vue3最大的改变是加入了这个灵感来源于React Hook的Composition API(组成API),这个API将对vue编程产生了根本性变革,但是vue3还是兼容vue2的Options API。除此之外,还引入了一些不兼容修改,具体可查看Vue3已合并的RFC。本文并不会全面介绍vue3的新特效,只会着重与vue3的核心——Composition API。
先看一下基础例程:
<template>
<button @click="increment">
Count is: {{ state.count }}, double is: {{ state.double }}
</button>
</template>
<script>
import { reactive, computed } from 'vue'
export default {
setup(props, context) {
const state = reactive({
count: 0,
double: computed(() => state.count * 2)
})
function increment() {
state.count++
}
return {
state,
increment
}
}
}
</script>
从上面的基础例程可以看到,vue3的.vue组件大体还是和vue2一致,由template``、script
和style
组成,作出的改变有以下几点:
data
、computed
等选项仍然支持,但使用setup
时不建议再使用vue2中的data
灯选项。reactive
、computed
、watch
、onMounted
等抽离的接口代替vue2中data
等选项。为什么使用Composition API代替Options API?难道将各种类型的代码分类写在对应的地方不比把所有的代码都写在一个setup函数中要好?要解答这两个问题,我们首先来思考一下Options API的局限。
Options API是把代码分类写在几个Option中:
export default {
components: {
},
data() {
},
computed: {
},
watch: {
},
mounted() {
},
}
当组件比较简单只有一个逻辑的时候,稍微麻烦的是用户必须在几个Option之间跳来跳去,这个是小问题,写代码哪能不用鼠标呢?挑战往往在复杂的情况下才会出现。当一个vue组件内存在多个逻辑时会怎样呢?
图片中相同的颜色表示一种逻辑。
必须指出的是,Composition API提高了代码的上限,也降低了代码的下限。在使用Options API时,即便再菜的鸟也能保证各种代码按其种类进行划分。但使用Composition API时,由于其开放性,出现什么代码是无法想象的。但毫无疑问,Options API到Composition API是vue的一个巨大进步,vue从此可以从容面对大型项目。
在vue2中要实现逻辑复用主要有两种方式:
// GenericSort.vue
<template>
<div>
<!-- 暴露逻辑的数据给子组件 -->
<slot :data="data"></slot>
</div>
</template>
<script>
export default {
// 在这里完成逻辑
data() {
}
}
</script>
使用的时候
<template>
<!--传入未排序的数据unSortdata-->
<GenericSort data="unSortdata">
<template v-slot="sortData">
<!-- 使用经过处理后的sortData数据 -->
<template>
</GenericSearch>
</template>
但这种方式还是有许多缺点:
<template>
<div>
<p> {{count}}</p>
<button @click="onClick" :disabled="state">Start</button>
</div>
</template>
<script>
import {ref} from 'vue'
// 倒计时逻辑的Composition Function
const useCountdown = (initialCount) => {
const count = ref(initialCount)
const state = ref(false)
const start = (initCount) => {
state.value = true;
if (initCount > 0) {
count.value = initCount
}
if (!count.value) {
count.value = initialCount
}
const interval = setInterval(() => {
if (count.value === 0) {
clearInterval(interval)
state.value = false
} else {
count.value--
}
}, 1000)
}
return {
count,
start,
state
}
}
export default {
setup() {
// 直接使用倒计时逻辑
const {count, start, state} = useCountdown(10)
const onClick = () => {
start()
}
return {count, onClick, state}
}
}
</script>
vue3建议使用如React hook中一样使用use开头命名抽取的逻辑函数,如上代码抽取的逻辑几乎如函数一般,使用的时候也极其方便,完胜vue2中抽取逻辑的方法。
vue2比较令人诟病的地方还是对ts的支持,对ts支持不好是vue2不适合大型项目的一个重要原因。其根本原因是Vue依赖单个this
上下文来公开属性,并且vue中的this
比在普通的javascript更具魔力(如methods对象下的单个method中的this
并不指向methods,而是指向vue实例)。换句话说,尤大大在设计Option API时并没有考虑对ts引用的支持。那vue2中是怎样做到对ts的支持?
<script lang="ts">
import { Vue, Component, Prop } from 'vue-property-decorator'
@Component
export default class YourComponent extends Vue {
@Prop(Number) readonly propA: number | undefined
@Prop({ default: 'default value' }) readonly propB!: string
@Prop([String, Boolean]) readonly propC: string | boolean | undefined
}
</script>
vue2对ts的支持主要是通过vue class component,还需引入vue-property-decorator包,该库完全依赖于vue-class-component包。咋一看,这不是支持了吗?下面聊聊这种方式的缺点:
这些原因让人望而却步,vue2的ts项目数量不多也是可以让人理解的。相比与vue2,vue3对ts的支持则好得多:
this
,大部分API大多使用普通的变量和函数,它们天然类型友好。Composition API是一系列接口的总称,下文将逐一介绍Composition API的各个接口。学习代码
setup
setup是vue新增的一个选项,它是组件内使用Composition API的入口。setup中是没有this
上下文的(js 函数都有this,但是setup的this跟vue2的this不是一个东西,已经完全没用了,故为没有)。
beforeCreate
钩子之前执行,这意味着setup中无法使用其它option(如data)中的变量,而其它option可以使用setup中返回的变量。<template>
<div>{{ count }} {{ object.foo }}</div>
</template>
<script>
import { ref, reactive } from 'vue'
export default {
setup() {
const count = ref(0)
const object = reactive({ foo: 'bar' })
// 暴露至模板中
return {
count,
object
}
}
}
</script>
import { h, ref, reactive } from 'vue'
export default {
setup() {
const count = ref(0)
const object = reactive({ foo: 'bar' })
return () => h('div', [
count.value,
object.foo
])
}
}
setup
的参数interface Data {
[key: string]: unknown
}
interface SetupContext {
attrs: Data
slots: Slots
parent: ComponentInstance | null
root: ComponentInstance
emit: ((event: string, ...args: unknown[]) => void)
}
function setup(
props: Data,
context: SetupContext
): Data
reactive
reactive
函数接收一个对象,并返回一个对这个对象的响应式代理。它与vue2中的Vue.obserable()
是等价的,为避免与RxJs中的observable
重名,故改名为reactive
。
<template>
<div>
<input type="text" v-model="state.input">
<p>input: {{state.input}}</p>
<p>computedInput: {{state.computedInput}}</p>
</div>
</template>
<script>
import {reactive, computed, watch} from 'vue'
export default {
setup(props) {
const state = reactive({
input: '',
computedInput: computed(() => '? '+state.input+' ?' )
})
return {state, singleComputed}
}
}
</script>
必须注意的是,reactive使用者必须始终保持对返回对象的引用,以保持反应性。该对象不能被破坏或散布,所有setup和组合函数中不能返回reactive的解构。
<template>
<div>
{{input}}
<button @click="onClick">change</button>
</div>
</template>
<script>
import {reactive, computed, watch, toRefs} from 'vue'
export default {
setup(props) {
const state = reactive({input: ''})
const onClick = () => {
state.input = '我已经改变了'
}
// return state 模板上下文丢失引用
// return {...state, onClick} 模板上下文丢失引用
return {...toRefs(state), onClick} // 正确的
}
}
</script>
如上组件如果不使用toRefs
,在点击change按钮时,组件并不会重新渲染,也就是说模板中的input还是之前的值。出现这种现象的根本原因是js中是值传递,并不是引用传递。解决方案是使用toRefs
,至于ref是什么,请看下文。
ref
ref函数接收一个用于初始化的值并返回一个响应式的和可修改的ref对象。该ref对象存在一个value属性,value保存着ref对象的值。
<template>
<div class="tf">
<div>
<!--在模板中使用时不需要使用count.value, 会自动解包-->
<p>{{count}}</p>
<button @click="onClick(true)">+</button>
<button @click="onClick(false)">-</button>
</div>
</div>
</template>
<script>
import {ref, reactive} from 'vue'
export default {
setup() {
const count = ref(0)
<!--在reactive中使用也不需要count.value, 也会解包-->
const state = reactive({count})
const onClick = (isAdd) => {
isAdd ? count.value++ : count.value --
}
return {count, onClick}
},
}
</script>
注意:在reactive和tempalte中使用的时候,不需要使用.value
,它们会自动解包。
isRef
isRef
用于判断变量是否为ref对象。
const unwrapped = isRef(foo) ? foo.value : foo
toRefs
toRefs
用于将一个reactive对象转化为属性全部为ref对象的普通对象。
const state = reactive({
foo: 1,
bar: 2
})
const stateAsRefs = toRefs(state)
/*
Type of stateAsRefs:
{
foo: Ref,
bar: Ref
}
*/
toRefs
在setup或者Composition Function的返回值特别有用:
import {reactive, toRefs} from 'vue'
function useFeatureX() {
const state = reactive({
foo: 1,
bar: 2
})
return state
}
function useFeature2() {
const state = reactive({
a: 1,
b: 2
})
return toRefs(state)
}
export default {
setup() {
// 使用解构之后foo和bar都丧失响应式
const { foo, bar } = useFeatureX()
// 即便使用了解构也不会丧失响应式
const {a, b}= useFeature2()
return {
foo,
bar
}
}
}
computed
computed函数与vue2中computed功能一致,它接收一个函数并返回一个value为getter返回值的不可改变的响应式ref对象。
const count = ref(1)
const plusOne = computed(() => count.value + 1)
console.log(plusOne.value) // 2
plusOne.value++ // 错误,computed不可改变
// 同样支持set和get属性
onst count = ref(1)
const plusOne = computed({
get: () => count.value + 1,
set: val => { count.value = val - 1 }
})
plusOne.value = 1
console.log(count.value) // 0
readonly
readonly
函数接收一个对象(普通对象或者reactive对象)或者ref并返回一个只读的参数对象代理,在参数对象改变时,返回的代理对象也会相应改变。如果传入的是reactive或ref响应对象,那么返回的对象也是响应的。
<template>
<!--vue3中允许template下有多个根元素-->
<input type="text" v-model="state.count">
<button @click="onClick">change</button>
</template>
<script>
import {reactive, readonly, watch} from 'vue'
export default {
setup() {
const state = reactive({count: 12})
const rnState = readonly(state)
watch(() => {
// state改变也会触发rnState的watch
console.log(rnState.count)
})
const planObj = {count: 12}
const rnPlanObj = readonly(planObj)
const onClick = () => {
planObj.count = 888
console.log(rnPlanObj.count) // 888
}
return {state, onClick}
}
}
</script>
watch
相比于vue2的watch,Composition API的watch接口不仅仅是将其逻辑抽取出来,其功能也得到了极大的丰富。下面看其类型定义:
type StopHandle = () => void
type WatcherSource<T> = Ref<T> | (() => T)
type MapSources<T> = {
[K in keyof T]: T[K] extends WatcherSource<infer V> ? V : never
}
type InvalidationRegister = (invalidate: () => void) => void
interface DebuggerEvent {
effect: ReactiveEffect
target: any
type: OperationTypes
key: string | symbol | undefined
}
interface WatchOptions {
lazy?: boolean
flush?: 'pre' | 'post' | 'sync'
deep?: boolean
onTrack?: (event: DebuggerEvent) => void
onTrigger?: (event: DebuggerEvent) => void
}
// basic usage
function watch(
effect: (onInvalidate: InvalidationRegister) => void,
options?: WatchOptions
): StopHandle
// wacthing single source
function watch<T>(
source: WatcherSource<T>,
effect: (
value: T,
oldValue: T,
onInvalidate: InvalidationRegister
) => void,
options?: WatchOptions
): StopHandle
// watching multiple sources
function watch<T extends WatcherSource<unknown>[]>(
sources: T
effect: (
values: MapSources<T>,
oldValues: MapSources<T>,
onInvalidate: InvalidationRegister
) => void,
options? : WatchOptions
): StopHandle
template>
<div>
<input type="text" v-model="state.count">{{state.count}}
<input type="text" v-model="inputRef">{{inputRef}}
</div>
</template>
<script>
import {watch, reactive, ref} from 'vue'
export default {
setup() {
const state = reactive({count: 0})
const inputRef = ref('')
// state.count与inputRef中任意一个源改变都会触发watch
watch(() => {
console.log('state', state.count)
console.log('ref', inputRef.value)
})
return {state, inputRef}
}
}
</script>
指定依赖源
在基础用法中:
解决上面两个问题可以为watch指定依赖源:
<template>
<div>
state2.count: <input type="text" v-model="state2.count">
{{state2.count}}<br/><br/>
ref2: <input type="text" v-model="ref2">{{ref2}}<br/><br/>
</div>
</template>
<script>
import {watch, reactive, ref} from 'vue'
export default {
setup() {
const state2 = reactive({count: ''})
const ref2 = ref('')
// 通过函数参数指定reative依赖源
// 只有在state2.count改变时才会触发watch
watch(
() => state2.count,
() => {
console.log('state2.count',state2.count)
console.log('ref2.value',ref2.value)
})
// 直接指定ref依赖源
watch(ref2,() => {
console.log('state2.count',state2.count)
console.log('ref2.value',ref2.value)
})
return {state, inputRef, state2, ref2}
}
}
</script>
<template>
<div>
<p>
<input type="text" v-model="state.a"><br/><br/>
<input type="text" v-model="state.b"><br/><br/>
</p>
<p>
<input type="text" v-model="ref1"><br/><br/>
<input type="text" v-model="ref2"><br/><br/>
</p>
</div>
</template>
<script>
import {reactive, ref, watch} from 'vue'
export default {
setup() {
const state = reactive({a: 'a', b: 'b'})
// state.a和state.b任意一个改变都会触发watch的回调
watch(() => [state.a, state.b],
// 回调的第二个参数是对应上一个状态的值
([a, b], [preA, preB]) => {
console.log('callback params:', a, b, preA, preB)
console.log('state.a', state.a)
console.log('state.b', state.b)
console.log('****************')
})
const ref1 = ref(1)
const ref2 = ref(2)
watch([ref1, ref2],([val1, val2], [preVal1, preVal2]) => {
console.log('callback params:', val1, val2, preVal1, preVal2)
console.log('ref1.value:',ref1.value)
console.log('ref2.value:',ref2.value)
console.log('##############')
})
return {state, ref1, ref2}
}
}
</script>
<template>
<div>
<input type="text" v-model="inputRef">
<button @click="onClick">stop</button>
</div>
</template>
<script>
import {watch, ref} from 'vue'
export default {
setup() {
const inputRef = ref('')
const stop = watch(() => {
console.log('watch', inputRef.value)
})
const onClick = () => {
// 取消watch,取消之后对应的watch不会再执行
stop()
}
return {inputRef, onClick}
}
}
</script>
<template>
<div>
<h3>Cleanup</h3>
<input type="text" v-model="inputRef">
<input type="text" v-model="inputRef2">
</div>
</template>
<script>
import {ref, watch} from 'vue'
export default {
setup() {
const inputRef = ref('')
const getData = (value) => {
const handler = setTimeout(() => {
console.log('已获得数据', value)
}, 5000)
return handler
}
watch((onCleanup) => {
const handler = getData(inputRef.value)
// 清除副作用
onCleanup(() => {
clearTimeout(handler)
})
})
// 另外一种获取onCleanup的方式
const inputRef2 = ref('')
watch(inputRef2, (val, oldVal, onCleanup) => {
const handler = getData(val)
// 清除副作用
onCleanup(() => {
clearTimeout(handler)
})
})
return {inputRef, inputRef2}
}
}
</script>
watch提供了一个onCleanup的副作用清除函数,该函数接收一个函数,在该函数中进行副作用清除。那么onCleanup什么时候执行?
watch的callback即将被第二次执行时先执行onCleanup。
watch被停止时,即组件被卸载之后。
watch选项
watch接口还支持配置一些选项以改变默认行为,配置选项可通过watch的最后一个参数传入。
lazy
vue2中的watch默认在组件挂载之后是不会执行的,但如果希望立刻执行,可设置immediate为true。而在vue3中,watch默认会在组件挂载之后执行,如果希望取得与vue2 watch同样的行为,可配置lazy为true:
<template>
<div>
<h3>Watch Option</h3>
<input type="text" v-model="inputRef">
</div>
</template>
<script>
import {watch, ref} from 'vue'
export default {
setup() {
const inputRef = ref('')
// 配置lazy为true之后,组件挂载不会执行,知道inputRef改变才执行
watch(() => {
console.log(inputRef.value)
}, {lazy: true})
return {inputRef}
}
}
</script>
deep
如vue2中的deep参数一般,在设置deep为true之后,深层对象的任意一个属性改变都会触发回调的执行。
<template>
<div>
<button @click="onClick">CHANGE</button>
</div>
</template>
<script>
import {watch, ref} from 'vue'
export default {
setup() {
const objRef = ref({a: {b: 123}, c: 123})
watch(objRef,() => {
console.log('objRef.value.a.b',objRef.value.a.b)
}, {deep: true})
const onClick = () => {
// 设置了deep之后,深层对象任意属性的改变都会触发watch回调的执行
objRef.value.a.b = 780
}
return {onClick}
}
}
</script>
flush
当同一个tick中发生许多状态突变时,Vue的反应性系统会缓冲观察者回调并异步刷新它们,以避免不必要的重复调用。默认的行为是:当调用观察者回调时,组件状态和DOM状态已经同步。
这种行为可以通过flush来进行配置,flush有三个值,分别是post
(默认)、pre
与sync
。sync
表示在状态更新时同步调用,pre
则表示在组件更新之前调用。
onTrack与onTrigger
onTrack与onTrigger用于调试:
<template>
<div>
<h4>test onTrigger & onTrack</h4>
<input type="text" v-model="debugRef">
</div>
</template>
<script>
import {watch, ref, reactive} from 'vue'
export default {
setup() {
const debugRef = ref(0)
// 在组件挂载之后只有onTrack被调用
// 在debugRef改变之后先调用onTrigger,再调用onTrack
watch(() => {
console.log('debugRef.value:',debugRef.value)
}, {
onTrack() {
debugger;
},
onTrigger() {
debugger;
}
})
return {inputRef, onClick, debugRef}
}
}
</script>
Composition API当然也提供了组件生命周期钩子的回调。
import { onMounted, onUpdated, onUnmounted } from 'vue'
const MyComponent = {
setup() {
onMounted(() => {
console.log('mounted!')
})
onUpdated(() => {
console.log('updated!')
})
onUnmounted(() => {
console.log('unmounted!')
})
}
}
对比vue2的生命周期钩子:
beforeCreate
-> 使用 setup()
created
-> 使用 setup()
beforeMount
-> onBeforeMount
mounted
-> onMounted
beforeUpdate
-> onBeforeUpdate
updated
-> onUpdated
beforeDestroy
-> onBeforeUnmount
destroyed
-> onUnmounted
errorCaptured
-> onErrorCaptured
provide
&inject
类似于vue2中provide
与inject
, vue3提供了对应的provide
与inject
API。
import { provide, inject } from 'vue'
const ThemeSymbol = Symbol()
const Ancestor = {
setup() {
provide(ThemeSymbol, 'dark')
}
}
const Descendent = {
setup() {
const theme = inject(ThemeSymbol, 'light' /* optional default value */)
return {
theme
}
}
}
当使用前文中的ref时会感到迷惑,模板中也有一个ref用于获取组件实例或dom对象,这样会不会冲突。而实际上在vue3中,ref会用来保存templates的ref。
<template>
<div ref="root">
Test Template Refs
</div>
</template>
<script>
import { ref, onMounted } from 'vue'
export default {
setup() {
const root = ref(null)
onMounted(() => {
// 在render初始化之后,DOM元素将被赋值为ref
console.log(root.value) // div dom对象
})
return {
root
}
}
}
</script>
defineComponent
该接口是为了支持ts类型引用:
import { defineComponent } from 'vue'
export default defineComponent({
props: {
foo: String
},
setup(props) {
props.foo // <- type: string
}
})
当只有setup
选项时:
import { defineComponent } from 'vue'
// provide props typing via argument annotation.
export default defineComponent((props: { foo: string }) => {
props.foo
})
总的来说,vue3的Composition API为开发者提供了更大的灵活,但更大的灵活需要更大的规则,对编程者的代码素质有一定的要求,一定程度上增加了vue的入门难度,另外为了持有原始类型的引用引入了ref,引入了一些复杂度,但相比于Composition API 所产生的效益,这些微不足道。至此本文已到尾声,本文讲解比较浅显,并没有深入讲解各个接口的实现原理,但对于使用Vue3的Composition API已经足够,有不足之处请指出及谅解。
Composition API RFC