响应系统设计—Part 3

298 阅读4分钟

说明

【vue.js 设计与实现】 霍春阳 学习笔记

接上文

实现

version 0.3 (支持嵌套Effect)

嵌套的Effect,指的是在Effect中又注册了Effect。什么场景下会出现嵌套的Effect?vue中比较常见的是组件渲染。

我们对嵌套的Effect的期望是:

  • 外层的Effect执行时,会引起内层的Effect执行
  • 只有和内层Effect相关的变化时,只引起内层Effect执行

那我们目前的代码如果写了嵌套的Effect能正常执行吗?

1.1 内层Effect可能会多次执行

/* 此时响应式的代码同v0.21,也就是part2的中的完整代码部分 */

/**
 * 以下是演示用例
 */
const proxyData = reactive({
    name: '牛牛手办',
    price: 10,
    count: 1,
});

function render(){
    console.log('执行render');
    let text = '';
    if(proxyData.count > 0){
        let money = proxyData.count * proxyData.price;
        text += `${money} 元`;
    }else{
        text += '0 元';
    }
    document.body.innerHTML =  `
        <h1>采购信息<h1>
        <h2>总金额</h2>
        <div>${text}</div>
    `;
    const node = document.createElement('div');
    document.body.appendChild(node);
    effect(renderDetail(node, proxyData));
}

function renderDetail(parentNode, product) {
    return function() {
        console.log('执行renderDetail')
        parentNode.innerHTML = '';
        const node = document.createElement('div');
        node.innerHTML = `
            <div>
                <h2>商品详情</h2>
                <p>商品名称:${product.name}</p>
                <p>商品价格:${product.price}</p>
            <div>
        `;
        parentNode.appendChild(node);
    }
}

window.onload = function() {
    effect(render);

    // 应该引起render和renderDetail的执行
    setTimeout(() => {
        console.log('\n\n更新count');
        proxyData.count = 30;
    }, 1000);

    // 应该只会引起renderDetail执行
    setTimeout(() => {
        console.log('\n\n更新name');
        proxyData.name = '星空雨伞';
    }, 1200);
}

Xnip2022-06-14_09-07-18.jpg 除了执行了两次renderDetail,其他符合我们的预期。为什么会执行两次勒?

  • renderDetailrender中注册的
  • render执行了两次,renderDetail也注册了两次
  • 虽然依赖是Set结构,但effectFn是有一个注册时生成的匿名函数,所以并不相同,尽管他们的原函数实际内容相同

按照这个逻辑,如果render再执行,renderDetail的执行次数还会增多

window.onload = function() {
    effect(render);

    // 应该引起render和renderDetail的执行
    setTimeout(() => {
        console.log('\n\n更新count');
        proxyData.count = 30;
    }, 1000);

    setTimeout(() => {
        console.log('\n\n更新count');
        proxyData.count = 50;
    }, 1100);

    // 应该只会引起renderDetail执行
    setTimeout(() => {
        console.log('\n\n更新name');
        proxyData.name = '星空雨伞';
    }, 1200);
}

结果确实如此
Xnip2022-06-14_09-15-05.jpg 怎么解决勒? 没想到。
这里原因是进入Set的是新创建的Effect,每次运行的结果都和前面不同,虽然注册使用的是同一个。那我们是不是可以借助注册的函数来判断?

const array = [];
function test(fn) {
    const newFn = () => {

    };
    newFn.raw = fn;
    let exist = array.find(item => item.raw === newFn.raw);
    if(!exist){
        array.push(newFn);
    }
    console.log('exist ', exist);
    return newFn;
}

const detail = () =>{
    console.log(detail);
}
function wrapper(){
    test(detail);
}
wrapper();
wrapper();

wrapper执行了两次,但通过增加raw属性,可以判断重复。 Xnip2022-06-14_09-35-58.jpg 但这种对test的参数有要求,需要是单独声明的函数,如果换成匿名函数,并不能判断重复。
这种和重复注册(外层也)一样,处理不了,不能算bug。

Xnip2022-06-14_09-37-49.jpg

看一下vue?
从结果来看,内层副作用函数也会重复执行

import { reactive} from 'vue' 
import { effect } from '@vue/reactivity' 

window.onload = function() {
   // 此demo来自于书籍,章节4.5
    const data = { foo: true, bar: true};
    const obj = reactive(data);
    
    let temp1, temp2;

    effect(function effectFn1() {
        console.log('执行effectFn1');
        effect(function effectFn2() {
            console.log('执行effectFn2');
            temp2 = obj.bar;
        });
        temp1 = obj.foo;
    });
    
    setTimeout(() =>{
        console.log('\n\n');
        obj.foo =  false;
    }, 500);

    setTimeout(() => {
        console.log('\n\n');
        obj.bar = false;
    }, 600);
}

obj.bar修改,effectFn2执行了两次
Xnip2022-06-14_09-22-05.jpg

1.2 副作用函数栈

1、修改name,触发effectFn1effectFn2

window.onload = function() {
    effect(function effectFn1() {
        effect(function effectFn2() {
            console.log('effect-商品价格:', proxyData.price);
        });
        // 和1.1测试不同点:外层的依赖晚于内存effect
        console.log('effect-商品名称:', proxyData.name);
    });

    setTimeout(() => {
        console.log('\n\n修改name')
        proxyData.name = '星空雨伞';
    }, 500);
}

Xnip2022-06-14_20-50-59.jpg 修改name后,无依赖副作用函数,为什么勒?

  1. 注册effectFn1activeEffect等于(准确说是相关的,新的匿名函数)effectFn1
  2. 运行effectFn1activeEffect等于effectFn2effectFn2运行完毕
  3. 继续effectFn1执行,读取对象的name属性,触发trigger
  4. triggeractiveEffect等于null (第2步运行后重置了activeEffect

矛盾点在于,activeEffect只能有一个,且执行完内层后会重置,继续回到外层函数中执行时,activeEffect不会还原。函数执行是借助栈的,同样,副作用函数也可以用栈来收集。

/* 新增 */
const effectStack = [];

function effect(effectFn) {
    const formatEffectFn = () => {
        for(let i = 0; i < formatEffectFn.deps.length; i++){
            const effectSet = formatEffectFn.deps[i];
            effectSet.delete(formatEffectFn);
        }
        formatEffectFn.deps.length = 0;
        activeEffect = formatEffectFn;
        /* 新增 */
        effectStack.push(formatEffectFn);
        
        effectFn();
        
        /* 新增 */
        effectStack.pop();
        /* 修改,原来是置为null */
        activeEffect = effectStack[effectStack.length - 1];
    };
   // 无变化 省略
}

Xnip2022-06-14_21-04-30.jpg

满足期望

2、修改price,触发effectFn2(使用第一步产生的Effect)

window.onload = function() {
    // 注册内容同第一步

    setTimeout(() => {
        console.log('\n\n修改price');
        proxyData.price = 20;
    }, 500);
}

Xnip2022-06-14_21-06-06.jpg
满足期望

1.3 完整代码

let activeEffect = null;
// 执行副作用函数
const effectStack = [];

/** 
 * 注册副作用函数
 * @param {Function} effectFn 
 */
function effect(effectFn) {
    /* effectFn对象,增加deps属性 */
    const formatEffectFn = () => {
        /* start-清理关联的set */
        for(let i = 0; i < formatEffectFn.deps.length; i++){
            const effectSet = formatEffectFn.deps[i];
            effectSet.delete(formatEffectFn);
        }
        // 重置数组
        formatEffectFn.deps.length = 0;
        /* end */

        // 一定要放在清理set之后,因为执行effectFn会重新建立关联
        activeEffect = formatEffectFn;
        effectStack.push(formatEffectFn);
        
        effectFn();

        effectStack.pop();
        /* effectFn执行后,重置 */
        activeEffect = effectStack[effectStack.length - 1];
    };
    formatEffectFn.deps = [];
    formatEffectFn.raw = effectFn;
    formatEffectFn();
}

// 存储副作用函数
const bucket = new WeakMap();

/**
 * 收集target对象key的副作用函数
 * @param {Object} target 
 * @param {String|Symbol} key 
 * @return {void}
 */
function track(target, key){
    if(!activeEffect) return;
    let targetMap = bucket.get(target);
    if(!targetMap){
        targetMap = new Map();
        bucket.set(target, targetMap);
    }
    let effectSet = targetMap.get(key);
    if(!effectSet){
        effectSet = new Set();
        targetMap.set(key, effectSet);
    }
    effectSet.add(activeEffect);

    /* effect中关联和它相关的set */
    activeEffect.deps.push(effectSet);
}

/**
 * 触发target对象key的副作用函数执行
 * @param {Object} target 
 * @param {String|Symbol} key 
 * @return {void}
 */
function trigger(target, key) {
    console.log('触发trigger');
    const targetMap = bucket.get(target);
    if(!targetMap) return;
    
    const effectSet = targetMap.get(key);
    if(!effectSet) return;
    
    const effectsToRun = new Set(effectSet);
    effectsToRun.forEach(effectFn => {
        /* 守卫条件 */
        if(effectFn !== activeEffect){
            console.log('取出并执行effectFn');
            effectFn();
        }else{
            console.log('不触发effectFn');
        }
    });

}

/**
 * 创建响应式对象
 * @param {*} obj 
 * @returns 
 */
function reactive(obj){
    return new Proxy(obj, {
        get(target, key) {
            // console.log(`触发get,key = ${key}`);
            /* 存入副作用函数 */
            track(target, key);

            return target[key];
        },
    
        set(target, key, newVal) {
            console.log(`触发set,key = ${key}`);
            target[key] = newVal;
            // 取出并执行副作用函数
            trigger(target, key);
            return true;
        }
    });
}

/**
 * 以下是演示用例
 */
const proxyData = reactive({
    name: '牛牛手办',
    price: 10,
    count: 1,
});

window.onload = function() {
    effect(function effectFn1() {
        effect(function effectFn2() {
            console.log('effect-商品价格:', proxyData.price);
        });
        console.log('effect-商品名称:', proxyData.name);
    });

    // setTimeout(() => {
    //     console.log('\n\n修改name')
    //     proxyData.name = '星空雨伞';
    // }, 500);

    setTimeout(() => {
        console.log('\n\n修改price');
        proxyData.price = 20;
    }, 500);
}

/*

function render(){
    console.log('执行render');
    let text = '';
    if(proxyData.count > 0){
        let money = proxyData.count * proxyData.price;
        text += `${money} 元`;
    }else{
        text += '0 元';
    }
    document.body.innerHTML =  `
        <h1>采购信息<h1>
        <h2>总金额</h2>
        <div>${text}</div>
    `;
    const node = document.createElement('div');
    node.id = 'detail-parent';
    document.body.appendChild(node);
    effect(renderDetail);
}

function renderDetail() {
    console.log('执行renderDetail')
    const parentNode = document.getElementById('detail-parent');
    parentNode.innerHTML = '';
    const node = document.createElement('div');
    node.innerHTML = `
        <div>
            <h2>商品详情</h2>
            <p>商品名称:${proxyData.name}</p>
            <p>商品价格:${proxyData.price}</p>
        <div>
    `;
    parentNode.appendChild(node);
}

window.onload = function() {
    effect(render);

    // 应该引起render和renderDetail的执行
    setTimeout(() => {
        console.log('\n\n更新count');
        proxyData.count = 30;
    }, 1000);

    setTimeout(() => {
        console.log('\n\n更新count');
        proxyData.count = 50;
    }, 1100);

    // 应该只会引起renderDetail执行
    setTimeout(() => {
        console.log('\n\n更新name');
        proxyData.name = '星空雨伞';
    }, 1200);
}

*/