Skip to content

05|Options API 与 Composition API 迁移 ​

Vue 2 的实例与选项基础从第十章连续学习。本篇关注逻辑组织迁移,下面的 LegacyNetworkStatus 使用 Vue 3 Options API,所以清理钩子是 beforeUnmount;实际 Vue 2 → Vue 3 的双环境对照见维护之后迁移。

第四章概览 / 05。目标:先锁住可观察行为和资源时机,再从 this 依赖图中提取 composable;迁移不是生命周期函数改名比赛。

先做行为清单,再做映射 ​

Vue 3 仍支持 Options API,Composition API 也能在 Options 组件的 setup() 中渐进接入。安全迁移单位应是一条业务能力,例如“在线时轮询订单”,而不是机械地把所有 data 放一处、所有 methods 放另一处。

Options APIComposition API迁移时真正要保留的事
beforeCreate / created直接写在 setup() 同步阶段此时不能依赖 DOM;Composition 侧没有 onCreated
beforeMount / mountedonBeforeMount / onMounted浏览器资源只在客户端挂载后建立
beforeUpdate / updatedonBeforeUpdate / onUpdated仍要避免在 updated 中循环写状态
Vue 2 beforeDestroy / destroyedonBeforeUnmount / onUnmounted保留“实例仍可用时”与“已卸载后”的差别
activated / deactivatedonActivated / onDeactivatedKeepAlive 暂停不是销毁
errorCapturedonErrorCaptured保留传播或截断策略

Vue 3 Options API 的名称已经是 beforeUnmount / unmounted;若源代码仍是 Vue 2 的 beforeDestroy / destroyed,先确认运行版本再迁移。

从一段旧代码提取真实依赖 ​

vue
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
  data: () => ({ online: true, checks: 0, timer: undefined as number | undefined }),
  computed: {
    label(): string {
      return this.online ? `在线:${this.checks}` : '离线'
    }
  },
  methods: {
    syncOnline() {
      this.online = navigator.onLine
    },
    start() {
      if (this.timer !== undefined) return
      window.addEventListener('online', this.syncOnline)
      window.addEventListener('offline', this.syncOnline)
      this.timer = window.setInterval(() => { this.checks += 1 }, 1000)
    },
    stop() {
      if (this.timer === undefined) return
      clearInterval(this.timer)
      this.timer = undefined
      window.removeEventListener('online', this.syncOnline)
      window.removeEventListener('offline', this.syncOnline)
    }
  },
  mounted() {
    this.syncOnline()
    this.start()
  },
  beforeUnmount() {
    this.stop()
  }
})
</script>

<template><p>{{ label }}</p></template>

迁移前先锁住:初次挂载读取在线状态;每秒只增长一次;监听器引用一致;卸载后停止;多个实例不共享 checks。这些才是合同。

先提取能力,再换组件语法 ​

ts
import { computed, onMounted, onScopeDispose, readonly, ref } from 'vue'

export function useNetworkStatus(intervalMs = 1000) {
  const online = ref(true)
  const checks = ref(0)
  let timer: ReturnType<typeof setInterval> | undefined

  function syncOnline() {
    online.value = navigator.onLine
  }

  function start() {
    if (timer !== undefined) return
    window.addEventListener('online', syncOnline)
    window.addEventListener('offline', syncOnline)
    timer = setInterval(() => { checks.value += 1 }, intervalMs)
  }

  function stop() {
    if (timer === undefined) return
    clearInterval(timer)
    timer = undefined
    window.removeEventListener('online', syncOnline)
    window.removeEventListener('offline', syncOnline)
  }

  onMounted(() => {
    syncOnline()
    start()
  })
  onScopeDispose(stop)

  return {
    online: readonly(online),
    checks: readonly(checks),
    label: computed(() => online.value ? `在线:${checks.value}` : '离线')
  }
}
vue
<script setup lang="ts">
import { useNetworkStatus } from './useNetworkStatus'

const { label } = useNetworkStatus()
</script>

<template><p>{{ label }}</p></template>

composable 在 setup 同步链中调用,所以 onMounted 与 onScopeDispose 都归属调用它的实例。每次调用重新创建 refs、timer 与函数引用;window / navigator 只在挂载后访问,SSR setup 阶段安全。

渐进迁移的单向桥 ​

Options 组件可以加入 setup(),再把返回值用于模板和后续 Options 钩子:

ts
export default {
  setup() {
    return useNetworkStatus()
  },
  mounted() {
    console.log(this.label)
  }
}

但 setup() 不能读取后面才创建的 this.someData / this.someMethod。因此迁移方向应是把依赖显式作为参数传入 composable,而不是让新代码继续寻找旧 this。

mixin 迁移为何不能整包复制 ​

mixin 的数据、方法与钩子会合并进组件,来源和冲突常常隐蔽。拆成 composable 时逐项画出:外部输入、内部状态、写动作、建立资源、停止资源、公开返回。若两个 mixin 都注册 resize,迁移后仍应有两个明确所有者或被合并成一条有理由的共享能力,不能因为函数名相同就误删行为。

失败路径与回滚点 ​

  • 若迁移后轮询变成每次激活重复启动,说明 start 不幂等或旧钩子仍在运行。
  • 若两个实例共享 timer / refs,说明状态被移到模块顶层。
  • 若 SSR 报 window is not defined,说明浏览器读取发生在 setup 根部或模块求值阶段。
  • 若取消监听失败,检查 add/remove 是否使用同一函数引用与一致 capture 选项。

每次只迁移一条能力,并用原行为测试覆盖新旧实现。组件可以短期混用两种 API;一致性不值得用一次不可回滚的大改来换。

练习:迁移在线轮询组件 ​

以 LegacyNetworkStatus.vue 为输入,完成上面的拆分,验收合同如下:

  • DOM 文本与旧组件一致;可把间隔传为 20ms 做测试。
  • 两个实例的 checks 独立;重复调用 start 不产生第二个计时器。
  • online / offline 使用同一监听器引用,scope 结束后全部移除。
  • SSR 渲染返回确定初值,不访问 window / navigator,不启动计时器。
  • 写一条失败测试:故意把 syncOnline 改成两个匿名函数时,测试必须发现监听器残留。

参考答案:查看第 05 题。

用心记录每一步成长