8x0 精读Vue官方文档 - Composition-API

2,271 阅读10分钟

精读 Vue 官方文档系列 🎉


注意:本篇内容更多是基于 @vue/composition-api 这个库上进行讲解的。

What is the Composition-API ?

Composition-API 的核心目的在于代码的复用。 Composition-API 赋予了开发者访问 Vue 底层响应式系统的能力,对比于传统的 Options API 会自行处理 data 返回的对象,现在 Composition-API 则需要在开发者手动在 setup 中定义响应式数据。

image.png

缺点是响应式数据的定义不再简单方便,优点则是响应式数据定义的时机、位置不再有严格的限制,可以更灵活的组装。

Options API 基于功能代码的不同选项(类别)进行拆分,例如将功能中的数据拆分到 data 选项中,将方法逻辑拆分到 methods 选项中,计算属性则拆分到 computed 选项中。

image.png

虽然这种排列条理清晰,但是一旦代码量增加,其可阅读性就会变差,并且也为组件逻辑的复用带来了挑战,例如,依然采用这一方式的 mixins 再实现代码复用时,就会都带来命名冲突、数据来源不清晰的隐患。

Composition API 则是将一个功能视为一个完整的整体。这个整体本身就囊括了datamethodscomputedlife-cycle 等选项,每个功能都被视为一个独立的部分。

image.png

现在,通过 Composition API 我们可以像传统 JavaScript 编写函数的方式那样来编写我们的组件逻辑了,此时,你可以发现响应式数据必须要通过手动声明,但好处也随之浮现,这些响应式对象与功能可以从组件中抽离,实现跨组件共享和复用。

image.png

其中逻辑关注点按照颜色进行分组,额外的好处,代码量很大的场景下,再也不需要用鼠标滚来滚去,以在不同的选项之间浏览属于同一个功能的内容。

Composition-API VS Options API

Options APIComposition-API
不利于复用方便代码复用,关注点分离
潜在命名冲突,数据源来源不清晰数据来源清晰
上下文丢失提供更好的上下文
有限类型支持更好的 TypeScript 支持
按 API 类型支持按功能/逻辑组织
按功能/逻辑组织方便代码复用
响应式数据必须在组件的 data 中定义可独立 Vue 组件使用

setup

setup 是一个新的组件选项,作为 Composition-API 的入口点,值是一个函数,且只会被执行一次,用于建立数据与逻辑的链接。

setup 执行时机位于 beforeCreatedcreated 之间,此时无法访问 this,并且 datamethodscomputed 等还未被解析所以也无法访问。

{
    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

需要注意一点的是,如果将 refreactive 结合使用,可以通过 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

检查对象是否是由 reactivereadonly 创建的 proxy。

isReactive

检查对象是否是由 reactive 创建的响应式代理。

注意:经过 readonly 包裹的 reactive 对象依然为true。

isReadonly

检查对象是否是由 readonly 创建的只读代理。

toRaw

返回 reactivereadonly 代理的原始对象。这是一个“逃生舱”,可用于临时读取数据而无需承担代理访问/跟踪的开销,也可用于写入数据而避免触发更改。

//原始对象
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,并返回一个一个带有 getset 属性的对象。

使用自定义 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() 函数中使用 beforeCreatecreated 两个组合生命周期。

但是可以使用以下生命周期方法:

  • onBeforeMount
  • onMounted
  • onBeforeUpdate
  • onUpdated
  • onBeforeUnmount
  • onUnmounted
  • onErrorCaptured
  • onRenderTracked
  • onRenderTriggered
import {onMounted} from 'composition-api';

setup(props, ctx){
    onMounted(()=>{
        console.log('mounted');
    });
}

最佳实践

ref && reactive

  1. 能够使用 ref 的尽可能使用 refref 因为有 .value 所以能更直观表明一个 ref 对象。
  2. 基本类型值使用 ref 定义。
  3. 对象类型有多个成员的情况,建议使用 reactive
const n = ref(0);
const data = ref([]);
const mouse = reactive({
    x:0,
    y:0
});

ref 自动解包

  1. 模板中自动解包。
  2. watch 监听的值会自动解包。
  3. 使用 reactive 包装 ref 对象,自动解包
const counter = ref(0);
const rc = reactive({
    foo:1,
    counter
});

rc.counter; //无需解包,自动解包
  1. 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

在一个共享的模块中,为 provideinject 声明具有类型安全的 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);
    
  }
};

参考