# 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。


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET 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?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
