【Vue3】Composition API 之 Reactivity APIs 
Vue 双向绑定原理是采用发布订阅者模式,在初始化时劫持数据的各个属性的 setter/getter,在数据变动时发布消息给订阅者,触发响应的监听回调。在 vue2.x 中,实现数据劫持的核心方法是 Object.defineProperty,即,通过劫持数据的 getter/setter 的方式,订阅到数据的变化,从而实现双向绑定。但是引用类型的内部引用,无法通过这个方法劫持。这也是为什么数组或者深层次的对象的内部数据发生变化,却没有引起视图变化的原因。所以在 vue2.x 中有一系列的方法($set、$delete 等等) 或者使用 watch 来解决这个问题。
提示
在 IE8 以上才有Object.defineProperty方法,所以 vue2.x 不兼容 IE8 及以下版本。
let obj = {},
  value = 1
Object.defineProperty(obj, 'a', {
  // 劫持了属性 a 的 getter
  get() {
    console.log('这里监听到了数据获取')
    return value
  },
  // 劫持了属性 a 的 setter
  set(newValue, value) {
    if (newValue !== value) {
      value = newValue
      console.log('这里监听到了数据更改')
    }
  }
})
console.log(obj.a) // 这里监听到了数据获取   1
obj.a = 2 // 这里监听到了数据更改
很明显,Object.defineProperty方法实现的双向绑定,不仅要遍历 data逐个劫持,还不能监听深层对象或者数组的变化。
这时候,ES6 中的 Proxy 就闪亮 ✨ 登场!!vue3.x 已经同时使用 Object.defineProperty 和 Proxy 来实现数据劫持,而前者只是用来兼容 IE 浏览器,而采用了 Proxy来实现的 响应式APIs拥有更简洁性能更好的表现。 Proxy字面意思为 “代理”。类似某明星(data),通过经纪人(Proxy)来对外交流(getter/setter)。
Proxy.handler 方法与Reflect相同, 可以用这些方法实现监听。
响应式的实现步骤:
当某个值发生变化时进行检测:我们不再需要这样做,因为 Proxy 允许我们拦截它- 跟踪更改它的函数:我们在 Proxy 中的 getter 中执行此操作,称为 effect
 - 触发函数以便它可以更新最终值:我们在 Proxy 中的 setter 中进行该操作,名为 trigger
 
let data = {
  msg: {
    a: 10
  },
  arr: [1, 2, 3]
}
let handler = {
  get(target, key) {
    // 懒监听,去获取的时候才监听对象里面的对象,而不是直接递归循环监听
    console.log('获取key: ' + key)
    if (typeof target[key] === 'object' && target[key] !== null) {
      return new Proxy(target[key], handler)
    }
    return Reflect.get(target, key)
  },
  set(target, key, value) {
    let oldValue = target[key]
    console.log('更新key: ' + key)
    if (oldValue !== value) {
      // 通知view更新
    }
    return Reflect.set(target, key, value)
  }
}
let proxy = new Proxy(data, handler)
proxy.arr.push(4)
/**
 *  打印结果如下:
    获取key: arr
    获取key: push
    获取key: length
    更新key: 3
    更新key: length
 */
为什么每次都有length,其实Proxy的监听数组实现是把数组变成了一个类数组对象而已。
// 类数组对象
const arr = {
  0: 'a',
  1: 'b',
  length: 2
}
在 vue2.x 中,提供了 Vue.observable() 来构建一个响应式对象,而在 vue3.x 中,提供了方法 reactive,来构建响应式的数据。
响应式基础 API 
响应式基础 API 是基于 Proxy,所以返回的响应式副本对象都为Proxy类型。 我们来学习一下这一系列的构建方法和辅助函数。
reactive 
reactive 返回参数对象的一个响应式副本。而且这个响应式是“深度”的,即,它将影响所有的嵌套属性。
<template>
  <div @click="handleClick">{{ state.name }} {{ state.age}}</div>
</template>
<script>
  import { reactive } from 'vue'
  export default {
    setup() {
      const state = reactive({
        name: 'kirito',
        age: 16
      })
      const handleClick = () => {
        // name和age 是经过 reactive 代理后的数据,它的变化将被观察,这里值变化,视图将更新
        state.name = 'fangzhi'
        state.age = 20
      }
      return {
        state,
        handleClick
      }
    }
  }
</script>
readonly 
获取一个对象 (响应式或纯对象) 或
ref并返回原始代理的只读代理。只读代理是深层的:访问的任何嵌套property也是只读的。
顾名思义,创建的对象是只读的。一般用于无法修改的枚举或者数据。
import { reactive, readonly } from 'vue'
const state = reactive({ name: 'kirito', age: 16 })
// 可以是一个响应式对象
const onlyreadState = readonly(state)
// 也可以是一个纯对象
const onlyreadObj = readonly({ name: 'tom', age: 22 })
isProxy 
用于检查对象是否由 reactive 或者 readonly 来创建的。
import { ref, reactive, readonly, isProxy } from 'vue'
const state = reactive({ name: 'kirito' })
const count = ref(0)
const obj = { name: 'sakura' }
const read = readonly(state)
const readByObj = readonly({ name: 'tom' })
isProxy(state) // true
isProxy(count) // false
isProxy(obj) // false
isProxy(read) // true
isProxy(readByObj) // true
其他辅助函数 
其他不常用的函数还有很多,这里列举以下:
- isReactive 检查对象是否由 
reactive创建。 - isReadonly 检查对象是否由 
readonly创建 - toRaw 返回响应式副本的原始对象
 - markRaw 标记一个对象,使其无法被响应式基础 API 转换为代理副本
 - shallowReactive 创建一个不嵌套的响应式副本(只有自身的 
property具有响应性 ) - shallowReadonly 创建一个不嵌套的只读响应式副本(只有自身的 
property为可读,深层嵌套的属性可以修改) 
Refs 
Refs 是基于响应式基础 API 的高阶方法。通过 ref 创建的响应式对象与 reactive创建的有所不同,它返回的是一个 RefImpl 对象,且响应式的数据被挂载到了它的 value属性上。
reactive 和 ref 返回对象有所不同
ref 
ref 方法可以创建响应式对象,不同于 reactive,它也可以接受一个基本类型参数。
<template>
  <div>{{ count }}</div>
</template>
<script lang="ts">
  import { defineComponent, ref } from 'vue'
  export default defineComponent({
    setup() {
      const count = ref(0)
      console.log(count.value)
      return {
        count
      }
    }
  })
</script>
这时,在 setup 中取值和赋值的,其实都是在其属性value下,如果使用的是 jsx 或者 render function,那么取值都要从value属性中获取。但是如果是 template,会被自动拆解,不需要在模板中额外的写.value。
// https://github.com/vuejs/vue-next/blob/master/packages/reactivity/src/ref.ts#L30
const convert = <T extends unknown>(val: T): T => (isObject(val) ? reactive(val) : val)
// https://github.com/vuejs/vue-next/blob/master/packages/reactivity/src/ref.ts#L41
export function ref(value?: unknown) {
  return createRef(value)
}
// https://github.com/vuejs/vue-next/blob/master/packages/reactivity/src/ref.ts#L54
class RefImpl<T> {
  private _value: T
  public readonly __v_isRef = true
  constructor(private _rawValue: T, public readonly _shallow = false) {
    this._value = _shallow ? _rawValue : convert(_rawValue)
  }
  get value() {
    track(toRaw(this), TrackOpTypes.GET, 'value')
    return this._value
  }
  set value(newVal) {
    if (hasChanged(toRaw(newVal), this._rawValue)) {
      this._rawValue = newVal
      this._value = this._shallow ? newVal : convert(newVal)
      trigger(toRaw(this), TriggerOpTypes.SET, 'value', newVal)
    }
  }
}
function createRef(rawValue: unknown, shallow = false) {
  if (isRef(rawValue)) {
    return rawValue
  }
  return new RefImpl(rawValue, shallow)
}
以上是官方 ref 实现的源码,我们可以看到,当你使用ref传入一个对象作为参数时,其实使用的还是reactive。
除了创建响应式对象,ref 还可以创建对组件实例的引用。
<template>
  <div ref="refDiv">I'm Kirito</div>
</template>
<script lang="ts">
  import { defineComponent, onMounted, ref } from 'vue'
  export default defineComponent({
    setup() {
      // 变量名需要和 template 中的 ref 保持一致
      const refDiv = ref<HTMLElement>()
      // 等同于 vue2.x 的 mounted 选项 挂载之后的生命周期
      onMounted(() => {
        console.log(refDiv.value?.innerText) // I'm Kirito
      })
      return {
        refDiv
      }
    }
  })
</script>
isRef 
检查对象是否为 RefImpl对象,即是否被ref创建。
import { ref, reactive, isRef } from 'vue'
const count = ref(0)
const state = reactive({ name: 'kirito' })
const obj = { name: 'fangzhi' }
isRef(count) // true
isRef(state) // false
isRef(obj) // false
unref 
用来返回被 ref 包装的值。如果入参不是 RefImpl 对象,则直接返回入参。语法糖其实就是 val = isRef(val) ? val.value : val
toRef 
用来为一个响应式对象(reactive)的属性创建一个 ref,可以保持对这个响应式对象属性的响应式连接。
const state = reactive({
  foo: 1,
  bar: 2
})
const fooRef = toRef(state, 'foo')
fooRef.value++
console.log(state.foo) // 2
state.foo++
console.log(fooRef.value) // 3
一般在 需要将 props 传递给一个复合函数时, toRef 就显得很有用。
export default {
  setup(props) {
    useSomeHook(toRef(props, 'foo'))
  }
}