什么是Vue.js的可组合性?

138 阅读6分钟

Vue 3引入了Composition API,此后它在社区中掀起了一场风暴。在我看来,Composition API的最大特点是能够将反应状态和功能提取到他们自己的可重用模块或 "可组合 "中。

什么是可组合的?

那么,什么是Vue.js的可组合模块?你几乎可以把可组合模块看作是选项API的混合组件的等同物。它们提供了一种方法来定义独立于任何特定组件的反应性数据和逻辑。不仅如此,它们还做得更好......好得多。另外,它们还能做得更多。

组合体和混合体解决的类似问题

让我们先来看看可组合程序和混集程序有什么相似之处。就像mixins一样,可组合程序允许我们提取被动数据、方法和计算属性,并在多个组件之间轻松地重复使用它们。

code comparison between mixin and composable

可组合物与混杂物

如果可组合程序和混集程序的目的相同,那么既然我们已经有了混集程序,为什么还要引入可组合程序呢?有两个原因。

  1. 它们能更好地解决同样的问题
  2. 而且它们可以解决更多的问题

数据/方法源的清晰性

混合器=数据源被遮蔽

使用混合函数最终会掩盖反应式数据和方法的来源,特别是当一个组件使用了多个混合函数或者一个混合函数被全局注册时。

//MyComponent.vue
import ProductMixin from './ProductMixin'
import BrandMixin from './BrandMixin'
import UserMixin from './UserMixin'
export default{
    mixins:[ProductMixin, BrandMixin, UserMixin],
    created(){
        // Where in the world did name come from? 
        // Let me look through each of the registered mixins to find out
        // Oh man, it's not in any of them...
        // It must be from a globally registered mixin
        console.log(this.site)

        // Oh, I got lucky here turns out the first mixin I inspected has the name
        console.log(this.name)
    }
}

可组合物=透明的数据和函数来源

然而,使用composables,我们可以准确地知道我们可重用的数据和函数来自哪里。这是因为我们必须导入可组合,然后明确地使用析构来获得我们的数据和函数。

//MyComponent.vue
import useProduct from './useProduct'
import useBrand from './useBrand'
import useUser from './useUser'
export default{
    setup(){
        const { name } = useProduct()

        return { name }
  }
    created(){
        // Where in the world did name come from? 
        // ah, it's not in setup anywhere... this doesn't exist and is an error
        console.log(this.site)

        // Oh, nice I can see directly in setup this came from useProduct
        console.log(this.name)
    }
}

命名冲突

混合器 = 命名冲突的风险

使用上面的同一个混合体例子,如果其中2个混合体实际上定义了一个name 数据属性呢?其结果是,最后一个混合器中的数据将获胜,而其他混合器中的数据将丢失。

//MyComponent.vue

import ProductMixin from './ProductMixin' // name = AirMax
import BrandMixin from './BrandMixin' // name = Nike
import UserMixin from './UserMixin' // name = John Doe
export default{
    mixins:[ProductMixin, BrandMixin, UserMixin],
    created(){  
        // Actually I'm not so lucky,
        // yeah I found the name in ProductMixin
        // but turns out UserMixin had a name too
        console.log(this.name) // John Doe
    }
}

可组合程序=没有命名冲突的风险

然而,对于可组合物来说,情况并非如此。组合体可以用相同的名字暴露数据或函数,但消费组件可以随心所欲地重命名这些变量。

//MyComponent.vue
import useProduct from './useProduct' // name = AirMax
import useBrand from './useBrand' // name = Nike
import useUser from './useUser' // name = John Doe
export default{
    setup(){
        const { name: productName } = useProduct()
        const { name: brandName } = useBrand()
        const { name: userName } = useUser()

        return { productName, brandName, userName }
  }
    created(){
        // Yay! Nothing is lost and I can get the name of each of the things
        // together in my component but when writing the composables
        // I don't have to think at all about what variable names might collide
        // with names in other composables
        console.log(this.productName)
        console.log(this.brandName)
        console.log(this.userName)
    }
}

从组件中突变模块的反应性数据

通常,我们希望可重用模块(混合体或可组合体)能够直接改变某些反应式数据的值,而不授予消耗组件这种能力。

混合组件=不能保护自己的反应性数据

RequestMixin 为例。

// RequestMixin.js
 export default {
  data(){
        return {
            loading: false,
            payload: null
        }
  },
    methods:{
        async makeRequest(url){
            this.loading = true
            const res = await fetch(url)
            this.payload = await res.json()
            this.loading = false
        }
    }
}

在这种情况下,我们可能不希望消费组件任意改变loadingpayload 的值。然而,对于混合组件,这是不可能的。混合体没有保护这些数据的机制。

混合组件=可以保护自己的反应性数据

现在把它与写成可组合的相同逻辑进行比较。

// useRequest.js
import { readonly, ref } from "vue";
export default () => {
    // data
  const loading = ref(false);
  const payload = ref(null);

    // methods
  const makeRequest = async (url) => {
    loading.value = true;
    const res = await fetch(url);
    payload.value = await res.json();
  };

    // exposed
  return {
    payload: readonly(payload), //notice the readonly here
    loading: readonly(loading), // and here
    makeRequest
  };
};

在这个可组合程序中,我们可以随心所欲地改变加载和有效载荷的值,但一旦我们把这些数据暴露给任何消费组件,我们就会使它们成为只读。非常好

可组合物的全局状态

组合体所拥有的最后一项能力是混合体所不具备的,是一个非常酷的能力。也许我最喜欢的部分之一就是它真的很简单。在混合器中,所有被定义的数据都会在每个新的组件实例中被重置。

//CounterMixins.js
export default{
    data(){
        return { count: 0 }
    },
    methods:{
        increment(){
            this.count ++
        }
    }
}

对于上面的混合器,每个组件的计数总是从0开始,在一个使用混合器的组件中增加计数将不会在另一个使用混合器的组件中增加计数。

我们可以用可组合的方式实现同样的功能。

//useCounter.js
import {ref, readonly} from 'vue'
export default () => {
  const count = ref(0)
    const increment = ()=> count.value++

    return {
        count: readonly(count), 
        increment
    }
}

很多时候,这就是我们想要的行为。但有时,我们希望反应式数据能在所有组件间同步,并且更像Vuex等定义的全局状态。

我们如何用可组合的方式实现这一点呢?通过一个简单的线条变化。

//useCounter.js
import {ref, readonly} from 'vue'
const count = ref(0)
export default () => {
    const increment = ()=> count.value++

    return {
        count: readonly(count), 
        increment
    }
}

你能发现其中的差别吗?我们所做的就是把count 的定义移到导出的函数之外。现在每次调用增量时,无论从哪个组件调用,都会引用同一个计数变量,因为计数被定义在导出函数的范围之外。

有很多问题可以用这个方法来解决。例如,你可以有一个useAuthUser 组合或一个useCart 组合。基本上,你可以将这种技术用于任何需要在整个应用程序中保持全局的数据。

结论

总而言之,可组合程序的意图通常与混搭程序的意图相同:能够从组件中提取可重用的逻辑以实现重用。然而,在实践中,mixins最终只是落空了,而composables却出色地完成了工作。