最近 Vue 官方公布了 Vue 3.0 最重要的RFC:Function-based component API,并发布了兼容 Vue 2.0 版本的 plugin:vue-function-api,可用于提早体验 Vue 3.0 版本的 Function-based component API。笔者出于学习的目的,提早在项目中尝试了vue-function-api。javascript
笔者计划写两篇文章,本文为笔者计划的第一篇,主要为笔者在体验 Vue Function API 的学习心得。第二篇计划写阅读vue-function-api的核心部分代码原理,包括setup
、observable
、lifecycle
。html
本文阅读时间约为15~20分钟。vue
Vue 2.x 及之前的高阶组件的组织形式或多或少都会面临一些问题,特别是在须要处理重复逻辑的项目中,一旦开发者组织项目结构组织得很差,组件代码极有可能被人诟病为“胶水代码”。而在 Vue 2.x 及以前的版本,解决此类问题的办法大体是下面的方案:java
笔者维护的项目也须要处理大量复用逻辑,在这以前,笔者一直尝试使用mixin
的方式来实现组件的复用。有些问题也一直会对开发者和维护者形成困惑,如一个组件同时mixin
多个组件,很难分清对应的属性或方法写在哪一个mixin
里。其次,mixin
的命名空间冲突也可能形成问题。难以保证不一样的mixin
不用到同一个属性名。为此,官方团队提出函数式写法的意见征求稿,也就是RFC:Function-based component API。使用函数式的写法,能够作到更灵活地复用组件,开发者在组织高阶组件时,没必要在组件组织上考虑复用,能够更好地把精力集中在功能自己的开发上。react
注:本文只是笔者使用 vue-function-api提早体验 Vue Function API ,而这个 API 只是 Vue 3.0 的 RFC,而并不是与最终 Vue 3.x API 一致。发布后可能有不一致的地方。
要想提早在Vue 2.x
中体验 Vue Function API ,须要引入vue-function-api,基本引入方式以下:git
import Vue from 'vue'; import { plugin as VueFunctionApiPlugin } from 'vue-function-api'; Vue.use(VueFunctionApiPlugin);
先来看一个基本的例子:github
<template> <div> <span>count is {{ count }}</span> <span>plusOne is {{ plusOne }}</span> <button @click="increment">count++</button> </div> </template> <script> import Vue from 'vue'; import { value, computed, watch, onMounted } from 'vue-function-api'; export default { setup(props, context) { // reactive state const count = value(0); // computed state const plusOne = computed(() => count.value + 1); // method const increment = () => { count.value++; }; // watch watch( () => count.value * 2, val => { console.log(`count * 2 is ${val}`); } ); // lifecycle onMounted(() => { console.log(`mounted`); }); // expose bindings on render context return { count, plusOne, increment, }; }, }; </script>
setup
函数是Vue Function API 构建的函数式写法的主逻辑,当组件被建立时,就会被调用,函数接受两个参数,分别是父级组件传入的props
和当前组件的上下文context
。看下面这个例子,能够知道在context
中能够获取到下列属性值:api
const MyComponent = { props: { name: String }, setup(props, context) { console.log(props.name); // context.attrs // context.slots // context.refs // context.emit // context.parent // context.root } }
value
函数建立一个包装对象,它包含一个响应式属性value
:数组
那么为什么要使用value
呢,由于在JavaScript
中,基本类型并无引用,为了保证属性是响应式的,只能借助包装对象来实现,这样作的好处是组件状态会以引用的方式保存下来,从而能够被在setup
中调用的不一样的模块的函数以参数的形式传递,既能复用逻辑,又能方便地实现响应式。并发
直接获取包装对象的值必须使用.value
,可是,若是包装对象做为另外一个响应式对象的属性,Vue
内部会经过proxy来自动展开包装对象。同时,在模板渲染的上下文中,也会被自动展开。
import { state, value } from 'vue-function-api'; const MyComponent = { setup() { const count = value(0); const obj = state({ count, }); console.log(obj.count) // 做为另外一个响应式对象的属性,会被自动展开 obj.count++ // 做为另外一个响应式对象的属性,会被自动展开 count.value++ // 直接获取响应式对象,必须使用.value return { count, }; }, template: `<button @click="count++">{{ count }}</button>`, };
若是某一个状态不须要在不一样函数中被响应式修改,能够经过state
建立响应式对象,这个state
建立的响应式对象并非包装对象,不须要使用.value
来取值。
watch
和computed
的基本概念与 Vue 2.x 的watch
和computed
一致,watch
能够用于追踪状态变化来执行一些后续操做,computed
用于计算属性,用于依赖属性发生变化进行从新计算。
computed
返回一个只读的包装对象,和普通包装对象同样能够被setup
函数返回,这样就能够在模板上下文中使用computed
属性。能够接受两个参数,第一个参数返回当前的计算属性值,当传递第二个参数时,computed
是可写的。
import { value, computed } from 'vue-function-api'; const count = value(0); const countPlusOne = computed(() => count.value + 1); console.log(countPlusOne.value); // 1 count.value++; console.log(countPlusOne.value); // 2 // 可写的计算属性值 const writableComputed = computed( // read () => count.value + 1, // write val => { count.value = val - 1; }, );
watch
第一个参数和computed
相似,返回被监听的包装对象属性值,不过另外须要传递两个参数:第二个参数是回调函数,当数据源发生变化时触发回调函数,第三个参数是options
。其默认行为与 Vue 2.x 有所不一样:
// double 是一个计算包装对象 const double = computed(() => count.value * 2); watch(double, value => { console.log('double the count is: ', value); }); // -> double the count is: 0 count.value++; // -> double the count is: 2
当watch
多个被包装对象属性时,参数都可以经过数组的方式进行传递,同时,与 Vue 2.x 的vm.$watch
同样,watch
返回取消监听的函数:
const stop = watch( [valueA, () => valueB.value], ([a, b], [prevA, prevB]) => { console.log(`a is: ${a}`); console.log(`b is: ${b}`); } ); stop();
注意:在 RFC:Function-based component API初稿中,有提到 effect-cleanup,是用于清理一些特殊状况的反作用的,目前已经在提案中被取消了。
全部现有的生命周期都有对应的钩子函数,经过onXXX
的形式建立,但有一点不一样的是,destoryed
钩子函数须要使用unmounted
代替:
import { onMounted, onUpdated, onUnmounted } from 'vue-function-api'; const MyComponent = { setup() { onMounted(() => { console.log('mounted!'); }); onUpdated(() => { console.log('updated!'); }); // destroyed 调整为 unmounted onUnmounted(() => { console.log('unmounted!'); }); }, };
上面的详解部分,主要抽取的是 Vue Function API 的常见部分,并不是RFC:Function-based component API的所有,例如其中的依赖注入,TypeScript
类型推导等优点,在这里,因为篇幅有限,想要了解更多的朋友,能够点开RFC:Function-based component API查看。我的也在Function-based component API讨论区看到了更多地一些意见:
setup
取不到组件实例this的问题,这个问题在笔者尝试体验时也遇到了,期待正式发布的 Vue 3.x 可以改进这个问题。TypeScript
类型推导、复用性和保留Vue
的数据监听,包装属性必须使用.value
来取值是讨论最激烈的value
和state
方法命名不清晰可能致使开发者误导等问题,已经在Amendment proposal to Function-based Component API这个提议中展开了讨论:setup() { const state = reactive({ count: 0, }); const double = computed(() => state.count * 2); function increment() { state.count++; } return { ...toBindings(state), // retains reactivity on mutations made to `state` double, increment, }; }
reactive
API 和 binding
API,其中reactive
API 相似于 state
API , binding
API 相似于 value
API。state
在 Vue 2.x 中可能被用做组件状态对象,致使变量命名空间的冲突问题,团队认为将state
API 改名为 reactive
更为优雅。开发者可以写出const state = ...
,而后经过state.xxxx
这种方式来获取组件状态,这样也相对而言天然一些。value
方法用于封装基本类型时,确实会出现不够优雅的.value
的状况,开发者可能会在直接对包装对象取值时忘记使用.value
,修正方案提出的 reactive
API,其含义是建立响应式对象,初始化状态state
就使用reactive
建立,可保留每项属性的getter
和setter
,这么作既知足类型推导,也能够保留响应式引用,从而可在不一样模块中共享状态值的引用。reactive
可能致使下面的问题,须要引入binding
API。 解决,如使用reactive
建立的响应式对象,对其使用拓展运算符...
时,则会丢失对象的getter
和setter
,提供toBindings
方法可以保留状态的响应式。下一篇文章中,笔者将阅读vue-function-api的核心部分代码原理,包括setup
、observable
、lifecycle
等,从内部探索 Vue Function API 可能带给咱们的改变。
固然,目前 Vue Function API 还处在讨论阶段,Vue 3.0 还处在开发阶段,仍是期待下半年 Vue 3.0 的第一版问世吧,但愿能给咱们带来更多的惊喜。