返回

Vue 3 只读对象特定键值修改策略

vue.js

Vue 3 中只读对象的特定键值修改策略

在 Vue 3 开发中,readonly() 函数可将一个响应式对象转换为只读版本。这个只读版本禁止对属性进行直接修改。特定情况下,需要允许对只读对象中的部分键值进行修改,这带来了挑战。针对如何允许在Vue 3 readonly 对象中编辑某些键这一问题,提供一些实用的解决方案。

问题根源:readonly() 的作用机制

readonly() 通过 Proxy 代理目标对象实现只读功能。代理会拦截对对象的所有 setter 操作,从而阻止对任何键值修改。这种机制保护了对象状态不被意外修改,也确保了应用的可预测性。需要修改的键会被 readonly() 创建的 Proxy 捕获并阻止修改。

方案一:使用 markRaw() 标记需要修改的键值

对于无需响应性的键,可以使用 markRaw() 阻止 readonly() 对这些属性施加只读保护。被标记的属性保持了响应式但能被修改。

原理说明

markRaw() 函数会将对象标记为“原始”状态。这个状态告诉 Vue 不要将其转换为响应式对象,防止对某个子对象添加 Proxy 代理。将希望允许修改的键对应的子对象使用 markRaw() 标记,该子对象在传递给 readonly() 函数时会被保留下来,因此 readonly 将允许修改这个被标记为"原始"的属性值。

操作步骤

  1. 使用 markRaw() 标记需修改键值。
  2. 使用 readonly() 创建只读对象。

代码示例

import { reactive, readonly, markRaw } from 'vue'

const obj = reactive({
  name: 'example',
  target: markRaw({ value: 'DOM' })
})

const readonlyObj = readonly(obj)

// 可以正常修改
readonlyObj.target.value = 'Modal'
console.log(readonlyObj.target.value) // 输出: Modal

// 下面会触发警告: 'Set operation on key "name" failed: target is readonly.'
readonlyObj.name = 'test' 

方案二:构建自定义的浅只读对象

基于需求创建一个允许修改指定键的自定义函数,这个函数需要基于 readonly 实现但排除指定键。可以封装此行为的函数实现类似readonly的功能,但可以根据需要开放某个属性的修改权限。

原理说明

customReadonly 实现一个自定义版本的 readonly() 函数。它接受一个对象和一个键名数组作为参数,其中数组指定要开放修改的键。内部使用 toRaw() 可以将 readonlyObj 转换为普通的非只读对象, 就可以对其进行操作修改, 修改后需要再返回 readonly 对象即可。这个自定义函数使用 Proxy 创建了一个只读对象,通过 toRaw 方法获取被 readonly 包装对象的原始值从而可以修改 excludedKeys 指定的键,然后继续将其转换为 readonly 对象进行保护,实现自定义功能。

操作步骤

  1. 定义自定义函数,利用 Proxy 创建代理。
  2. 设置 setter,检查是否为允许修改的键。
  3. 对于允许的键,通过原始对象修改;否则,阻止修改。

代码示例

import { reactive, readonly, toRaw } from 'vue'

function customReadonly(obj, excludedKeys) {
  return new Proxy(obj, {
    get(target, key, receiver) {
      const res = Reflect.get(target, key, receiver);
      return  res;
    },
    set(target, key, value, receiver) {
      if (excludedKeys.includes(key)) {
        const rawTarget = toRaw(target); //将target转化为普通对象再进行修改, toRaw()方法无法对基础类型数据起效,必须为复杂对象数据类型。
        rawTarget[key] = value;
        //返回新的 readonly() 对象进行保护。
        return true;
      } else {
        console.warn(`Set operation on key "${String(key)}" failed: target is readonly.`, target);
        return true;
      }
    }
  });
}

const obj = reactive({
  name: 'example',
  target: 'DOM',
  info: {
      title: 'content'
  }
})

const readonlyObj = customReadonly(obj, ['target', 'info']);
// 修改排除的键,正常执行。
readonlyObj.target = 'Modal'
readonlyObj.info.title = 'test'
// 会输出告警
readonlyObj.name = 'new name';

方案三:结合计算属性派生新对象

创建一个计算属性,该计算属性基于原只读对象生成一个新的对象。允许修改特定键,但不直接改变原对象,创建计算属性可以在一个独立的范围内去创建和修改部分属性, 同时维持 readonly 属性原本的属性不变,提供安全保障。

原理说明

使用 computed() 创建一个计算属性。这个计算属性返回一个新的对象,新对象从原始的只读对象派生而来。通过对象展开运算符浅拷贝只读对象的属性到一个新的对象中, 然后在这个新的对象中就可以根据需要, 单独允许修改需要的键。由于计算属性只在其依赖项发生更改时重新计算,且没有 setter 属性, 在需要响应性和不可变性保证的同时处理例外情况有明显作用。

操作步骤

  1. 定义计算属性。
  2. 使用展开运算符和键值覆盖创建新对象。
  3. 返回该新对象。

代码示例

import { reactive, readonly, computed, ref } from 'vue'

const obj = reactive({
  name: 'example',
  target: 'DOM'
})

const readonlyObj = readonly(obj)

const derivedObj = computed(() => ({
  ...readonlyObj,
  // 通过修改此处的值,可以随时修改
  target: 'Modal'
}))

console.log(derivedObj.value.target); // 输出: Modal
// 再次触发告警: 'Set operation on key "name" failed: target is readonly.'
readonlyObj.name = 'test'

安全建议

当尝试绕过 readonly 保护时,要注意应用状态的稳定。直接修改 readonly 对象可能导致意外副作用,尤其是在多人协作或复杂状态管理的项目中。遵循 Vue 3 的设计理念,尽量避免破坏 readonly 的完整性,使用上述方法来确保修改符合预期。进行一些键值上的修改时候, 应当保证数据类型的统一,对于类型改变、空值等异常状态需要做单独的处理以防影响原本对象的类型和结构, 导致应用运行期间报错,通过合理的使用和实践, 可以更好的控制项目的开发状态, 同时对异常情况处理也能保证安全性。