likes
comments
collection
share

探索Vue3:Composition API的深入理解和实践

作者站长头像
站长
· 阅读数 58

引言

Vue.js 升级到 Vue3 之后带来了许多令人兴奋的特性和改进,其中最吸引人注意的是引进了 Composition API ,它带来一种新的编写组件逻辑的方式,提供了更灵活、可组合和可重用的代码结构,使得开发者能够更好地组织和管理复杂的前端逻辑。

使用 Composition API 可以解决在编写复杂组件的时候避免在 Options API 大对象中写一大堆让人难以理解的代码,所以有必要深入探索Vue 3的Composition API,熟练运用在项目中,编写高质量可维护的代码。

Composition API 简介

Options API 回顾

在Vue 2中,我们主要使用Options API来创建和管理Vue组件。Options API的主要思想是,将一个组件的不同部分(如data、methods、computed等)定义在不同的选项中。这种方式的优点是结构清晰,易于上手,编辑小组件比较方便。但是,当组件变得越来越复杂时,这种方式可能会导致代码的可读性和可维护性下降。

例如,假设我们有一个非常复杂的组件,它涉及到多个功能模块。在Options API中,我们需要将这些功能模块的代码分散到不同的选项中。这可能会让我们在阅读和理解代码时感到困惑,因为我们需要在不同的选项之间来回跳跃。

Options API还有一些其他的问题。例如,它不支持类型推断,这使得在TypeScript中使用Vue变得困难。此外,Options API也不支持代码的复用。虽然我们可以使用 mixins 来重用代码,但 mixins 有其自身的问题,比如命名冲突数据来源不清晰等。

// 传统Options API的示例
export default {
  data() {
    return {
      count: 0,
    };
  },
  computed: {
    doubleCount() {
      return this.count * 2;
    },
  },
  methods: {
    increment() {
      this.count++;
    },
  },
};

Composition API 解决痛点

为了解决Options API的这些问题,Vue 3引入了Composition API。Composition API的主要思想是,提供一种新的、更加灵活的方式来组织和重用代码。使用Composition API,我们可以按照功能模块来组织代码,而不是按照Vue的选项。

例如,假设我们有一个非常复杂的组件,它涉及到多个功能模块。在Composition API中,我们可以将每个功能模块的代码放在一起,而不是分散到不同的选项中。这使得我们可以更容易地理解和维护复杂的组件。

此外,Composition API还提供了更好的类型推断,使得我们可以更容易地在TypeScript中使用Vue。总的来说,使用Composition API可以使我们的代码更加干净、易读和可维护。

// 使用Composition API的示例
import { ref, computed } from 'vue';

export default {
  setup() {
    const count = ref(0);
    const doubleCount = computed(() => count.value * 2);

    function increment() {
      count.value++;
    }

    return {
      count,
      doubleCount,
      increment,
    };
  },
};

Composition API 核心概念

setup函数

在Composition API中,我们主要在 setup 函数中编写代码。setup 函数是一个特殊的函数,它在组件初始化时被调用,我们可以在这个函数中定义和返回我们的响应式数据和函数。

setup 函数接收两个参数:propscontext

  • props: 是一个对象,组件通信一种方式,不能使用 ES6 解构,会消除 prop 的响应特性,此时需要借用 toReftoRefs 取值,使用它还有另外一个好处,可以遵循 props 单向数据流,修改 props 值
  • context: 上下文环境对象,属性有 attrs,slots,emit,expose
    • attrs 是一个非响应式对象,主要接收 no-props 属性,经常用来传递一些样式属性
    • slots 是一个包含了所有插槽的对象,其中 slots.default() 获取到的是一个数组插槽内容
    • emit 由于 setup 内不存在 this,所以 emit 用来替换 之前 this.$emit 的,用于子传父时,触发自定义事件
    • expose 控制组件暴露的属性和方法,在 script setup 中默认不暴露接口
setup(props,context){
   const { msg,ans } = toRefs(props)
   console.log(msg.value);
   console.log(ans.value);

  const { attrs, slots, emit, expose } = context
   // attrs 获取组件传递过来的属性值,
   // slots 组件内的插槽
   // emit 自定义事件 子组件
   // expose 暴露的接口
  }

setup 函数会在 created 之前执行,内部没有 this,不能挂载 this 相关的东西。我们可以使用Vue的各种响应式API,如 reactiveref。setup 内部的属性和方法想要在组件模版中使用,必须 return 暴露出来

import { ref } from 'vue';

export default {
  setup() {
    const count = ref(0);

    function increment() {
      count.value++;
    }

    return {
      count,
      increment
    };
  }
};

setup 返回值还有一种特殊的使用场景,在使用 jsx 开发,setup 如果返回是函数会作为一个 h 渲染函数,用于渲染模版,因为 jsx 不能像在 template 编写模版标签。

下面是用 jsx 编写一个计算器组件示例

import { ref, defineComponent } from 'vue';

export default defineComponent({
  setup() {
    const count = ref(0);

    const increment = () => {
      count.value++;
    };

    const decrement = () => {
      count.value--;
    };

    return (
      <div>
        <h1>计数器</h1>
        <p>当前计数: {count.value}</p>
        <button onClick={increment}>增加</button>
        <button onClick={decrement}>减少</button>
      </div>
    );
  }
});

这种写法不好的地方在于 Vue Devtools 开发者工具不会检测到在 setup 函数定义的数据展示,如 count 不能看到它的值,比较常见的写法是模版写在 render 函数中,模版和逻辑分离

import { ref, defineComponent } from 'vue';

export default defineComponent({
  setup() {
    const count = ref(0);

    const increment = () => {
      count.value++;
    };

    const decrement = () => {
      count.value--;
    };

    return {
      count,
      increment,
      decrement
    };
  },

  render() {
    return (
      <div>
        <h1>计数器</h1>
        <p>当前计数: {this.count}</p>
        <button onClick={this.increment}>增加</button>
        <button onClick={this.decrement}>减少</button>
      </div>
    );
  }
});

响应式函数

Vue 3中的响应式数据是指在应用程序中使用的数据,当数据发生变化时,相关的视图会自动更新以反映这些变化。Vue3 中的响应式实现是使用 Proxy 来劫持追踪数据的变化,开发者只需要管理和维护应用程序的状态和数据,不用操作底层的 DOM 就能更新页面视图

Vue 3提供了一些响应性函数,如 refreactivecomputed,用于定义响应式数据。

  • reactive:用于将一个普通的JavaScript对象转换为响应式对象。所有的属性都会变成响应式

  • ref :用于创建一个包装器,将普通的JavaScript数据变成响应式的,一般用于创建基本数值,创建对象数值底层还是调用 reactive 实现。使用ref函数创建的变量需要通过 .value 来访问其内部值。template 不需要通用 .value 访问,因为自动解构了

  • computed:用于创建一个计算属性,它会根据它所依赖的响应式数据自动更新。

reactive 使用限制

  • reactive 仅对对象类型有效(对象、数组和 Map、Set 这样的集合类型),而对 string、number 和 boolean 这样的 原始类型 无效
  • 不可以随意地“替换”一个响应式对象,会导致对初始引用的响应性连接丢失,如果有替换场景,考虑使用 ref 定义,或作为 reactive 对象一个属性
let state = reactive({ count: 0 })

// 上面的引用 ({ count: 0 }) 将不再被追踪(响应性连接已丢失!)
state = { count: 1 }

// 使用 ref 替换,响应式正常
let state1 = ref({ count: 0 })

ref.value = { count: 1 }
  • reactiveprops 不能直接解构,会失去响应式,原因是 vue 响应式的追踪依靠代理对象 proxy 的引用,解构相当于创建了新的内存地址,引用就改变了。解决办法,使用 toReftoRefs 取值,或将值赋给 reactive 的属性

响应式函数代码示例

<template>
  <div>
    <h1>{{ fullName }}</h1>
    <input v-model="firstName" placeholder="姓氏" />
    <input v-model="lastName" placeholder="名字" />
  </div>
</template>

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

export default defineComponent({
  setup() {
    const firstName = ref('');
    const lastName = ref('');

    const fullName = computed(() => {
      return `${firstName.value} ${lastName.value}`;
    });

    return {
      firstName,
      lastName,
      fullName
    };
  }
});
</script>

分析:

  1. 使用 ref 函数将 firstName 和 lastName 包装成响应式引用类型,使得它们的变化可以被自动追踪和更新视图。

  2. 使用 computed 函数创建了一个计算属性 fullName ,它会根据 firstName 和 lastName 的值自动计算出全名。computed 函数内部依赖于 firstName.valuelastName.value ,当它们的值发生变化时,fullName 会自动更新。

  3. setup 函数中,我们将 firstName 、lastName 和 fullName 作为返回值暴露给组件实例,使得它们可以在模板中被访问和使用。

Computed 计算函数

在Vue中,我们可以使用 computed 函数来创建计算属性。计算属性是一种特殊的响应式引用,它的值是由一个函数计算得出的,而且,这个函数的结果会被缓存。当这个函数的依赖变化时,Vue会自动重新计算这个属性的值,并更新视图。

computed 计算属性使用场景,一般用于优化模版的计算逻辑,如 classstyle 依赖其他数据的计算,减少 if-else 模版的编写,计算结果被缓存,减少计算量优化性能

<template>
  <div>
    <h1 :class="headingClasses">Hello, Vue 3!</h1>
    <div :style="boxStyles"></div>
    <button @click="toggleColor">Toggle Color</button>
  </div>
</template>

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

export default defineComponent({
  setup() {
    const isRed = ref(true);

    // 计算属性:根据isRed的值返回不同的类名
    const headingClasses = computed(() => {
      return {
        'red-text': isRed.value,
        'blue-text': !isRed.value
      };
    });

    // 计算属性:根据isRed的值返回不同的样式对象
    const boxStyles = computed(() => {
      return {
        backgroundColor: isRed.value ? 'red' : 'blue',
        width: '200px',
        height: '200px'
      };
    });

    const toggleColor = () => {
      isRed.value = !isRed.value;
    };

    return {
      headingClasses,
      boxStyles,
      toggleColor
    };
  }
});
</script>

在上述示例中,我们使用了 computed 函数来创建两个计算属性:headingClassesboxStyles。根据 isRed 的值,这两个计算属性动态地返回不同的类名和样式对象。

computed 使用注意事项

  • getter 不应有副作用:例如不要在 getter 中做异步请求或者更改 DOM,getter 的职责应该仅为计算和返回该值
  • 避免直接修改计算属性值,可以定义 setter

总结下 computed 优点

  • 响应式:computed 函数会自动追踪依赖的响应式数据,当依赖数据发生变化时,computed 函数会重新计算并更新结果。这样可以保证计算属性的值始终是最新的。
  • 缓存:computed 函数会缓存计算结果,只有当依赖数据发生变化时才会重新计算。这样可以避免不必要的计算,提高性能。
  • 简洁:通过使用 computed 函数,可以将复杂的逻辑封装在计算属性中,使模板更加简洁和易读。计算属性的结果可以直接在模板中使用,而不需要在模板中编写复杂的逻辑。
  • 可重用性:计算属性可以在组件内部多次使用,提高代码的重用性。如果多个组件需要相同的计算逻辑,可以将计算属性定义在一个函数中,在多个组件中引用。

computed 使用注意

  • 额外内存开销:computed 函数会创建一个新的响应式对象来存储计算结果,这会占用一定的内存空间。如果计算属性的逻辑比较复杂或计算结果比较大,可能会导致内存开销较大。
  • 不适合异步操作:computed 函数只适用于同步计算逻辑,不适合处理异步操作。如果需要进行异步计算,应该使用 watch 函数或 async/await 来处理。

Watch 侦听函数

在Vue中,我们可以使用 watch 函数来创建侦听器。侦听器是一种特殊的函数,它会监听一些响应式引用或计算属性的变化,然后执行一些副作用。当被监听的引用或属性的值变化时,Vue会自动执行侦听器。

watch 函数可以监听对象、数组、函数等多种数据类型

// 监听引用对象
watch(state.data, (newValue,oldValue) => {
  // 执行操作
})

// 监听对象某个值,要使用函数
watch(() => state.data.id, (newValue,oldValue) => {
  // 执行操作
})

// 监听多个值,使用数组
watch([data1, data2], ([newVal1,newVal2], [oldVal1,oldVal2]) => {
  // 执行操作
})

// 停止,调用回调函数
const stop = watch(data, (newValue,oldValue) => {
  // 执行操作
})
// 停止监听
stop()

watch 使用场景分析

1、监听表单输入变化

watch('formData', (newValue,oldValue) => {
  // 执行表单验证操作
})

在表单中,可以使用 watch 函数监听表单数据的变化,然后执行表单验证操作。当 formData 发生变化时,watch 函数会自动执行回调函数。

2、监听路由参数的变化

watch(route.params.id, (newValue,oldValue) => {
  // 执行页面数据更新操作
})

在使用 Vue Router 进行路由跳转时,可以使用 watch 函数监听路由参数的变化,然后执行页面数据更新操作。当路由参数发生变化时,watch 函数会自动执行回调函数。

3、监听异步请求结果变化

watch('asyncData', (newValue,oldValue) => {
  // 执行页面渲染操作
})

在进行异步请求时,可以使用 watch 函数监听异步请求结果的变化,然后执行页面渲染操作。当 asyncData 发生变化时,watch 函数会自动执行回调函数。

4、监听全局状态变化

watch(() => store.state.globalData,(newValue,oldValue) => {
  // 执行全局状态更新操作
})

在使用 Pinia 或 Vuex 进行全局状态管理时,可以使用 watch 函数监听全局状态的变化,然后执行全局状态更新操作。当 globalData 发生变化时,watch 函数会执行回调函数。

其他的场景,如

  • 监听计算属性的变化,执行相应的操作
  • 监听数组的变化,执行相应的操作,数组内部元素的变化,需要设置 deep 选项为 true(注意,使用deep耗费性能,谨慎使用)

watch 优点分析

  • 更加灵活:Vue 3 的 watch 函数相比于 Vue 2 的 方法更加灵活。它可以监听多个数据源,并且可以根据需要执行相应的操作,可以更加方便地处理复杂的数据变化
  • 更好的性能:Vue3的watch函数使用了基于Proxy的响应式系统,相比于Vue2的Object.defineProperty,具有更好的性能表现。Proxy可以直接拦截对象的读取、赋值、删除等操作,从而实现更细粒度的数据变化追踪,减少了不必要的更新操作,提高了性能
  • 更好的类型推导:Vue3的watch函数支持TypeScript,可以更好地进行类型推导。通过类型声明,可以在编码阶段捕获潜在的错误,提高代码的可维护性和可读性

watch 使用注意

  • 需要手动处理深层次嵌套数据:Vue 3 的 watch 函数默认只监听对象的第一层属性变化,如果需要监听深层次嵌套数据的变化,需要手动设置 deep 选项。这增加了一些额外的代码和处理逻辑
  • 可能导致重复执行:如果在回调函数中修改了监听的数据,可能会导致回调函数被重复执行。在编写代码时注意避免这种情况,否则可能会导致无限循环或其他意外的结果。

生命周期钩子函数

生命周期钩子函数是用来在组件不同生命周期阶段执行逻辑的特殊函数。它们提供了一种在组件创建、挂载、更新和卸载等阶段执行代码的机制

Vue 3中常用的生命周期钩子函数及其作用的介绍和分析:

  • onBeforeCreate:在组件实例被创建之前调用。此时,组件的数据、计算属性和方法都尚未初始化,无法访问这些属性和方法。

  • onCreated:在组件实例被创建之后调用。此时,组件的数据、计算属性和方法已经初始化,但DOM尚未渲染。

  • onBeforeMount:在组件挂载之前调用。此时,组件的模板已经编译完成,但尚未渲染到DOM中。

  • onMounted:在组件挂载之后调用。此时,组件已经被渲染到DOM中,可以进行DOM操作和异步请求等操作。

  • onBeforeUpdate:在组件更新之前调用。当组件的数据发生变化,但尚未重新渲染时,会触发该钩子函数。

  • onUpdated:在组件更新之后调用。当组件的数据发生变化并重新渲染完成后,会触发该钩子函数。

  • onBeforeUnmount:在组件卸载之前调用。当组件即将被销毁时,会触发该钩子函数。可以在此处清理定时器、取消订阅等操作。

  • onUnmounted:在组件卸载之后调用。当组件已经被销毁,DOM节点从页面中移除后,会触发该钩子函数。

探索Vue3:Composition API的深入理解和实践

在Composition API中,结合具体的业务场景,例如在 onMounted 钩子函数中可以进行DOM操作、订阅事件或发送异步请求。而在 onBeforeUnmount 钩子函数中可以清理资源、取消订阅或清除定时器等操作代码

<template>
  <div>
    <h1>{{ message }}</h1>
  </div>
</template>

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

export default defineComponent({
  setup() {
    const message = ref('Hello, Vue 3!');

    onMounted(() => {
      // 在组件挂载后执行的操作
      console.log('组件已挂载');
      performDOMOperation();
      subscribeToEvent();
      fetchData();
    });

    onBeforeUnmount(() => {
      // 在组件卸载前执行的操作
      console.log('组件将卸载');
      cleanUpResources();
      unsubscribeFromEvent();
      clearTimer();
    });

    const performDOMOperation = () => {
      // 执行DOM操作
      const element = document.getElementById('my-element');
      // ...
    };

    const subscribeToEvent = () => {
      // 订阅事件
      window.addEventListener('resize', handleResize);
    };

    const fetchData = async () => {
      // 发送异步请求
      const response = await fetch('https://api.example.com/data');
      const data = await response.json();
      // ...
    };

    const cleanUpResources = () => {
      // 清理资源
      // ...
    };

    const unsubscribeFromEvent = () => {
      // 取消事件订阅
      window.removeEventListener('resize', handleResize);
    };

    const clearTimer = () => {
      // 清除定时器
      clearInterval(timer);
    };

    const handleResize = () => {
      // 处理事件回调
      // ...
    };

    let timer;
    onMounted(() => {
      // 在组件挂载后执行的操作
      console.log('组件已挂载');
      timer = setInterval(() => {
        // 定时更新数据
        message.value = 'Updated message';
      }, 1000);
    });

    onBeforeUnmount(() => {
      // 在组件卸载前执行的操作
      console.log('组件将卸载');
      clearInterval(timer);
    });

    return {
      message
    };
  }
});
</script>

onMounted 钩子函数中,执行了以下操作:

  • performDOMOperation:执行DOM操作,例如获取元素、操作样式等。
  • subscribeToEvent:订阅事件,例如窗口大小调整事件。
  • fetchData:发送异步请求,例如获取远程数据。

onBeforeUnmount 钩子函数中,我们执行了以下操作:

  • cleanUpResources:清理资源,例如释放内存、关闭连接等。
  • unsubscribeFromEvent:取消事件订阅,例如取消窗口大小调整事件的订阅。
  • clearTimer:清除定时器,例如停止定时更新数据的操作。

在适当的生命周期钩子函数中执行这些操作,我们可以确保在组件生命周期的特定阶段进行必要的操作,并在组件销毁之前进行清理。这样可以提高代码的可靠性和性能,并避免可能的内存泄漏和不必要的资源占用

Composition API 最佳实践

为了编写出高质量的代码,在使用Vue 3的Composition API时,根据自身开发经验总结一些个人认为好的实践

Composition API 编写规范

1、响应式数据的定义

setup 函数中定义的响应式数据,应该放在函数的顶层,避免在循环或条件语句中定义。这样可以确保响应式数据的正确性和一致性。

import { reactive, ref } from 'vue';

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

  return {
    state
  };
}

2、编写顺序

为了减少在 setup 函数中太灵活的编写,导致代码逻辑散乱。约定编写顺序规范,可以从响应式函数、计算函数、watch监听函数、生命周期钩子、自定义方法等顺序编写,在团队中按照一定的逻辑顺序编写,可以使代码更具可读性和可维护性

import { ref, computed, watch, onMounted, onBeforeUnmount, defineComponent } from 'vue';

export default defineComponent({
  setup() {
    // 响应式数据
    const firstName = ref('John');
    const lastName = ref('Doe');
    const age = ref(30);

    // 计算属性
    const fullName = computed(() => `${firstName.value} ${lastName.value}`);

    // 观察者
    watch(
      age,
      (newAge, oldAge) => {
        console.log(`年龄从 ${oldAge} 变为 ${newAge}`);
      }
    );

    // 生命周期钩子函数
    onMounted(() => {
      console.log('组件已挂载');
      // 可以在此处执行一些初始化操作
    });

    onBeforeUnmount(() => {
      console.log('组件将卸载');
      // 可以在此处清理资源或取消订阅
    });

    // 方法
    const increaseAge = () => {
      age.value++;
    };

    // 返回数据和方法
    return {
      firstName,
      lastName,
      age,
      fullName,
      increaseAge
    };
  }
});

自定义 Hooks

自定义 Hooks 是借鉴 React 的思想,Vue 3 的 Composition API 灵活性和组合,可以帮助我们封装可复用的逻辑,并在不同的组件中共享。

假如我们有一个这样的业务场景,有多个组件需要获取用户的地理位置信息,我们可以创建一个名为 useGeolocation 的自定义Hook,封装获取地理位置的逻辑,以便在需要时在多个组件中复用。

以下是代码示例

<template>
  <div>
    <p>经度: {{ latitude }}</p>
    <p>纬度: {{ longitude }}</p>
  </div>
</template>

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

// 自定义Hook
function useGeolocation() {
  const latitude = ref(null);
  const longitude = ref(null);

  const successCallback = (position) => {
    latitude.value = position.coords.latitude;
    longitude.value = position.coords.longitude;
  };

  const errorCallback = (error) => {
    console.error('获取地理位置失败:', error);
  };

  onMounted(() => {
    // 获取地理位置
    navigator.geolocation.getCurrentPosition(successCallback, errorCallback);
  });

  onBeforeUnmount(() => {
    // 取消地理位置获取
    navigator.geolocation.clearWatch(watchId);
  });

  return {
    latitude,
    longitude
  };
}

export default defineComponent({
  setup() {
    const { latitude, longitude } = useGeolocation();

    return {
      latitude,
      longitude
    };
  }
});
</script>

useGeolocation 函数中,我们使用 ref 函数创建了 latitudelongitude 两个响应式数据。在 successCallback 回调函数中,我们获取到用户的地理位置,并将经纬度赋值给相应的响应式数据。

onMounted 钩子函数中,我们调用 navigator.geolocation.getCurrentPosition 方法来获取地理位置。在 onBeforeUnmount 钩子函数中,我们调用 navigator.geolocation.clearWatch 方法取消地理位置获取。

通过这种方式,我们可以在多个组件中复用获取地理位置的逻辑,而不需要在每个组件中重复编写相同的代码。这提高了代码的可复用性和可维护性,并使逻辑更清晰。

Composition API 总结

最后总结下 Composition API 开发规范

  1. 单一职责原则:将相关的逻辑和状态组织到单个自定义函数中,使代码更清晰、可维护性更高。

  2. 使用响应式函数:使用 ref 来包装基本类型的数据,使用 reactive 来包装对象或数组,以便进行响应式跟踪。

  3. 使用 computed:通过 computed 函数创建计算属性,根据依赖的数据动态计算值,避免重复计算和手动追踪依赖。

  4. 使用 watch:使用 watch函数来观察响应式数据的变化,执行相应的操作,例如发送网络请求、更新状态等。

  5. 组合多个函数:可以通过调用多个自定义函数来组合逻辑,使代码更可复用和可测试。

  6. 使用 provideinject :使用 provide 函数在父组件中提供数据,然后使用 inject 函数在子组件中注入这些数据,以实现跨组件的数据共享。

  7. 利用生命周期钩子函数:使用 onMountedonBeforeUnmount 等生命周期钩子函数来执行相应的操作,例如订阅事件、发送请求等。

  8. 分离副作用代码:将有副作用的代码(例如定时器、网络请求等)放在 onMountedonBeforeUnmount 钩子函数中,以确保正确的初始化和清理。

转载自:https://juejin.cn/post/7253290993239048248
评论
请登录