响应系统设计—Part 5

77 阅读2分钟

说明

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

接上文

实现

Version 0.5(支持计算属性 computed)

要如何设计computed

const amount = computed(() => {
    return proxyData.count * proxyData.price;
});
// 读取计算属性
console.log(amount.value);
  • 计算属性依赖了响应式对象,是响应式对象的副作用函数
  • 希望是在读取计算属性时,再得到运行结果。其他Effect,是一注册就执行

如何实现?

1. init

/**
 * 注册计算属性
 * @param {*} getter 计算属性对象是的getter函数
 */
function computed(getter) {
   // lazy: 注册Effect时,不执行副作用函数
   const effectFn = effect(getter, {lazy: true});
    const obj = {
        get value() {
            // 读取value时才执行
            return effectFn();
        }
    }
    return obj;
}

此时,需要修改一下effect函数

2. effect函数支持延迟执行

/**
 * 注册副作用函数
 * @param {Function} effectFn 
 * @param {Object} options // 新增   
 * @param {Function} [options.scheduler] 副作用函数执行调度器
 * @param {Function} [options.lazy] 延迟执行
 */
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);
        
        /* 新增:暂存执行结果 */
        const result = effectFn();

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

        /* 新增:返回结果 */
        return result;
    };
    formatEffectFn.deps = [];
    formatEffectFn.raw = effectFn;
    
    formatEffectFn.options = options;

    /* 新增:如果是延迟执行,此时不执行副作用函数 */
    if(!options.lazy) {
        formatEffectFn();
    }
    return formatEffectFn;
}

Xnip2022-06-20_09-17-01.jpg

  1. [✓]computed对象是在读取值后才进行计算,符合预期
  2. [⚠️]依赖的响应式对象属性发生变更后,触发trigger,计算属性重新执行了。此时不执行更好,希望读取amount.value时再执行。

3. 计算属性增加缓存

此时有个小问题,读取value时,副作用函数都会再运行,即使依赖的对象没有发生变更。 Xnip2022-06-20_09-12-00.jpg 我们需要给计算结果添加缓存,需要借助Part 4实现的调度执行

function computed(getter) {
    /** 新增:1. 标记响应对象的有关属性是否发生改变 */
   let isDirty = true;
   let val;

    // lazy: 注册Effect时,不执行副作用函数
   const effectFn = effect(getter, {
        lazy: true,
        /* 新增:2. 调度器中(依赖发生改变触发trigger,执行调度器),重置isDirty */
        scheduler: function() {
            isDirty = true;
            console.log('change isDirty=true');
        }
    });
   
   const obj = {
       get value() {
        /* 新增:3. 如果数据脏(改变)了,再执行副作用函数,否则返回缓存值 */
        if(isDirty){
            val = effectFn();
            isDirty = false;
        }else{
            console.log('计算属性不需要重新计算');
        }
           return val;
       }
   }
   return obj;
}

Xnip2022-06-21_09-04-07.jpg

  1. [✓] 依赖未发生改变,多次读取计算属性得值,未重复执行副作用函数
  2. [✓] 依赖发生改变,计算属性未立即执行副作用函数,而是再次读取时才执行。解决了上文提到的的⚠️

4. 计算属性支持响应式(嵌套Effect)

我们注册一个依赖计算属性的Effect

Xnip2022-06-21_09-25-45.jpg 可以看到,计算属性重新读取后,值更新了,但依赖于它的副作用函数却未重新执行。

Xnip2022-06-21_09-34-05.jpg

  • render依赖amount
  • amount并不是响应式对象(reactive创建的) 所以amount变化时,也不会触发render的重新执行。从代码执行的角度来说,这其实也是嵌套的Effect。

要支持这种场景,在computed内部进行tracktrigger即可

{
    ...
    scheduler: function() {
            isDirty = true;
            console.log('change isDirty=true');
            /** 新增:1. 计算属性依赖的响应式对象发生变更时,手动调用obj的trigger */
            trigger(obj, 'value');
        }
}
   const obj = {
       get value() {
        if(isDirty){
            val = effectFn();
            isDirty = false;
        }else{
            console.log('计算属性不需要重新计算');
        }
        /* 新增:2. 读取value时,手动调用track函数,追踪obj */
        console.log('读取属性value');
        track(obj, 'value');

        return val;
       }
   }

Xnip2022-06-21_20-59-42.jpg 过程:

  • render执行时读取amount.value
  • amount.value读取时
    • effectFn执行,proxyData.priceproxyData.count关联computed getter
    • 调用track,此时的activeEffectrender,所以track执行后,amount.value关联了render
  • priceData.price更新后,触发了computed getter
  • computed getter有调度器,触发了scheduler执行
  • scheduler中触发了amount.value的依赖—render 由此,计算属性也支持响应式了

5. 完整代码

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

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

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

        return result;
    };
    formatEffectFn.deps = [];
    formatEffectFn.raw = effectFn;
    
    formatEffectFn.options = options;

    /* 如果是延迟执行,此时不执行副作用函数 */
    if(!options.lazy) {
        formatEffectFn();
    }
    return 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;
        }
    });
}

/**
 * 注册计算属性
 * @param {*} getter 计算属性对象是的getter函数
 */
function computed(getter) {
   let isDirty = true;
   let val;

    // lazy: 注册Effect时,不执行副作用函数
   const effectFn = effect(getter, {
        lazy: true,
        /* 调度器中(依赖发生改变触发trigger,执行调度器),重置isDirty */
        scheduler: function() {
            isDirty = true;
            /** 计算试行依赖的响应式对象发生变更时,手动调用obj的trigger */
            trigger(obj, 'value');
        }
    });
   
   const obj = {
       get value() {
        if(isDirty){
            val = effectFn();
            isDirty = false;
        }else{
            console.log('计算属性不需要重新计算');
        }
        console.log('读取属性value');
        /* 读取value是,手动调用track函数,追踪obj */
        track(obj, 'value');

        return val;
       }
   }
   return obj;
}

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

window.onload = function() {
    const amount = computed(() => {
        console.log('执行计算');
        return proxyData.count * proxyData.price;
    });
    console.log('computed延迟执行');
    const render = function() {
        document.body.innerHTML =`总金额为:${amount.value}`;
    }
    effect(render);

    setTimeout(() => {
        console.log('\n\n涨价之后');
        proxyData.price = 20;
    }, 500);
}