第二篇:响应式系统
第四章:响应式系统的作用与实现
Vue3 采用 Proxy 来实现响应式数据的代理。副作用函数指的就是会产生副作用的函数
reactive.ts
import { isObject } from "@vue/shared";
import { mutableHandlers } from "./baseHandler";
export const enum ReactiveFlags {
IS_REACTIVE = "__v_isReactive",
}
// 映射表 -> 源对象:代理对象
const reactiveMap = new WeakMap();
export function reactive(target) {
// 不是对象类型的变量
if (!isObject(target)) return;
// 如果源对象就是代理对象就直接返回(原因是访问代理对象的属性会走到Get方法,所以通过一个标识来判定)
if (target[ReactiveFlags.IS_REACTIVE]) return target;
// 如果target对象已经被代理过了就返回代理对象
const existsProxy = reactiveMap.get(target);
if (existsProxy) return existsProxy;
const proxy = new Proxy(target, mutableHandlers);
// 缓存一下 代理过的对象 下次在进行代理的时候直接拿出来即可
reactiveMap.set(target, proxy);
return proxy;
}
import { isObject } from "@vue/shared";
import { mutableHandlers } from "./baseHandler";
export const enum ReactiveFlags {
IS_REACTIVE = "__v_isReactive",
}
// 映射表 -> 源对象:代理对象
const reactiveMap = new WeakMap();
export function reactive(target) {
// 不是对象类型的变量
if (!isObject(target)) return;
// 如果源对象就是代理对象就直接返回(原因是访问代理对象的属性会走到Get方法,所以通过一个标识来判定)
if (target[ReactiveFlags.IS_REACTIVE]) return target;
// 如果target对象已经被代理过了就返回代理对象
const existsProxy = reactiveMap.get(target);
if (existsProxy) return existsProxy;
const proxy = new Proxy(target, mutableHandlers);
// 缓存一下 代理过的对象 下次在进行代理的时候直接拿出来即可
reactiveMap.set(target, proxy);
return proxy;
}
baseHandler.ts
import { isObject } from "@vue/shared";
import { track, trigger } from "./effect";
import { reactive, ReactiveFlags } from "./reactive";
// target:源目标对象, key:访问的属性, value:设置的新值, receiver:当前的代理对象
export const mutableHandlers = {
get(target, key, receiver) {
// 代表target已经是代理对象了
if (key === ReactiveFlags.IS_REACTIVE) return true;
// 依赖收集
track(target, key);
// Reflect处理了this问题
let r = Reflect.get(target, key, receiver);
// 处理深度代理情况(只用用户取值的时候才会二次代理,不用担心性能)
if (isObject(r)) {
return reactive(r);
}
return r;
},
set(target, key, value, receiver) {
let oldValue = target[key];
// Reflect处理了this问题
let r = Reflect.set(target, key, value, receiver);
if (oldValue !== value) {
// 派发更新
trigger(target, key, value, oldValue);
}
return r;
},
};
import { isObject } from "@vue/shared";
import { track, trigger } from "./effect";
import { reactive, ReactiveFlags } from "./reactive";
// target:源目标对象, key:访问的属性, value:设置的新值, receiver:当前的代理对象
export const mutableHandlers = {
get(target, key, receiver) {
// 代表target已经是代理对象了
if (key === ReactiveFlags.IS_REACTIVE) return true;
// 依赖收集
track(target, key);
// Reflect处理了this问题
let r = Reflect.get(target, key, receiver);
// 处理深度代理情况(只用用户取值的时候才会二次代理,不用担心性能)
if (isObject(r)) {
return reactive(r);
}
return r;
},
set(target, key, value, receiver) {
let oldValue = target[key];
// Reflect处理了this问题
let r = Reflect.set(target, key, value, receiver);
if (oldValue !== value) {
// 派发更新
trigger(target, key, value, oldValue);
}
return r;
},
};
effect.ts
export let activeEffect; // 当前的副作用函数 ReactiveEffect
// 每次执行依赖收集之前执行依赖清理操作
function cleanupEffect(effect) {
let { deps } = effect;
for (let i = 0; i < deps.length; i++) {
deps[i].delete(effect);
}
effect.deps.length = 0;
}
export class ReactiveEffect {
public active = true;
public deps = []; // 记录该副作用函数依赖哪一些依赖项
public parent = null; // 处理副作用函数嵌套问题
// scheduler 调度执行,我们可以自己决定副作用函数执行的时机、次数、及执行方式
constructor(public fn, private scheduler) {}
run() {
// 如果是非激活状态只要执行一次函数即可
if (!this.active) {
return this.fn();
}
try {
// 其他情况下 意思着是激活的状态
this.parent = activeEffect;
// 记录当前的副作用函数
activeEffect = this;
// 每次执行effect之前 应该清理一遍effect中依赖的所有属性
cleanupEffect(this);
return this.fn();
} finally {
activeEffect = this.parent;
this.parent = undefined;
}
}
stop() {
if (this.active) {
cleanupEffect(this); // 先将effect清除掉
this.active = false; // 在将其变成失活状态
}
}
}
export function effect(fn, options: any = () => {}) {
const _effect = new ReactiveEffect(fn, options.scheduler);
_effect.run();
const runner = _effect.run.bind(_effect); // 保证 _effect执行的时候this是当前的effect
runner.effect = _effect;
return runner;
}
// WeakMap: { Map: Set }
const targetMap = new WeakMap();
/**
* 依赖收集
* @param target
* @param key
* @returns
*/
export function track(target, key) {
// 取值操作没有发生在effect中
if (!activeEffect) return;
let depsMap = targetMap.get(target);
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()));
}
let dep = depsMap.get(key);
if (!dep) {
depsMap.set(key, (dep = new Set()));
}
trackEffects(dep);
}
export function trackEffects(dep) {
let shouldTrack = !dep.has(activeEffect);
// 一个属性对应多个effect,一个effect对应着多种属性
// 属性和effect的关系是多对对的关系
if (shouldTrack) {
dep.add(activeEffect);
activeEffect.deps.push(dep); // 后续需要通过effect来清理的时候可以去使用
}
}
/**
* 派发更新
* @param target
* @param key
* @param newValue
* @param oldValue
*/
export function trigger(target, key, newValue, oldValue) {
const depsMap = targetMap.get(target);
if (!depsMap) return;
const dep = depsMap.get(key);
if (dep) {
triggerEffects(dep);
}
}
export function triggerEffects(dep) {
const effects = [...dep];
effects.forEach((effect) => {
if (activeEffect !== effect) {
if (!effect.scheduler) {
effect.run(); // 每次调run方法都会重新依赖收集
} else {
effect.scheduler();
}
}
});
}
export let activeEffect; // 当前的副作用函数 ReactiveEffect
// 每次执行依赖收集之前执行依赖清理操作
function cleanupEffect(effect) {
let { deps } = effect;
for (let i = 0; i < deps.length; i++) {
deps[i].delete(effect);
}
effect.deps.length = 0;
}
export class ReactiveEffect {
public active = true;
public deps = []; // 记录该副作用函数依赖哪一些依赖项
public parent = null; // 处理副作用函数嵌套问题
// scheduler 调度执行,我们可以自己决定副作用函数执行的时机、次数、及执行方式
constructor(public fn, private scheduler) {}
run() {
// 如果是非激活状态只要执行一次函数即可
if (!this.active) {
return this.fn();
}
try {
// 其他情况下 意思着是激活的状态
this.parent = activeEffect;
// 记录当前的副作用函数
activeEffect = this;
// 每次执行effect之前 应该清理一遍effect中依赖的所有属性
cleanupEffect(this);
return this.fn();
} finally {
activeEffect = this.parent;
this.parent = undefined;
}
}
stop() {
if (this.active) {
cleanupEffect(this); // 先将effect清除掉
this.active = false; // 在将其变成失活状态
}
}
}
export function effect(fn, options: any = () => {}) {
const _effect = new ReactiveEffect(fn, options.scheduler);
_effect.run();
const runner = _effect.run.bind(_effect); // 保证 _effect执行的时候this是当前的effect
runner.effect = _effect;
return runner;
}
// WeakMap: { Map: Set }
const targetMap = new WeakMap();
/**
* 依赖收集
* @param target
* @param key
* @returns
*/
export function track(target, key) {
// 取值操作没有发生在effect中
if (!activeEffect) return;
let depsMap = targetMap.get(target);
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()));
}
let dep = depsMap.get(key);
if (!dep) {
depsMap.set(key, (dep = new Set()));
}
trackEffects(dep);
}
export function trackEffects(dep) {
let shouldTrack = !dep.has(activeEffect);
// 一个属性对应多个effect,一个effect对应着多种属性
// 属性和effect的关系是多对对的关系
if (shouldTrack) {
dep.add(activeEffect);
activeEffect.deps.push(dep); // 后续需要通过effect来清理的时候可以去使用
}
}
/**
* 派发更新
* @param target
* @param key
* @param newValue
* @param oldValue
*/
export function trigger(target, key, newValue, oldValue) {
const depsMap = targetMap.get(target);
if (!depsMap) return;
const dep = depsMap.get(key);
if (dep) {
triggerEffects(dep);
}
}
export function triggerEffects(dep) {
const effects = [...dep];
effects.forEach((effect) => {
if (activeEffect !== effect) {
if (!effect.scheduler) {
effect.run(); // 每次调run方法都会重新依赖收集
} else {
effect.scheduler();
}
}
});
}
4.4 分支切换与 cleanup
如果在副作用函数内部出现三元表达式,会根据条件判断执行不同的代码分支。当条件判断依赖于响应式数据时,值的变化执行的表达式也会不同。这就是分支切换
而分支切换可能会遗留之前的副作用函数,而当前并不依赖,所以要在每次执行副作用函数之前清除当前副作用函数所依赖的副作用函数,在执行副作用函数重新进行依赖收集
4.5 嵌套的 effect 与 effect 栈
副作用函数是可以被嵌套的,而内部的副作用函数应该只收集自身内部的响应式变量属性,相互并不影响
可以通过给 ReactiveEffect 添加一个属性 parent(默认是 undefined
) 来记录自己的父级副作用函数
而在执行完当前的副作用函数将自己的父级返回给activeEffect
export class ReactiveEffect {
public active = true;
public deps = []; // 记录该副作用函数依赖哪一些依赖项
public parent = null; // 处理副作用函数嵌套问题
constructor(public fn, private scheduler) {}
run() {
// 如果是非激活状态只要执行一次函数即可
if (!this.active) {
return this.fn();
}
try {
// 其他情况下 意思着是激活的状态
this.parent = activeEffect;
// 记录当前的副作用函数
activeEffect = this;
// 每次执行effect之前 应该清理一遍effect中依赖的所有属性
cleanupEffect(this);
return this.fn();
} finally {
// 将自己的父级赋值给activeEffect
activeEffect = this.parent;
this.parent = undefined;
}
}
}
export class ReactiveEffect {
public active = true;
public deps = []; // 记录该副作用函数依赖哪一些依赖项
public parent = null; // 处理副作用函数嵌套问题
constructor(public fn, private scheduler) {}
run() {
// 如果是非激活状态只要执行一次函数即可
if (!this.active) {
return this.fn();
}
try {
// 其他情况下 意思着是激活的状态
this.parent = activeEffect;
// 记录当前的副作用函数
activeEffect = this;
// 每次执行effect之前 应该清理一遍effect中依赖的所有属性
cleanupEffect(this);
return this.fn();
} finally {
// 将自己的父级赋值给activeEffect
activeEffect = this.parent;
this.parent = undefined;
}
}
}
4.6 避免无限递归循环
有可能会出现这种情况在当前执行的副作用函数去修改所依赖的响应式数据,那么又会触发派发更新函数,导致无限递归造成栈溢出问题
解决方案:在派发更新函数 trigger
中执行副作用函数添加一个判断:如果派发更新所执行的副作用函数和当前执行的副作用函数是同一个,则不触发执行
export function triggerEffects(dep) {
const effects = [...dep];
effects.forEach((effect) => {
// 添加如下判断(只有不等于的时候情况会执行)
if (activeEffect !== effect) {
effect.run();
}
});
}
export function triggerEffects(dep) {
const effects = [...dep];
effects.forEach((effect) => {
// 添加如下判断(只有不等于的时候情况会执行)
if (activeEffect !== effect) {
effect.run();
}
});
}
4.7 调度执行
可调度性是响应式系统非常重要的特性。是所谓可调度,指的是当 trigger 触发副作用函数重新执行时,开发者有能力决定副作用函数执行的时机、次数以及方式
所以在 effect
函数添加第二个参数 scheduler
调度器
effect.ts
options.scheduler
export class ReactiveEffect {
public active = true;
public deps = []; // 记录该副作用函数依赖哪一些依赖项
public parent = null; // 处理副作用函数嵌套问题
// scheduler 调度执行,我们可以自己决定副作用函数执行的时机、次数、及执行方式
constructor(public fn, private scheduler) {}
run() {
// 如果是非激活状态只要执行一次函数即可
if (!this.active) {
return this.fn();
}
try {
// 其他情况下 意思着是激活的状态
this.parent = activeEffect;
// 记录当前的副作用函数
activeEffect = this;
// 每次执行effect之前 应该清理一遍effect中依赖的所有属性
cleanupEffect(this);
return this.fn();
} finally {
activeEffect = this.parent;
this.parent = undefined;
}
}
}
export function effect(fn, options: any = {}) {
const _effect = new ReactiveEffect(fn, options.scheduler);
_effect.run();
const runner = _effect.run.bind(_effect); // 保证 _effect执行的时候this是当前的effect
runner.effect = _effect;
return runner;
}
export class ReactiveEffect {
public active = true;
public deps = []; // 记录该副作用函数依赖哪一些依赖项
public parent = null; // 处理副作用函数嵌套问题
// scheduler 调度执行,我们可以自己决定副作用函数执行的时机、次数、及执行方式
constructor(public fn, private scheduler) {}
run() {
// 如果是非激活状态只要执行一次函数即可
if (!this.active) {
return this.fn();
}
try {
// 其他情况下 意思着是激活的状态
this.parent = activeEffect;
// 记录当前的副作用函数
activeEffect = this;
// 每次执行effect之前 应该清理一遍effect中依赖的所有属性
cleanupEffect(this);
return this.fn();
} finally {
activeEffect = this.parent;
this.parent = undefined;
}
}
}
export function effect(fn, options: any = {}) {
const _effect = new ReactiveEffect(fn, options.scheduler);
_effect.run();
const runner = _effect.run.bind(_effect); // 保证 _effect执行的时候this是当前的effect
runner.effect = _effect;
return runner;
}
当响应式数据发生变化时,最后就会走到 triggerEffects 函数中
export function triggerEffects(dep) {
const effects = [...dep];
effects.forEach((effect) => {
if (activeEffect !== effect) {
if (!effect.scheduler) {
effect.run(); // 每次调run方法都会重新依赖收集
} else {
// 如果传递了 scheduler 则去执行用户的调度函数 而不去执行副作用函数
effect.scheduler();
}
}
});
}
export function triggerEffects(dep) {
const effects = [...dep];
effects.forEach((effect) => {
if (activeEffect !== effect) {
if (!effect.scheduler) {
effect.run(); // 每次调run方法都会重新依赖收集
} else {
// 如果传递了 scheduler 则去执行用户的调度函数 而不去执行副作用函数
effect.scheduler();
}
}
});
}
4.8 计算属性 computed
计算属性的目的是根据状态衍生属性,我们希望这个属性有缓存功能,如果依赖的数据不变就不会重新计算
默认不执行,当我们取值时才会调用(具有缓存功能当取值多次只执行一次,只有当依赖的值发生变化才会重新执行)
计算属性 内部有个变量来控制是否重新执行 dirty(默认是 true,此时用户会执行此方法,拿到返回结果返回并且缓存起来,将 dirty 变为 false)。再次取值则 dirty 为 false 就去拿缓存的结果
如果依赖项发生变化,会再次更新 dirty 变为 true,再取值的时候就会执行拿到新值
import { isFunction } from "@vue/shared";
import {
activeEffect,
ReactiveEffect,
trackEffects,
triggerEffects,
} from "./effect";
const noop = () => {};
class ComputedRefImpl {
dep = undefined;
effect = undefined;
__v_isRef = true; // 意思着有这个属性,需要用 .value来取值
_dirty = true; // 默认是脏的
_value; // 默认的缓存结果
constructor(public getter, public setter) {
// getter 函数里的响应式变量会被收集,当发生变化是就会执行调度函数里的内容
this.effect = new ReactiveEffect(getter, () => {
// 只有数据一遍,就把 _dirty改成true 下次取值value就又会重新执行
this._dirty = true;
// 触发执行计算属性该所依赖的副作用函数
triggerEffects(this.dep);
// 最后:达到响应式数据发生变化,下一次访问计算属性时数据发生更新,依赖计算属性的副作用函数也进行触发
});
}
get value() {
if (activeEffect) {
// 如果有 activeEffect 意味着这个计算属性是在副作用函数调用的 我们需要让计算属性收集副作用函数
trackEffects(this.dep || (this.dep = new Set()));
}
if (this._dirty) {
// 取值才执行,并且把取到的值缓存起来
this._value = this.effect.run();
// 意味着这去过了 被缓存了
this._dirty = false;
}
return this._value;
}
set value(newValue) {
this.setter(newValue);
}
}
export function computed(getterOptions) {
let onlyGetter = isFunction(getterOptions);
let getter;
let setter;
if (onlyGetter) {
getter = getterOptions;
setter = noop;
} else {
getter = getterOptions.get;
setter = getterOptions.set || noop;
}
return new ComputedRefImpl(getter, setter);
}
import { isFunction } from "@vue/shared";
import {
activeEffect,
ReactiveEffect,
trackEffects,
triggerEffects,
} from "./effect";
const noop = () => {};
class ComputedRefImpl {
dep = undefined;
effect = undefined;
__v_isRef = true; // 意思着有这个属性,需要用 .value来取值
_dirty = true; // 默认是脏的
_value; // 默认的缓存结果
constructor(public getter, public setter) {
// getter 函数里的响应式变量会被收集,当发生变化是就会执行调度函数里的内容
this.effect = new ReactiveEffect(getter, () => {
// 只有数据一遍,就把 _dirty改成true 下次取值value就又会重新执行
this._dirty = true;
// 触发执行计算属性该所依赖的副作用函数
triggerEffects(this.dep);
// 最后:达到响应式数据发生变化,下一次访问计算属性时数据发生更新,依赖计算属性的副作用函数也进行触发
});
}
get value() {
if (activeEffect) {
// 如果有 activeEffect 意味着这个计算属性是在副作用函数调用的 我们需要让计算属性收集副作用函数
trackEffects(this.dep || (this.dep = new Set()));
}
if (this._dirty) {
// 取值才执行,并且把取到的值缓存起来
this._value = this.effect.run();
// 意味着这去过了 被缓存了
this._dirty = false;
}
return this._value;
}
set value(newValue) {
this.setter(newValue);
}
}
export function computed(getterOptions) {
let onlyGetter = isFunction(getterOptions);
let getter;
let setter;
if (onlyGetter) {
getter = getterOptions;
setter = noop;
} else {
getter = getterOptions.get;
setter = getterOptions.set || noop;
}
return new ComputedRefImpl(getter, setter);
}