【Vue3 响应式核心原理】:Proxy → track → ReactiveEffect → trigger

目录

  • 引言
  • 举个例子
  • [Proxy 是什么](#Proxy 是什么)
  • [ReactiveEffect 是什么](#ReactiveEffect 是什么)
  • [track 是什么(记录依赖)](#track 是什么(记录依赖))
    • [track 的作用](#track 的作用)
  • [trigger 是什么(触发更新)](#trigger 是什么(触发更新))
  • 完整流程图
  • [为什么 WeakMap → Map → Set](#为什么 WeakMap → Map → Set)
  • [computed 怎么利用这套机制](#computed 怎么利用这套机制)
  • [watchEffect 怎么利用这套机制](#watchEffect 怎么利用这套机制)
  • [watch 怎么利用这套机制](#watch 怎么利用这套机制)
  • Vue3响应式系统最核心的一张图

引言

Vue3 的响应式系统本质上只有一句话:

数据被读取时记录依赖(track),数据被修改时通知依赖(trigger)

整个流程:

javascript 复制代码
响应式对象
    │
    ▼
 Proxy
    │
    ├── get -> track()
    │
    └── set -> trigger()
                 │
                 ▼
          ReactiveEffect
                 │
                 ▼
             重新执行

举个例子

例如:

javascript 复制代码
const state = reactive({
  count: 0
})

watchEffect(() => {
  console.log(state.count)
})

state.count++

输出:

javascript 复制代码
0
1

为什么呢?Vue到底做了什么呢?

实际上:

javascript 复制代码
读取 count
    ↓
记录依赖

修改 count
    ↓
找到依赖

执行依赖

Proxy 是什么

vue2使用:Object.defineProperty()

vue3使用:proxy

例如:

javascript 复制代码
const obj = {
  count: 0
}

const proxy = new Proxy(obj, {
  get(target, key) {
    console.log("读取")
    return target[key]
  },

  set(target, key, value) {
    console.log("修改")
    target[key] = value
    return true
  }
})

读取 proxy.count,输出:读取

修改 proxy.count = 1, 输出:修改

javascript 复制代码
Proxy
   │
   ├── get
   │
   └── set

vue就是利用这两个拦截器!!!!!!

ReactiveEffect 是什么

看一个最简单例子:

javascript 复制代码
function effect() {
  console.log(state.count)
}

问题来了,vue怎么知道state.count变化时要重新执行 effect?

必须建立联系。

例如:

javascript 复制代码
count
  │
  ▼
effect

这个联系就是:依赖关系 (Dependency)

Vue内部:

javascript 复制代码
class ReactiveEffect {
  run() {
    fn()
  }
}

简化后:

javascript 复制代码
const effect = new ReactiveEffect(
	() => {
		console.log(state.count)
	}
)

本质:ReactiveEffect = 包装后的副作用函数

例如:

javascript 复制代码
watchEffect(() => {
  console.log(state.count)
})

内部其实类似:

javascript 复制代码
new ReactiveEffect(() => {
  console.log(state.count)
})

track 是什么(记录依赖)

例如:

javascript 复制代码
const state = reactive({
  count: 0
})

执行:

javascript 复制代码
watchEffect(() => {
  console.log(state.count)
})

内部:

javascript 复制代码
effect.run()

开始执行:

javascript 复制代码
console.log(state.count)

读取:

javascript 复制代码
state.count

触发:

javascript 复制代码
Proxy.get()

此时vue发现:当前正在执行 effect,于是执行:

javascript 复制代码
track(target, key)

即:

javascript 复制代码
track(state, "count")

track 的作用

记录谁依赖了这个属性

例如:

javascript 复制代码
state.count
      │
      ▼
	effect

那么vue就要保存:count -> effect的关系。

内部数据结构:WeakMap

javascript 复制代码
WeakMap
│
├── state对象
│
└── Map
      │
      ├── count
      │      │
      │      ▼
      │   Set(effect)
      │
      └── name
             │
             ▼
         Set(effect)

源码思想:

javascript 复制代码
targetMap = WeakMap<
    target,
    Map<
        key,
        Set<effect>
    >
>

例如:state.count 被effect使用,就会存储:

javascript 复制代码
WeakMap
 │
 ▼
state
 │
 ▼
count
 │
 ▼
effect

这一步就叫做:依赖收集/track

trigger 是什么(触发更新)

state.count++发生了什么?

执行:

javascript 复制代码
Proxy.set()

触发:

javascript 复制代码
trigger(state, "count")

vue去依赖表查找:

javascript 复制代码
state
  │
  ▼
count
  │
  ▼
Set(effect)

找到effect,然后执行effect.run()

重新执行:

javascript 复制代码
console.log(state.count)

于是输出1

这一步就叫 触发更新,也就是trigger

完整流程图

javascript 复制代码
watchEffect(() => {
    console.log(state.count)
})
javascript 复制代码
effect.run()
     │
     ▼
读取 count
     │
     ▼
Proxy.get
     │
     ▼
track()
     │
     ▼
记录:
count -> effect

修改:

javascript 复制代码
state.count++
javascript 复制代码
Proxy.set
     │
     ▼
trigger()
     │
     ▼
找到:
count -> effect
     │
     ▼
effect.run()
     │
     ▼
重新执行

为什么 WeakMap → Map → Set

javascript 复制代码
targetMap = WeakMap<
    target,
    Map<
        key,
        Set<effect>
    >
>
javascript 复制代码
WeakMap
│
├── state对象
│
└── Map
      │
      ├── count
      │      │
      │      ▼
      │   Set(effect)
      │
      └── name
             │
             ▼
         Set(effect)

第一层 WeakMap

保存对象:

javascript 复制代码
const obj1 = {}
const obj2 = {}

需要:

javascript 复制代码
obj1
 └── count

obj2
 └── name

所以WeakMap,key:target对象

第二层Map

保存属性:

javascript 复制代码
state.count
state.name

需要区分name,所以Map

第三层Set

保存多个 Effect

javascript 复制代码
watchEffect(() => {
  console.log(state.count)
})

watchEffect(() => {
  document.title = state.count
})

两个 effect,结构:

javascript 复制代码
count
  │
  ▼
Set
 ├── effect1
 └── effect2

所以是set,防止重复

computed 怎么利用这套机制

javascript 复制代码
const double = computed(() => {
  return count.value * 2
})

内部也是:

javascript 复制代码
new ReactiveEffect(...)

依赖:

javascript 复制代码
count
   │
   ▼
computedEffect

count变化:

javascript 复制代码
trigger()
    │
    ▼
computedEffect

重新计算。

所以说,computedwatchwatchEffect 底层全部都是ReactiveEffect

watchEffect 怎么利用这套机制

javascript 复制代码
watchEffect(() => {
  console.log(count.value)
})

第一次:

javascript 复制代码
effect.run()
    │
    ▼
读取 count
    │
    ▼
track()

记录:count -> effect

修改:

javascript 复制代码
count.value++

执行:

javascript 复制代码
trigger()
   │
   ▼
effect.run()

重新执行。

watch 怎么利用这套机制

watch的会更高级一点

javascript 复制代码
watch(
  () => state.count,
  (newVal, oldVal) => {}
)

内部也会创建ReactiveEffect

监听:

javascript 复制代码
() => state.count

读取:

javascript 复制代码
state.count
   │
   ▼
 track()

建立:

javascript 复制代码
count -> effect

变化:

javascript 复制代码
trigger()

执行scheduler -> 执行callback

Vue3响应式系统最核心的一张图

javascript 复制代码
               reactive()
                   │
                   ▼
                 Proxy
            ┌────────────┐
            │            │
            ▼            ▼
         get()        set()
            │            │
            ▼            ▼
        track()      trigger()
            │            │
            ▼            ▼
      收集依赖       找到依赖
            │            │
            ▼            ▼
      	  ReactiveEffect.run()
       		     │
           		 ▼
     		   执行副作用

着重看:

javascript 复制代码
packages/reactivity/src/
│
├── reactive.ts
├── baseHandlers.ts
├── effect.ts
├── dep.ts
├── computed.ts
├── watch.ts

理解顺序

javascript 复制代码
Proxy
  ↓
track
  ↓
trigger
  ↓
ReactiveEffect
  ↓
computed
  ↓
watchEffect
  ↓
watch

Vue3 的响应式系统本质上就是一个"发布-订阅系统"的高级实现。

javascript 复制代码
track() = 订阅

trigger() = 发布

ReactiveEffect = 订阅者

Proxy = 负责监听数据访问和修改