第六章、原始值的响应式方案

Proxy 只能代理对象,无法对字符串之类的原始值进行代理,如果要代理这些原始值,需要做一层包装,包装成一个对象后再进行代理。

// 定义一个包装函数,将原始值包装成一个对象
function ref(val) {
  const wrapper = {
    value: val,
  }

  // 返回响应式对象
  return reactive(wrapper)
}

const refVal = ref(1)

effect(() => {
  console.log(refVal.value)
})

// 修改能够触发副作用函数执行
refVal.value = 2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

需要一个方法来区别一个数据是否是 ref 对象,方便实现后面的自动脱 ref 功能。

function ref(val) {
  const wrapper = {
    value: val,
  }
  // 在wrapper上面定义一个不可枚举属性且不可写属性__v_isRef,表示其是一个ref对象
  Object.defineProperty(wrapper, '__v_isRef', {
    value: true,
  })
  return reactive(wrapper)
}
1
2
3
4
5
6
7
8
9
10

现在的响应式对象在使用扩展运算符的时候,会出现响应式丢失的情况,可以利用 ref 来解决这个问题。

const obj = reactive({ foo: 1, bar: 2 })

const newObj = {
  ...obj,
}

effect(() => {
  console.log(newObj.foo)
})
// 不会触发响应,因为副作用函数内的newObj是一个普通对象,不会进行依赖收集,没有响应能力
obj.foo = 2

// -----------------------------------

// 定义一个函数来将属性转为响应式的
function toRef(obj, key) {
  // 这个对象具有一个访问器属性value,当访问value时,本质相当于读取了响应式对象obj上面的属性
  const wrapper = {
    get value() {
      return obj[key]
    },
  }

  Object.defineProperty(wrapper, '__v_isRef', {
    value: true,
  })

  return wrapper
}

const obj = reactive({ foo: 1, bar: 2 })

// 将newObj的属性改为通过toRef函数读取
const newObj = {
  foo: toRef(obj, 'foo'),
  bar: toRef(obj, 'bar'),
}

effect(() => {
  console.log(newObj.foo.value)
})
// 能够触发响应
obj.foo = 2

// -----------------------------------

// 属性过多时,可以创建一个批量转换的函数来处理
function toRefs(obj) {
  const ret = {}
  // 遍历响应式对象上面的属性,将其转换为ref属性
  for (const key in obj) {
    ret[key] = toRef(obj, key)
  }
  return ret
}

const obj = reactive({ foo: 1, bar: 2 })

// 可以直接使用扩展运算符创建新的响应式对象了
const newObj = { ...toRefs(obj) }

effect(() => {
  console.log(newObj.foo.value)
})
// 能够触发响应
obj.foo = 2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66

目前 toRef 方法返回的对象的 value 属性只有 get 访问器,没有 set 访问器,这会导致创建的 ref 对象变成只读的,需要实现 set 操作。

function toRef(obj, key) {
  const wrapper = {
    get value() {
      return obj[key]
    },
    // 设置属性
    set value(val) {
      obj[key] = val
    },
  }

  Object.defineProperty(wrapper, '__v_isRef', {
    value: true,
  })

  return wrapper
}

const obj = reactive({ foo: 1, bar: 2 })
const newObj = toRef(obj, 'foo')
effect(() => {
  console.log(newObj.value)
})
// 直接修改newObj,能够触发响应
newObj.value = 2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

目前的 ref 属性需要通过 value 属性才能访问和修改,需要一种方式能直接通过属性本身访问和修改,类似于在 vue3 模板中可以直接写属性名称而不需要带.value。

// 定一个代理函数来实现自动脱勾ref
function proxyRefs(target) {
  return new Proxy(target, {
    // 代理读取属性
    get(target, key, receiver) {
      const value = Reflect.get(target, key, receiver)
      // 通过判断之前设置的__v_isRef属性,如果是ref就返回.value,不是就直接返回
      return value.__v_isRef ? value.value : value
    },
    set(target, key, newValue, receiver) {
      const value = target[key]
      // set操作时也一样,如果是ref,就设置.value,否则直接调用原来的Reflect.set
      if (value.__v_isRef) {
        value.value = newValue
        return true
      }
      return Reflect.set(target, key, newValue, receiver)
    },
  })
}

const obj = reactive({ foo: 1, bar: 2 })
const newObj = proxyRefs({ ...toRefs(obj) })

// 能够直接访问属性
console.log(newObj.foo)
console.log(newObj.bar)

// 能够直接设置属性
newObj.foo = 2
console.log(newObj.foo)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31