解释 Vue 3 的 Composition API(组合式 API)如何解决 Vue 2 Options API 的逻辑复用和代码组织问题。

各位观众老爷,晚上好!我是你们的老朋友,今天咱们来聊聊 Vue 3 的 Composition API,这玩意儿可是 Vue 2 Options API 的救星,专门解决逻辑复用和代码组织那些破事儿的。

咱们先回忆回忆 Vue 2 的 Options API,写起来是挺顺手的,data、methods、computed、watch 一字排开,井井有条,像个模范生。但是,一旦项目大了,组件逻辑复杂了,这玩意儿就变成了一团乱麻,各种功能的代码散落在不同的 options 里,想找个东西得翻箱倒柜,想复用一段逻辑更是难上加难。

Options API 的痛苦:代码分散与复用困难

先看个简单的例子,假设我们需要做一个带搜索功能的组件,搜索结果要分页显示:

<template>
  <div>
    <input type="text" v-model="searchText" @input="searchData">
    <ul>
      <li v-for="item in displayedData" :key="item.id">{{ item.name }}</li>
    </ul>
    <button @click="prevPage" :disabled="currentPage === 1">上一页</button>
    <button @click="nextPage" :disabled="currentPage === totalPages">下一页</button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      searchText: '',
      allData: [], // 假设从服务器获取的数据
      currentPage: 1,
      pageSize: 10,
    };
  },
  computed: {
    filteredData() {
      return this.allData.filter(item => item.name.includes(this.searchText));
    },
    totalPages() {
      return Math.ceil(this.filteredData.length / this.pageSize);
    },
    displayedData() {
      const startIndex = (this.currentPage - 1) * this.pageSize;
      const endIndex = startIndex + this.pageSize;
      return this.filteredData.slice(startIndex, endIndex);
    },
  },
  methods: {
    async searchData() {
      // 模拟从服务器获取数据
      await new Promise(resolve => setTimeout(resolve, 500));
      this.allData = [
        { id: 1, name: 'Apple' },
        { id: 2, name: 'Banana' },
        { id: 3, name: 'Orange' },
        { id: 4, name: 'Apple Pie' },
        { id: 5, name: 'Banana Bread' },
        { id: 6, name: 'Orange Juice' },
        { id: 7, name: 'Grape' },
        { id: 8, name: 'Grapefruit' },
        { id: 9, name: 'Lemon' },
        { id: 10, name: 'Lime' },
        { id: 11, name: 'Mango' },
        { id: 12, name: 'Watermelon' },
      ];
    },
    prevPage() {
      this.currentPage--;
    },
    nextPage() {
      this.currentPage++;
    },
  },
};
</script>

看起来还行?那是因为这个组件很简单。 如果再加上排序、筛选、联动选择等等功能,代码就会变得更加臃肿,相关的逻辑分散在 datacomputedmethods 中,维护起来简直要命。

更可怕的是,如果另一个组件也需要分页功能,你想把这段分页逻辑复用一下,你会发现,根本没法直接拿来用! 你要么复制粘贴,要么写个 mixin,但 mixin 有命名冲突的风险,而且来源不明,用起来心里没底。

Composition API 的救赎:逻辑聚合与复用

Composition API 的出现,就是为了解决这个问题。 它的核心思想是:把相关的逻辑组织在一起,形成一个个可复用的函数,然后在组件的 setup 函数中调用这些函数,把它们返回给模板使用。

咱们用 Composition API 重写上面的搜索分页组件:

<template>
  <div>
    <input type="text" v-model="searchText" @input="searchData">
    <ul>
      <li v-for="item in displayedData" :key="item.id">{{ item.name }}</li>
    </ul>
    <button @click="prevPage" :disabled="currentPage === 1">上一页</button>
    <button @click="nextPage" :disabled="currentPage === totalPages">下一页</button>
  </div>
</template>

<script>
import { ref, computed, onMounted } from 'vue';

// 封装搜索逻辑
function useSearch(allData) {
  const searchText = ref('');
  const filteredData = computed(() => {
    return allData.value.filter(item => item.name.includes(searchText.value));
  });

  return {
    searchText,
    filteredData,
  };
}

// 封装分页逻辑
function usePagination(data, pageSize) {
  const currentPage = ref(1);
  const totalPages = computed(() => Math.ceil(data.value.length / pageSize));
  const displayedData = computed(() => {
    const startIndex = (currentPage.value - 1) * pageSize;
    const endIndex = startIndex + pageSize;
    return data.value.slice(startIndex, endIndex);
  });

  const prevPage = () => {
    currentPage.value--;
  };
  const nextPage = () => {
    currentPage.value++;
  };

  return {
    currentPage,
    totalPages,
    displayedData,
    prevPage,
    nextPage,
  };
}

export default {
  setup() {
    const allData = ref([]);

    const { searchText, filteredData } = useSearch(allData);
    const { currentPage, totalPages, displayedData, prevPage, nextPage } = usePagination(filteredData, 10);

    const searchData = async () => {
      // 模拟从服务器获取数据
      await new Promise(resolve => setTimeout(resolve, 500));
      allData.value = [
        { id: 1, name: 'Apple' },
        { id: 2, name: 'Banana' },
        { id: 3, name: 'Orange' },
        { id: 4, name: 'Apple Pie' },
        { id: 5, name: 'Banana Bread' },
        { id: 6, name: 'Orange Juice' },
        { id: 7, name: 'Grape' },
        { id: 8, name: 'Grapefruit' },
        { id: 9, name: 'Lemon' },
        { id: 10, name: 'Lime' },
        { id: 11, name: 'Mango' },
        { id: 12, name: 'Watermelon' },
      ];
    };

    onMounted(searchData); // 组件挂载后立即搜索数据

    return {
      searchText,
      displayedData,
      currentPage,
      totalPages,
      prevPage,
      nextPage,
      searchData,
    };
  },
};
</script>

看到了吗? 我们把搜索逻辑和分页逻辑分别封装成了 useSearchusePagination 两个函数。 这样,如果其他组件也需要分页功能,直接 import { usePagination } from './usePagination' 就行了,省时省力。

Composition API 的优势总结

  • 更好的代码组织: 相关的逻辑组织在一起,代码可读性更高,更容易维护。
  • 更强的逻辑复用: 可以把逻辑封装成可复用的函数(Composables),在多个组件中共享。
  • 更灵活的类型推导: Typescript 支持更好,可以更好地利用 Typescript 的类型检查功能。
  • 更小的 bundle 体积: Tree-shaking 优化更好,可以减少打包后的代码体积。

深入理解 Composition API 的核心概念

Composition API 涉及几个核心概念,咱们一个个来看:

  1. setup() 函数: 这是 Composition API 的入口,所有的逻辑都在这里编写。它接收两个参数:

    • props: 组件的 props 对象。
    • context: 组件的上下文对象,包含 attrsemitslots

    setup() 函数必须返回一个对象,这个对象中的属性和方法才能在模板中使用。

  2. ref()reactive() 这两个函数用于创建响应式数据。

    • ref(): 用于创建单个值的响应式数据,例如 ref(0)ref('hello')。 通过 .value 访问和修改 ref 的值。
    • reactive(): 用于创建对象或数组的响应式数据,例如 reactive({ count: 0 })reactive([1, 2, 3])。 直接访问和修改 reactive 对象的属性。
    import { ref, reactive } from 'vue';
    
    export default {
      setup() {
        const count = ref(0);
        const state = reactive({
          name: 'Vue',
          version: 3,
        });
    
        const increment = () => {
          count.value++;
          state.version++;
        };
    
        return {
          count,
          state,
          increment,
        };
      },
    };
  3. computed() 用于创建计算属性,它的值会根据依赖的响应式数据自动更新。

    import { ref, computed } from 'vue';
    
    export default {
      setup() {
        const price = ref(10);
        const quantity = ref(2);
    
        const total = computed(() => price.value * quantity.value);
    
        return {
          price,
          quantity,
          total,
        };
      },
    };
  4. watch()watchEffect() 用于监听响应式数据的变化。

    • watch(): 监听指定的响应式数据,当数据发生变化时,执行回调函数。 可以精确地控制监听哪些数据。
    • watchEffect(): 立即执行一次回调函数,并在回调函数中自动收集依赖的响应式数据。 当依赖的响应式数据发生变化时,再次执行回调函数。 更方便,但是依赖关系可能不明确。
    import { ref, watch, watchEffect } from 'vue';
    
    export default {
      setup() {
        const count = ref(0);
        const name = ref('Vue');
    
        // watch
        watch(count, (newValue, oldValue) => {
          console.log(`count changed from ${oldValue} to ${newValue}`);
        });
    
        // watch 多个数据源
        watch([count, name], ([newCount, newName], [oldCount, oldName]) => {
          console.log(`count changed from ${oldCount} to ${newCount}, name changed from ${oldName} to ${newName}`);
        });
    
        // watchEffect
        watchEffect(() => {
          console.log(`count is ${count.value}, name is ${name.value}`);
        });
    
        const increment = () => {
          count.value++;
        };
    
        const changeName = () => {
          name.value = 'React';
        };
    
        return {
          count,
          name,
          increment,
          changeName,
        };
      },
    };
  5. 生命周期钩子: Composition API 提供了与 Options API 对应的生命周期钩子函数,例如 onMountedonUpdatedonUnmounted 等。

    import { onMounted, onUpdated, onUnmounted } from 'vue';
    
    export default {
      setup() {
        onMounted(() => {
          console.log('Component mounted');
        });
    
        onUpdated(() => {
          console.log('Component updated');
        });
    
        onUnmounted(() => {
          console.log('Component unmounted');
        });
    
        return {};
      },
    };
  6. Provide / Inject: 用于跨组件传递数据,类似于 Options API 的 provideinject。 但是用法上更加灵活,可以配合 Symbol 使用,避免命名冲突。

    // Parent Component
    import { provide } from 'vue';
    
    const mySymbol = Symbol('mySymbol');
    
    export default {
      setup() {
        provide(mySymbol, 'Hello from parent');
    
        return {};
      },
    };
    
    // Child Component
    import { inject } from 'vue';
    
    export default {
      setup() {
        const message = inject(mySymbol);
        console.log(message); // Output: Hello from parent
    
        return {
          message,
        };
      },
    };

Composables:代码复用的利器

Composables 就是把组件逻辑封装成可复用的函数。 它们通常以 use 开头,例如 useMouseuseFetchuseLocalStorage 等。

咱们来创建一个 useMouse composable,用于追踪鼠标的位置:

// useMouse.js
import { ref, onMounted, onUnmounted } from 'vue';

export function useMouse() {
  const x = ref(0);
  const y = ref(0);

  const update = (event) => {
    x.value = event.pageX;
    y.value = event.pageY;
  };

  onMounted(() => {
    window.addEventListener('mousemove', update);
  });

  onUnmounted(() => {
    window.removeEventListener('mousemove', update);
  });

  return {
    x,
    y,
  };
}

然后在组件中使用它:

<template>
  <div>
    Mouse position: x = {{ x }}, y = {{ y }}
  </div>
</template>

<script>
import { useMouse } from './useMouse';

export default {
  setup() {
    const { x, y } = useMouse();

    return {
      x,
      y,
    };
  },
};
</script>

这样,任何组件都可以通过 useMouse 来追踪鼠标的位置,大大提高了代码的复用性。

Options API 与 Composition API 的对比

为了更清晰地理解 Composition API 的优势,咱们用表格对比一下 Options API 和 Composition API:

特性 Options API Composition API
代码组织 基于 Options (data, methods, computed, watch) 基于逻辑功能 (Composables)
逻辑复用 Mixins (易冲突,来源不明) Composables (清晰,可测试)
类型推导 较弱 更强 (Typescript 支持更好)
可读性 小组件易读,大组件混乱 大小组件都易读,逻辑清晰
Bundle 大小 较大 较小 (Tree-shaking 优化更好)
学习曲线 简单易上手 稍难,需要理解 ref, reactive 等概念,但长期来看更高效

最佳实践与注意事项

  • 合理拆分 Composables: 不要把所有的逻辑都塞到一个 Composables 里面,尽量保持 Composables 的单一职责。
  • 命名规范: Composables 应该以 use 开头,例如 useFetchuseLocalStorage
  • 类型安全: 尽量使用 Typescript 来编写 Composables,提高代码的健壮性。
  • 不要过度使用 watchEffect 尽量使用 watch 来精确地监听需要监听的数据,避免不必要的性能开销。
  • 与 Options API 混合使用: 在 Vue 3 中,可以同时使用 Options API 和 Composition API。 可以逐步将 Options API 的代码迁移到 Composition API。

总结

Composition API 是 Vue 3 中一项强大的特性,它解决了 Options API 在大型项目中代码组织和逻辑复用方面的痛点。 通过将相关的逻辑组织成可复用的 Composables,Composition API 提高了代码的可读性、可维护性和可测试性。 虽然学习曲线稍陡峭,但长期来看,Composition API 能够显著提高开发效率和代码质量。

好了,今天的讲座就到这里,希望大家有所收获! 记住,技术是为我们服务的,选择最适合自己的方案才是最重要的! 感谢大家的观看,下次再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注