> For the complete documentation index, see [llms.txt](https://1425816423.gitbook.io/my-knowledge-base/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://1425816423.gitbook.io/my-knowledge-base/yuan-ma-jie-xi/vue-yuan-ma-jie-xi/vue3-yuan-ma-jie-xi-xi-lie-xiang-ying-xi-tong.md).

# Vue3源码解析系列-响应系统

## 响应式核心API

### Ref vs Reactive

从源码的角度来说，它们之间的区别是：

* **reactive使用Proxy进行数据劫持**
* **ref使用对象的getter和setter进行数据劫持。**

这一点可以看官方的解释：

> 在 JavaScript 中有两种劫持属性访问的方式：[**getter**](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/get)/\*\*[setters](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/set)\*\*和 [**Proxies**](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy)。Vue 2使用getter/setters 完全由于需支持更旧版本浏览器的限制。而在 Vue 3 中使用了 Proxy 来创建响应式对象，将 getter/setter 用于 ref。下面的伪代码将会说明它们是如何工作的：

```jsx
function reactive(obj) {
  return new Proxy(obj, {
    get(target, key) {
      track(target, key)
      return target[key]
    },
    set(target, key, value) {
      target[key] = value
      trigger(target, key)
    }
  })
}

function ref(value) {
  const refObject = {
    get value() {
      track(refObject, 'value')
      return value
    },
    set value(newValue) {
      value = newValue
      trigger(refObject, 'value')
    }
  }
  return refObject
}
```

两者收集依赖和通知依赖更新的方式都差不多，但是在实现数据劫持的方式上有所不同。

使用proxy替代vue2中的Object.defineProperty来实现数据劫持，有以下优势

* 不用提前在data中声明，不再需要$set等API
* 使用Hook的方式创建响应式数据，利于Typescript
* 支持map、set、weakmap和weakset
* 支持数组

但是Proxy只支持对象，如果变量是一个原始值那就难办了。vue3的解决方法是将原始值放到对象的value属性中，并设置getter和setter实现数据劫持，这就解决了原始值响应式的问题。并且ref不仅仅支持接收原始值，也可以传入一个对象。

> 如果将一个对象赋值给 ref，那么这个对象将通过 \*\*[reactive()](https://staging-cn.vuejs.org/api/reactivity-core.html#reactive)\*\*转为具有深层次响应式的对象。这也意味着如果对象中包含了嵌套的 ref，它们将被深层地解包。

在使用中也有区别，请看这段代码

```jsx
a.value = []
b = []
console.log(isRef(a))  // true
console.log(isReactive(b)) // false
```

ref类型的a仍然是响应式的，但是reactive类型的b却不再是响应式的。这是因为变量a的引用被改变了，变量a指向了一个新的对象，而不是之前的reactive对象。而ref改变的只是对象的一个属性，变量a仍然指向ref对象。

因此，当你清空一个列表的时候，你可以这样做：

```jsx
const list = ref([])
list.value.push(...data)
list.value = []  // 直接改变value的指向，丢弃原数据
```

### readonly

readonly类似于reactive，不同的是readonly返回的对象的所有属性都是只读的，并且这种只读代理是深层的。

```jsx
const c = readonly({a: 1})
console.log(c)
// Proxy{a: 1}
c.a++ // set operation on key "a" failed: target is readonly.
```

而readonly的实现方式也很简单，Proxy拦截set和delete操作，下面代码是readonly的`proxy handler`。

```jsx
export const readonlyHandlers: ProxyHandler<object> = {
  get: readonlyGet,
  set(target, key) {
    if (__DEV__) {
      warn(
        `Set operation on key "${String(key)}" failed: target is readonly.`,
        target
      )
    }
    return true
  },
  deleteProperty(target, key) {
    if (__DEV__) {
      warn(
        `Delete operation on key "${String(key)}" failed: target is readonly.`,
        target
      )
    }
    return true
  }
}
```

### watchEffect

watchEffect和watch很像，但是watch不需要手动制定监听的源对象，能够自动收集依赖，并且还会在初始化时执行一次。官网的介绍是：watchEffect会立即运行一个函数，同时响应式地追踪其依赖，并在依赖更改时重新执行。

```jsx
let a = ref(1)
const closeEffect = watchEffect(()=>{
  console.log(a.value)
})
// 1
a.value++
// 2
closeEffect() // 停止
a.value++
```

> 需要注意的是，**`watchEffect` 仅会在其同步执行期间，才追踪依赖。在使用异步回调时，只有在第一个 `await` 正常工作前访问到的 property 才会被追踪。**

watchEffect可以用这个函数来理解：

```jsx
function watchEffect(update) {
  const effect = () => {
    activeEffect = effect
    update()
    activeEffect = null
  }
  effect()
}
```

首先将一个全局变量`activeEffect` 指向`effect`副作用函数自身，然后执行用户传入的回调函数`update`，`update`执行的过程中会出发响应式变量的getter，getter函数会将`activeEffect`(也就是`effect`函数)收集作为变量的依赖，当该响应式变量更新时就会通知所有依赖更新，也就是重新执行`effect`函数。

通常情况下computed和watch已经够用了，watchEffect常见的场景是用于操作DOM。

```jsx
const count = ref(0)

watchEffect(() => {
  document.body.innerHTML = `计数：${count.value}`
})

// 更新 DOM
count.value++
```

默认情况下，watchEffect回调函数是在组件更新前调用，但是可以通过传递flush参数改为组件更新后执行。

```jsx
watch(source, callback, {
  flush: 'post'
})

watchEffect(callback, {
  flush: 'post'
})
```

## 响应式工具类API

### toRef和toRefs

前面说到reactive使用Proxy来实现响应式，但是这种响应式方式并非完美的，当你通过Proxy对象访问数据时，这会触发setter和getter，此时是响应的，但是当你把原始值属性传递到变量，通过变量去操作数据时，就会失去响应式。

```jsx
let o = reactive({num: 1})
o.num++  // 响应式
let num = o.num // 失去响应
num++ 
console.log(o.num)  // 2

// 如果是对象，那么仍然是响应式的
o = reactive({num: 1, obj:{n: 1}})
let obj = o.obj  // 仍然是响应式的
obj.n++
console.log(o.obj.n) // 2
```

这是因为当属性是原始值，赋值操作实际上是将值拷贝了一份保存给变量num，此时num和o.num已经没有关系了，但是如果属性是引用值(对象)时，那么变量obj实际上保存的是对象o.obj的引用，obj操作的仍然是o.obj对象。

toRef和toRefs可以解决这个问题，它为响应式对象上的属性创建 ref，并且这样创建的 ref 与其源属性保持同步。

```jsx
let o = reactive({num: 1, obj:{n: 1}})
o.num++  // 响应式
let num = toRef(o, 'num') 
num.value++ 
console.log(o.num) // 3
```

toRef用于“提取”单个属性，而toRefs则“提取”所有属性，这在使用结构赋值的时候很有用。

```jsx
let o = reactive({num: 1, obj:{n: 1}})
let {num, obj} = toRefs(o)
obj.n++
num.value++
```

### is系列API

vue3提供isReactive、isRef等api用来判断变量的响应式类型，其实背后的原理也很简单，vue在响应式变量中添加了一些标记，通过这些标记就可以很方便地判断出类型。

```jsx
export const enum ReactiveFlags {
  SKIP = '__v_skip',
  IS_REACTIVE = '__v_isReactive',
  IS_READONLY = '__v_isReadonly',
  IS_SHALLOW = '__v_isShallow',
  RAW = '__v_raw'
}
export function isReactive(value: unknown): boolean {
  if (isReadonly(value)) {
    return isReactive((value as Target)[ReactiveFlags.RAW])
  }
  return !!(value && (value as Target)[ReactiveFlags.IS_REACTIVE])
}
```

ref直接将标记作为属性保存，而proxy则是在getter中判断。

```tsx
export function isRef<T>(r: Ref<T> | unknown): r is Ref<T>
export function isRef(r: any): r is Ref {
  return !!(r && r.__v_isRef === true)
}

console.log(ref(1))

/*
dep: undefined
__v_isRef: true
__v_isShallow: false
_rawValue: 1
_value: 1
value: 1
*/
```

```jsx
function createGetter(isReadonly = false, shallow = false) {
  return function get(target: Target, key: string | symbol, receiver: object) {
    if (key === ReactiveFlags.IS_REACTIVE) {
      return !isReadonly
    } else if (key === ReactiveFlags.IS_READONLY) {
      return isReadonly
    } else if (key === ReactiveFlags.IS_SHALLOW) {
      return shallow
    } else if (
      key === ReactiveFlags.RAW &&
      receiver ===
        (isReadonly
          ? shallow
            ? shallowReadonlyMap
            : readonlyMap
          : shallow
          ? shallowReactiveMap
          : reactiveMap
        ).get(target)
    ) {
      return target
    }
	// ...
}
```

### shallow系列API

vue中的reactive、ref等API都是深度，vue提供一些对应的浅层形式的API，这些API有

* shallowReactive

以Reactive为例，它实现深层作用的方法是在getter中对对象类型的属性递归执行reactive函数。

```jsx
function createGetter(isReadonly = false, shallow = false) {
  return function get(target: Target, key: string | symbol, receiver: object) {
		// ...
		const res = Reflect.get(target, key, receiver)

    if (!isReadonly) {
      track(target, TrackOpTypes.GET, key)
    }

    if (shallow) {
      return res
    }

    if (isRef(res)) {
      // ref unwrapping - skip unwrap for Array + integer key.
      return targetIsArray && isIntegerKey(key) ? res : res.value
    }

    if (isObject(res)) {
      // Convert returned value into a proxy as well. we do the isObject check
      // here to avoid invalid value warning. Also need to lazy access readonly
      // and reactive here to avoid circular dependency.
      return isReadonly ? readonly(res) : reactive(res)
    }

    return res
  }
}
```

实现shallow的方式也很简单，如果是shallow模式则不对对象类型的属性调用reactive。
