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