Vue3源码II-响应式实现

Vue3响应式原理

1. Vue3响应式系统与Vue2响应式系统的区别

  • 底层实现 Vue 2 使用了 Object.defineProperty 来追踪属性的变化,并触发相应的更新。而 Vue 3 改用了 JavaScript 的 Proxy API 来实现响应式系统。Proxy API 提供了更强大和灵活的功能,使得 Vue 3 的响应式系统更高效和可扩展

  • 性能优化 Vue 3 在响应式系统方面进行了一些优化,提高了性能。Vue 3 使用了基于 Proxy 的跟踪机制,可以更精确地追踪属性的变化,避免了 Vue 2 中的一些性能瓶颈。此外,Vue 3 还引入了静态标记(Static Marking)树摇(Tree Shaking)等优化技术,减少了不必要的更新和渲染操作

  • 嵌套响应式 Vue 2 的响应式系统在嵌套对象或数组的情况下存在一些限制,需要通过 Vue.set 或 this.$set 进行手动触发更新。而 Vue 3 的响应式系统可以自动追踪嵌套对象和数组的变化,无需手动触发更新

  • Composition API Vue 3 引入了 Composition API,这是一个基于函数的 API 风格,可以更好地组织和重用组件逻辑。Composition API 提供了更灵活的响应式能力,使得开发者可以更自由地定义响应式数据和副作用

  • TypeScript 支持 Vue 3 对 TypeScript 的支持更加完善,提供了更准确的类型推断和类型检查。Vue 3 的响应式系统与 TypeScript 的类型系统更好地集成,使得在使用 TypeScript 开发时更加方便和可靠

2. 实现Vue响应式

Vue3中 reactivity 和 effect 基本用法

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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app"></div>
<script type="module">
// 单独引入响应式模块
import { reactive, effect} from '../../../node_modules/@vue/reactivity/dist/reactivity.esm-browser.js'
// 1) 创建一个响应式对象 reactive
const state = reactive({name:'gzy',age:18})

// 2) effect 所有的渲染都是基于他来实现的 computed watch 组件渲染
// 默认叫响应式effect,数据变化后会重新执行此函数
effect(()=>{
app.innerHTML = state.name + state.age
})
setTimeout(()=> {
state.name = 'ggg'
},1000)
// 3) 属性会收集effect (数据的依赖收集) 数据会记录自己在那个effect中使用了,稍后数据变化可以找到对应的effect执行

// 输出: 页面初始化会渲染 gzy18 1s后渲染为ggg18
</script>
</body>
</html>

可以得知Vue3的响应式 离不开 reactiveeffect 的互相配合,下面来一步步实现Vue3的响应式

reactivity源码实现

使用 ES6 的 Proxy API 来做一层代理

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
import { isObject } from "@vue/shared";

export function reactive(target){

// reactive 只能处理对象类型的数据,不是对象不处理
if(!isObject(target)) return target

const proxy = new Proxy(target,{
// target 指的是原对象
// key 指取那个属性
// receiver 指的是代理对象(这里的receiver就是proxy)
get(target,key,receiver){
// 我们在使用proxy的时候要搭配reflect来使用,用来解决this问题
// 取值的时候,让这个属性 和 effect产生关系
// return target[key]

return Reflect.get(target,key ,receiver) // 取值,并且让target中的this变为代理对象
},
set(target,key,value,receiver){
// 更新的数据
// 找到这个属性对应的effect让他执行

return Reflect.set(target,key,value,receiver)
}
})

return proxy
}

这里会有一个问题 为什么取值和设置值的时候不直接通过 return target[key] 而是要 Reflect.get(target,key ,receiver)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

let person = {
name: 'gzy',
get aliasName(){
return `**${this.name}**`
}
}

let proxyPerson = new Proxy(person, {
get(target, key , receiver){
return target[key]
}
})

proxyPerson.aliasName
  • proxyPerson.aliasName 来取值的时候,会走 proxyPerson 的 get 方法 , 其中 target 为 person对象,key为 person.aliasName 方法,这时候的 this是person ,返回的是 person.name

  • 为了name 的依赖收集,需要再获取aliasName的时候 ,也希望name属性也会触发get , 所以需要调用 Reflect.get(target,key ,receiver)

  • 而 receiver 指的是代理对象,所以会从代理对象上取name属性 ,这样就达到了我们的需求

为了代码的简介和复用, 新建 handlers.ts 文件并且把 proxy的对象参数提取 成为 mutableHandlers 方法

src/handler.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

export const mutableHandlers = {
// target 指的是原对象 key 指取那个属性 receiver 指的是代理对象(这里的receiver就是proxy)
get(target,key,receiver){
// 我们在使用proxy的时候要搭配reflect来使用,用来解决this问题
// 取值的时候,让这个属性 和 effect产生关系
// return target[key]
return Reflect.get(target,key ,receiver) // 取值,并且让target中的this变为代理对象
},
set(target,key,value,receiver){
// 更新的数据
// 找到这个属性对应的effect让他执行
return Reflect.set(target,key,value,receiver)
}
}

src/reactivity.ts

1
2
3
4
5
6
7
8
9
10
11
12
import { isObject } from "@vue/shared";
import { mutableHandlers } from "./handlers";

export function reactive(target){

// reactive 只能处理对象类型的数据,不是对象不处理
if(!isObject(target)) return target

const proxy = new Proxy(target,mutableHandlers)

return proxy
}

代理对象缓存

1
2
3
4
5
6
import { reactive } from './reactivity.js'
const obj = {name:'gzy',age:18}
const state1 = reactive(obj)
const state2 = reactive(obj)

console.log(state1 === state2)

当对同一个对象创建两次代理对象,应该只需要代理一次就行,使用WeakMap做一下缓存表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import { isObject } from "@vue/shared";
import { mutableHandlers } from "./handlers";

const reactiveMap = new WeakMap();

export function reactive(target){

// reactive 只能处理对象类型的数据,不是对象不处理
if(!isObject(target)) return target

// 缓存可以采用映射表 { {target} -> proxy }
let existingProxy = reactiveMap.get(target)

if(existingProxy) return existingProxy // 代理过直接返回

const proxy = new Proxy(target,mutableHandlers)

reactiveMap.set(target,proxy)

return proxy
}

处理对象循环代理

1
2
3
4
5
6
7
import { reactive } from './reactivity.js'
const obj = {name:'gzy',age:18}
const state1 = reactive(obj)
const state2 = reactive(state1)

console.log(state1 === state2)

代理过的对象继续代理 (已经被代理过的对象不能再代理了)

  • 在vue3.0的时候 会创建一个反向映射表 { 代理的结果 -> 原内容 }
  • 目前不用创建反向映射表, 用的方式是,如果这个对象被代理过了说明已经被proxy拦截过了(添加属性)

src/reactivity.ts

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
import { isObject } from "@vue/shared";
import { mutableHandlers } from "./handlers";

export const enum ReactiveFlags {
IS_REACTIVE = '_v_isReactive'
}

const reactiveMap = new WeakMap();

export function reactive(target){

if(!isObject(target)) return target

let existingProxy = reactiveMap.get(target)

if(existingProxy) return existingProxy

// 这里是关键
if(target[ReactiveFlags.IS_REACTIVE]){
return target
}

const proxy = new Proxy(target,mutableHandlers)

reactiveMap.set(target,proxy)

// 1) 在vue3.0的时候 会创建一个反向映射表 { 代理的结果 -> 原内容 }
// 2) 目前不用创建反向映射表, 用的方式是,如果这个对象被代理过了说明已经被proxy拦截过了
return proxy
}

src/handler.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { ReactiveFlags } from "./reactivity"

export const mutableHandlers = {
// target 指的是原对象 key 指取那个属性 receiver 指的是代理对象(这里的receiver就是proxy)
get(target,key,receiver){
// 我们在使用proxy的时候要搭配reflect来使用,用来解决this问题
// 取值的时候,让这个属性 和 effect产生关系

// 这里是关键
if(key === ReactiveFlags.IS_REACTIVE){
return true
}

return Reflect.get(target,key ,receiver) // 取值,并且让target中的this变为代理对象
},
set(target,key,value,receiver){
// 更新的数据
// 找到这个属性对应的effect让他执行

return Reflect.set(target,key,value,receiver)
}
}

effect源码实现

effect的基础用法

1
2
3
4
5
6
7
8
import { reactive, effect} from './reactivity.js'

const state = reactive({name:'gzy',age:18})

effect(()=>{
app.innerHTML = state.name + state.age + state.name
})

effect.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
export let activeEffect = undefined // 先把自己放到全局

export class ReactiveEffect {
// 默认会将fn挂载到类的实例上
constructor(private fn){ }

run(){
activeEffect = this
return this.fn()
}
}

export function effect(fn){
// 创建一个响应式effect,并且让effect执行
const _effect = new ReactiveEffect(fn)
_effect.run()
}
  • effect默认会执行一次
  • 执行的时候会先把当前的实例挂载到全局 activeEffect属性
  • 然后调用effect的函数执行,执行期间会调用响应式对象的get取值,这是在get里就能拿到 activeEffect属性值

多次调用effect

1
2
3
4
5
6
7
effect(()=>{
app.innerHTML = state.name
})
state.age // 不会收集依赖
effect(()=>{
app.innerHTML = state.age
})

需要每次执行完effect的方法后,把activeEffect赋值为null,这样不在effect中进行取值的话就不会收集依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

export let activeEffect = undefined // 先把自己放到全局

export class ReactiveEffect {
constructor(private fn){ }

run(){
try{
activeEffect = this
return this.fn()
}finally{
activeEffect = null
}
}
}

export function effect(fn){
// 创建一个响应式effect,并且让effect执行
const _effect = new ReactiveEffect(fn)
_effect.run()
}

effect嵌套

1
2
3
4
5
6
7
effect(()=>{
app.innerHTML = state.name
effect(()=>{
app.innerHTML = state.age
})
app.innerHTML = state.address
})
  • effect1执行,activeEffect = effect1, 这时候去取 name 收集 effect1
  • effect2执行 会把 activeEffect = effect2 ,这时 age 收集 effect2 ,执行完毕后会把全局的 activeEffect = null
  • 下一次去 state 取 address 就找activeEffect无法收集

原来的是 stack 方法解决,3.2版本后采用的树解决

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
export let activeEffect = undefined // 先把自己放到全局

export class ReactiveEffect {
constructor(private fn){ }
parent = undefined
run(){
try{
this.parent = activeEffect
activeEffect = this
return this.fn()
} finally {
activeEffect = this.parent
this.parent = undefined
}
}
}

export function effect(fn){
// 创建一个响应式effect,并且让effect执行
const _effect = new ReactiveEffect(fn)
_effect.run()
}

  • effect1执行 parent 为 undefined, activeEffect = effect1, 这时 name 收集 effect1
  • effect2执行 因为全局的 activeEffect = effect1,所以先执行 parent = effect1 , 再把全局的 activeEffect = effect2, 这时 age 收集 effect2 , 最后会把 全局的 activeEffect 设置为 effect1
  • 下一次去 state 取 address 收集 effect1

依赖收集(属性和effect关联起来)

src/handler.ts

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
import { activeEffect, track, trigger } from "./effect"
import { ReactiveFlags } from "./reactivity"

export const mutableHandlers = {
get(target,key,receiver){
if(key === ReactiveFlags.IS_REACTIVE){
return true
}

// 做依赖收集 记录属性和当前effect的关系
const res = Reflect.get(target,key ,receiver) // 先拿到新值
track(target,key)
return res // 取值,并且让target中的this变为代理对象
},
set(target,key,value,receiver){

let oldValue = target[key]

const r = Reflect.set(target,key,value,receiver)

if(oldValue !== value){
trigger(target,key,value,oldValue)
}

return r
}
}

构造依赖关系映射表

src/effect.ts

做一个映射表 来收集依赖关系

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
// 映射表结构应该是这样
// weakmap : map : set
// {name:'gzy',age:18}:'name' -> [effect,effect]
// {name:'gzy',age:18}:'age' -> [effect]

const targetMap = new WeakMap()
export function track(target,key){
// 让这个对象上的属性 记录当前的activeEffect
if(activeEffect){
// 说明用户是在effect中使用的这个数据
let depsMap = targetMap.get(target)

// 如果没有创建一个映射表
if(!depsMap){
targetMap.set(target,(depsMap = new Map))
}

// 如果有这个映射表来查一下有没有这个属性
let dep = depsMap.get(key)

// 如果没有set集合创建集合
if(!dep){
depsMap.set(key, (dep = new Set()))
}

// 如果有则看一下set中有没有这个effect
let shouldTrack = !dep.has(activeEffect)
if(shouldTrack){
dep.add(activeEffect)
activeEffect.deps.push(dep)
}
}
}

在浏览器打开应该是这样的

属性和effect映射表

属性改变时执行对应的effect

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app"></div>
<script type="module">
import { reactive, effect} from './reactivity.js'
const state = reactive({name:'gzy',age:18})
effect(()=>{
state.name = Math.random()
app.innerHTML = state.name + state.age + state.name
})

setTimeout(()=>{
state.name = 'ggggg'
},2000)
</script>
</body>
</html>

src/effect.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
export function trigger(target,key,newVal,oldVal){
// 通过对象找到对应的属性 让这个属性对应的effect重新执行
const depsMap = targetMap.get(target)
if(!depsMap) {
return
}

const dep = depsMap.get(key); // name 或者 age对应的所有 effect

dep && dep.forEach(effect => {
// 正在执行的effect , 不要多次执行
if(effect !== activeEffect) effect.run()
})
}

cleanupEffect

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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app"></div>
<script type="module">
import { reactive, effect} from './reactivity.js'

const state = reactive({name:'gzy',age:18,flag:true})

effect(()=>{
// flag 和 name 属性会触发收集
// 下一次应该清理掉flag和name属性,重新收集 flag 和 age 属性会触发收集
app.innerHTML = state.flag ? state.name : state.age
})

setTimeout(()=>{
state.flag = false
setTimeout(()=>{
// 等会改了name , 还是会触发effect
state.name = 'xxx'
},1000)
},2000)
</script>
</body>
</html>

在 ReactiveEffect 类上新增 deps = [],并且保存 new Set(effect)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// track
// ...
// 如果有这个映射表来查一下有没有这个属性
let dep = depsMap.get(key)

// 如果没有set集合创建集合
if(!dep){
depsMap.set(key, (dep = new Set()))
}

// 如果有则看一下set中有没有这个effect(去重)
let shouldTrack = !dep.has(activeEffect)
if(shouldTrack){
dep.add(activeEffect)

// name = new Set(effect)
// age = new Set(effect)

// 我可以通过当前的effect 找到这两个集合中的自己 将其移除掉就可以了
activeEffect.deps.push(dep)
}

在收集依赖之前清理上一次的依赖收集

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
export class ReactiveEffect {
// 默认会将fn挂载到类的实例上
constructor(private fn){ }
parent = undefined
deps = [] // 依赖了那些列表
run(){
try{
this.parent = activeEffect
activeEffect = this

cleanupEffect(this) // 清理了上一次的依赖收集

return this.fn() // fn执行会触发依赖收集
} finally {
activeEffect = this.parent
this.parent = undefined
}
}
stop(){

}
}
1
2
3
4
5
6
7
8
9

function cleanupEffect(effect){ // 在收集的列表中将自己移除掉
const { deps } = effect
for(let i = 0 ; i < deps.length;i++){
// 找到set ,让set移除掉自己
deps[i].delete(effect)
}
effect.deps.length = 0 // 清空依赖的列表
}

为了防止删除Set中的effect和往Set种新增effect造成死循环,对trigger方法进行修改

trigger

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
export function trigger(target,key,newVal,oldVal){
// 通过对象找到对应的属性 让这个属性对应的effect重新执行
const depsMap = targetMap.get(target)
if(!depsMap) {
return
}

const dep = depsMap.get(key); // name 或者 age对应的所有 effect

const effects = [...dep]
// 运行的是数组 删除的是set
effects &&
effects.forEach(effect => {
// 正在执行的effect , 不要多次执行
if(effect !== activeEffect) effect.run()
})