HcySunYang / vue-design

📖 master分支:《渲染器》
http://hcysun.me/vue-design/zh/
6k stars 918 forks source link

《Vue.js设计与实现》第4章 响应系统的作用与实现 4.5小节 嵌套的effect与effect栈 #329

Open wangnan0916 opened 2 years ago

wangnan0916 commented 2 years ago

在进行嵌套定义effect时,如果多次修改外部effect中使用的响应式数据,会间接调用内部effect。

let data = {foo: true, bar: false, text: "data.text"};

const bucket = new WeakMap;

const effectStack = [];
effectStack.activeEffect = function () {
    if (this.length) return this[this.length - 1];
    return undefined;
}

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

function track(target, key) {
    if (!effectStack.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(effectStack.activeEffect());
    effectStack.activeEffect().deps.push(deps);
}

function trigger(target, key) {
    let depsMap = bucket.get(target);
    if (!depsMap) return;
    let effectsToRun = new Set(depsMap.get(key));
    effectsToRun.forEach(fn => fn());
}

function effect(fn) {
    function effectFn() {
        cleanup(effectFn);
        effectStack.push(effectFn);

        fn();
        effectStack.pop();
    }

    effectFn.sourceFn = fn;

    effectFn.deps = [];
    effectFn();
}

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

function line(text = "") {
    let cnt = 15;
    console.log("--" + text + "-".repeat(cnt));
}

line("init")
effect(function () {
    console.log(`outer effect:obj.foo=${obj.foo}`);
    effect(function () {
        console.log(`inner effect:obj.bar=${obj.bar}`);
    });
});
line("let obj.foo be false");
obj.foo = false;
obj.foo = false;
line("let obj.bar be true");
obj.bar = true;
  1. effect(fn)会将fn包装为effectFn
  2. 修改obj.foo进行重新绑定时,会导致内部的依赖再次绑定
  3. 由于step1,所以bucket的数据结构中的Set无法对effectFn进行去重,也就导致注册多个重复的依赖。

程序的输出结果为:

--init---------------
outer effect:obj.foo=true
inner effect:obj.bar=false
--let obj.foo be false---------------
outer effect:obj.foo=false
inner effect:obj.bar=false
outer effect:obj.foo=false
inner effect:obj.bar=false
--let obj.bar be true---------------
inner effect:obj.bar=true
inner effect:obj.bar=true
inner effect:obj.bar=true

在对内部的响应式数据进行修改时,触发了三个effectFn,其中有初始化的1个,两次对obj.foo进行修改时,间接添加的effectFn。 在我个人看来,应该只需要执行一个effectFn,而不是三个,不知道是本意如此,还是意外的BUG。 如果是BUG,我想到了一个不是很好解决的方法:通过将函数转为string来判断是否为相同的函数。例如:

let set = new Set();

function effect(fn) {
    function effectFn() {
        fn();
    }

    set.add(fn.toString());
    effectFn();
}

effect(() => console.log(1));
effect(() => console.log(1));
console.log(set);

涉及修改的代码如下:

function track(target, key) {
    if (!effectStack.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 Map));
    //以函数文本作为key,达到去重效果
    deps.set(effectStack.source, effectStack.activeEffect());
    effectStack.activeEffect().deps.push(deps);
}

function trigger(target, key) {
    let depsMap = bucket.get(target);
    if (!depsMap) return;
    let effectsToRun = new Map(depsMap.get(key));
    effectsToRun.forEach(fn => fn());
}

function effect(fn) {
    function effectFn() {
        cleanup(effectFn);
        effectStack.push(effectFn);

        fn();
        effectStack.pop();
    }

    //存储函数文本
    effectFn.source = fn.toString();

    effectFn.deps = [];
    effectFn();
}

以上是个人看法,如有错误,还请霍老师指正😳。

TLovers commented 2 years ago

@wangnan0916 我照着书上敲 activeEffect 和effectStack 的实现 咋和你这个不一样? 不过我倒是发现一个问题,可能我还没有看到后面,就是effectStack 每次pop.最后一次肯定是undefined。那么activeEffect 也会是undefined 那么书上53页track 第一行代码, if(!activeEffect) return 岂不是get不到值了

wisonxiang commented 6 months ago

嵌套这里确实有bug,外层更新数据时,会触发内层重新执行effect函数,这时内层的cleanup(effectFn),并不会清掉上一次绑定副作用函数