V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
mizuhashi
V2EX  ›  Vue.js

用 reactive 來實現 oo 對象的封裝

  •  
  •   mizuhashi · 13 小时 32 分钟前 · 699 次点击

    我不確定這是否是普遍的用法,但我發現 reactive 非常適合用來建立類似 OO 的封裝。

    reactive會把 refs 展平,所以可以這麼寫:

    function useClient() {
      const name = ref('Alice')
      const greeting = computed(() => `Hello ${name.value}`)
    
      function updateName(newName: string) {
        name.value = newName
      }
    
      return reactive({
        name,
        greeting,
        updateName
      })
    }
    

    在 component 裏:

    const client = useClient()
    client.greeting // => 'Hello Alice'
    
    client.updateName('Bob')
    client.greeting // => 'Hello Bob'
    

    現在client管理它自己的狀態,其暴露的接口可以直接用在模板裏。

    我們也可以組合這些對象和保留嚮應性:

    function useOrder(client: ReturnType<typeof useClient>) {
      const createdBy = computed(() => `Created by ${client.name}`)
    
      // 你也可以在這裏調用 client.updateName
    
      return reactive({
        createdBy
      })
    }
    
    const client = useClient()
    const order = useOrder(client)
    order.createdBy // => 'Created by Alice'
    client.updateName('Bob')
    order.createdBy // => 'Created by Bob'
    

    我覺得這是 vue 相對於其他庫特別的屬性,我只需要傳遞一個對象,而它擁有自己的狀態和方法。

    在現實中,這些對象一般會基於後端數據,我們可以在後端數據的基礎上擴展狀態和方法。

    async function useOrder(client: ReturnType<typeof useClient>) {
      const orderData = reactive(await fetchOrderData())
    
      const paid = ref(false)
    
      async function pay() {
        const res = await paymentAPI()
        paid.value = res.success
      }
    
      return reactive({
        ...toRefs(orderData), // 所有 orderData 的屬性會被暴露
        // 我們需要 toRefs 來保持嚮應性
        paid,
        pay
      })
    }
    

    現在給定一個 order ,我們可以直接在模板裏綁定order.paidorder.pay

    本帖在 reddit 上的版本: https://www.reddit.com/r/vuejs/comments/1owezu4/reactive_as_an_object_encapsulation/

    15 条回复    2025-11-14 20:05:26 +08:00
    XCFOX
        1
    XCFOX  
       12 小时 57 分钟前   ❤️ 1
    我都是写 reactive + class 的。
    class 简直是天然的 store ,状态和方法一目了然。

    ```ts
    import { reactive } from 'vue'

    class ClientStore {
      name = 'Alice'

      get greeting() {
        return `Hello ${this.name}`
      }

      updateName(newName: string) {
        this.name = newName
      }
    }

    export const client = reactive(new ClientStore())
    ```
    lynchuh
        2
    lynchuh  
       9 小时 27 分钟前   ❤️ 1
    牛,学到了!
    mizuhashi
        3
    mizuhashi  
    OP
       9 小时 1 分钟前
    @XCFOX 好像也可以,不過這樣沒有用上 computed 的緩存
    jspatrick
        4
    jspatrick  
       8 小时 55 分钟前
    分享下我项目内的常见写法,这是表格相关的功能,几乎完全是 v2 的迁移版...

    ```javascript
    const state = reactive({
    loading: false,
    list: [],
    searchParams: {
    department: null,
    company: null,
    vehicleType: [],
    thisWeek: false,
    thisMonth: false,
    thisYear: false,
    startTime: computed(() => {
    if (!state.searchParams.time) return undefined;
    const startTime = state.searchParams.time[0];
    return dayjs(startTime).startOf('day').format('YYYY-MM-DD HH:mm:ss');
    }),
    endTime: computed(() => {
    if (!state.searchParams.time) return undefined;
    const endTime = state.searchParams.time[1];
    return dayjs(endTime).endOf('day').format('YYYY-MM-DD HH:mm:ss');
    }),
    time: [dayjs().startOf('month').format('YYYY-MM-DD'), dayjs().endOf('day').format('YYYY-MM-DD')]
    },
    options: {
    department: [],
    company: [],
    vehicleType: []
    },
    columns: hooks.useDefaultColumns([]),
    pagination: hooks.usePagination(() => state.search()),
    selectTimeMode(idx) {
    },
    async search() {
    }
    });
    ```
    jspatrick
        5
    jspatrick  
       8 小时 50 分钟前   ❤️ 1
    mizuhashi
        6
    mizuhashi  
    OP
       8 小时 26 分钟前
    @jspatrick 原來可以這樣引用自己
    duuu
        7
    duuu  
       8 小时 24 分钟前   ❤️ 2
    楼主的 useClient 其实就是我们常用的 Composable 的写法,只是在 return 里套了一层 reactive 。好处暂时看来是用的地方不用写.value 了。但是我感觉这样写反而会有心智负担,因为简单从代码里看 name ,和 greeting 的用法都是要.value 的,如果用了 useClient 后,client.greeting 和,client.name 都不用.value 反而在每次写的时候要思考一下。如果这种写法在团队里没有形成规范的话,也会让同事调用你的代码写起来很困惑
    duuu
        8
    duuu  
       8 小时 21 分钟前
    @jspatrick vue3 的优点就是每个模块的代码可以集中写,你这样按 vue2 的 data 来放,就丢失这个优点了~
    jspatrick
        9
    jspatrick  
       8 小时 13 分钟前
    @duuu #8 其实就是抽离不抽离的关系,如果文件复杂度上升,就抽出去做成 hooks 来用,如果只是简单的页面,这么写还是有好处的,所有的表单逻辑其实都被写在了 state 里,这整个 state 其实就是 hooks 本来要导出的东西,关注点也不会被其他 hooks 打断,并不是页面全被塞进 state ,而是某个业务相关的东西被单独做成一个 state ,一个页面也可能有多个不同业务的 state
    dumbass
        10
    dumbass  
       8 小时 10 分钟前
    @duuu #8 正确的。我会把 loading/list/searchParams/options... 拆成不同的 ref
    duuu
        11
    duuu  
       8 小时 5 分钟前
    @jspatrick 理解,不过如果是我的习惯的话,如果不抽成 hooks ,我不会在外面套一层 reactive 。我会跟#10 一样拆成不同的 ref
    lizhenda
        12
    lizhenda  
       7 小时 51 分钟前
    这不就是 vue3 推荐的使用方法么 ···
    UnluckyNinja
        13
    UnluckyNinja  
       6 小时 33 分钟前   ❤️ 1
    这其实就是 composable 的用法,但官方指南不建议在 composable 函数里对返回值用 reactive 包装,这样会导致解构语法响应性不正确。
    比如你这个例子,如果使用者写 `const { name } = useClient()`,name 就会失去响应性。
    官方的推荐做法是返回 ref 作为返回对象的属性,如果调用方想使用整体响应性并免除.value ,可以自己用 reactive 包装。

    详见 https://cn.vuejs.org/guide/reusability/composables
    大量 composable 参考可以浏览 VueUse https://vueuse.org/
    mizuhashi
        14
    mizuhashi  
    OP
       4 小时 20 分钟前 via iPhone
    @UnluckyNinja 原來文檔裏有提到用 reactive 包,我知道官方推薦的返回 ref 。不過對於 domain object 我從沒想要解構,就是想要它的封裝性,如果解構了就都散了
    Ketteiron
        15
    Ketteiron  
       3 分钟前
    @UnluckyNinja 官方文档这么写问题很大,似乎是暗示 return 一个 reactive 会丢失响应式,我第一次看文档时也是这么认为的。实际并不会,因为返回的是代理对象,只有对代理对象进行解构才会丢失响应式。
    我想没人会 return obj ,正常都是 return { obj }
    ref x,y 和 reactive ({x,y}) 是等价的两种写法,只是后者不能解构
    const {obj:{x,y}} = useXxx()
    我想也没人这么闲用嵌套解构语法
    文档只需说不建议返回 reactive 对象就行了,初学者可能会以为 {reactive()} 也是不行的。
    关于   ·   帮助文档   ·   自助推广系统   ·   博客   ·   API   ·   FAQ   ·   Solana   ·   2653 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 21ms · UTC 12:09 · PVG 20:09 · LAX 04:09 · JFK 07:09
    ♥ Do have faith in what you're doing.