它是什么
它是 Vue3
推出的重磅功能之一, 是一种新的编写 vue
组件的方式,实现了类似于React hooks
的逻辑组成与复用, 使用方式灵活简单,并且增强了类型推断能力,让 Vue
在构建大型应用上也有了用武之地。
为什么要使用
因为 OptionsAPI
的推出,其实就是满足于 JQ 时代的开发者快速的上手接入,因为这个写法和和以前的写法很像。
但是,当下的 Web 应用越来越复杂,前端会做更多的事情,使用 OptionsAPI 会带来非常多的困惑,仅仅使用 data、 computed、 methods、 watch
这些,在绝大多数的情况,是可以理解的,因为心智负担没有这么重,也是很容易理解的。但是,当组件变的更大时,业务变的更复杂是,逻辑关注点的区域就会变长,而屏幕的显示是有限的。我需要在不同的区域来回跳转,当我新增一部分逻辑的时候,需要反复横跳,在代码中来回穿梭。
复用之殇
也许你会说,把代码抽离出来啊,我们有 mixins
,我们还有 extends
,我们不仅有组合,还有继承,但是,由于这是 vue
来实现的,并不是 native
的, 所以甚至没法 peek
, cmmand + 左键
只会告诉你找不到定义
this.productList = this.generateProducts(productGroups);
比如这行代码,很常见,但是如果我告诉你, generateProducts
并不在当前文件下,你怎么搜?
相信聪明的你会直接全局搜索,假如这是一个微前端应用,你的方法来自于基座应用,是不是还得切应用搜或者是开一个工作区?
或者你对文件结构很熟悉,先去 extends
里搜索基类, 再去 mixins
数组里挨个搜索,最后再去全局 install
里搜,甚至去搜 Vue.prototype.xx
,数据来源完全模糊,this 是个黑盒,终于,你找到了那段方法,你知道了它的逻辑,但是已经深夜了。女朋友都睡着了。
命名冲突
聪明的你,已经解决了上述问题,同时,也学会了使用 mixins
这样的高端操作,代码可以复用起来了,兴冲冲地写了个 mixins
export default {
data() {
return {
x: 0,
};
},
methods: {
// ...
},
computed: {
double() {
return this.x * 2;
},
},
};
然后另外一个你引用队友里的 mixin
里,也有这个,最终结果就是导致有程序运算出现问题,需要解决命名冲突。所以你为了查这段问题,查到了次日深夜,女朋友又睡着了。
在 Vue 2 中,mixin 是将部分组件逻辑抽象成可重用块的主要工具。但是,他们有几个问题:
- mixin 很容易发生冲突:因为每个特性的属性都被合并到同一个组件中,所以为了避免
property
名冲突和调试,你仍然需要了解其他每个特性。 - 可重用性是有限的:我们不能向
mixin
传递任何参数来改变它的逻辑,这降低了它们在抽象逻辑方面的灵活性
总结
对象式 API 存在的问题
- 按 API 类型组织代码,复杂度高了后反复横跳
- 不利于复用
- 潜在命名冲突
- 上下文丢失
组合式 API 提供的的能力
- 按功能/逻辑组织
- 极易复用
- 可灵活组合
- 提供更好的上下文支持
基本用法
综上所述,如果我们能够将与同一个逻辑关注点相关的代码配置在一起会更好。Vue 重磅推出了CompositionAPI
来帮助我们做到这一点。
setup
执行机制
setup
是在创建组件实例并完成props
初始化之后执行的,也是在beforeCreate 钩子之后,created之前执行,无法访问 option(data、comupted、methods 等)选项,而option
可使用setup
中返回的变量。也就是说,即使用了nuxt
,我们在nuxt
给定的那些options
下比如fetch、asyncData
,也可以使用this.xxx
来访问到setup
返回的 xxx它只会执行一次,并且它的 return 的值,就是作为数据和逻辑之间的连接点。
- 没有 this:在解析其他组件选项之前就已经调用了 setup()
接受两个参数:
- props:组件传参
- context:执行上下文,包含三个属性方法:
attrs
、slots
、emit
import { toRefs } from "vue"; export default { props: { title: String, }, setup(props) { console.log(props.title); const { title } = toRefs(props); console.log(title.value); }, };
其中,props 是响应式的,当然你别解构它,一旦解构,就会丢失响应状态。如果想要解构它,需要 toRefs
来安全的完成操作。
而 context
是一个普通的JavaScript
对象,也就是说,它不是响应式的,这意味着你可以安全地对 context
使用 ES6 解构。
Ref/Reactive
利用这两个函数,让数据成为响应式的,但是这两者也是有区别的,如果你使用的是值类型,你最好使用
ref
,如果是对象类型,就用reactive
。其中,
ref
的使用,需要.value
, 而reactive
的对象,是会自动UnWrap
,不需要使用.value
,但是,不能解构,因为会让它丢失响应状态,如果非要的话,toRefs
import { ref, reactive } from "vue"; let foo = 0; let bar = ref(0); foo = 1; bar = 1; // ts-error bar.value = 1; // 关于.value,官方说法就是可以利用它,良好的区分数据是ref的还是普通的,当然,使用起来确实烦了一点 const foo = { prop: 0 }; const bar = reactive({ prop: 0 }); foo.prop = 1; bar.prop = 1;
当然,在模板中和 watch中,是不需要.value 的,它都会自动 unwrap
const counter = ref(0);
watch(counter, (count) => {
console.log(count);
});
当然,有时候你会说,如果我一个函数,它既可以接受响应式数据,又可以接收非响应式数据,这怎么写?还好,官方同样提供了一个 unref
操作
如果传入一个 Ref,返回这个 .value
,否则,原样返回
function unref(r) {
return isRef(r) ? r.value : r;
}
- 生命周期
选项式 API | Hook inside setup |
---|---|
beforeCreate | Not needed* |
created | Not needed* |
beforeMount | onBeforeMount |
mounted | onMounted |
beforeUpdate | onBeforeUpdate |
updated | onUpdated |
beforeUnmount | onBeforeUnmount |
unmounted | onUnmounted |
errorCaptured | onErrorCaptured |
renderTracked | onRenderTracked |
renderTriggered | onRenderTriggered |
其实就是加上 on
export default {
setup() {
// mounted
onMounted(() => {
console.log("Component is mounted!");
});
},
};
因为setup
是围绕beforeCreate
和created
生命周期钩子运行的,所以不需要显式地定义它们。换句话说,在这些钩子中编写的任何代码都应该直接在setup
函数中编写。
Provide / Inject
我们也可以在组合式 API 中使用 provide/inject。两者都只能在当前活动实例的 setup() 期间调用因为用于在
setup
中,所以也更加灵活,可以把他们变成响应式的。当然,需要注意的一点就是,一旦变成响应式了后,修改对应的值的地方应该在数据提供的位置修改。约定大于配置。因为你在里面改也是可以的,但是那样会让数据变的不可控。
<!-- src/components/MyApp.vue --> <template> <Marker /> </template> <script> import { provide, reactive, ref } from 'vue' import MyMarker from './MyMarker.vue; export default { setup() { const location = ref("North Pole"); const geolocation = reactive({ longitude: 90, latitude: 135, }); const updateLocation = () => { location.value = "South Pole"; }; provide("location", location); provide("geolocation", geolocation); provide("updateLocation", updateLocation); } } </script> <!-- src/components/MyMarker.vue --> import { inject } from 'vue' export default { setup() { const userLocation = inject('location', 'The Universe') const userGeolocation = inject('geolocation') const updateUserLocation = inject('updateLocation') return { userLocation, userGeolocation, updateUserLocation } } }
逻辑区域在一起的优势
如果在 vue2.x 中,如果写一段带有请求 loading、error 视图的页面,我们可能会这样写
<template>
<div>
<div v-if="error">failed to load</div>
<div v-else-if="loading">loading...</div>
<div v-else>hello {{ fullName }}!</div>
</div>
</template>
<script>
import { createComponent, computed } from 'vue'
export default {
data() {
// 集中式的data定义 如果有其他逻辑相关的数据就很容易混乱
return {
data: {
firstName: '',
lastName: ''
},
loading: false,
error: false,
},
},
async created() {
try {
// 管理loading
this.loading = true
// 取数据
const data = await this.$axios('/api/user')
this.data = data
} catch (e) {
// 管理error
this.error = true
} finally {
// 管理loading
this.loading = false
}
},
computed() {
// 没人知道这个fullName和哪一部分的异步请求有关 和哪一部分的data有关 除非仔细阅读
// 在组件大了以后更是如此
fullName() {
return this.data.firstName + this.data.lastName
}
}
}
</script>
数据和逻辑被分散在了各个 option
中,这还只是一个逻辑,如果又多了一些逻辑,多了 data、computed、methods?如果你是一个新接手这个文件的人,你如何迅速的分辨清楚这个 method
是和某两个 data
中的字段关联起来的?
那么假如,假如我们用上了 vue3,配合一个库叫 vue-request
<template>
<div>
<div v-if="error">failed to load</div>
<div v-else-if="loading">loading...</div>
<div v-else>hello {{ fullName }}!</div>
</div>
</template>
<script>
import { computed } from "vue";
import useSWR from "vue-request";
export default {
setup() {
// useRequest帮你管理好了取数、缓存、甚至标签页聚焦重新请求、甚至Suspense...
const { data, loading, error } = useRequest("/api/user", fetcher);
// 轻松的定义计算属性
const fullName = computed(() => data.firstName + data.lastName);
return { data, fullName, loading, error };
},
};
</script>
多么的简单明了。但是,如果逻辑依然很多,有人会批,还是会写出意大利面条代码,那么就要看强大的复用操作了。
复用
前面我们说了,使用 mixins
的缺点,那么使用了组合 API 后,我们复用就容易的多。
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 };
}
这是一个鼠标移动的例子。在自定义 hook
中,我们返回了 x,y
值,说到底,其实就是,我们只关心 x
和y
的值。
又或者是一个倒计秒的简单例子,我传进去一个值,返回剩余秒数。
import { ref, onMounted, onUnmounted } from "vue";
export function useCountDown(initSec = 60) {
const sec = ref(initSec);
function tiktok() {
sec.value--;
}
let timer = null;
onMounted(() => {
timer = setInterval(() => {
tiktok();
}, 1000);
});
onUnmounted(() => {
timer && clearInterval(timer);
});
return sec;
}
在组件中使用他们
import { useMousePosition } from "./mouse";
export default {
setup() {
const { x, y } = useMousePosition();
const sec = useCountDown(100);
return { x, y, sec };
},
};
简直太容易了有没有。代码是不是也很简单,没有可恶的 this 了。同样的,你甚至可以 useA 里面使用 useB, useB 里面使用 useC
对比 React Hooks
首先,我们先看上面在复用里写的useCountDown
把它用react hooks
写出来就是
import { useState, useEffect } from "react";
function useCount() {
const [count, setCount] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
setCount((v) => v - 1);
}, 1000);
return () => clearInterval(timer);
}, []);
return {
count,
};
}
export default function App() {
const { count } = useCount();
return <div>{count}</div>;
}
很像对不对。
其实 React Hook 的问题也有很多
- 不要在循环,条件或嵌套函数中调用 Hook,因为它是一个链表的结构
- useEffect 的依赖容易造成心智负担,所有人阅读这段代码,都需要完整的阅读完这些依赖触发的地方
- 由于闭包的原因,useEffect 等内部捕获的,都是过时的变量。
而 Vue 以上三个问题都没有。并且因为 setup 函数只调用一次,性能上占优,当然,根本原因就是因为它的数据是响应式的,我直接改就可以读取到最新的值。
VueUse
一个使用了组合式 API,你一定会想用的一个 hooks 库。来自于官方团队。
我还需要 vuex 吗?
因为我们用了组合式 API,其实你不一定需要 vuex
,因为我复用起来非常容易,直接创建一个reactive
就完事了不是吗?
有点类似于 react 官方出的 recoil 状态管理,主要是为了concurrent
模式准备的。
// react
export const state = atom({
key: "testState",
default: {
foo: 1,
bar: "hello",
},
});
const [state, setState] = useRecoilState(state);
setState({
...state,
foo: 2,
});
// vue
// shared.ts
export const state = reactive({
foo: 1,
bar: "hello",
});
// a.vue
import { state } from "./shared.ts";
state.foo = 2;
// b.vue
import { state } from "./shared.ts";
console.log(state.foo); // 2
但是 vue
这样写的状态共享方案是不支持 ssr
的
想要兼容 ssr
的话,就得写一个插件,利用 应用级别的provide
来搞定
export const globalStateKey = Symbol();
export function providerGlobalState() {
const state = {};
return {
install(app) {
app.provide(globalStateKey, reactive(state));
},
};
}
export function useGlobalState() {
return inject(globalStateKey);
}
// main.ts
app.use(providerGlobalState());
// xx.vue
import { useGlobalState } from "";
const state = useGlobalState();
两个 function, 思路其实挺像 react hooks 中的 createContext / useContext
的。 react-context
包括 vue-routerV4
中的 useRouter
和 createRouter
也是类似方式。createRouter 源码位置
useRouter 源码位置
但是这个方案只能在 vue3
中使用,在 vue2.x
中的话,可能还是离不开 vuex
,但是组合式一定程度已经解决了很大部分的状态问题了。
如何在 Vue 2.x 中使用
安装 @vue/composition-api
当迁移到 Vue 3
时,只需简单的将 @vue/composition-api
替换成 vue
即可。你现有的代码几乎无需进行额外的改动。
几乎零成本,因为它本身和 optionsAPI
又是兼容的,所以,大胆的使用吧。
script-setup
它的动机是 “该提案的主要目标是通过将<script setup>
的上下文直接暴露给模板来减少 SFC 内部 Composition API
使用的冗长性。”
简单的解释就是,使用 setup 方法的话
<script>
import { ref, computed } from 'vue'
export default {
setup () {
const a = ref(3)
const b = computed(() => a.value + 2)
const changeA = () => { a.value = 4 }
return { a, b, changeA } // have to return everything!
}
}
</script>
而使用<script setup>
,只需要
<script setup>
import { ref, computed } from "vue";
import Foo from './Foo.vue'
import MyComponent from './MyComponent.vue'
const a = ref(3);
const b = computed(() => a.value + 2);
const changeA = () => {
a.value = 4;
};
</script>
<template>
<Foo />
<!-- kebab-case also works -->
<my-component />
<div>{{ a }}</div>
<div @click="changeA">{{ b }}</div>
</template>
它会自动的把你声明的所有的东西,不仅是数据,计算的属性和方法,甚至是指令和组件也可以在我们的 template
中自动获得。
这样的好处是,你不用写那些繁琐的东西,你的代码更加的精简了,你也不用写 return
但是,我发现的两个问题,第一,lint
的问题,它是 unused
的,第二,我用 setup function
我可以选择 return
谁,而用了这个,我可能只是用于声明计算的临时变量,也会被暴露出去,如果这个边界问题无法解决,我想它仅使用于一些自定义 Hooks 的使用,这样在获取的时候解构我要的就行了。
最后总结
compostionAPI
的出现,让使用vue
写复杂应用,不再变得捉襟见肘- 有了
compostionAPI
,可以让我们的代码相对于以前写的更好(熟练掌握各设计模式)、写的更优雅 - 对于一个新人来说,如果是从
setup
看起,会比看options
来的更容易 - 未来
vue
和react
会更像,要知道,react
为了推出concurrentmode
做了大量的铺垫。不知道vue
会如何跟进类似策略。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。