响应系统设计—Part 4

68 阅读1分钟

说明

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

接上文

实现

version 0.4 (调度执行)

1. 案例(需求)

function render() {
    console.log('执行render');
    let money = proxyData.count * proxyData.price;
    let text = `采购信息:总金额 = ${money}`;
    document.body.innerHTML = text;
}

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

    setTimeout(function () {
        console.log('更新count');
        proxyData.count++;
        console.log('\n\n更新price');
        proxyData.price = proxyData * 2;
    }, 500);
}

先后修改countprice,目前会两次触发triggerrender
这两个是执行同一个副作用函数,如果能更新数据完成后,再执行一次render,性能会更好。(就是vue.js连续多次修改响应数据,但只会触发一次渲染) Xnip2022-06-15_20-59-32.jpg

如何实现勒?

2. 响应系统支持副作用函数调度执行

调度执行指的是当trigger执行触发副作用函数执行时,有能力决定副作用函数执行的时机、次数以及方式

注册时存储调度器

/**
 * 注册副作用函数
 * @param {Function} effectFn 
 * @param {Object} options // 新增   
 * @param {Function} [options.scheduler] 副作用函数执行调度器
 */
function effect(effectFn, options  = {}) {
    /* 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.options = options;

    formatEffectFn();
}

trigger触发执行时将执行副作用的控制权交给调度器

/**
 * 触发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');
            /* 新增 */
            if(effectFn.options.scheduler) {
                console.log('effectFn执行由调度器来控制');
                effectFn.options.scheduler(effectFn);
            }else{
                effectFn();
            }
        }else{
            console.log('不执行effectFn');
        }
    });
}

3. 案例所需的调度器书写

有几点需要注意:

  • 任务队列(需要执行的副作用函数)是Set,这样可以去重
  • 需要一个值标记任务已经执行了,使的在任务完成前,即使多次调用任务也只会执行一次
  • 副作用函数的执行放到微任务中,这样才能让响应式对象的多个属性更新(同步)完成后再执行副作用函数
window.onload = function() {
    // 任务(副作用函数)队列
    const jobQueue = new Set();
    let isRunning = false;
    // 用它将任务添加到微任务队列中
    const promise = Promise.resolve();
    const runJob = () => {
        console.log('执行runJob');
        if(isRunning) {
            console.log('任务队列不执行');
            return;
        }
        console.log('准备执行任务队列');
        isRunning = true;
        promise
            .then(() => {
                console.log('微任务执行');
                jobQueue.forEach(job => {
                    console.log('副作用函数即将执行');
                    job();
                })
                isRunning = false;
                console.log('微任务执行完毕,isRunning还原');
            })
            .catch(() => {
                isRunning = false;
                console.log('微任务执行完毕(有错误),isRunning还原');
            });
    }
    effect(render, {
        scheduler: function(effectFn) {
            jobQueue.add(effectFn);
            runJob();
        }
    });

    setTimeout(function () {
        console.log('更新count');
        proxyData.count++;
        console.log('\n\n更新price');
        proxyData.price = proxyData.price * 2;
    }, 500);
}

Xnip2022-06-16_08-56-58.jpg

如我们期望的,render只执行了一次

4. 完整代码

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

/**
 * 注册副作用函数
 * @param {Function} effectFn 
 * @param {Object} options // 新增   
 * @param {Function} [options.scheduler] 副作用函数执行调度器
 */
function effect(effectFn, options  = {}) {
    /* 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.options = options;

    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');
            /* 新增 */
            if(effectFn.options.scheduler) {
                console.log('effectFn执行由调度器来控制');
                effectFn.options.scheduler(effectFn);
            }else{
                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,
});

function render() {
    console.log('执行render');
    let money = proxyData.count * proxyData.price;
    let text = `采购信息:总金额 = ${money}`;
    document.body.innerHTML = text;
}

window.onload = function() {
    // 任务(副作用函数)队列
    const jobQueue = new Set();
    let isRunning = false;
    // 用它将任务添加到微任务队列中
    const promise = Promise.resolve();
    const runJob = () => {
        console.log('执行runJob');
        if(isRunning) {
            console.log('任务队列不执行');
            return;
        }
        console.log('准备执行任务队列');
        isRunning = true;
        promise
            .then(() => {
                console.log('微任务执行');
                jobQueue.forEach(job => {
                    console.log('副作用函数即将执行');
                    job();
                })
                isRunning = false;
                console.log('微任务执行完毕,isRunning还原');
            })
            .catch(() => {
                isRunning = false;
                console.log('微任务执行完毕(有错误),isRunning还原');
            });
    }
    effect(render, {
        scheduler: function(effectFn) {
            jobQueue.add(effectFn);
            runJob();
        }
    });

    setTimeout(function () {
        console.log('更新count');
        proxyData.count++;
        console.log('\n\n更新price');
        proxyData.price = proxyData.price * 2;
    }, 500);
}