Vue Composition API 初体验

2,174 阅读6分钟

前言

Vue Composition API 是 Vue3.0 RFC 中提出的一个重大的新功能,随着浏览器的升级和前端圈函数式编程的火热趋势,Vue3 和它的 Composition API,会在将来被越来越多的开发者选择,因此了解和学习 Composition API 非常重要。目前大部分新版浏览器已经兼容了 Proxy, 而且也提供了兼容版本的 Composition API,可以在 Vue 2.0 中使用,我们可以通过 Composition API 应用在我们的新项目中,以为了将来迁移和函数式编程模式开发做准备。

什么是 Vue Composition API

Vue Composition API: 是一组附加的基于函数的 API,可以灵活组合组件逻辑。

a set of additive, function-based APIs that allow flexible composition of component logic.

从官方的介绍来看,它有三个特点:

  1. 附加的
  2. 基于函数
  3. 可以组合组件逻辑

附加的

它是一项新功能,但它不会也不会取代您从 Vue 1 和 2 中了解和喜爱的优秀的“选项 API”。只需将此新 API 视为您工具箱中的另一个工具,它可能在某些使用 Options API 解决起来有点笨拙的情况下会派上用场。

总的来说,就是 Composition API 不会影响你继续使用 Options API, Composition API 更适合用于组织你的代码逻辑。

基于函数

Composition API 都是函数式的 API,可以利用函数的特性自由地去使用诸如 Vue 响应式、生命周期、监听和计算属性等功能。这种变化在使用过程有以下几点好处:

  1. 不再局限于在 Vue 选项中使用,可以聚焦逻辑关注点,避免反复横跳地阅读代码

    比如一个业务逻辑实现需要用到 data、methods、computed 的选项,如果很多其他业务逻辑也用到这些功能时,那么就会影响我们阅读。当不再局限在 Vue 选项中使用这些功能时,我们就可以将实现该逻辑点的代码写在一起了!

  2. 把各自功能拆分成函数,我们可以单独地去引入具体的功能。这意味着,我们可以通过外部文件去分功能模块,把功能内聚在各自的模块当中。
  3. 大多数 API 函数符合单一责任原则,而且没有外部的函数副作用,这提高了代码可读性。

可以组合组件逻辑

Composition API 可以在外部 js 文件或其他地方使用,只需要引入你需要用到的 Composition API,因此可以通过拆分成 js 功能模块,最后去组合逻辑。

举个例子,一个筛选框 + 列表 + 打印的页面,打印、筛选配置和列表的列配置之间是没有关系的,但在 Options API 中,我们可能会写在一个 Vue 单文件中。而 Composition API 基于函数的方式,可以让我们根据逻辑功能拆分,重新组合组件整体逻辑。

如下图:组件逻辑被拆分成四个模块,分别是筛选、权限、打印和表格的功能。

bbbbbWX20211217-011136@2x.png

最后在组件内引用,把需要模版引用的值通过 setup 函数返回给模版 bbbbbbbWX20211217-011052@2x.png

常用 API 的使用方法

setup

参数:

  1. [props={}] (Observer): 传递给组件的 props
  2. [context] (Object): 上下文对象,包含 attrs、 slots、emit、root,其中 root 为根实例,在root 可以找到注册到全局的方法和属性,打印结果如下:

AAAAA.png

返回值:返回一个对象,暴露给 template

官方示例:

<template>
  <div>{{ collectionName }}: {{ readersNumber }} {{ book.title }}</div>
</template>
<script>
  import { ref, reactive } from 'vue'
  export default {
    props: {
      collectionName: String
    },
    setup(props) {
      const readersNumber = ref(0)
      const book = reactive({ title: 'Vue 3 Guide' })
      return {
        readersNumber,
        book
      }
    }
  }
</script>

响应式 API

ref

接受一个内部值并返回一个响应式且可变的 ref 对象。ref 对象仅有一个 .value property,指向该内部值。

如果将对象分配为 ref 值,则它将被 reactive 函数处理为深层的响应式对象。

const readersNumber = ref(0);
console.log(readersNumber.value); // 注意要用 .value 获取值

reactive

返回对象的响应式副本,一般用于包装引用类型的数据

setup() {
    const obj = reactive({ count: 0 })
    return {
        obj,
    }
}

toRefs

解构会让数据失去响应式,可以用 toRefs 来解构赋值

setup(props) {
    const {name, sex} = toRefs(Props)
    const user = reactive({ username: 'xxx', age: 11 });
    return {
       ...toRefs(user),
    }
}

解构为什么会让数据失去响应式?

因为解构赋值只是将值拷贝了,但是没有经过 Proxy 代理处理响应式,因此解构赋值后,得到的数据会失去响应式。

Methods

方法在 Comosiotion API 没有特殊处理,直接声明函数即可,如果 template 有引用,就需要在 setup 中返回

setup() {
  const changeTitle = () => {
    book.title = 'Vue Composition Api 编程指北';
  };
  return {
    changeTitle,
  }
}

watch

watch(
  () => book.title,
  (newVal, oldVal) => {
    console.log(`监听到标题从 ${oldVal} 更改为 ${newVal}`);
  },
  { immediate: true }
);

watchEffect

立即执行传入的一个函数,同时响应式追踪其依赖,并在其依赖变更时重新运行该函数。

官方示例:

const count = ref(0)

watchEffect(() => console.log(count.value))
// -> logs 0

setTimeout(() => {
  count.value++
  // -> logs 1
}, 100)

从上面的示例中,可以发现 watchwatchEffect 之间的区别:

watch 会明确监听具体的响应式数据并且能获取数据更新前后的值 (newVal, oldVal),watchEffect 只需依赖更新就是执行这个 Effect 函数

computed

setup() {
  const book = reactive({ title: 'Vue 3 Guide' });
  const titleLen = computed(() => book.title.length);
  return {
    book,
    titleLen
  }
}

getCurrentInstance

getCurrentInstance 支持访问内部组件实例。

const vm = getCurrentInstance();
console.log(vm)

打印输出当前组件实例, 就和 Options API 中的 this 是一样的,虽然我们通过这种方式得到组件实例,但不建议使用在应用代码中。

getCurrentInstance 只暴露给高阶使用场景,典型的比如在库中。强烈反对在应用的代码中使用 getCurrentInstance。请不要把它当作在组合式 API 中获取 this 的替代方案来使用。

{
    attrs: (...)
    data: (...)
    emit: ƒ ()
    emitted: (...)
    isDeactivated: (...)
    isMounted: (...)
    isUnmounted: (...)
    parent: {proxy: VueComponent, type: {}, uid: 90, update: ƒ, emit: ƒ, }
    props: (...)
    proxy: VueComponent {_uid: 124, _isVue: true, $options: {}, _renderProxy: Proxy, _self: VueComponent, }
    refs: (...)
    root: {proxy: Vue, type: {}, uid: 4, update: ƒ, emit: ƒ, }
    scope: EffectScopeImpl {active: true, effects: Array(0), cleanups: Array(0), vm: VueComponent}
    setupContext: {slots: {}, }
    slots: (...)
    type: {parent: VueComponent, _parentVnode: VNode, propsData: undefined, _parentListeners: undefined, _renderChildren: undefined, }
    uid: 124
    update: ƒ ()
    vnode:
}

组件通信

组件通信可以分为父子组件之间的通信和跨组件之间的通信

父子组件之间的通信

  1. 父组件通过 props 传给子组件, 子组件通过 emit 传给父组件

    • 因为 Composition API 是附加的,我们依然可以用选项式 API 声明 props
    • emit 可以从 setup 的第二个参数 context 获得,
// 子组件
export default defineComponent({
  props: {
    message: String,
  },
  setup(props, { emit }) {
    console.log('props', props.message);
    const handleFeedBack = () => {
      emit('onFeedBack', '收到');
    };
    return {
      handleFeedBack,
    };
  },
});
  1. 实现 v-model

在 Options API 中,可以使用 .sync 实现 Prop 的“双向绑定”,还可以使用 model 选项实现 v-model,那么如何使用 Composition API 实现数据双向绑定呢?

实际上用法也是一样的,通过 emit('update:propname', newVal)

export default defineComponent({
  props: {
    visible: Boolean,
  },
  setup(props, { emit }) {
    const handleClose = () => {
      emit('update:visible', false);
    };
    return {
      handleClose,
    };
  },
});
<ChildComponent :title.sync="pageTitle" />

<!-- 替换为 -->

<ChildComponent v-model:title="pageTitle" />

至于 Model 选项也是非常好用的,我在 Composition API 中没有找到相关的 API,可以直接结合一起使用。

export default defineComponent({
  model: {
    prop: 'visible',
    event: 'change',
  },
  setup(props, { emit }) {
    const handleClose = () => {
      emit('change', false);
    };
    return {
      handleClose,
    };
  },
});

数据和事件透传

在 Vue2.0 中我们可以使用 $attrs$listeners 来实现数据透传和事件传递.

$attrs

包含了父作用域中不作为 prop 被识别 (且获取) 的 attribute 绑定 (class 和 style 除外)。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定 (class 和 style 除外),并且可以通过 v-bind="$attrs" 传入内部组件——在创建高级别的组件时非常有用。

在 Vue3.0 中 $attrs 仍然保留这个功能,而 $listeners 被移除, 事件监听器是 $attrs 的一部分。因此事件传递在 Vue3.0 中使用变得更加方便了,不需要写 $listeners, 只需要写上 v-bind="$attrs" 就可以了。

父组件

<template>
  <div>
    <s-button @click="handleOpenDialog">打开弹窗</s-button>
    <Child v-model="dialogVisible" message="回家吃饭!" @onGrandChildAnswer="onGrandChildAnswer" />
  </div>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue'
import Child from './child.vue'

export default defineComponent({
  components: {
    Child
  },
  setup() {
    const dialogVisible = ref(false)
    const handleOpenDialog = () => {
      dialogVisible.value = true
    }
    const onGrandChildAnswer = (answer: string) => {
      // eslint-disable-next-line no-alert
      alert(`来自孙子的回应:${answer}`)
    }
    return {
      dialogVisible,
      handleOpenDialog,
      onGrandChildAnswer
    }
  }
})
</script>

子组件:需要将 $attrs 传给孙子组件

<template>
  <el-dialog :visible="visible" :append-to-body="false" title="标题" :show-default-footer="false" @close="handleClose">
    <grand-child v-bind="$attrs"></grand-child>
  </el-dialog>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import GrandChild from './grandChild.vue'

export default defineComponent({
  components: {
    GrandChild
  },
  model: {
    prop: 'visible',
    event: 'change'
  },
  props: {
    visible: Boolean
  },
  emits: ['change'],
  setup(props, { emit }) {
    const handleClose = () => {
      emit('change', false)
    }
    return {
      handleClose
    }
  }
})
</script>

孙子组件: 能接收到来自祖父的 message,并且做出回应

<template>
  <div @click="grandChildAnswer">来自祖父的消息 {{ message }}</div>
</template>
<script lang="ts">
import { defineComponent } from '@vue/composition-api';
export default defineComponent({
  props: {
    message: {
      type: String,
      default: '',
    },
  },
  setup(props, { emit }) {
    const grandChildAnswer = () => {
      emit('onGrandChildAnswer', '我收到了,祖父!');
    };
    return {
      grandChildAnswer,
    };
  },
});
</script>

使用 Vuex

跨组件的通信方式,可以使用 Vuex 和 事件中心,Vuex 也有 Composition API, 使用 useStore 可以访问 store, 具体使用可以参照 Vuex 组合式 API

总结

本文主要介绍了 Composition API、常用 API 使用介绍、还有比较常见的组件通信功能实现。这已经满足大部分开发的需求,通过使用 Composition API,我感受到了使用其带来的诸多好处,特别是提高复用性和分模块处理业务逻辑方面,使用起来比 Vue2 方便了许多,希望大家读完有所收获并且可以使用起来来感受其极佳的使用体验。

参考资料

Vue 组合式 API

Composition API RFC

Vuex 组合式 API