MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Vue Composition API 如何组织与复用代码逻辑

2024-07-112.3k 阅读

Vue Composition API 基础概述

Vue Composition API 是 Vue 3.0 推出的一套基于函数的 API,用于更灵活、高效地组织和复用组件逻辑。在 Vue 2.x 中,我们主要通过选项式 API(Options API)来编写组件,例如 datamethodscomputed 等选项。虽然选项式 API 易于理解和上手,但当组件逻辑变得复杂时,代码维护和复用会变得困难。

Vue Composition API 通过将逻辑提取到可复用的函数中,打破了选项式 API 的限制。例如,我们可以将一个组件中关于数据获取和状态管理的逻辑提取到一个单独的函数中,然后在不同的组件中复用这个逻辑。

setup 函数

setup 函数是 Vue Composition API 的入口点。它在组件创建之前执行,在 datamethods 等选项初始化之前被调用。setup 函数接收两个参数:propscontext

<template>
  <div>
    <p>{{ count }}</p>
    <button @click="increment">Increment</button>
  </div>
</template>

<script>
import { ref } from 'vue';

export default {
  setup() {
    const count = ref(0);
    const increment = () => {
      count.value++;
    };
    return {
      count,
      increment
    };
  }
};
</script>

在上述代码中,ref 是 Vue Composition API 提供的一个函数,用于创建一个响应式数据。count 是一个响应式变量,increment 是一个用于增加 count 值的函数。通过 returncountincrement 暴露给模板使用。

响应式数据

Vue Composition API 提供了两种创建响应式数据的方式:refreactive

ref

ref 用于创建一个包含响应式数据的引用。它可以接受任何类型的值,并且当值发生变化时,相关的 DOM 会自动更新。

import { ref } from 'vue';

const count = ref(0);
console.log(count.value); // 0

count.value++;
console.log(count.value); // 1

在模板中使用 ref 定义的变量时,不需要使用 .value,Vue 会自动解包。

reactive

reactive 用于创建一个响应式对象。它只能接受对象类型的值(包括数组)。

import { reactive } from 'vue';

const user = reactive({
  name: 'John',
  age: 30
});

console.log(user.name); // John

user.age++;
console.log(user.age); // 31

ref 不同,reactive 创建的对象在模板和 JavaScript 代码中都不需要额外的 .value 来访问属性。

逻辑组织与复用

提取逻辑到独立函数

通过将相关逻辑提取到独立的函数中,可以提高代码的复用性和可维护性。例如,假设我们有多个组件都需要实现一个计数器功能。

import { ref } from 'vue';

// 计数器逻辑函数
function useCounter() {
  const count = ref(0);
  const increment = () => {
    count.value++;
  };
  const decrement = () => {
    count.value--;
  };
  return {
    count,
    increment,
    decrement
  };
}

export default useCounter;

然后在组件中使用这个函数:

<template>
  <div>
    <p>{{ counter.count }}</p>
    <button @click="counter.increment">Increment</button>
    <button @click="counter.decrement">Decrement</button>
  </div>
</template>

<script>
import useCounter from './useCounter';

export default {
  setup() {
    const counter = useCounter();
    return {
      counter
    };
  }
};
</script>

这样,不同的组件都可以复用 useCounter 函数,而无需在每个组件中重复编写计数器逻辑。

组合多个逻辑函数

在实际项目中,一个组件可能需要组合多个不同的逻辑。例如,我们有一个组件既需要计数器功能,又需要获取用户信息的功能。

// 获取用户信息逻辑函数
import { ref } from 'vue';

function useUserInfo() {
  const user = ref({
    name: 'John',
    age: 30
  });
  return {
    user
  };
}

// 计数器逻辑函数
function useCounter() {
  const count = ref(0);
  const increment = () => {
    count.value++;
  };
  const decrement = () => {
    count.value--;
  };
  return {
    count,
    increment,
    decrement
  };
}

在组件中组合使用这两个逻辑函数:

<template>
  <div>
    <p>User: {{ userInfo.user.name }}, Age: {{ userInfo.user.age }}</p>
    <p>Count: {{ counter.count }}</p>
    <button @click="counter.increment">Increment</button>
    <button @click="counter.decrement">Decrement</button>
  </div>
</template>

<script>
import useUserInfo from './useUserInfo';
import useCounter from './useCounter';

export default {
  setup() {
    const userInfo = useUserInfo();
    const counter = useCounter();
    return {
      userInfo,
      counter
    };
  }
};
</script>

通过这种方式,我们可以轻松地将不同的逻辑组合在一起,使组件的逻辑更加清晰和可维护。

响应式数据的依赖跟踪

在 Vue Composition API 中,响应式数据的依赖跟踪是自动的。当一个计算属性或副作用函数依赖于某个响应式数据时,Vue 会自动跟踪这个依赖关系。

computed

computed 用于创建一个计算属性。计算属性的值会根据其依赖的响应式数据自动更新。

import { ref, computed } from 'vue';

const count = ref(0);
const doubleCount = computed(() => {
  return count.value * 2;
});

console.log(doubleCount.value); // 0

count.value++;
console.log(doubleCount.value); // 2

在上述代码中,doubleCount 是一个计算属性,它依赖于 count。当 count 的值发生变化时,doubleCount 会自动重新计算。

watch

watch 用于监听响应式数据的变化,并在数据变化时执行副作用函数。

import { ref, watch } from 'vue';

const count = ref(0);

watch(count, (newValue, oldValue) => {
  console.log(`Count changed from ${oldValue} to ${newValue}`);
});

count.value++;
// 输出: Count changed from 0 to 1

watch 函数接收两个参数:要监听的响应式数据和副作用函数。副作用函数会在监听的数据发生变化时被调用。

生命周期钩子

Vue Composition API 也提供了与选项式 API 中生命周期钩子相对应的函数,用于在组件生命周期的不同阶段执行代码。

onMounted

onMounted 函数在组件挂载到 DOM 后执行。

<template>
  <div>
    <p>Component is mounted</p>
  </div>
</template>

<script>
import { onMounted } from 'vue';

export default {
  setup() {
    onMounted(() => {
      console.log('Component has been mounted');
    });
  }
};
</script>

onUpdated

onUpdated 函数在组件更新后执行。

<template>
  <div>
    <p>{{ count }}</p>
    <button @click="increment">Increment</button>
  </div>
</template>

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

export default {
  setup() {
    const count = ref(0);
    const increment = () => {
      count.value++;
    };

    onUpdated(() => {
      console.log('Component has been updated');
    });

    return {
      count,
      increment
    };
  }
};
</script>

onUnmounted

onUnmounted 函数在组件从 DOM 中卸载后执行。

<template>
  <div>
    <p>Component will be unmounted</p>
    <button @click="unmount">Unmount</button>
  </div>
</template>

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

export default {
  setup() {
    const unmount = () => {
      // 模拟卸载组件
      this.$destroy();
    };

    onUnmounted(() => {
      console.log('Component has been unmounted');
    });

    return {
      unmount
    };
  }
};
</script>

通过这些生命周期钩子函数,我们可以在组件的不同生命周期阶段执行必要的逻辑,例如初始化第三方库、清理定时器等。

处理 props 和上下文

props

setup 函数中,props 作为第一个参数传递进来。props 是一个响应式对象,其值来自父组件传递的数据。

<template>
  <div>
    <p>{{ props.message }}</p>
  </div>
</template>

<script>
export default {
  props: {
    message: String
  },
  setup(props) {
    return {
      props
    };
  }
};
</script>

在上述代码中,父组件传递的 message 属性可以通过 props.messagesetup 函数中访问。

context

context 作为 setup 函数的第二个参数,包含了一些组件的上下文信息,例如 attrsslotsemit

attrs

attrs 包含了所有传递给组件但未被定义为 props 的属性。

<template>
  <div>
    <p>{{ context.attrs.extra }}</p>
  </div>
</template>

<script>
export default {
  setup(props, context) {
    return {
      context
    };
  }
};
</script>

slots

slots 用于访问组件的插槽内容。

<template>
  <div>
    <slot></slot>
  </div>
</template>

<script>
export default {
  setup(props, context) {
    return {
      context
    };
  }
};
</script>

emit

emit 用于触发组件的自定义事件。

<template>
  <div>
    <button @click="emitEvent">Emit Event</button>
  </div>
</template>

<script>
export default {
  setup(props, context) {
    const emitEvent = () => {
      context.emit('custom-event');
    };
    return {
      emitEvent
    };
  }
};
</script>

通过 propscontext,我们可以在 Vue Composition API 中有效地处理组件间的通信和数据传递。

处理复杂逻辑场景

处理表单验证

在处理表单时,表单验证是一个常见的需求。我们可以使用 Vue Composition API 来组织表单验证逻辑。

<template>
  <div>
    <form @submit.prevent="submitForm">
      <label for="username">Username:</label>
      <input type="text" v-model="formData.username" />
      <span v-if="errors.username">{{ errors.username }}</span>
      <br />
      <label for="password">Password:</label>
      <input type="password" v-model="formData.password" />
      <span v-if="errors.password">{{ errors.password }}</span>
      <br />
      <button type="submit">Submit</button>
    </form>
  </div>
</template>

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

function useFormValidation() {
  const formData = reactive({
    username: '',
    password: ''
  });
  const errors = reactive({
    username: '',
    password: ''
  });

  const validateUsername = () => {
    if (formData.username.length < 3) {
      errors.username = 'Username must be at least 3 characters long';
    } else {
      errors.username = '';
    }
  };

  const validatePassword = () => {
    if (formData.password.length < 6) {
      errors.password = 'Password must be at least 6 characters long';
    } else {
      errors.password = '';
    }
  };

  const submitForm = () => {
    validateUsername();
    validatePassword();
    if (!errors.username &&!errors.password) {
      console.log('Form submitted successfully');
    }
  };

  return {
    formData,
    errors,
    submitForm
  };
}

export default {
  setup() {
    const form = useFormValidation();
    return {
     ...form
    };
  }
};
</script>

在上述代码中,useFormValidation 函数封装了表单数据、验证逻辑和提交逻辑。通过这种方式,我们可以在不同的表单组件中复用相同的验证逻辑。

处理异步操作

在前端开发中,异步操作如数据获取是非常常见的。Vue Composition API 提供了方便的方式来处理异步操作。

<template>
  <div>
    <button @click="fetchData">Fetch Data</button>
    <div v-if="loading">Loading...</div>
    <ul v-if="data">
      <li v-for="item in data" :key="item.id">{{ item.name }}</li>
    </ul>
  </div>
</template>

<script>
import { ref } from 'vue';
import axios from 'axios';

function useFetchData() {
  const data = ref(null);
  const loading = ref(false);

  const fetchData = async () => {
    loading.value = true;
    try {
      const response = await axios.get('/api/data');
      data.value = response.data;
    } catch (error) {
      console.error('Error fetching data:', error);
    } finally {
      loading.value = false;
    }
  };

  return {
    data,
    loading,
    fetchData
  };
}

export default {
  setup() {
    const fetch = useFetchData();
    return {
     ...fetch
    };
  }
};
</script>

在上述代码中,useFetchData 函数封装了数据获取的异步操作、加载状态和数据存储。通过这种方式,我们可以在不同的组件中复用数据获取逻辑,并且更好地管理加载状态。

与 Vuex 的结合使用

在 Vuex 中使用 Composition API

Vuex 是 Vue 的状态管理库。在 Vuex 中,我们可以使用 Vue Composition API 来组织和复用模块的逻辑。

首先,定义一个 Vuex 模块:

import { ref } from 'vue';

const useCounterModule = () => {
  const count = ref(0);
  const increment = () => {
    count.value++;
  };
  const decrement = () => {
    count.value--;
  };
  return {
    count,
    increment,
    decrement
  };
};

const counterModule = {
  namespaced: true,
  state: () => {
    const { count } = useCounterModule();
    return {
      count
    };
  },
  mutations: {
    increment(state) {
      state.count++;
    },
    decrement(state) {
      state.count--;
    }
  },
  actions: {
    incrementAsync({ commit }) {
      setTimeout(() => {
        commit('increment');
      }, 1000);
    }
  },
  getters: {
    doubleCount(state) {
      return state.count * 2;
    }
  }
};

export default counterModule;

在组件中使用这个 Vuex 模块:

<template>
  <div>
    <p>{{ count }}</p>
    <button @click="increment">Increment</button>
    <button @click="decrement">Decrement</button>
    <button @click="incrementAsync">Increment Async</button>
    <p>Double Count: {{ doubleCount }}</p>
  </div>
</template>

<script>
import { useStore } from 'vuex';

export default {
  setup() {
    const store = useStore();
    const count = () => store.state.counter.count;
    const increment = () => store.commit('counter/increment');
    const decrement = () => store.commit('counter/decrement');
    const incrementAsync = () => store.dispatch('counter/incrementAsync');
    const doubleCount = () => store.getters['counter/doubleCount'];

    return {
      count,
      increment,
      decrement,
      incrementAsync,
      doubleCount
    };
  }
};
</script>

通过在 Vuex 模块中使用 Vue Composition API,我们可以更好地组织模块的逻辑,并且在组件中更方便地使用 Vuex 的功能。

共享状态逻辑复用

在 Vuex 中,我们可以将一些共享的状态逻辑提取到单独的函数中,然后在不同的模块中复用。例如,假设我们有多个模块都需要处理用户登录状态。

import { ref } from 'vue';

function useUserLogin() {
  const isLoggedIn = ref(false);
  const login = () => {
    isLoggedIn.value = true;
  };
  const logout = () => {
    isLoggedIn.value = false;
  };
  return {
    isLoggedIn,
    login,
    logout
  };
}

const userModule = {
  namespaced: true,
  state: () => {
    const { isLoggedIn } = useUserLogin();
    return {
      isLoggedIn
    };
  },
  mutations: {
    login(state) {
      state.isLoggedIn = true;
    },
    logout(state) {
      state.isLoggedIn = false;
    }
  },
  actions: {
    loginAsync({ commit }) {
      setTimeout(() => {
        commit('login');
      }, 1000);
    }
  },
  getters: {
    isUserLoggedIn(state) {
      return state.isLoggedIn;
    }
  }
};

const orderModule = {
  namespaced: true,
  state: () => {
    const { isLoggedIn } = useUserLogin();
    return {
      isLoggedIn
    };
  },
  // 可以根据订单模块的需求,对登录相关逻辑进行扩展
  mutations: {
    // 例如,在订单提交成功后,检查登录状态
    checkLoginAfterOrder(state) {
      if (!state.isLoggedIn) {
        // 处理未登录情况
      }
    }
  }
};

export { userModule, orderModule };

通过这种方式,我们可以在不同的 Vuex 模块中复用用户登录状态的逻辑,提高代码的复用性和可维护性。

与 TypeScript 的集成

基本类型定义

在使用 Vue Composition API 与 TypeScript 集成时,我们需要对 props、响应式数据等进行类型定义。

<template>
  <div>
    <p>{{ count }}</p>
    <button @click="increment">Increment</button>
  </div>
</template>

<script lang="ts">
import { defineComponent, ref } from 'vue';

export default defineComponent({
  setup() {
    const count = ref<number>(0);
    const increment = () => {
      count.value++;
    };
    return {
      count,
      increment
    };
  }
});
</script>

在上述代码中,ref<number>(0) 定义了 count 是一个 number 类型的响应式变量。

props 类型定义

对于 props 的类型定义,我们可以使用 PropType

<template>
  <div>
    <p>{{ props.message }}</p>
  </div>
</template>

<script lang="ts">
import { defineComponent } from 'vue';
import { PropType } from 'vue';

interface Props {
  message: string;
}

export default defineComponent({
  props: {
    message: {
      type: String as PropType<string>,
      required: true
    }
  },
  setup(props: Props) {
    return {
      props
    };
  }
});
</script>

在上述代码中,我们通过 interface 定义了 props 的类型,并且在 setup 函数中使用这个类型定义。

自定义逻辑函数的类型定义

当我们定义可复用的逻辑函数时,也需要进行类型定义。

import { ref } from 'vue';

interface Counter {
  count: number;
  increment: () => void;
  decrement: () => void;
}

export function useCounter(): Counter {
  const count = ref<number>(0);
  const increment = () => {
    count.value++;
  };
  const decrement = () => {
    count.value--;
  };
  return {
    count: count.value,
    increment,
    decrement
  };
}

在上述代码中,我们通过 interface 定义了 useCounter 函数返回值的类型,确保函数的返回值符合预期的结构。

通过与 TypeScript 的集成,我们可以在使用 Vue Composition API 时获得更强大的类型检查和代码提示,提高代码的质量和可维护性。

性能优化与注意事项

避免不必要的响应式数据创建

在使用 Vue Composition API 时,要注意避免创建不必要的响应式数据。例如,如果一个数据不会在模板中使用,或者不会影响其他响应式数据,就不需要将其定义为响应式数据。

// 不必要的响应式数据
import { ref } from 'vue';

const notReactiveData = 'This is not reactive data';
// 这里不需要使用 ref,因为这个数据不会响应式更新
// const notReactiveData = ref('This is not reactive data');

const setupFunction = () => {
  // 使用 notReactiveData 进行一些计算
  const result = notReactiveData.length;
  return {
    result
  };
};

合理使用 computed 和 watch

computedwatch 是强大的工具,但过度使用可能会导致性能问题。computed 适用于依赖响应式数据计算出一个新的值,并且这个值会被多次使用的场景。而 watch 适用于监听数据变化并执行副作用操作的场景。

// 合理使用 computed
import { ref, computed } from 'vue';

const count = ref(0);
const doubleCount = computed(() => {
  return count.value * 2;
});

// 避免不必要的 watch
// 如果只是简单地根据 count 计算 doubleCount,使用 computed 更合适
// watch(count, (newValue) => {
//   const doubleValue = newValue * 2;
// });

注意生命周期钩子的使用

在使用生命周期钩子函数时,要确保在适当的阶段执行逻辑。例如,onMounted 用于在组件挂载后执行初始化操作,而 onUnmounted 用于在组件卸载时清理资源。

<template>
  <div>
    <p>Component with lifecycle hooks</p>
  </div>
</template>

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

export default {
  setup() {
    let timer;
    onMounted(() => {
      timer = setInterval(() => {
        console.log('Component is running');
      }, 1000);
    });
    onUnmounted(() => {
      clearInterval(timer);
    });
  }
};
</script>

通过合理使用生命周期钩子,我们可以确保组件在不同阶段的行为符合预期,并且避免内存泄漏等问题。

注意响应式数据的更新机制

在 Vue Composition API 中,响应式数据的更新机制与选项式 API 略有不同。对于 ref 创建的响应式数据,需要通过 .value 来修改值。对于 reactive 创建的对象,直接修改对象的属性即可触发更新。

import { ref, reactive } from 'vue';

const count = ref(0);
// 正确更新 ref 数据
count.value++;

const user = reactive({
  name: 'John',
  age: 30
});
// 正确更新 reactive 对象
user.age++;

理解响应式数据的更新机制可以帮助我们更好地编写和调试代码,避免出现数据更新但视图未更新的问题。

通过注意这些性能优化和使用注意事项,我们可以在使用 Vue Composition API 时编写高效、可维护的前端代码。