Vue 3.0 function-based API尝鲜(三):包装对象

关键词:value()computed()

其实本来想自己写写的,但是尤大写得实在太过详细,实在不好狗尾续貂。干脆直接搬过来吧:

value() 返回的是一个 value wrapper (包装对象)。一个包装对象只有一个属性:.value,该属性指向内部被包装的值。在上面的例子中,msg 包装的是一个字符串。包装对象的值可以被直接修改:

// 读取
console.log(msg.value) // 'hello'
// 修改
msg.value = 'bye'

为什么需要包装对象?

我们知道在 JavaScript 中,原始值类型如 string 和 number 是只有值,没有引用的。如果在一个函数中返回一个字符串变量,接收到这个字符串的代码只会获得一个值,是无法追踪原始变量后续的变化的。

因此,包装对象的意义就在于提供一个让我们能够在函数之间以引用的方式传递任意类型值的容器。这有点像 React Hooks 中的 useRef —— 但不同的是 Vue 的包装对象同时还是响应式的数据源。有了这样的容器,我们就可以在封装了逻辑的组合函数中将状态以引用的方式传回给组件。组件负责展示(追踪依赖),组合函数负责管理状态(触发更新):

setup() {
  const valueA = useLogicA() // valueA 可能被 useLogicA() 内部的代码修改从而触发更新
  const valueB = useLogicB()
  return {
    valueA,
    valueB
  }
}

包装对象也可以包装非原始值类型的数据,被包装的对象中嵌套的属性都会被响应式地追踪。用包装对象去包装对象或是数组并不是没有意义的:它让我们可以对整个对象的值进行替换 —— 比如用一个 filter 过的数组去替代原数组:

const numbers = value([1, 2, 3])
// 替代原数组,但引用不变
numbers.value = numbers.value.filter(n => n > 1)

如果你依然想创建一个没有包装的响应式对象,可以使用 stateAPI(和 2.x 的 Vue.observable()等同):

import { state } from 'vue'

const object = state({
  count: 0
})

object.count++

Value Unwrapping(包装对象的自动展开)

在上面的一个例子中你可能注意到了,虽然 setup()返回的 msg是一个包装对象,但在模版中我们直接用了 {{ msg }}这样的绑定,没有用 .value。这是因为当包装对象被暴露给模版渲染上下文,或是被嵌套在另一个响应式对象中的时候,它会被自动展开 (unwrap) 为内部的值。

比如一个包装对象的绑定可以直接被模版中的内联函数修改:

const MyComponent = {
  setup() {
    return {
      count: value(0)
    }
  },
  template: `<button @click="count++">{{ count }}</button>`
}

当一个包装对象被作为另一个响应式对象的属性引用的时候也会被自动展开:

const count = value(0)
const obj = state({
  count
})

console.log(obj.count) // 0

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

count.value++
console.log(obj.count) // 2
console.log(count.value) // 2

以上这些关于包装对象的细节可能会让你觉得有些复杂,但实际使用中你只需要记住一个基本的规则:只有当你直接以变量的形式引用一个包装对象的时候才会需要用 .value 去取它内部的值 —— 在模版中你甚至不需要知道它们的存在。

Computed Value (计算值)

除了直接包装一个可变的值,我们也可以包装通过计算产生的值:

import { value, computed } from 'vue'

const count = value(0)
const countPlusOne = computed(() => count.value + 1)

console.log(countPlusOne.value) // 1
count.value++
console.log(countPlusOne.value) // 2

计算值的行为跟计算属性 (computed property) 一样:只有当依赖变化的时候它才会被重新计算。

computed() 返回的是一个只读的包装对象,它可以和普通的包装对象一样在 setup() 中被返回 ,也一样会在渲染上下文中被自动展开。默认情况下,如果用户试图去修改一个只读包装对象,会触发警告。

双向计算值可以通过传给 computed 第二个参数作为 setter 来创建:

const count = value(0)
const writableComputed = computed(
  // read
  () => count.value + 1,
  // write
  val => {
    count.value = val - 1
  }
)

从这个层面上说,包装对象的意义和Vuex其实有点像,都是为了确保可追踪、可预测的变化。同时,包装对象也可以封装一些方法,就类似于JavaScript(和其他语言)中的String、Boolean这种对象。

事实上,官方也是这么做的。可以看看源码,AbstractWrapper类里有一个抽象方法protected abstract exposeToDevtool(): void;,明显就是为了追踪变化的;计算值和包装值都继承了这个类。从源码里可以看出,在非生产环境下,包装值应该是可以通过Vue DevTools观测的。比如,这个是ValueWrapper的实现:

exposeToDevtool() {
    if (process.env.NODE_ENV !== 'production') {
        const vm = this._vm!;
        const name = this._propName!;
        proxy(vm._data, name, {
            get: () => this.value,
            set: (val: any) => {
                this.value = val;
            },
        });
    }
}

但是包装对象不是万能的,对于对象的变化的检测还是有问题。比如,对于一个被watch的对象,修改了它的值,并不会因为它是包装对象,就能检测到变化,还是需要赋值才能触发。只不过,比起之前用$set,体验要好得多了:

const foo = value({width: 100})
watch(
    () => foo,
    () => {console.log('foo');}
);
foo.value.width = 120;   // 不会触发
foo.value = {width: 120} // 'foo'
值得一提的watch
目录

Vue 3.0 function-based API尝鲜(一):前言

Vue 3.0 function-based API尝鲜(二):配置与启动

Vue 3.0 function-based API尝鲜(四):值得一提的watch

Vue 3.0 function-based API尝鲜(五):生命周期

Vue 3.0 function-based API尝鲜(六):组件间通信

Vue 3.0 function-based API尝鲜(七):This与Refs

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值