Skip to content

6. 原始值的响应式方案

6.1  引入 ref 的概念

由于 Proxy 的代理目标必须是非原始值,所以需要使用一个非原始值去“包裹”原始值

js
const wrapper = {
  value: "vue",
};
// 可以使用 Proxy 代理 wrapper,间接实现对原始值的拦截
const name = reactive(wrapper);
name.value; // vue
name.value = "vue3";

封装一个函数

js
// 封装一个 ref 函数
function ref(val) {
  // 在 ref 函数内部创建包裹对象
  const wrapper = {
    value: val,
  };
  // 将包裹对象变成响应式数据
  return reactive(wrapper);
}

但是为了让它有自动脱 ref的能力,我们有必要区分一个数据到底是不是 ref

js
function ref(val) {
  const wrapper = {
    value: val,
  };
  // 使用 Object.defineProperty 在 wrapper 对象上定义一个不可枚举的属性 __v_isRef,并且值为 true
  Object.defineProperty(wrapper, "__v_isRef", {
    value: true,
  });

  return reactive(wrapper);
}

6.2  响应丢失问题

通过解构对象的方式会使模板数据丢失响应式

js
export default {
  setup() {
    // 响应式数据
    const obj = reactive({ foo: 1, bar: 2 });

    // 将数据暴露到模板中
    return {
      ...obj,
    };
    // 相当于
    // return {
    //   foo: 1,
    //   bar: 2,
    // };
  },
};

如何解决?

js
// obj 是响应式数据
const obj = reactive({ foo: 1, bar: 2 });

// newObj 对象下具有与 obj 对象同名的属性,并且每个属性值都是一个对象,
// 该对象具有一个访问器属性 value,当读取 value 的值时,其实读取的是 obj 对象下相应的属性值
const newObj = {
  foo: {
    get value() {
      return obj.foo;
    },
  },
  bar: {
    get value() {
      return obj.bar;
    },
  },
};

effect(() => {
  // 在副作用函数内通过新的对象 newObj 读取 foo 属性值
  console.log(newObj.foo.value);
});

// 这时能够触发响应了
obj.foo = 100;

foobar 方法封装成一个toRef方法

js
function toRef(obj, key) {
  const wrapper = {
    get value() {
      return obj[key];
    },
  };

  return wrapper;
}

// 使用
return {
  foo: toRef(obj, "foo"),
  bar: toRef(obj, "bar"),
};

继续优化,批量解构

js
function toRefs(obj) {
  const ret = {};
  // 使用 for...in 循环遍历对象
  for (const key in obj) {
    // 逐个调用 toRef 完成转换
    ret[key] = toRef(obj, key);
  }
  return ret;
}

// 使用
return {
  ...toRefs(obj),
};

完整 toRef 方法

js
function toRef(obj, key) {
  const wrapper = {
    get value() {
      return obj[key];
    },
    // 允许设置值
    set value(val) {
      obj[key] = val;
    },
  };

  // 将通过 toRef 或 toRefs 转换后得到的结果视为真正的 ref 数据
  Object.defineProperty(wrapper, "__v_isRef", {
    value: true,
  });

  return wrapper;
}

6.3  自动脱 ref

js
const obj = reactive({ foo: 1, bar: 2 });
obj.foo; // 1
obj.bar; // 2

const newObj = { ...toRefs(obj) };
// 必须使用 value 访问值
newObj.foo.value; // 1
newObj.bar.value; // 2

使用 ProxynewObj 创建一个代理对象,通过代理来实现脱 ref

js
function proxyRefs(target) {
  return new Proxy(target, {
    get(target, key, receiver) {
      const value = Reflect.get(target, key, receiver);
      // 自动脱 ref 实现:如果读取的值是 ref,则返回它的 value 属性值
      return value.__v_isRef ? value.value : value;
    },
  });
}

// 调用 proxyRefs 函数创建代理
const newObj = proxyRefs({ ...toRefs(obj) });

实际上,我们在编写 Vue.js 组件时,组件中的 setup 函数所返回的数据会传递给 proxyRefs 函数进行处理:

js
const MyComponent = {
  setup() {
    const count = ref(0);

    // 返回的这个对象会传递给 proxyRefs
    return { count };
  },
};

proxyRefs 添加设置值的能力

js
function proxyRefs(target) {
  return new Proxy(target, {
    get(target, key, receiver) {
      const value = Reflect.get(target, key, receiver);
      return value.__v_isRef ? value.value : value;
    },
    set(target, key, newValue, receiver) {
      // 通过 target 读取真实值
      const value = target[key];
      // 如果值是 Ref,则设置其对应的 value 属性值
      if (value.__v_isRef) {
        value.value = newValue;
        return true;
      }
      return Reflect.set(target, key, newValue, receiver);
    },
  });
}

上次更新于: