前言:Composition API 官网的解释是,一组附加的,基于函数的api,允许灵活组合组件的逻辑。本文首先讲述vue3相比vue2变更的地方,然后再逐一讲解常用的Cmposition API。
篇幅较长,如果想直接查看composition api,而不想看vue3与vue2的变更, 点这里
下面讲述vue3如何安装和使用:
vite
npm init vite-app hello-vue3 # 或者 yarn create vite-app hello-vue3vue-cli v4.5.0 以上版本
npm install -g @vue/cli # 或者 yarn global add @vue/cli vue create hello-vue3 # 然后选择vue3的预设下面讲述Vue3破坏性变更的地方
全局api已迁移至 createApp()创建的实例下
2.x全局API3.x实例API(app)Vue.config.production已经删除Vue.componentapp.componentVue.directiveapp.directiveVue.mixinapp.mixinVue.useapp.usetree shaking主要作用是打包项目时会将用不到的方法不打包进项目中,这样得以优化项目体积。
import { nextTick } from 'vue' nextTick(() => { // something DOM-related })以前的api改为es模块导入,以及vShow,transition等内部助手方法,都启用了摇树优化(tree shaking),只有实际用到的才会被打包进去。
下面讲述模板指令相关破坏性变更的地方:
v-model prop 和 event 默认的名称已经更改
<ChildComponent v-model="pageTitle" /> <!-- 上下等价 --> <!-- value-> modelValue --> <!-- input-> update:modelValue --> <ChildComponent :modelValue="pageTitle" @update:modelValue="pageTitle = $event" />同时,v-model.sync的修饰符也已经删除, v-model支持绑定不同的数据,可以作为其替代。
<ChildComponent v-model:title="pageTitle" v-model:content="pageContent" /> <!-- 上下等价 --> <ChildComponent :title="pageTitle" @update:title="pageTitle = $event" :content="pageContent" @update:content="pageContent = $event" />如果用于同一元素,v-if则优先级高于v-for
在2.x中,当在同一元素上使用v-if和v-for时,v-for将优先使用。在3.x中,v-if优先级始终高于v-for。v-bind的绑定顺序将影响渲染结果。
<!-- template --> <div id="red" v-bind="{ id: 'blue' }"></div> <!-- result --> <div id="blue"></div> <!-- template --> <div v-bind="{ id: 'blue' }" id="red"></div> <!-- result --> <div id="red"></div>规则是,绑定相同属性,在后边的具有最高优先级。(在2.x中,单个属性的优先级,比v-bind高)。
ref的用法发生改变,需要一个变量来接收dom对象的引用, 不再自动合并到$refs
<div v-for="item in list" :ref="setItemRef"></div> export default { data() { return { itemRefs: [] // 存储节点引用 } }, methods: { setItemRef(el) { this.itemRefs.push(el) } }, beforeUpdate() { this.itemRefs = [] }, updated() { console.log(this.itemRefs) } }组件方面发生变更的地方
函数式组件:
在2.x中,函数式组件的性能提升现在可以忽略不计,因此我们建议仅使用有状态组件只能使用接收一个普通的函数来创建函数式组件,参数包括props和context(即,slots,attrs,emit)<template> 与 单文件组件中的 functional选项已经删除通过函数创建:
import { h } from 'vue' const DynamicHeading = (props, context) => { return h(`h${props.level}`, context.attrs, context.slots) } DynamicHeading.props = ['level'] export default DynamicHeading通过单文件组件创建
<template> <component v-bind:is="`h${$props.level}`" v-bind="$attrs" // v-on="listeners" listeners现在作为$attrs的一部分传递,可以删除 /> </template> <script> export default { props: ['level'] } </script>异步组件发生改变的地方:
新增了defineAsyncComponent方法定义异步组件component 选项重命名为 loader加载函数本身不接收resolve和reject传递参数,必须返回Promise2.x中:
const asyncPage = () => import('./NextPage.vue')3.x中:
import { defineAsyncComponent } from 'vue' import ErrorComponent from './components/ErrorComponent.vue' import LoadingComponent from './components/LoadingComponent.vue' // 无选项 const asyncPage = defineAsyncComponent(() => import('./NextPage.vue')) // 带选项 const asyncPageWithOptions = defineAsyncComponent({ loader: () => import('./NextPage.vue'), delay: 200, timeout: 3000, errorComponent: ErrorComponent, loadingComponent: LoadingComponent })渲染函数,这个改变不会影响使用<template>的用户,不用render api的可以略过。
有以下改变:
h函数以全局导入的方式替代render函数参数传递的方式vnode的props的格式变的扁平化2.x中:
export default { render(h) { return h('div') } }3.x中:
import { h, reactive } from 'vue' // 手动导入h export default { setup(props, { slots, attrs, emit }) { const state = reactive({ count: 0 }) function increment() { state.count++ } // 返回一个渲染函数 return () => h( 'div', // 节点名 { onClick: increment // 节点属性 }, state.count // 子节点 ) } }vnode的属性结构(h的第二个参数):
// 2.x { staticClass: 'button', class: {'is-outlined': isOutlined }, staticStyle: { color: '#34495E' }, style: { backgroundColor: buttonColor }, attrs: { id: 'submit' }, domProps: { innerHTML: '' }, on: { click: submitForm }, key: 'submit-button' } // 3.x Syntax { class: ['button', { 'is-outlined': isOutlined }], style: [{ color: '#34495E' }, { backgroundColor: buttonColor }], id: 'submit', innerHTML: '', onClick: submitForm, key: 'submit-button' }注册组件,2.x中:
// 假设有个ButtonCounter的自定义组件 export default { render(h) { return h('button-counter') } }3.x中
import { h, resolveComponent } from 'vue' export default { setup() { const ButtonCounter = resolveComponent('button-counter') // 先解析组件 return () => h(ButtonCounter) // 再传递组件 } }变化:
this.$slots 现在将slots公开为功能this.$scopedSlots 已经移除在渲染函数中使用:
// 2.x 语法 h(LayoutComponent, [ h('div', { slot: 'header' }, this.header), h('div', { slot: 'content' }, this.content) ]) // 3.x 语法 h(LayoutComponent, {}, { header: () => h('div', this.header), content: () => h('div', this.content) })编程方式使用:
// 2.x 语法 this.$scopedSlots.header // 3.x 语法 this.$slots.header()有以下发生改变:
自定义元素的白名单,在编译器选项中配置。is属性的使用,仅限于保留组件的标签名新增指令v-is,解决html 元素限制。如果想要指定vue之外的自定义元素(比如web组件),以plastic-button为例
<plastic-button></plastic-button>2.x中
Vue.config.ignoredElements = ['plastic-button'] // 将plastic-button列入白名单3.x中有两种方式可选,一种是作为编译选项配置,一种是运行时配置:
// webpack的vue-loader里配置 rules: [ { test: /\.vue$/, use: 'vue-loader', options: { compilerOptions: { isCustomElement: tag => tag === 'plastic-button' // 指定组件加入白名单 } } } // ... ] // 运行时配置 const app = Vue.createApp({}) app.config.isCustomElement = tag => tag === 'plastic-button'在2.x中,它被解释为使用is的值作为组件的name去解析并渲染plastic-button组件,但该做法阻止了原生button元素的行为。
在3.x中,仅当使用<component>标签的时候,is才会和2.x中的用法相同。
<component is="plastic-button"/>在普通组件上使用,它的行为类似与普通属性:
<foo is="bar" /> 2.x行为: 渲染bar组件3.x行为: 渲染foo组件,并传递is属性。在普通元素上使用:
<button is="plastic-button">Click Me!</button> 2.x行为:渲染plastic-button组件。3.x行为:通过调用呈现原生按钮 // 创建了web组件plastic-button的实例,但保留了button的特性 document.createElement('button', { is: 'plastic-button' })In-Dom模板,主要用于需要遵守html特定元素的解析规则的情况,比如<ul>,<ol>,<table>和<select>有什么元素可以在其内部出现的限制,以及一些元素,如<li>,<tr>和<option>只能出现某些其他元素中。
2.x中通常是这样使用:
<table> <tr is="blog-post-row"></tr> </table>而3.x则改成了v-is:
<table> <!-- 注意v-is是指令,里面接受的是表达式,要填字符串,必须加引号‘’ --> <tr v-is="'blog-post-row'"></tr> </table>以下为移除的API
由于KeyboardEvent.keyCode已弃用, 因此vue3不再支持该功能。
v-on不再支持使用数字(即keyCodes)作为修饰符config.keyCodes不再受支持 <!-- 不支持 --> <input v-on:keyup.13="submit" /> <!-- 支持 --> <input v-on:keyup.delete="confirmDelete" /> Vue.config.keyCodes = { // 不支持 f1: 112 }$on、$off、$once 不再支持,官方建议使用第三方库mitt或者tiny-emitter替换。$emit仍作为现有api,触发父组件事件而受支持。
filters选项已被移除,在vue3中不再受支持(因为该语法打破了{}内只是javascript的假设),官方建议用方法或者computed代替。
如果你想使用全局过滤器,那么可以这么做:
// main.js const app = createApp(App) app.config.globalProperties.$filters = { currencyUSD(value) { return '$' + value } } <template> <h1>Bank Account Balance</h1> <p>{{ $filters.currencyUSD(accountBalance) }}</p> </template>使用这种方法时,你只能使用方法,而不能使用计算属性。因为后者仅在单个组件的上下文中定义才有意义。
内联模板不再受支持:
<!-- 这个没用过= =! --> <my-component inline-template> <div> <p>These are compiled as the component's own template.</p> <p>Not parent's transclusion content.</p> </div> </my-component>官方建议用script标签或者slot代替。具体用法见官网
$destroy实例方法。用户不应再手动管理各个Vue组件的生命周期。
目前所有的官方库和工具都支持vue3,但其中大多数仍处于beta(公测)状态。官方计划在2020年底之前稳定并切换所有项目以使用dist标签。
从v4.5.0开始,vue-cli现在提供了内置选项,可以在创建新项目时选择Vue 3预设。
vue router 4.0 提供vue3的支持,并且具有许多重大更改。
Vuex 4.0通过与3.x大致相同的API提供了Vue 3支持。唯一的重大变化是插件的安装方式
下面开始正式讲解常用的组合式API。
reactive 基本等价于2.x中的Vue.observable(),返回一个响应式对象,就像2.x中定义在data选项里的数据一样,最终都会被转换成响应式对象。基于 ES2015 的 Proxy 实现。
import { reactive } from 'vue' // state 现在是一个响应式的状态 const state = reactive({ count: 0, })接受一个参数值并返回一个响应式且可改变的 ref 对象。ref 对象拥有一个指向内部值的单一属性.value
const count = ref(0) // 相当于返回{value:0} console.log(count.value) // 0 count.value++ console.log(count.value) // 1可能有些同学会问了,既然reactive和ref都能创建响应式对象,他们之间有什么区别呢,或者说各自使用在哪种场景呢?下面看一个例子:
// 风格 1: 将变量分离 let x = ref(0) let y = ref(0) function updatePosition(e) { x.value = e.pageX y.value = e.pageY } // --- 与下面的相比较 --- // 风格 2: 单个对象 const pos = reactive({ x: 0, y: 0, }) function updatePosition(e) { pos.x = e.pageX pos.y = e.pageY }从上可以看出,他们的使用符合js的值类型和引用类型的概念:
ref 适合基础类型值reactive 适合对象类型的值有人会说,既然这样,那为什么不把变量全塞对象里直接用reactive呢?这是因为对象解构的时候,数据会丢失响应式特性,如下:
const pos = reactive({ x: 0, y: 0, }) function updatePosition(e) { // 解构对象,导致响应式丢失,相当于重新将值赋给了一个变量,之后的更改不会改变原属性的值 let {x,y} = pos x = e.pageX y = e.pageY }正因为此,官方提供了toRefs与toRef的函数,来将一个响应式对象的基础类型属性值转换为ref对象,这才不可避免的有了ref的概念。
const state = reactive({ foo: 1, bar: 2, }) const fooRef = toRef(state, 'foo') // 转换单个的foo属性为ref对象 fooRef.value++ console.log(state.foo) // 2 state.foo++ console.log(fooRef.value) // 3 const state = reactive({ foo: 1, bar: 2, }) const stateAsRefs = toRefs(state) // 转换state对象的所有属性为ref对象 /* stateAsRefs 的类型如下: { foo: Ref<number>, bar: Ref<number> } */注意:ref对象在以下情况会自动解套,也就是,不需要写.value也能访问值。
当嵌套在reactive Object 中当作为setup的返回值返回setup是组件的新选项,作为在组件内使用 Composition API 的入口点。
调用时机 创建组件实例,初始化props → 调用setup → beforeCreate
模板中使用
<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>注意 setup 返回的 ref 在模板中会自动解开,不需要写 .value。
参数 第一个参数收传递给组件的属性,第二个参数,从原来的this上下文选择性暴露了一些属性。 export default { props: { name: String, }, setup(props, ctx) { // 不要解构props,会导致其失去响应式 watchEffect(() => { console.log(`name is: ` + props.name) }) conosole.log(ctx) // context.attrs // context.slots // context.emit }, } this的用法 this 在 setup() 中不可用。由于 setup() 在解析 2.x 选项前被调用,setup() 中的 this 将与 2.x 选项中的 this 完全不同。预期接收一个含有副作用的函数,仅当该过程中用到的响应式状态发生改变时,会重新执行该函数。
import { reactive, watchEffect } from 'vue' const state = reactive({ count: 0, }) onMounted(()=>{ // 立即执行一次,之后会在state.count发生改变的时候执行,组件卸载的时候销毁 watchEffect(() => { document.body.innerHTML = `count is ${state.count}` }) })watchEffect回调的执行时机:
立即执行传入的一个函数,并响应式追踪其依赖依赖变更时重新运行该函数(里面用到的响应式属性发生变更时) 清除副作用组件卸载时候会自动停止侦听器,当然也有显式调用停止的方式:
// 同步的方式 const stop = watchEffect(() => { /* ... */ }) // 之后 stop() // 如果是回调里有异步,可以用回调的参数onInvalidate去取消监听 const data = ref(0) watchEffect((onInvalidate) => { // 立即执行,其后data改变,组件更新后执行 console.log(data.value) const timer = setInterval(()=>{ data.value ++ },1000) // 第一次初始化时候不执行该回调,仅注册回调,data改变时以及停止侦听时,会触发该回调 onInvalidate(() => { // 取消定时器 clearInterval(timer) }) }) // output: 0 1onInvalidate 触发时机
副作用即将重新执行时(也就是追踪的依赖发生改变时)侦听器被停止时(如果在 setup() 或 生命周期钩子函数中使用了 watchEffect, 则在卸载组件时)watch API 完全等效于 2.x this.$watch (以及 watch 中相应的选项)。watch 需要侦听特定的数据源,并在回调函数中执行副作用。默认情况是懒执行的,也就是说仅在侦听的源变更时才执行回调。
对比watchEffect,watch 的作用:
懒执行副作用(回调);更明确哪些状态的改变会触发侦听器重新运行副作用;能够侦听到数据变化的新值与旧值。watch的数据源 watch的数据源可以是一个或多个拥有返回值的 getter 函数,也可以是 ref:
// 侦听一个 getter const state = reactive({ count: 0 }) watch( () => state.count, // 返回count的getter (count, prevCount) => { // 回调,新值旧值 /* ... */ } ) // 直接侦听一个 ref const count = ref(0) watch(count, (count, prevCount) => { // 监听ref /* ... */ }) // 监听多个属性,参数以数组方式传递即可 watch([fooRef, barRef], ([foo, bar], [prevFoo, prevBar]) => { /* ... */ })清除副作用与watchEffect类似,不同的地方就是onInvalidate会作为回调的第三个参数传入。
const data = ref(0) watch(data, (newData, oldData, onInvalidate) => { console.log(newData.value) onInvalidate(() => { // 取消定时器 clearInterval(timer) }) })传入一个 getter 函数,返回一个默认不可手动修改的 ref 对象。
const count = ref(1) const plusOne = computed(() => count.value + 1) console.log(plusOne.value) // 2 plusOne.value++ // 错误用法,由于默认只设置了getter!传入一个拥有 get 和 set 函数的对象,创建一个可手动修改的计算状态。
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传入一个对象(响应式或普通)或 ref,返回一个原始对象的只读代理。只读代理是“深只读”,对象内部任何嵌套的属性也都是只读的。
const original = reactive({ count: 0 }) const copy = readonly(original) watchEffect(() => { // 依赖追踪 console.log(copy.count) }) // original 上的修改会触发 copy 上的侦听 original.count++ // 无法修改 copy 并会被警告 copy.count++ // warning!直接导入onXXX即可使用周期函数:
import { onMounted, onUpdated, onUnmounted } from 'vue' const MyComponent = { setup() { // beforeMount onMounted(() => { console.log('mounted!') }) // beforeUpdate onUpdated(() => { console.log('updated!') }) // beforeUnmount onUnmounted(() => { console.log('unmounted!') }) }, }注意,这些生命周期钩子函数只能在setup中使用,因为他们依赖当前组件的实例。
组件实例上下文也是在生命周期钩子同步执行期间设置的,因此,在卸载组件时,在生命周期钩子内部同步创建的watch和computed也将自动删除。
与2.x相比:
beforeCreate -> 使用 setup()created -> 使用 setup()beforeMount -> onBeforeMountmounted -> onMountedbeforeUpdate -> onBeforeUpdateupdated -> onUpdatedbeforeDestroy -> onBeforeUnmountdestroyed -> onUnmountederrorCaptured -> onErrorCaptured新增的钩子函数
onRenderTrackedonRenderTriggered两个钩子函数都接收一个 DebuggerEvent:
export default { onRenderTriggered(e) { debugger // 检查哪个依赖性导致组件重新渲染 }, }配合 render 函数 / JSX 的用法
export default { setup() { const root = ref(null) // 使用render函数渲染 return () => h('div', { ref: root, }) // 使用 JSX , 有木有感觉跟react很像:) return () => <div ref={root} /> }, }在v-for中使用:
<template> <div v-for="(item, i) in list" :ref="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([]) // 确保在每次变更之前重置引用 onBeforeUpdate(() => { divs.value = [] }) return { list, divs, } }, } </script>具体用法见官网
customRef 用于自定义一个 ref,可以显式地控制依赖追踪和触发响应。
<template> <input v-model="text" /> </template> <script> function useDebouncedRef(value, delay = 200) { let timeout return customRef((track, trigger) => { return { get() { track() // 调用track收集依赖 return value }, set(newValue) { clearTimeout(timeout) timeout = setTimeout(() => { value = newValue trigger() // 调用trigger,触发响应 }, delay) }, } }) } export default { setup() { return { text: useDebouncedRef('hello'), } }, } </script>显式标记一个对象为“永远不会转为响应式代理”,函数返回这个对象本身。作用有点类似Object.freeze, 去除响应式。
const foo = markRaw({}) console.log(isReactive(reactive(foo))) // false // 如果被 markRaw 标记了,即使在响应式对象中作属性,也依然不是响应式的 const bar = reactive({ foo }) console.log(isReactive(bar.foo)) // false与reactive类似,唯一的区别就是只创建“浅代理”,嵌套对象不会变成响应式。
const state = shallowReactive({ foo: 1, nested: { bar: 2, }, }) // 变更 state 的自有属性是响应式的 state.foo++ // ...但不会深层代理 isReactive(state.nested) // false state.nested.bar++ // 非响应式与readonly类似,唯一的区别就是只限制“浅只读”。嵌套对象仍然可以赋值。
const state = shallowReadonly({ foo: 1, nested: { bar: 2, }, }) // 变更 state 的自有属性会失败 state.foo++ // ...但是嵌套的对象是可以变更的 isReadonly(state.nested) // false state.nested.bar++ // 嵌套属性依然可修改与ref类似,唯一的区别只是“浅引用” ,只会追踪它的 .value 更改操作,但是如果赋值的是一个对象,则该对象不是可响应,并且后续的对象的属性更改均不会触发视图响应。
const foo = shallowRef({}) foo.value.a = 1 // 这个a也不会响应到视图上去 isReactive(foo.value) // false // 更改对操作会触发响应 foo.value = [] // 但上面新赋的这个对象并不会变为响应式对象,只是会同步这个值,视图上会同步显示[] isReactive(foo.value) // false const bar = shallowRef(0) bar.value ++ // 1 , 这个是响应式的返回由 reactive 或 readonly 方法转换成响应式代理的普通对象。简单来说就是返回代理之前的原始对象。
const foo = {} const reactiveFoo = reactive(foo) console.log(toRaw(reactiveFoo) === foo) // true前面已经讲述了常用Composition API,下面再看看,在实际使用中如何提取重用逻辑的。
// mouse.js import { ref, onMounted, onUnmounted } from 'vue' export function useMousePosition() { const x = ref(0) const y = ref(0) function update(e) { x.value = e.pageX y.value = e.pageY } onMounted(() => { window.addEventListener('mousemove', update) }) onUnmounted(() => { window.removeEventListener('mousemove', update) }) return { x, y } } // 组件中使用 import { useMousePosition } from './mouse' export default { setup() { const { x, y } = useMousePosition() // 官方推荐组合函数命名,以use打头,= =!莫名有点像hook // 其他逻辑... return { x, y } }, }可以看到,使用这种方式的好处在于,可以将组件中任意一段逻辑提取出来复用。并且通过规范的命名,还能看出来这个函数的功能是做什么的,易于维护,不再像2.x那样,只是一堆选项配置的堆砌,无法直白的看出,某个地方具体作用。