1

它是什么

它是 Vue3 推出的重磅功能之一, 是一种新的编写 vue 组件的方式,实现了类似于React hooks的逻辑组成与复用, 使用方式灵活简单,并且增强了类型推断能力,让 Vue 在构建大型应用上也有了用武之地。

为什么要使用

因为 OptionsAPI 的推出,其实就是满足于 JQ 时代的开发者快速的上手接入,因为这个写法和和以前的写法很像。

但是,当下的 Web 应用越来越复杂,前端会做更多的事情,使用 OptionsAPI 会带来非常多的困惑,仅仅使用 data、 computed、 methods、 watch 这些,在绝大多数的情况,是可以理解的,因为心智负担没有这么重,也是很容易理解的。但是,当组件变的更大时,业务变的更复杂是,逻辑关注点的区域就会变长,而屏幕的显示是有限的。我需要在不同的区域来回跳转,当我新增一部分逻辑的时候,需要反复横跳,在代码中来回穿梭。

动画演示

复用之殇

也许你会说,把代码抽离出来啊,我们有 mixins ,我们还有 extends ,我们不仅有组合,还有继承,但是,由于这是 vue 来实现的,并不是 native 的, 所以甚至没法 peekcmmand + 左键只会告诉你找不到定义

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 是将部分组件逻辑抽象成可重用块的主要工具。但是,他们有几个问题:

  1. mixin 很容易发生冲突:因为每个特性的属性都被合并到同一个组件中,所以为了避免property名冲突和调试,你仍然需要了解其他每个特性。
  2. 可重用性是有限的:我们不能向 mixin 传递任何参数来改变它的逻辑,这降低了它们在抽象逻辑方面的灵活性

总结

对象式 API 存在的问题

  • 按 API 类型组织代码,复杂度高了后反复横跳
  • 不利于复用
  • 潜在命名冲突
  • 上下文丢失

组合式 API 提供的的能力

  • 按功能/逻辑组织
  • 极易复用
  • 可灵活组合
  • 提供更好的上下文支持

基本用法

综上所述,如果我们能够将与同一个逻辑关注点相关的代码配置在一起会更好。Vue 重磅推出了CompositionAPI来帮助我们做到这一点。

tu

setup

  • 执行机制
    setup 是在创建组件实例并完成 props 初始化之后执行的,也是在beforeCreate 钩子之后,created之前执行,无法访问 option(data、comupted、methods 等)选项,而 option 可使用 setup 中返回的变量。也就是说,即使用了 nuxt ,我们在 nuxt 给定的那些 options 下比如 fetch、asyncData,也可以使用this.xxx来访问到setup返回的 xxx

    它只会执行一次,并且它的 return 的值,就是作为数据和逻辑之间的连接点。

  • 没有 this:在解析其他组件选项之前就已经调用了 setup()
  • 接受两个参数:

    1. props:组件传参
    2. context:执行上下文,包含三个属性方法:attrsslotsemit
    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;
}
  • 生命周期
选项式 APIHook inside setup
beforeCreateNot needed*
createdNot needed*
beforeMountonBeforeMount
mountedonMounted
beforeUpdateonBeforeUpdate
updatedonUpdated
beforeUnmountonBeforeUnmount
unmountedonUnmounted
errorCapturedonErrorCaptured
renderTrackedonRenderTracked
renderTriggeredonRenderTriggered

其实就是加上 on

export default {
  setup() {
    // mounted
    onMounted(() => {
      console.log("Component is mounted!");
    });
  },
};
因为 setup 是围绕beforeCreatecreated生命周期钩子运行的,所以不需要显式地定义它们。换句话说,在这些钩子中编写的任何代码都应该直接在 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 值,说到底,其实就是,我们只关心 xy 的值。

又或者是一个倒计秒的简单例子,我传进去一个值,返回剩余秒数。

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 的问题也有很多

  1. 不要在循环,条件或嵌套函数中调用 Hook,因为它是一个链表的结构
  2. useEffect 的依赖容易造成心智负担,所有人阅读这段代码,都需要完整的阅读完这些依赖触发的地方
  3. 由于闭包的原因,useEffect 等内部捕获的,都是过时的变量。

而 Vue 以上三个问题都没有。并且因为 setup 函数只调用一次,性能上占优,当然,根本原因就是因为它的数据是响应式的,我直接改就可以读取到最新的值。

VueUse

一个使用了组合式 API,你一定会想用的一个 hooks 库。来自于官方团队。

image.png

我还需要 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 中的 useRoutercreateRouter 也是类似方式。createRouter 源码位置
useRouter 源码位置

但是这个方案只能在 vue3 中使用,在 vue2.x 中的话,可能还是离不开 vuex,但是组合式一定程度已经解决了很大部分的状态问题了。

如何在 Vue 2.x 中使用

安装 @vue/composition-api

composition-api README

当迁移到 Vue 3 时,只需简单的将 @vue/composition-api 替换成 vue 即可。你现有的代码几乎无需进行额外的改动。

几乎零成本,因为它本身和 optionsAPI 又是兼容的,所以,大胆的使用吧。

script-setup

最新的 script setup rfc 提案

它的动机是 “该提案的主要目标是通过将<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 的使用,这样在获取的时候解构我要的就行了。

最后总结

  1. compostionAPI的出现,让使用vue写复杂应用,不再变得捉襟见肘
  2. 有了 compostionAPI,可以让我们的代码相对于以前写的更好(熟练掌握各设计模式)、写的更优雅
  3. 对于一个新人来说,如果是从 setup 看起,会比看 options 来的更容易
  4. 未来 vuereact 会更像,要知道,react 为了推出concurrentmode 做了大量的铺垫。不知道 vue 会如何跟进类似策略。

参考文章

  1. VueUse 作者 Anthony Fu 分享可组合的 Vue
  2. 那个忙了一夜的 Vue3 动画很好,就是太短了
  3. Vue3 究竟好在哪里?(和 React Hook 的详细对比)
  4. 介绍一下 Vue Conf 21 大会上:尤大提到 script setup 语法!
  5. Vue CompositionAPI 陷阱
  6. Vue Conf 21 script setup
  7. Vue2.x vueuse demo
  8. Vueuse 文档
  9. Vue-demi 帮助你自动引入 vue2 还是 vue3 的库,开发插件必备

jansen
130 声望16 粉丝

学习不能止步,学习就是兴趣!终生学习是目标