第二篇 响应系统

第四章 响应系统的作用与实现

设计完善的响应式系统

借助 Proxy ,我们实现一个基本的响应式系统。书中代码我改成可以在 Node 中运行的代码了。

const bucket = new Set();

const data = { text: 'hello world' };

const obj = new Proxy(data, {
    get(target, key) {
        bucket.add(effect);
        return target[key];
    },
    set(target, key, newVal) {
        target[key] = newVal;
        bucket.forEach(fn => fn());
        return true;
    }
});

function effect() {
    console.log(obj.text);
}

effect();

setTimeout(() => {
    obj.text = 'Hello Vue';
}, 1000);
// Output:
// hello world
// Hello Vue

这里是写死了 effect ,不过不用太过纠结。

基本思路是:

  • 读取的时候,副作用函数执行的时候,触发读操作,并把副作用函数放入桶内。
  • 修改时,拦截写操作,执行副作用函数。

由于我们的副作用函数是硬编码,使得我们的代码非常不好维护。为此,我们还需要一个依赖收集函数来收集副作用函数。

const bucket = new Set();

const data = { text: 'hello world' };

let activeEffect;

function effect(fn) {
    activeEffect = fn;
    fn();
}

const obj = new Proxy(data, {
    get(target, key) {
        if (activeEffect) {
            bucket.add(activeEffect);
        }
        return target[key];
    },
    set(target, key, newVal) {
        target[key] = newVal;
        bucket.forEach(fn => fn());
        return true;
    }
});

effect(() => {
    console.log(obj.text);
});

setTimeout(() => {
    obj.text = 'Hello Vue';
}, 1000);

但如果我们为 obj 添加一个不存在的属性,我们会发现副作用方法仍然被执行了。而且,如果我们添加多个 target ,只要任意一个 target 被执行,就会引起所有的副作用函数被执行。因为我们并没有在副作用函数与数据字段之间建立起明确的关系,为此我们需要重新设计我们的数据结构。

const bucket = new WeakMap();

const data = { text: "hello world" };
let activeEffect;

function effect(fn) {
    activeEffect = fn;
    fn();
}

const obj = new Proxy(data, {
    get(target, key) {
        if (!activeEffect) return target[key];
        let depsMap = bucket.get(target);
        if (!depsMap) {
            bucket.set(target, ( depsMap = new Map() ));
        }
        let deps = depsMap.get(key);
        if (!deps) {
            depsMap.set(key, (deps = new Set()));
        }
        deps.add(activeEffect);
        return target[key];
    },
    set(target, key, value) {
        target[key] = value;
        const depsMap = bucket.get(target);
        if (!depsMap) return;
        const effects = depsMap.get(key);
        effects && effects.forEach(fn => fn());
    }
});

effect(() => {
    console.log(obj.text);
});

setTimeout(() => {
    obj.text = "Hello Vue";
}, 1000);

首先,我们需要用 WeakMap 来收集 target ,确保对应的 target 变动只会影响与之有关的副作用函数。使用 WeakMap 可以确保引用关系是弱引用,从而不影响 GC 。

其次,我们收集 target 对应的 key 的副作用函数,这些函数将构成一个集合,在需要的时候被执行。

我们简单重构一下代码:

const bucket = new WeakMap();

const data = { text: "hello world" };
let activeEffect;

function effect(fn) {
    activeEffect = fn;
    fn();
}

const obj = new Proxy(data, {
    get(target, key) {
        track(target, key);
        return target[key];
    },
    set(target, key, value) {
        target[key] = value;
        trigger(target, key);
    }
});

function track(target, key) {
    if (!activeEffect) return;
    let depsMap = bucket.get(target);
    if (!depsMap) {
        bucket.set(target, ( depsMap = new Map() ));
    }
    let deps = depsMap.get(key);
    if (!deps) {
        depsMap.set(key, (deps = new Set()));
    }
    deps.add(activeEffect);
}

function trigger(target, key) {
    const depsMap = bucket.get(target);
    if (!depsMap) return;
    const effects = depsMap.get(key);
    effects && effects.forEach(fn => fn());
}

effect(() => {
    console.log(obj.text);
});

setTimeout(() => {
    obj.text = "Hello Vue";
}, 1000);

思考以下代码:

const data = { ok: true, text: "hello world" };
// 前略

effect(() => {
  console.log(obj.ok ? obj.text : "not");
});

obj.oktrue 时,依赖收集会为我们收集 oktext ,为 false 时,会收集到 ok 字段。但 obj.ok 是会变动的,而这种变动显然会引起依赖的变动,而目前我们的代码显然无法收集这种变动。这会引起依赖变动的缺失和冗余。为了解决这个问题,我们可以副作用函数执行时,重新绑定依赖。

const bucket = new WeakMap();

const data = { text: "hello world" };
let activeEffect;

function effect(fn) {
    const effectFn = () => {
        // 切断依赖
        cleanup(effectFn);
        activeEffect = effectFn;
        // 重新收集依赖
        fn();
    };
    effectFn.deps = [];
    effectFn();
}

function cleanup(effectFn) {
    for (let i = 0; i < effectFn.deps.length; i++) {
        const deps = effectFn.deps[i];
        deps.delete(effectFn);
    }
    effectFn.deps = [];
}

const obj = new Proxy(data, {
    get(target, key) {
        track(target, key);
        return target[key];
    },
    set(target, key, value) {
        target[key] = value;
        trigger(target, key);
    }
});

function track(target, key) {
    if (!activeEffect) return;
    let depsMap = bucket.get(target);
    if (!depsMap) {
        bucket.set(target, ( depsMap = new Map() ));
    }
    let deps = depsMap.get(key);
    if (!deps) {
        depsMap.set(key, (deps = new Set()));
    }
    deps.add(activeEffect);
    // 添加依赖
    activeEffect.deps.push(deps);
}

function trigger(target, key) {
    const depsMap = bucket.get(target);
    if (!depsMap) return;
    const effects = depsMap.get(key);
    // 复制一份 Set ,防止原依赖集合变动从而引起循环
    const effectsToRun = new Set(effects);
    effectsToRun.forEach(fn => fn());
}

effect(() => {
    console.log(obj.text);
});

setTimeout(() => {
    obj.text = "Hello Vue";
}, 1000);

嵌套 effect

我们当前的实现并不支持嵌套 effect ,因为我们使用了全局变量 ativeEffect 来注册 effect 函数,在 effect 嵌套时,就会引起覆盖。

const data = { foo: true, bar: true };
let temp1, temp2;

// 省略

effect(() => {
    console.log('effectFn1 执行');
    effect(function effectFn2() {
        console.log('effectFn2 执行');
        temp2 = obj.bar;
    });
    temp1 = obj.foo;
});

setTimeout(() => {
    obj.foo = "Hello Vue";
}, 1000);

// Output:
// effectFn1 执行  
// effectFn2 执行  
// effectFn2 执行

我们需要一个 effectStack 来解决这个问题。

let activeEffect;
let effectStack = [];
function effect(fn) {
    const effectFn = () => {
        cleanup(effectFn);
        effectStack.push(effectFn);
        activeEffect = effectFn;
        fn();
        effectStack.pop();
        activeEffect = effectStack[effectStack.length - 1];
    };
    effectFn.deps = [];
    effectFn();
}

activeEffect 相当于栈顶指针, effectStack 相当于执行栈。当嵌套发生时,位于栈顶的 effect 则始终等于当前值的 effect ,从而避免发生错乱。

避免无限循环

如果 trigger 触发执行的副作用函数与当前正在执行的副作用函数相同,则不触发执行。

const data = { foo: 1 };

// 省略

function trigger(target, key) {
    const depsMap = bucket.get(target);
    if (!depsMap) return;
    const effects = depsMap.get(key);
    const effectsToRun = new Set();
    // 过滤与当前执行 effect 相同的副作用函数
    effects && effects.forEach(effectFn => {
        if (effectFn !== activeEffect) {
            effectsToRun.add(effectFn);
        }
    });
    effectsToRun.forEach(effectFn => effectFn());
}

effect(() => {
    obj.foo++;
});

未完待续。