vue3源码阅读与实现: 响应式系统-reactive模块
reactive模块
模块总览
vue
中reactive
函数可以创建复杂数据类型的响应式数据,在vue
官网中提到reactive
有以下几个短板
- 有限的值类型:
reactive
只能处理复杂数据类型的响应式 - 不能替换整个响应式对象: 使用
reactive
创建一个响应式数据之后,不能对这个响应式数据重新赋值,即使赋值的是与第一次相同的数据,也会丢失第一次创建的响应式连接 - 解构: 由
reactive
创建的响应式对象,解构后,数据会丢失响应性
让我们带着问题,查看vue
到底是如何实现的reactive
函数
debugger
通过debugger
查看reactive
模块执行过程,使用下面的测试用例,用例有三个关注点:
- 创建reactive响应式数据
- 访问响应式数据
- 设置响应式数据
debugger
时重点关注vue
在这三个关键点做了什么事情
测试用例:
//在packages/vue/examples/test/reactive.html
<html>
<head>
...
<script src="../../dist/vue.global.js"></script>
</head>
<body>
<div id="app"></div>
<script>
const { reactive, effect } = Vue
debugger
const data = reactive({ name: '哈哈哈' }) // 创建reactive响应式数据
debugger
effect(() => {
document.querySelector('#app').innerHTML = data.name // 访问响应式数据
})
setTimeout(() => {
debugger
data.name = '嘿嘿' // 设置响应式数据
}, 2000)
</script>
</body>
</html>
-
npm run build
对源码进行打包 -
打开测试用例并
open with live server
运行,进入debugger
-
点击1进入下一个
debugger
点击2:
setp next over function
点击3:
setp next into function
点击4:
setp out of current function
在次强调一遍调试时,只关注核心,没有进入的条件判断分支无需关注
关注点1:创建响应式数据
点击3进入reactive
函数,查看创建响应式数据的逻辑
-
reactive
函数中,返回了createReactiveObject()
的返回值
-
进入
createReactiveObject()
,最后返回了一个新创建的proxy
实例,并将这个实例以target
为键,proxy
实例为值保存在reactiveMap
中,与target
形成映射
proxy
实例的配置由baseHandlers
提供,至于baseHandlers
中get
和set
的逻辑,之后触发的时候再去看
总结
reactive
函数做了两件事情:
- 创建
proxy
实例 - 保存
proxy
实例到reactiveMap
关注点2:依赖收集
点击按钮1进入下一个debugger
,在这里使用effect(fn)
函数并在回调中触发name
属性的get
-
进入
effect(fn)
函数,该函数中创建了ReactiveEffect
实例_effect
,
-
在
ReactiveEffect
类中fn
属性保存了传递给构造函数的参数fn
,除了fn
属性外,还有个run
函数,至于这个run
函数做了什么,暂时不去考虑
-
实例创建完成后,调用了
_effect
的run
方法 -
进入
run
方法,将当前实例保存在了全局变量activeEffect
中,这个全局变量保存的就是当前key
所谓的依赖.保存后最后调用了fn
函数,也就是effect(fn)
函数的回调fn
,由于这个函数中我们访问了name
属性,所以会触发响应式数据的get
,然后进入get
的逻辑
-
get
逻辑在createGetter
函数中,在这个函数中:-
通过
const res = Reflect.get(target, key, receiver)
拿到属性的值 -
然后进入
track(target, TrackOpTypes.GET, key)
函数,这个函数是重点他是专门做依赖收集的
-
-
在
track
函数中,首先获取到全局变量activeEffect
的值,这个变量在执行effect(fn)
函数的时候已经赋值为ReactiveEffect
实例了,所以这里拿到的就是刚刚创建的ReactiveEffect
实例 -
接下来就从
targetMap
中取出target
对应的依赖,是一个Map对象,如果没有就创建一个新的Map,然后将key和activeEffect保存在map中,其中targetMap保存的就是所有数据的所有依赖,其数据结构伪代码是这样的:targetMap: weakMap{ target1: map{ key1:set[ReactiveEffect实例1,ReactiveEffect实例2....] key2:set[] }, target2: map{ key1:set[] key2:set[] }, } 这里的target就是测试用例中的: {name:'哈哈哈'} target中的key1就是name key1对应的set中存放的是每一个ReactiveEffect实例, ReactiveEffect实例有一个fn属性,存放的就是effect(fn)函数的fn
有了这个数据结构,就建立起了每个
key
和依赖的关系,当key
的值变化时,根据这个结构,来触发对应的依赖,对应的源码为:
- 最后返回了第五步
res
,此时页面上应该能显示出name
的值哈哈哈
这样就完成了依赖收集的过程
总结
effect
函数做了1件事情
- 根据依赖创建
ReactiveEffect
实例,并保存在全局变量activeEffect
中
get
函数做了2件事情
- 获取
target
中的值并返回 - 通过
activeEffect
变量获取当前key
对应的依赖,将依赖存放在targetMap
中
关注点3:依赖触发
点击按钮1进入最后一个debugger
;在这里我对name
属性重新赋值了一下,所以会触发响应式数据的set
-
进入
createSetter()
函数,执行set
对应的逻辑,首先获取该属性的旧值
-
通过
const result = Reflect.set(target, key, value, receiver)
设置了新值到target
-
然后根据
hasChanged
函数,判断该属性的值是否发生了变化,如果变化需要触发依赖,然后进入依赖触发函数trigger(target, TriggerOpTypes.SET, key, value, oldValue)
,这个函数也是重点
-
在
trigger
中,首先从targetMap.get(target)
拿到target
对应的所有依赖depsMap
,在根据depsMap
和key
拿到key
所对应的所有依赖dep
,最后通过triggerEffects
,触发dep
中所有ReactiveEffect
实例的run
函数,实际上触发的就是依赖收集时保存的fn
函数
总结
set
函数做了两件事情:
- 为属性设置新的值
- 判断属性的值是否发生了变化,如果变化,则进行依赖触发操作: 获取
targetMap
中收集对应依赖,然后执行依赖实例的run
函数
总结
整个流程已经走完了,对其中的一些重要点进行总结一下:
重要的全局变量/类:
reactiveMap
:存放target
和其proxy
实例的映射targetMap
: 存放所有依赖activeEffect
:存放当前触发getter
的key
对应的依赖ReactiveEffect
: 创建effect
实例,实例在fn
属性中保存了依赖函数
,每当执行实例的run
属性时,都会先赋值activeEffect为this
,然后执行fn
触发依赖收集,这个类非常重要,可以简单记为创建出来的effect
实例就是依赖,响应式数据收集依赖时就是收集的这个实例
整体流程:
reactive
函数通过proxy
实现每个key
对应的get
行为和set
行为监听,在触发某个key
的getter
时收集依赖存储在targetMap
中,在触发某个key
的setter
时触发targetMap
中存储的对应依赖- 全局变量
activeEffect
是响应式数据和依赖产生联系的一个桥梁:effect
函数创建依赖实例ReactiveEffect
,并保存在全局变量activeEffect
中,由于getter
行为是在effect
函数的回调触发的,所以effect
函数总是在getter
触发前先执行,先给activeEffect
赋值,因此,getter
触发时,activeEffect
存放的一定式是触发这次getter
的key
的依赖.这样getter
中收集依赖时才能保证:key
与依赖实例准确对应起来
实现reactive模块
reactive函数
主要用来创建proxy
实例,
在packages/reactivity/src/reactive.ts
,
import { isObject } from "@vue/shared";
import { mutableHandlers } from "./basehandlers";
// 缓存target对应的proxyObj
const reactiveMap: WeakMap<object, any> = new WeakMap();
export function reactive(target: object) {
return createReactiveObject(target, mutableHandlers, reactiveMap);
}
// 根据target生成proxy实例,并构建两者的缓存映射
function createReactiveObject(
target: object,
mutableHandlers: ProxyHandler<object>,
proxyMap: WeakMap<object, any>
) {
let existingProxy = proxyMap.get(target);
if (existingProxy) {
return existingProxy;
}
existingProxy = new Proxy(target, mutableHandlers);
// 构建target和proxy的映射
proxyMap.set(target, existingProxy);
return existingProxy;
}
basehandlers模块
在这里创建了get
处理函数和set
处理函数,在get
中进行依赖收集,在set
中进行依赖触发
在packages/reactivity/src/basehandlers.ts
import { track, trigger } from "./effect";
const get = createGetter();
const set = createSetter();
export const mutableHandlers: ProxyHandler<object> = {
get,
set,
};
function createGetter() {
return function (target: object, key: string | symbol, receiver: object) {
const res = Reflect.get(target, key, receiver);
// 依赖收集
track(target, key);
return res;
};
}
function createSetter() {
return function (
target: object,
key: string | symbol,
value: any,
receiver: object
) {
Reflect.set(target, key, value, receiver);
// 触发依赖
trigger(target, key, value);
return true;
};
}
effect模块
这里主要做:
- 创建全局变量
targetMap
保存所有依赖, - 创建全局变量
activeEffect
保存触发当前key
的依赖 - 创建
ReactiveEffect
类,用来生成reactiveEffect
实例 - 进行依赖收集
track
- 进行依赖触发
trigger
在packages/reactivity/src/effect.ts
import { isArray } from "@vue/shared";
import { createDep, Dep } from "./deps";
type KeyToMap = Map<any, Set<ReactiveEffect>>;
// 存放每个target和其对应的所有依赖
let targetMap = new WeakMap<any, KeyToMap>();
// 存放当前收集到的ReactveEffect
export let activeEffect: ReactiveEffect | undefined;
/**
* @message: 依赖收集,存储target每个属性对应的依赖
*/
export function track(target: object, key: string | symbol) {
// 如果没有依赖对象,则不用收集
if (!activeEffect) {
return;
}
let depsMap = targetMap.get(target);
if (!depsMap) {
// depsMap不存在,说明是第一次收集到依赖,创建一个新的来存储key对应的依赖对象
depsMap = new Map();
targetMap.set(target, depsMap);
}
// 多依赖处理
let dep = depsMap.get(key);
if (!dep) {
dep = createDep();
depsMap.set(key, dep);
}
trackEffets(dep);
}
/**
* @message: 依赖触发,触发track收集的所有依赖
*/
export function trigger(target: object, key: string | symbol, value: any) {
const depsMap = targetMap.get(target); // 获取target对应的所有依赖
if (!depsMap) {
return;
}
const dep = depsMap.get(key); // 根据setter修改的key,找到key对应的所有依赖,并触发
if (!dep) {
return;
}
triggerEffects(dep);
}
/**
* @message: 将依赖创建为依赖对象,并保存到全局变量activeEffect
*/
export function effect<T = any>(fn: () => T) {
const _effect = new ReactiveEffect(fn);
_effect.run(); // 调用run 保存这个实例到activeEffect,并执行依赖
}
// 包含依赖实例
export class ReactiveEffect<T = any> {
public fn: () => T;
constructor(fn: () => T) {
this.fn = fn; // 将依赖函数保存在fn中,这样每个实例都能保存对应的依赖,保存这个实例,就可以拿到依赖函数了
}
run() {
const preCache = activeEffect
activeEffect = this; // 指定当前处理的依赖,以便收集的时候获取
this.fn();
activeEffect = preCache // 依赖收集完毕,将activeEffect还原
}
}
/**
* @message: 添加依赖到dep中
*/
export function trackEffets(dep: Dep) {
dep.add(activeEffect!);
}
/**
* @message: 触发所有的依赖
*/
export function triggerEffects(dep: Dep) {
const effects = isArray(dep) ? dep : [...dep];
for (const effect of effects) {
triggerEffect(effect);
}
}
/**
* @message: 触发指定依赖
*/
export function triggerEffect(effect: ReactiveEffect) {
effect.run();
}
deps模块
主要用来生成一个存放依赖的Set
在packages/reactivity/src/deps.ts
import { ReactiveEffect } from "./effect";
export type Dep = Set<ReactiveEffect>;
/**
* @message:依据effects生成dep实例,保存key对应的所有依赖
*/
export function createDep(effets?: ReactiveEffect[]): Dep {
const dep = new Set<ReactiveEffect>(effets);
return dep;
}
到此简易版的reactive
模块就实现好了,可以在vue
下创建之前debugger
的测试用例来测试一下.
总结
回到本节刚开始的三个问题:
- 有限的值类型:
reactive
函数通过proxy
来创建响应式数据,proxy
只能代理对象,因此reactive
无法处理简单数据类型, - 不能替换整个响应式对象: 直接替换响应式对象将丢失之前创建的
proxy
实例的引用 - 解构: 只有对
proxy
代理对象的get,set
操作才能被监听到,因此,将proxy
对象解构后,对解构数据进行get,set
无法触发proxy
代理对象的get,set
.那么数据自然就丢失响应性了
转载自:https://juejin.cn/post/7395473411651256359