精读 Vue 官方文档系列 🎉
注意:本篇内容更多是基于
@vue/composition-api这个库上进行讲解的。
What is the Composition-API ?
Composition-API 的核心目的在于代码的复用。
Composition-API 赋予了开发者访问 Vue 底层响应式系统的能力,对比于传统的 Options API 会自行处理 data 返回的对象,现在 Composition-API 则需要在开发者手动在 setup 中定义响应式数据。
缺点是响应式数据的定义不再简单方便,优点则是响应式数据定义的时机、位置不再有严格的限制,可以更灵活的组装。
Options API 基于功能代码的不同选项(类别)进行拆分,例如将功能中的数据拆分到 data 选项中,将方法逻辑拆分到 methods 选项中,计算属性则拆分到 computed 选项中。
虽然这种排列条理清晰,但是一旦代码量增加,其可阅读性就会变差,并且也为组件逻辑的复用带来了挑战,例如,依然采用这一方式的 mixins 再实现代码复用时,就会都带来命名冲突、数据来源不清晰的隐患。
而 Composition API 则是将一个功能视为一个完整的整体。这个整体本身就囊括了data、methods、computed、life-cycle 等选项,每个功能都被视为一个独立的部分。
现在,通过 Composition API 我们可以像传统 JavaScript 编写函数的方式那样来编写我们的组件逻辑了,此时,你可以发现响应式数据必须要通过手动声明,但好处也随之浮现,这些响应式对象与功能可以从组件中抽离,实现跨组件共享和复用。
其中逻辑关注点按照颜色进行分组,额外的好处,代码量很大的场景下,再也不需要用鼠标滚来滚去,以在不同的选项之间浏览属于同一个功能的内容。
Composition-API VS Options API
| Options API | Composition-API |
|---|---|
| 不利于复用 | 方便代码复用,关注点分离 |
| 潜在命名冲突,数据源来源不清晰 | 数据来源清晰 |
| 上下文丢失 | 提供更好的上下文 |
| 有限类型支持 | 更好的 TypeScript 支持 |
| 按 API 类型支持 | 按功能/逻辑组织 |
| 按功能/逻辑组织 | 方便代码复用 |
响应式数据必须在组件的 data 中定义 | 可独立 Vue 组件使用 |
setup
setup 是一个新的组件选项,作为 Composition-API 的入口点,值是一个函数,且只会被执行一次,用于建立数据与逻辑的链接。
setup 执行时机位于 beforeCreated 与 created 之间,此时无法访问 this,并且 data、methods、computed 等还未被解析所以也无法访问。
{
setup(props, context){
context.attrs; //Attributes
context.slots; //slots
context.emit; //tirgger event
context.listeners; // events
context.root; // root component instance
context.parent; // parent component isntance
context.refs; // all refs
return {};
}
}
setup 方法的返回值会合并到“模板”的上下文中参与数据的渲染。
API 详解
getCurrentInstance
获取当前执行 setup 函数的组件实例。
需要注意的是,getCurrentInstance 只能在 setup 中执行或者在生命周期钩子中执行。
import {getCurrentInstance} from 'composition-api';
setup(props, ctx){
const vm = getCurrentInstace();
onMounted(()=>{
vm = getCurrentInstance();
});
}
ref && Ref
定义响应式的 ref 对象,ref 对象内部只有单个名为 value 的 property。
import { ref } from 'composition-api';
setup(props, ctx){
const title = ref('this is a title!');
setTimeout(()=>{
title.value = 'change title text';
},1000);
return {title}
}
类型声明
// ref值的类型结构
interface Ref<T>{
value:T
}
//ref 函数的类型结构
function ref<T>(value:T):Ref<T>{}
具体使用,我们可以在调用 ref() 方法时传入泛型的值来覆盖默认的推断时传递的泛型参数,也可以直接使用 as Ref<state extends string> 的方式进行断言声明。
ref 在
setup方法中需要解包使用,但是在模板中无需解包。
isRef
检查一个值是否是 Ref 类型的对象。默认 ref() 函数已经自带了此功能,当接受的值已经是一个 Ref 类型,则什么都不会处理,否则将其转为为 Ref 类型的值。
unRef
语法糖,其功能类似于 isRef(val) ? val.value : val。
toRef / toRefs
基于源响应式对象上的某个 Property 映射出一个对应的 ref 对象。这个 ref 对象依然保持着与源响应式对象上对应的 property 的响应式链接。
import {reactive, toRef} from 'composition-api';
setup(props, ctx){
const state = reactive({foo:1, bar:2});
//从源响应式对象的property上映射出一个ref对象。
const fooRef = toRef(state, 'foo');
//依然保留对源响应式对象的响应式链接
fooRef.value = 2;
console.log(state.foo);
state.foo++;
console.log(fooRef);
}
就算要映射的源响应式上的 property 不存在,
toRef也不会报错,而是完全建立一个没有链接关系的新ref对象。
toRefs() 是 toRef() 的快捷操作,用于将源响应式对象上的所有 property 都转换为 ref 对象。
reactive
创建响应式对象,可以使用 toRefs 方法进行解构为多个 Ref 对象的引用。
setup(props, ctx){
const userInfo = reactive({
firstName:'shen',
lastName:'guotao'
});
return {...toRefs(userInfo)}
}
类型声明:
function reactive<T extends object>(target: T) : UnwrapNestedRefs<T>
这说明 reactive 方法接受的泛型必须是继承 object 对象,然后用作传参的类型约束,其返回值则用 UnwrapNestedRefs 的泛型再包裹 T。
需要注意一点的是,如果将 ref 与 reactive 结合使用,可以通过 reactvie 方法重新定义 ref 对象,会自动展开 ref 对象的原始值,类似与自动解包无需再通过 .value 方式访问其值。当然,这并不会解构原始 ref 对象。
const foo = ref('');
const r = reactive({foo});
r.foo === foo.value;
但是不能通过字面量的形式将一个 ref 添加到一个响应式对象中。
const foo = ref('');
const r = reactive({});
r.foo = foo; //bad
readonly
接受一个响应式对象或普通对象,返回一个它们的只读代理。
import { readonly, toRefs } from 'composition-api';
setup(props, ctx){
const originalUserInfo = readonly(userInfo);
//覆盖响应式对象
userInfo = originalUserInfo ;
return {
...toRefs(userInfo)
}
}
isProxy
检查对象是否是由 reactive 或 readonly 创建的 proxy。
isReactive
检查对象是否是由 reactive 创建的响应式代理。
注意:经过
readonly包裹的reactive对象依然为true。
isReadonly
检查对象是否是由 readonly 创建的只读代理。
toRaw
返回 reactive 或 readonly 代理的原始对象。这是一个“逃生舱”,可用于临时读取数据而无需承担代理访问/跟踪的开销,也可用于写入数据而避免触发更改。
//原始对象
const foo = {};
//readonlyFoo
const readonyFoo = readonly(foo);
//reactiveFoo
const reactiveFoo = reactive(foo);
//再次获得原始对象
let orignal = toRaw(reactiveFoo);
不建议保留对原始对象的持久引用。请谨慎使用。
markRaw
标记一个对象,使其永远不会转换为 proxy。返回对象本身。
computed
Composition-API 中提供的计算属性功能,与 OptionsAPI 中提供的 computed 选项相同。
import {computed} from 'composition-api';
setup(props, ctx){
const fullName = computed(()=>{
return userInfo.firstName + userInfo.lastName;
});
const pass = computed(()=>{
if(userInfo.score >= 60) return '及格';
if(userInfo.score < 60) return '不及格'
})
};
computed 存在计算缓存。但是当计算属性被使用时(在模板中),那么就必然会执行一次 computed 函数,然后如果当 computed 中的计算属性发生改变,也会重新执行 computed 函数,返回最新的计算属性的值。
watchEffect && watch
watchEffect
- 会立即执行副作用方法。并且当内部所依赖的响应式值发生改变时也会重新执行。
- 不需要指定监听属性,可以自动收集依赖。
- 可以通过
onInvalidate取消监听。
import {reactive, watchEffect, toRefs} from 'composition-api';
setup(props, ctx) {
const data = reactive({
num:0,
count:0,
});
const stop = watchEffect(()=>{
//立即执行,输出0
//每隔1秒钟值发生改变是,重新执行watchEffect。
//count虽然是每2秒更新一次,但并不会触发当前的 watchEffect,因为它不属于当前 watchEffect 的依赖项。
console.log(data.num);
//nInvalidate(fn)传入的回调会在watchEffect重新运行或者watchEffect停止的时候执行。
onInvalidate(() => {
// 取消异步api的调用。
apiCall.cancel()
})
});
setInterval(()=>{
data.num++;
},1000);
setInterval(()=>{
data.count++;
},2000);
return {
...toRefs(data),
onStop(){stop()}
}
}
需要注意,当副作用函数中执行的函数,若该函数又改变了响应式的数据,可能会造成死循环问题。
watch
- 具有懒执行的特性,并不会立即执行。
- 要明确哪些依赖项的状态改变,触发侦听器的重新执行,支持监听多个依赖。
- 能够获得状态变更前后的值。
- 可以手动停止监听
//只能对响应式对象进行监听,而不能对响应式对象的属性进行监听。
watch(data, (newValue, oldValue)=>{
console.log(newValue,oldValue)
})
监听多个数据源:
import { watch, reactive } from 'vue';
export default {
setup () {
const state = reactive({
count: 0,
msg: 'hello'
})
const stop = watch([()=> state.count, ()=> state.msg],([count, msg], [prevCount, prevMsg])=>{
console.log(count, msg);
console.log('---------------------');
console.log(prevCount, prevMsg);
})
setTimeout(()=>{
state.count++;
state.msg = 'hello world';
},1000);
return {
state
};
}
};
provide && inject
Composition-API 风格的依赖注入:
Parent:
import { provide, ref } from 'composition-api';
setup(){
const title = ref('learn vue');
const changeTitle = ()=>{ title.value = 'learn vue and typescript!' };
provide("title", title);
return {changeTitle}
}
Son
import { inject } from 'composition-api';
setup(){
const title = inject('title');
setTimeout(()=>{title.value ='learn success!'},1000);
return {title}
}
shallowReactive
只处理对象最外层属性的响应式(也就是浅响应式),所以最外层属性发生改变,更新视图,其他层属性改变,视图不会更新.
{
setup(){
const obj = {
x:{
y:{
z:0
}
}
};
const shallowObj = shallowReactive(obj);
shallowObj.x.y.z=1; //不会触发更新
return {shallowObj}
}
}
shallowRef
只处理了 value 的响应式,对于引用类型的值,不会对引用值进行 reactive 处理。
customRef
customRef 用于创建自定义 ref,可以显式地控制依赖追踪和触发响应,接受一个工厂函数,两个参数分别是用于追踪的 track 和用于触发响应的 trigger,并返回一个一个带有 get 和 set 属性的对象。
使用自定义 ref 实现带防抖功能的 v-model :
<input v-model="text" />
function useDebouncedRef(value, delay = 200) {
let timeout
return customRef((track, trigger) => {
return {
get() {
track()
return value
},
set(newValue) {
clearTimeout(timeout)
timeout = setTimeout(() => {
value = newValue
trigger()
}, delay)
},
}
})
}
export default {
setup() {
return {
text: useDebouncedRef('hello'),
}
},
}
LifeCycle Hooks
由于 setup() 是在 beforeCreate, created 之前执行,因此:
- 不能在
setup()函数中使用this,因为此时组件并没有完全实例化。 - 不能在
setup()函数中使用beforeCreate与created两个组合生命周期。
但是可以使用以下生命周期方法:
- onBeforeMount
- onMounted
- onBeforeUpdate
- onUpdated
- onBeforeUnmount
- onUnmounted
- onErrorCaptured
- onRenderTracked
- onRenderTriggered
import {onMounted} from 'composition-api';
setup(props, ctx){
onMounted(()=>{
console.log('mounted');
});
}
最佳实践
ref && reactive
- 能够使用
ref的尽可能使用ref,ref因为有.value所以能更直观表明一个ref对象。 - 基本类型值使用
ref定义。 - 对象类型有多个成员的情况,建议使用
reactive。
const n = ref(0);
const data = ref([]);
const mouse = reactive({
x:0,
y:0
});
ref 自动解包
- 模板中自动解包。
watch监听的值会自动解包。- 使用
reactive包装ref对象,自动解包
const counter = ref(0);
const rc = reactive({
foo:1,
counter
});
rc.counter; //无需解包,自动解包
unref解包方法。
当我们不能确定接收的值是否为一个
Ref类型,但是期望最终的结果是一个非Ref类型时,该方法会场有用
接受 Ref 参数返回一个响应式结果。
function add (a: Ref<number>, b: Ref<number>) {
return computer(()=>a.value + b.value);
}
兼容非响应式场景
function add (a: Ref<number> | number, b: Ref<number> | number) {
return computer(()=> unref(a) + unref(b));
}
isRef() && ref()
ref 函数自带了判断功能,这在编写不确定类型的时候非常有用。
isRef(foo) ? foo : ref(foo) ==== ref(foo);
返回一个 ref 成员构成的对象更加有用
返回一个 ref 成员构成的对象更加有用:
const data = {
x: ref(0),
y: ref(1),
z: ref(2)
}
使用 Es6 解构使用时:
const {x, y ,z} = data;
x.value = 1;
通过对象引用的方式使用,再通过 reactive() 进行包装一层。
const rcData = reactive(data);
rcData.x = 1;
自动清除副作用
自我们封装的 use 方法中使用 onUnmounted 钩子自动清理依赖,例如事件解绑、依赖清除。
类型安全的 provide / inject
在一个共享的模块中,为 provide 与 inject 声明具有类型安全的 key。
例如在一个共享的 context.ts 模块中声明 key。
//context.ts
import {InjectionKey} from '@vue/composition-api'
interface UserInfo {
name:string;
id:number;
}
export default const InjectionKeyUser : InjectionKey<UserInfo> = Symbol();
Used:
import {InjectionKeyUser} from './context';
{
setup(){
provide(InjectionKeyUser, {name:'zhangsan', id:10001})
}
}
{
setup(){
const user = inject(InjectionKeyUser);
if(user){
console.log(user.name);
}
}
}
状态共享
状态可以独立于组件被创建并使用。 但是最普通的方式并不支持 SSR,为了支持 SSR 我们应该基于 provide/inject 进行状态共享。
//context.ts
//....
export default const InjectionKeyState : InjectionKey<State> = Symbol();
// useState.ts
export function createState () {
const state = { /**/ };
return {
install(app:App){
app.provide(InjectionKeyState, state);
}
}
}
export function useState () :State {
const {inject} = '@vue/composition-api';
return inject(InjectionKeyState)!;
}
通过 ref 获取 DOM 节点
<img src="demo.jpg" ref="domRef" />
{
setup(){
const domRef = ref(null);
onMounted(()=>{
console.log(domRef.value)
})
return {domRef}
}
}
mayBeRef
export type mayBeRef<T> = Ref<T> | T;
安全解构 reactive 对象。
如果使用 ES6 解构一个 reactive() 方法定义的响应式对象,会破坏其响应式特征。
一个好的方法就是使用 toRefs() 进行结构。
const rc = reactive({
x:0,
y:1
});
//bad
const {x, y} = rc;
isRef(x); //false
//good;
const {x, y} = toRefs(rc);
props 不能使用 ES6 解构
setup(props) 的方法 props 是一个 proxy 对象,所以不能直接使用 ES6 解构。
在 setup 中使用 $nextTick 等
export default {
setup(props, { root }) {
const { $nextTick } = root;
console.log($nextTick);
}
};