前言
reactivity 是 Vue 的响应式核心模块,里面有两个重要的函数,reactive 和 effect。
-
reactive的作用是创建一个响应性对象。 -
effect的作用是收集数据所依赖的函数,并在数据发生变化时触发更新。
接下来讲一下这两个函数是如何实现依赖的收集和更新的。
以下的代码舍弃了源码中一些边界条件和特殊处理,只保留核心逻辑。
reactive
export function reactive(target) {
// 创建响应性对象
return createReactiveObject(target);
}
在 reactive 函数中,核心是调用了 createReactiveObject 函数来创建一个响应性对象.
接下来我们看一下 createReactiveObject 函数是如何实现的。
// 响应性对象缓存
const proxyMap = new WeakMap();
function createReactiveObject(target) {
// 如果不是对象,直接返回
if (!isObject(target)) {
return target;
}
// 判断这个对象是否是代理后的对象,如果是直接返回这个对象
// 这里定义了一个标记 __v_isReactive,用来标记是否是响应性对象
// 如果是响应性对象,就会到get方法里,返回一个 true
if (target[ReactiveFlags.IS_REACTIVE]) {
return target;
}
// 先从缓存中取,存在直接返回
const existProxy = proxyMap.get(target);
if (existProxy) {
return existProxy;
}
// 创建代理对象
let proxy = new Proxy(target, mutableHandlers);
proxyMap.set(target, proxy);
return proxy;
}
首先,我们在全局定义了一个 WeakMap 缓存来存储响应性对象。
在 createReactiveObject 函数中,首先判断 target 是否是对象,如果不是对象,直接返回。
然后判断 target 是否是响应性对象,这里通过读取 target 身上的一个特殊属性 __v_isReactive,然后在 get 方法里返回 true。也就是说,一个对象如果被代理过了,无论读取什么属性,这个对象身上有没有这个属性,都会走到 get 方法里面。
然后判断缓存里面有没有这个对象的代理对象,如果有直接拿来用,没有就创建一个代理对象,存到缓存 Map 里面。
最后,把这个代理对象返回。
mutableHandlers
在创建代理对象时,我们给 Proxy 传递了一个 mutableHandlers 对象,接下来我们看一下这个对象做了什么。
// 响应性对象处理器
export const mutableHandlers: ProxyHandler<any> = {
get(target, key, receiver) {
// 这里判断是否是响应性对象,如果是直接返回 true
if (key === ReactiveFlags.IS_REACTIVE) {
return true;
}
// 取值的时候依赖收集
track(target, key);
let res = Reflect.get(target, key, receiver);
// 如果取到的值是对象,则返回一个响应性对象, 递归代理
if (isObject(res)) {
return reactive(res);
}
return res;
},
set(target, key, value, receiver) {
let oldValue = target[key];
let result = Reflect.set(target, key, value, receiver);
if (oldValue !== value) {
// 触发依赖更新
trigger(target, key, value, oldValue);
}
return result;
},
};
mutableHandlers 是一个 ProxyHandler,用来处理响应性对象的 get 和 set 操作。
- get
get 方法里面首先通过拦截__v_isReactive属性返回 true,表示是响应性对象,直接返回,就不做处理了。
如果没有被拦截,说明不是响应性对象,那么就通过 track函数进行依赖收集。
如果读取的值依然是一个对象,那么就递归调用 reactive 函数,返回一个新的响应性对象。
- set
set 方法里面首先获取旧值,然后通过 Reflect.set 设置新值,如果新值和旧值不同,说明值发生了变化,那么就触发 trigger 函数进行依赖更新。
这里面的重点的是 track 和 trigger 函数。这两个函数等讲完 effect 函数再讲.
effect
export function effect(fn, options?) {
// 创建一个响应性函数,数据变化后可以重新执行
const _effect = new ReactiveEffect(fn, () => {
_effect.run();
});
// 先调用一次这个函数
_effect.run();
if (options) {
// 覆盖 _effect 的属性
Object.assign(_effect, options);
}
// 返回一个函数,可以手动触发更新
const runner = _effect.run.bind(_effect);
// 给 runner 添加一个 effect 属性,指向 _effect
runner.effect = _effect;
return runner;
}
effect 函数的核心作用是创建一个响应性函数对象 ReactiveEffect,这个对象身上有个 run 方法,调用这个方法可以执行传进来的 fn 函数。
在创建完对象后会先调用一次 run 方法,执行 fn 函数。
后面是一些自定义选项的处理和返回 run 函数,这离我们不过多关注,我们重点关注 ReactiveEffect 这个类。
ReactiveEffect
export class ReactiveEffect {
_trackId = 0; // 用来记录当前 effect 执行了几次
deps = []; // 依赖列表
_depsLength = 0; // deps列表的索引
_running = 0; // 是否正在运行 0表示没有运行
public active = true; // 是否激活
constructor(public fn, public scheduler) {}
// 运行函数 fn
run() {
if (!this.active) {
return this.fn();
}
// 保存上一个激活的 effect 主要为了解决嵌套 effect 的问题
let lastEffect = activeEffect;
try {
activeEffect = this;
// 每次执行函数前,将 _depsIndex 置为 0, _trackId 加 1
// 目的是后续进比较
this._depsLength = 0;
this._trackId++;
// 标记正在运行
this._running++;
return this.fn();
} finally {
// 标记不在运行
this._running--;
if (activeEffect.deps.length > activeEffect._depsLength) {
for (let i = activeEffect._depsLength; i < activeEffect.deps.length; i++) {
activeEffect.deps[i].delete(activeEffect); // 删除多余的 dep
if (activeEffect.deps[i].size === 0) {
activeEffect.deps[i].cleanup(); // 删除 key
}
}
activeEffect.deps.length = activeEffect._depsLength;
}
// 函数执行完毕后恢复上一个激活的 effect
activeEffect = lastEffect;
}
}
stop() {
this.active = false;
}
}
ReactiveEffect 类主要用来管理依赖。
首先,它有几个属性:
_trackId:用来记录当前fn函数执行了几次deps:依赖列表,用来记录当前fn函数依赖了哪些数据,在fn函数执行期间,每读取一个变量都会将该变量对应的dep加入到deps列表中_depsLength:deps列表的长度,后面会当做索引使用_running:fn函数是否正在运行,0 表示没有运行,1 表示正在运行active:fn函数是否激活,默认为true
此外,它会把构造函数的参数 fn 和 scheduler 保存到自己的属性上。
然后,它有两个方法:
run:运行fn函数。stop:停止fn函数的执行。
我们重点来看 run 方法。
在此之前,我们会在全局创建一个 activeEffect 变量,用来储存当前的 effect 对象。由于这个 activeEffect 变量是全局的,因此就可以被 track 函数进行收集。
run 方法的核心作用就是在执行 fn 函数之前,将当前的 effect 保存到 activeEffect 变量中,然后执行 fn 函数,执行完毕后,再恢复 activeEffect 变量。
只不过,在上面的代码中做了很多优化操作,比如:嵌套 effect 的问题、循环依赖、依赖发生变化后清理依赖等问题。
了解了核心机制,剩下的优化操作我们简单看一下就好,看不懂也没关系:
if (!this.active) {
return this.fn();
}
首先判断 fn 函数是否激活,如果不激活,就只运行一遍 fn 函数,不做任何处理。
// 保存上一个激活的 effect
let lastEffect = activeEffect;
try {
// 将当前的 effect 保存到 activeEffect 变量中
activeEffect = this;
// ...
//执行函数
return this.fn();
} finally {
// ...
// 函数执行完毕后恢复上一个激活的 effect
activeEffect = lastEffect;
}
为了解决嵌套 effect 的问题,会在给 activeEffect 变量赋值的时候,保存上一个激活的 effect 对象,然后在执行完 fn 函数后,恢复上一个激活的 effect 对象。这里其实利用了函数栈的机制,让每个函数运行期间保存自己的 effect 对象
try {
// ...
// 每次执行函数前,将 _depsIndex 置为 0, _trackId 加 1
this._depsLength = 0;
this._trackId++;
// 标记正在运行
this._running++;
return this.fn();
} finally {
// 标记不在运行
this._running--;
// ...
}
然后,在每次调用 fn 函数之前,会将 _depsLength 置为 0,_trackId 加 1。
这里的目的是为了后续进行比较 将多余的依赖清理掉,这里我们讲依赖收集的时候再说。
还有就是会在 fn 函数执行期间,将 _running 加 1,表示 fn 函数正在运行,然后在函数执行完毕后,将 _running 减 1。表示 fn 函数执行完毕。这里是为了避免在 effect 函数中修改变量触发递归执行。
// 判断 deps 是否有多余的 dep
if (activeEffect.deps.length > activeEffect._depsLength) {
for (let i = activeEffect._depsLength; i < activeEffect.deps.length; i++) {
activeEffect.deps[i].delete(activeEffect); // 删除多余的 dep
if (activeEffect.deps[i].size === 0) {
activeEffect.deps[i].cleanup(); // 删除 key
}
}
activeEffect.deps.length = activeEffect._depsLength;
}
最后,在执行完函数之后,如果依赖发生了变化,会将一些多余的 dep 进行清理,比如:deps 列表中多余的 dep 等。
这部分如果看不明白,先不着急,后面的 track 和 trigger 函数再讲。
track & trigger
在讲 track 和 trigger 函数之前,需要先在全局创建一个 targetMap 来存储依赖关系。
type TargetMap = WeakMap<any, Map<string | symbol, Map<ReactiveEffect, number>>>;
const targetMap: TargetMap = new WeakMap(); // 存放依赖的对象和 key 映射关系
大致的结构关系如下:
targetMap: WeakMap<Target, depsMap>;
depsMap: Map<Key, dep>;
dep: Map<ReactiveEffect, number>;
比如,我们有这样一段代码:
const state = reactive({ name: 'foo', count: 0 });
effect(() => {
console.log(state.name);
console.log(state.count);
});
那么生成的 targetMap 结构如下:
targetMap = {
state => {
'count' => {
ReactiveEffect => 1
},
'name' => {
ReactiveEffect => 1
}
}
}
了解了这个结构,再来看一下 track 和 trigger 函数。
track
export function track(target: TargetMap, key: string | symbol) {
// 如果当前没有激活的 effect,则不进行依赖收集
if (!activeEffect) {
return;
}
let depsMap = targetMap.get(target);
if (!depsMap) {
depsMap = new Map();
targetMap.set(target, depsMap);
}
let dep = depsMap.get(key);
if (!dep) {
dep = createDep(() => depsMap.delete(key), key);
depsMap.set(key, dep);
}
// 将当前的 effect 加入到 dep 里
trackEffects(activeEffect, dep);
}
首先判断 activeEffect 是否有值,如果没有,则不进行依赖收集。
先从 targetMap 中取出 target 对应的 depsMap,如果没有,则创建一个新的 depsMap。并把 depsMap 存入 targetMap 中。
再从 depsMap 中取出 key 对应的 dep,如果没有,则创建一个新的 dep。并把 dep 存入 depsMap 中。
需要注意的是,在创建 dep 的时候,封装了一个 createDep 函数,并传入一个 cleanup 函数,用来在 dep 被清理时,调用 cleanup 函数。 调用这个 cleanup 函数时,会从 depsMap 中删除 key 对应的 dep。
createDep 函数如下:
function createDep(cleanup: () => void, key: string | symbol) {
// 每个 key 身上 对应的 effect 用 map 来存储,早期是用的 set
let dep = new Map() as any;
// 给 dep 身上添加一个 删除 的方法
dep.cleanup = cleanup;
// 给 dep 身上添加一个 key 作为名字,作为一个标记,方便调试,可加可不加
dep.name = key;
return dep;
}
最后调用 trackEffects 函数,并传入 activeEffect 和 dep。
关键的依赖收集逻辑就在这个函数里。
function trackEffects(effect: ReactiveEffect, dep: Map<ReactiveEffect, number>) {
// 判断 dep 中是否已经有了当前的 effect, 有直接返回,不再收集
if (dep.get(effect) === effect._trackId) {
return;
}
// 将当前的 effect 对应的值 设置为 effect._trackId
dep.set(effect, effect._trackId);
// 依次从 effect的 deps 数组中取出对应的 dep 比较,如果不一致,则替换
let oldDep = effect.deps[effect._depsLength];
// 如果不一致
if (oldDep !== dep) {
if (oldDep) {
//旧的存在 就删除旧的
oldDep.delete(effect);
if (oldDep.size === 0) {
oldDep.cleanup(); // 删除 key
}
}
// 替换新的 dep
effect.deps[effect._depsLength] = dep;
// effect._depsLength 加 1
effect._depsLength++;
} else {
// effect._depsLength 加 1
effect._depsLength++;
}
}
首先判断 dep 中是否已经有了当前的 effect,如果有,说明在同一个 effect 函数执行期间再次读取了某个 key,则直接跳过,不再重复收集。
如果不存在,就将 effect 加入到 dep 中, 值为 effect._trackId。
这里其实已经完成了依赖的收集,但是,如果 effect 函数中的依赖发生了变化,需要进行一些额外的处理,清理一些多余的依赖,这里就要用到 effect 的 deps 数组。
let oldDep = effect.deps[effect._depsLength];
从 effect 的 deps 数组中取出对应的 dep,与当前的 dep 进行比较,如果不一致,则替换。
由于我们在执行 effect 函数时,会将 effect._depsLength 置为 0,因此,在触发 track 函数时,会从头开始取,依次进行比较。
if (oldDep !== dep) {
if (oldDep) {
//旧的存在 就删除旧的
oldDep.delete(effect);
if (oldDep.size === 0) {
oldDep.cleanup(); // 删除 key
}
}
// 替换新的 dep
effect.deps[effect._depsLength] = dep;
effect._depsLength++;
} else {
effect._depsLength++;
}
如果 oldDep 与 dep 相等,则说明 effect 函数中的依赖没有发生变化,则只需要将 effect._depsLength 加 1。
如果 oldDep 与 dep 不相等,有两种情况:
oldDep为undefined,说明是第一次收集依赖,不需要额外做处理。oldDep存在,则说明effect函数中的依赖发生了变化,这时需要将旧的dep中的effect删掉,如果删完为空,则调用cleanup函数,删掉整个key。
最后将新的 dep 加入到 effect.deps 数组中,并将 effect._depsLength 加 1。 方便下次比较。
以上就是 track 函数进行依赖收集的过程。
trigger
export function trigger(target: TargetMap, key: string | symbol, newValue, oldValue) {
const depsMap = targetMap.get(target); // 获取 对象对应的 Map
if (!depsMap) {
return;
}
const dep = depsMap.get(key); // 获取 key 对应的 Map
if (dep) {
// 如果有 dep 说明有依赖,则触发依赖更新
triggerEffects(dep);
}
}
trigger 函数就比较简单了。
先从 targetMap 中取出 target 对应的 depsMap。
再从 depsMap 中取出 key 对应的 dep。
执行 triggerEffects 函数,传入 dep。
export function triggerEffects(dep) {
for (const effect of dep.keys()) {
if (effect._running) {
return;
}
if (effect.scheduler) {
effect.scheduler();
}
}
}
triggerEffects 函数会遍历 dep 中的 effect,如果 effect 正在运行,则直接返回,不再触发。
如果 effect 未运行,则调用 scheduler 函数,scheduler 函数中会调用 effect 的 run 方法,触发更新。
至此,依赖收集和更新的过程就结束了。