随着2020年4月份 Vue3.0 beta
发布,惊喜于其性能的提高,友好的 TS
支持(语法补全),改写ES export
写法,利用Tree shaking
减小打包大小,Composition API
,Custom Renderer API
新功能拓展及其RECs
文档的完善。固然,还有一些后续工做(vuex
, vue-router
, cli
, vue-test-utils
, DevTools
, Vetur
, Nuxt
)待完成,当前还不稳定,正式在项目中使用(目前能够在小型新项目中),还需在2020 Q2稳定版本以后。html
Vue3.0
的到来已只是时间问题,未雨绸缪,何不先来尝鲜一波新特性~vue
组件 API
设计所面对的核心问题之一就是如何组织逻辑,以及如何在多个组件之间抽取和复用逻辑。基于 Vue 2.x
目前的 API
咱们有一些常见的逻辑复用模式,但都或多或少存在一些问题。这些模式包括:react
以上这些模式存在如下问题:git
mixin
的时候,光看模版会很难分清一个属性究竟是来自哪个 mixin
。HOC
也有相似的问题。mixin
没法保证不会正好用到同样的属性或是方法名。HOC
在注入的 props
中也存在相似问题。HOC
和 Renderless Components
都须要额外的组件实例嵌套来封装逻辑,致使无谓的性能开销。Composition API
受 React Hooks
的启发,提供了一个全新的逻辑复用方案,且不存在上述问题。使用基于函数的 API
,咱们能够将相关联的代码抽取到一个 "composition function"(组合函数)
中 —— 该函数封装了相关联的逻辑,并将须要暴露给组件的状态以响应式的数据源的方式返回出来。这里是一个用组合函数来封装鼠标位置侦听逻辑的例子:github
function useMouse() { const x = ref(0) const y = ref(0) const update = e => { x.value = e.pageX y.value = e.pageY } onMounted(() => { window.addEventListener('mousemove', update) }) onUnmounted(() => { window.removeEventListener('mousemove', update) }) return { x, y } } // 在组件中使用该函数 const Component = { setup() { const { x, y } = useMouse() // 与其它函数配合使用 const { z } = useOtherLogic() return { x, y, z } }, template: `<div>{{ x }} {{ y }} {{ z }}</div>` }
从以上例子中能够看到:vue-router
3.0
的一个主要设计目标是加强对 TypeScript
的支持。
基于函数的 API 自然对类型推导很友好,由于 TS
对函数的参数、返回值和泛型的支持已经很是完备。vuex
基于函数的 API
每个函数均可以做为 named ES export
被单独引入,这使得它们对 tree-shaking
很是友好。没有被使用的 API
的相关代码能够在最终打包时被移除。同时,基于函数 API
所写的代码也有更好的压缩效率,由于全部的函数名和 setup
函数体内部的变量名均可以被压缩,但对象和 class
的属性/方法名却不能够。vue-cli
除了渲染函数 API 和做用域插槽语法以外的全部内容都将保持不变,或者经过兼容性构建让其与
2.x
保持兼容
Vue 3.0
并不像 Angular
那样超强跨度版本,致使不兼容,而是在兼容 2.x
基础上作改进。typescript
在这里能够在 2.x
中经过引入 @vue/composition-api
,使用 Vue 3.0
新特性。npm
一、安装 vue-cli3
npm install -g @vue/cli
二、建立项目
vue create vue3
三、项目中安装 composition-api
npm install @vue/composition-api --save
四、在使用任何 @vue/composition-api
提供的能力前,必须先经过 Vue.use()
进行安装
import Vue from 'vue' import VueCompositionApi from '@vue/composition-api' Vue.use(VueCompositionApi)
Vue3
引入一个新的组件选项,setup()
,它会在一个组件实例被建立时,初始化了 props
以后调用。 会接收到初始的 props
做为参数:
export default { props: { name: String }, setup(props) { console.log(props.name) } }
传进来的 props
是响应式的,当后续 props
发生变更时它也会被框架内部同步更新。但对于用户代码来讲,它是不可修改的(会致使警告)。
同时,setup()
执行时机至关于 2.x
生命周期 beforeCreate
以后,且在 created
以前:
export default { beforeCreate() { console.log('beforeCreate') }, setup() { console.log('setup') }, created() { console.log('created') } } // 打印结果 // beforeCreate // setup // created
在 setup()
中 this
再也不是 vue
实例对象了,而是 undefined
,能够理解为此时机实例尚未建立。在 setup()
第二个参数是上下文参数,提供了一些 2.x
this
上有用属性。
export default { setup(props, context) { console.log('this: ', this) console.log('context: ', context) } } // 打印结果 // this: undefined // context: { // attrs: Object // emit: f() // isServer: false // listeners: Object // parent: VueComponent // refs: Object // root: Vue // slots: {} // ssrContext: undefined // }
相似 data()
,setup()
能够返回一个对象,这个对象上的属性将会暴露给模版的渲染上下文:
<template> <div>{{ name }}</div> </template> <script> export default { setup() { return { name: 'zs' } } } </script>
等价于 vue 2.x
中的 Vue.observable()
函数,vue 3.x
中提供了 reactive()
函数,用来建立响应式的数据对象。
当(引用)数据直接改变不会让模版响应更新渲染:
<template> <div>count: {{state.count}}</div> </template> <script> export default { setup() { const state = { count: 0 } setTimeout(() => { state.count++ }) return { state } } } // 一秒后页面没有变化 </script>
reactive
建立的响应式数据对象,在对象属性发生变化时,模版是能够响应更新渲染的:
<template> <div>count: {{state.count}}</div> </template> <script> import { reactive } from '@vue/composition-api' export default { setup() { const state = reactive({ count: 0 }) setTimeout(() => { state.count++ }, 1000) return { state } } } // 一秒后页面数字从0变成1 </script>
在 Javascript
中,原始类型(如 String
,Number
)只有值,没有引用。若是在一个函数中返回一个字符串变量,接收到这个字符串的代码只会得到一个值,是没法追踪原始变量后续的变化的。
<template> <div>count: {{state.count}}</div> </template> <script> import { ref } from '@vue/composition-api' export default { setup() { const count = 0 setTimeout(() => { count++ }, 1000) return { count } } } // 页面没有变化 </script>
所以,包装对象 ref()
的意义就在于提供一个让咱们可以在函数之间以引用的方式传递任意类型值的容器。这有点像 React Hooks
中的 useRef
—— 但不一样的是 Vue
的包装对象同时仍是响应式的数据源。有了这样的容器,咱们就能够在封装了逻辑的组合函数中将状态以引用的方式传回给组件。组件负责展现(追踪依赖),组合函数负责管理状态(触发更新)。
ref()
返回的是一个 value reference
(包装对象)。一个包装对象只有一个属性:.value ,该属性指向内部被包装的值。包装对象的值能够被直接修改。
<script> import { ref } from '@vue/composition-api' export default { setup() { const count = ref(0) console.log('count.value: ', count.value) count.value++ // 直接修改包装对象的值 console.log('count.value: ', count.value) } } // 打印结果: // count.value: 0 // count.value: 1 </script>
当包装对象被暴露给模版渲染上下文,或是被嵌套在另外一个响应式对象中的时候,它会被自动展开 (unwrap) 为内部的值:
<template> <div>ref count: {{count}}</div> </template> <script> import { ref } from '@vue/composition-api' export default { setup() { const count = ref(0) console.log('count.value: ', count.value) return { count // 包装对象 value 属性自动展开 } } } </script>
也能够用 ref()
包装对象做为 reactive()
建立的对象的属性值,一样属性值 ref()
包装对象也会模版上下文被展开:
<template> <div>reactive ref count: {{state.count}}</div> </template> <script> import { reactive, ref } from '@vue/composition-api' export default { setup() { const count = ref(0) const state = reactive({count}) return { state // 包装对象 value 属性自动展开 } } } </script>
在 Vue 2.x
中用实例上的 $refs
属性获取模版元素中 ref
属性标记 DOM
或组件信息,在这里用 ref()
包装对象也能够用来引用页面元素和组件;
<template> <div><p ref="text">Hello</p></div> </template> <script> import { ref } from '@vue/composition-api' export default { setup() { const text = ref(null) setTimeout(() => { console.log('text: ', text.value.innerHTML) }, 1000) return { text } } } // 打印结果: // text: Hello </script>
若是参数是一个 ref
则返回它的 value
,不然返回参数自己。它是 val = isRef(val) ? val.value : val
的语法糖。
检查一个值是否为一个 ref
对象。
把一个响应式对象转换成普通对象,该普通对象的每一个 property
都是一个 ref
,和响应式对象 property
一一对应。而且,当想要从一个组合逻辑函数中返回响应式对象时,用 toRefs
是颇有效的,该 API
让消费组件能够 解构 / 扩展(使用 ...
操做符)返回的对象,并不会丢失响应性:
<template> <div> <p>count: {{count}}</p> <button @click="increment">+1</button> </div> </template> <script> import { reactive, toRefs } from '@vue/composition-api' export default { setup() { const state = reactive({ count: 0 }) const increment = () => { state.count++ } return { ...toRefs(state), // 解构出来不丢失响应性 increment } } } </script>
computed()
用来建立计算属性,computed()
函数的返回值是一个 ref
的实例。这个值模式是只读的:
import { ref, computed } from '@vue/composition-api' export default { setup() { const count = ref(0) const plusOne = computed(() => count.value + 1) plusOne.value = 10 console.log('plusOne.value: ', plusOne.value) console.log('count.value: ', count.value) } } // 打印结果: // [Vue warn]: Computed property was assigned to but it has no setter. // plusOne.value: 1 // count.value: 0
或者传入一个拥有 get
和 set
函数的对象,建立一个可手动修改的计算状态:
import { ref, computed } from '@vue/composition-api' export default { setup() { const count = ref(0) const plusOne = computed({ get: () => count.value + 1, set: val => { count.value = val - 1 } }) plusOne.value = 10 console.log('plusOne.value: ', plusOne.value) console.log('count.value: ', count.value) } } // 打印结果: // plusOne.value: 10 // count.value: 9
watchEffect()
监测反作用函数。当即执行传入的一个函数,并响应式追踪其依赖,并在其依赖变动时从新运行该函数:
<template> <div> <p>count: {{count}}</p> <button @click="increment">+1</button> </div> </template> <script> import { ref, watchEffect } from '@vue/composition-api' export default { setup() { // 监视 ref 数据源 const count = ref(0) // 监视依赖有变化,马上执行 watchEffect(() => { console.log('count.value: ', count.value) }) const increment = () => { count.value++ } return { count, increment } } } </script>
中止侦听。当 watchEffect
在组件的 setup()
函数或生命周期钩子被调用时, 侦听器会被连接到该组件的生命周期,并在组件卸载时自动中止。
在一些状况下(好比超时就无需继续监听变化),也能够显式调用返回值以中止侦听:
<template> <div> <p>count: {{state.count}}</p> <button @click="increment">+1</button> </div> </template> <script> import { reactive, watchEffect } from '@vue/composition-api' export default { setup() { // 监视 reactive 数据源 const state = reactive({ count: 0 }) const stop = watchEffect(() => { console.log('state.count: ', state.count) }) setTimeout(() => { stop() }, 3000) const increment = () => { state.count++ } return { state, increment } } } // 3秒后,点击+1按钮再也不打印 </script>
清除反作用。有时候当观察的数据源变化后,咱们可能须要对以前所执行的反作用进行清理。举例来讲,一个异步操做在完成以前数据就产生了变化,咱们可能要撤销还在等待的前一个操做。为了处理这种状况,watchEffect
的回调会接收到一个参数是用来注册清理操做的函数。调用这个函数能够注册一个清理函数。清理函数会在下属状况下被调用:
setup()
或 生命周期钩子函数中使用了 watchEffect
, 则在卸载组件时)咱们之因此是经过传入一个函数去注册失效回调,而不是从回调返回它(如 React useEffect
中的方式),是由于返回值对于异步错误处理很重要。
const data = ref(null) watchEffect(async (id) => { data.value = await fetchData(id) })
async function
隐性地返回一个 Promise
- 这样的状况下,咱们是没法返回一个须要被马上注册的清理函数的。除此以外,回调返回的 Promise
还会被 `Vue 用于内部的异步错误处理。
在实际应用中,在大于某个频率(请求 padding
状态)操做时,能够先取消以前操做,节约资源:
<template> <div> <input type="text" v-model="keyword"> </div> </template> <script> import { ref, watchEffect } from '@vue/composition-api' export default { setup() { const keyword = ref('') const asyncPrint = val => { return setTimeout(() => { console.log('user input: ', val) }, 1000) } watchEffect( onInvalidate => { const timer = asyncPrint(keyword.value) onInvalidate(() => clearTimeout(timer)) console.log('keyword change: ', keyword.value) }, { flush: 'post' // 默认'post',同步'sync','pre'组件更新以前 } ) return { keyword } } } // 实现对用户输入“防抖”效果 </script>
watch API
彻底等效于 2.x
this.$watch
(以及 watch
中相应的选项)。watch
须要侦听特定的数据源,并在回调函数中执行反作用。默认状况是懒执行的,也就是说仅在侦听的源变动时才执行回调。
watch()
接收的第一个参数被称做 “数据源”,它能够是:
第二个参数是回调函数。回调函数只有当数据源发生变更时才会被触发:
watch( // getter () => count.value + 1, // callback (value, oldValue) => { console.log('count + 1 is: ', value) } ) // -> count + 1 is: 1 count.value++ // -> count + 1 is: 2
上面提到第一个参数的“数据源”能够是一个包含函数和包装对象的数组,也就是能够同时监听多个数据源。同时,watch
和 watchEffect
在中止侦听, 清除反作用 (相应地 onInvalidate
会做为回调的第三个参数传入),等方面行为一致。下面用上面“防抖”例子用 watch
改写:
<template> <div> <input type="text" v-model="keyword"> </div> </template> <script> import { ref, watch } from '@vue/composition-api' export default { setup() { const keyword = ref('') const asyncPrint = val => { return setTimeout(() => { console.log('user input: ', val) }) } watch( keyword, (newVal, oldVal, onCleanUp) => { const timer = asyncPrint(keyword) onCleanUp(() => clearTimeout(timer)) }, { lazy: true // 默认未false,即初始监听回调函数执行了 } ) return { keyword } } } </script>
和 2.x
的 $watch
有所不一样的是,watch()
的回调会在建立时就执行一次。这有点相似 2.x watcher
的 immediate: true
选项,但有一个重要的不一样:默认状况下 watch()
的回调老是会在当前的 renderer flush
以后才被调用 —— 换句话说,watch()
的回调在触发时,DOM
老是会在一个已经被更新过的状态下。 这个行为是能够经过选项来定制的。
在 2.x
的代码中,咱们常常会遇到同一份逻辑须要在 mounted
和一个 watcher
的回调中执行(好比根据当前的 id
抓取数据),3.0
的 watch()
默认行为能够直接表达这样的需求。
能够直接导入 onXXX
一族的函数来注册生命周期钩子。
import { onMounted, onUpdated, onUnmounted } from '@vue/composition-api' const MyComponent = { setup() { onMounted(() => { console.log('mounted!') }) onUpdated(() => { console.log('updated!') }) onUnmounted(() => { console.log('unmounted!') }) }, }
这些生命周期钩子注册函数只能在 setup()
期间同步使用, 由于它们依赖于内部的全局状态来定位当前组件实例(正在调用 setup()
的组件实例), 不在当前组件下调用这些函数会抛出一个错误。
组件实例上下文也是在生命周期钩子同步执行期间设置的,所以,在卸载组件时,在生命周期钩子内部同步建立的侦听器和计算状态也将自动删除。
2.x
的生命周期函数与新版 Composition API
之间的映射关系:
注意:beforeCreate
和 created
在 Vue3
中已经由 setup
替代。
provide
和 inject
提供依赖注入,功能相似 2.x
的 provide/inject
。二者都只能在当前活动组件实例的 setup()
中调用。
可使用 ref
来保证 provided
和 injected
之间值的响应。
父依赖注入,做为提供者,传给子组件:
import { ref, provide } from '@vue/composition-api' import ComParent from './ComParent.vue' export default { components: { ComParent }, setup() { let treasure = ref('传国玉玺') provide('treasure', treasure) setTimeout(() => { treasure.value = '尚方宝剑' }, 1000) return { treasure } } }
子依赖注入,可做为使用者:
import { inject } from '@vue/composition-api' import ComChild from './ComChild.vue' export default { components: { ComChild }, setup() { const treasure = inject('treasure') return { treasure } } }
孙组件依赖注入,做为使用者使用,当祖级依赖传入的值改变时,也能响应:
import { inject } from '@vue/composition-api' export default { setup() { const treasure = inject('treasure') console.log('treasure: ', treasure) } }
新的
API
使得动态地检视/修改一个组件的选项变得更困难(原来是一个对象,如今是一段没法被检视的函数体)。
这多是一件好事,由于一般在用户代码中动态地检视/修改组件是一类比较危险的操做,对于运行时也增长了许多潜在的边缘状况(特别是组件继承和使用 mixin
的状况下)。新 API
的灵活性应该在绝大部分状况下均可以用更显式的代码达成一样的结果。
缺少经验的用户可能会写出 “面条代码”,由于新 API 不像旧
API
那样强制将组件代码基于选项切分开来。
基于函数的新 API
和基于选项的旧 API
之间的最大区别,就是新 API
让抽取逻辑变得很是简单 —— 就跟在普通的代码中抽取函数同样。也就是说,咱们没必要只在须要复用逻辑的时候才抽取函数,也能够单纯为了更好地组织代码去抽取函数。
基于选项的代码只是看上去更整洁。一个复杂的组件每每须要同时处理多个不一样的逻辑任务,每一个逻辑任务所涉及的代码在选项 API
下是被分散在多个选项之中的。举例来讲,从服务端抓取一份数据,可能须要用到 props
, data()
, mounted
和 watch
。极端状况下,若是咱们把一个应用中全部的逻辑任务都放在一个组件里,这个组件必然会变得庞大而难以维护,由于每一个逻辑任务的代码都被选项切成了多个碎片分散在各处。
对比之下,基于函数的 API
让咱们能够把每一个逻辑任务的代码都整理到一个对应的函数中。当咱们发现一个组件变得过大时,咱们会将它切分红多个更小的组件;一样地,若是一个组件的 setup()
函数变得很复杂,咱们能够将它切分红多个更小的函数。而若是是基于选项,则没法作到这样的切分,由于用 mixin
只会让事情变得更糟糕。
Vue 3.0
的 API
的调整其实并不大,熟悉 2.x
的童鞋就会有一种似曾相识的感受,过渡成本极小。更可能是源码层面的重构,让其更好用(从选项式到函数式,基于 typescript
重写,强制类型检查和提示补全),性能更强(重写了虚拟 Dom
的实现,采用原生 Proxy
监听)。
本文案例代码能够戳这里
本文参考了:
Vue Function-based API RFC
Vue Composition API
抄笔记:尤雨溪在Vue3.0 Beta直播里聊到了这些…完~