响应系统设计—Part 6

94 阅读7分钟

说明

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

接上文

实现—Version 0.6(支持watch)

watch的作用是什么?
通过watch监听响应式对象,对象发生变更时,执行回调

如何监听对象发生变更?

  1. 可以通过一个读取对象的effect来实现,对象所有的属性变更都需要,所以要遍历读取对象的属性
  2. 也允许开发者自定义只监听的对象的某个属性,此时传入一个读取属性的函数即可

监听到变更后,怎执行回调?
可以通过Part4中讲到的调度执行来支持,当副作用函数有传入scheduler时,就会触发scheduler执行而不是副作用函数执行。

1. init

function watch(source, callback) {
    // 1. 读取对象属性的函数
    let getter;
    if(typeof source === 'function'){
        // 1.1 自定义要监听的属性
        getter = source;
    }else {
        // 1.2 遍历所有属性
        getter = () =>{
            traverse(source);
        };
    }
    //2. effect注册getter,建立联系;执行回调的scheduler函数
   effect(getter, 
        {
            scheduler: function() {
                console.log('调度执行callback');
                callback();
            }
        }
    );
}

function traverse(value, seen = new Set()) {
    if(typeof value !== 'object' || value === null || seen.has(value)) {
        return;
    }
    // 数据遍历后,存起来,表示已读取,避免循环引用引起死循环
    seen.add(value);
    // 假设value是个对象,其他数据结构暂不考虑
    for(const key in value){
        traverse(value[key], seen);
    }
    return value;
}

Xnip2022-06-28_09-03-04.jpg

2. callack的参数

我们在使用callback时,还需要拿到对象(某个属性)的新值和旧值,如何获取勒?
在Part5中,我们用到了lazy属性,它使得在注册Effect时,不执行副作用函数。

if(typeof source === 'function'){
    // ...
}else {
    // 增加return
    getter = () =>{
        return traverse(source);
    };
}
// 监听的响应式对象变更前后的值
let newValue;
let oldValue;
const effectFn = effect(getter, 
    {
        lazy: true, // 注册时不执行getter
        scheduler: function() {
            console.log('调度执行callback');
            newValue = effectFn();
            callback(newValue, oldValue);
            // 这次的新值,是下一次的旧值
            oldValue = newValue;
        }
    }
);
// 第一次读取,执行getter建立联系,并得到初始化的旧值
oldValue = effectFn();

Xnip2022-06-28_09-10-44.jpg 如果watch的是响应式对象 Xnip2022-06-28_09-18-21.jpg 因为是对象,oldValuenewValue都是proxyData的引用,读取属性值时自然相同。

3. 立即执行watch

我们使用watch监听时,默认情况下是watch的回调只会在响应式数据发生变化时才执行。在vue.js中,还可以配置immediate: true来指定回调立即执行,如何实现勒?

/* watch函数增加options参数 */ 
function watch(source, callback, options = {}) {
    // 读取对象属性的函数
    let getter;
    // 省略getter部分逻辑
    let newValue;
    let oldValue;
    /* 将scheduler中的逻辑独立出来,以便单独调用 */
    const job = function() {
        newValue = effectFn();
        console.log('调度执行callback');
        callback(newValue, oldValue);
        // 这次的新值,是下一次的旧值
        oldValue = newValue;
    };
    const effectFn = effect(
        getter, 
        {
            lazy: true, // 注册时不执行getter
            scheduler: function() {
               job();
            }
        }
    );
    /* 
    * 如果声明了立即执行,则立即执行job
    * 第一次执行,此时的oldValue一定是undefined,也符合预期
    */
    if(options.immediate) {
        job();
    }else{
        // 第一次读取,执行getter建立联系,并得到初始化的旧值
        oldValue = effectFn();
    }
}

Xnip2022-06-28_09-33-59.jpg

4.过期的副作用

开发中watch中可能会处理一些异步的情况,常见的就是发请求了。 如下:

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

// 发送请求,控制响应的时机
let time = 500;
function request(data) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(data);
        }, time);
    });
}

window.onload = function() {
    watch(
        proxyData,
        function(newValue) {
             // 使用name属性值,方便观察响应是第几次请求的结果
             request(newValue.name).then((data) => {
                const str = 'name=' + data;
                console.log('更新html为:' + str);
                document.body.innerHTML = str;
             });
             // 使后续的请求能更快的响应
             time = time - 200;
        }
    );

    proxyData.name = '鼠标键盘';

    setTimeout(() => {
        proxyData.name = '收纳包';
    }, 50);
}

过程:

  • 修改name为【鼠标键盘】
  • 触发watch发送第一次请求
  • 50ms后修改name为【收纳包】
  • 触发watch发送第二次请求
  • 收到第二次请求的响应,修改html为【收纳包】
  • 收到第一次请求的响应,修改html为【鼠标键盘】

Xnip2022-06-28_21-17-06.jpg 很明显,这个结果不是我们期望的。我们总是希望最后发出的请求的响应是有效的。 如何支持勒?
1)watch函数的callback增加第三个参数

function watch(source, callback, options = {}) {
    //... getter
    
    let newValue;
    let oldValue;
    /** 新增:清理函数,由回调传入 */
    let cleanUp;
    /** 新增:callback的第三个参数 */
    function onInvalidate(fn) {
        console.log('\n注册cleanUp\n\n')
        cleanUp = fn;
    }
    const job = function() {
        newValue = effectFn();
        /** 新增:执行callback前先清理 */
        if(cleanUp) {
            console.log('执行cleanUp')
            cleanUp();
        }
        console.log('调度执行callback');
        /* 新增:传入onInvalidate参数 */
        callback(newValue, oldValue, onInvalidate);
        // 这次的新值,是下一次的旧值
        oldValue = newValue;
    };
    
    //... 注册Effect等
}

2)watch使用部分,callback中增加对onInvalidate的使用

window.onload = function() {
    watch(
        proxyData,
        function(newValue, _, onInvalidate) {
            /*新增: 标记本次回调已过期*/
            let expired = false;
            let flag = newValue.name;
            onInvalidate(() => {
                console.log('expired 过期, ' + flag);
                expired = true;
            });
            request(flag).then((data) => {
                console.log('收到响应,expired: ' , expired);
                /*新增:未过期才赋值 */
                if(!expired){
                    const str = 'name=' + data;
                    console.log('更新html为:' + str);
                    document.body.innerHTML = str;
                }
             });
            time = time - 200;
        }
    );

    proxyData.name = '鼠标键盘';

    setTimeout(() => {
        proxyData.name = '收纳包';
    }, 50);
}

满足期望: Xnip2022-06-29_09-04-27.jpg

  • 修改name为【鼠标键盘】
  • 触发watch,执行job
    • cleanUp为空
    • 执行callback回调。运行onInvalidate注册了cleanUp,随后发送了第一次请求
  • 50ms后修改name为【收纳包】
  • 触发watch,执行job
    • cleanUp,执行cleanUp使上一次副作用函数中的expiredtrue
    • 执行callback,注册cleanUp,随后发送第二次请求
  • 收到第二次请求的响应,修改html为【收纳包】
  • 收到第一次请求的响应,副作用函数内的expired=true,不赋值

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;
}

function watch(source, callback, options = {}) {
    // 1. 读取对象属性的函数
    let getter;
    if(typeof source === 'function'){
        // 1.1 自定义要监听的属性
        getter = source;
    }else {
        // 1.2 遍历所有属性
        getter = () => {
            return traverse(source);
        };
    }
    let newValue;
    let oldValue;
    /** 新增:清理函数,由回调传入 */
    let cleanUp;
    /** 新增:callback的第三个参数 */
    function onInvalidate(fn) {
        console.log('\n注册cleanUp\n\n')
        cleanUp = fn;
    }
    const job = function() {
        newValue = effectFn();
        /** 新增:执行callback前先清理 */
        if(cleanUp) {
            console.log('执行cleanUp')
            cleanUp();
        }
        console.log('调度执行callback');
        /* 新增:传入onInvalidate参数 */
        callback(newValue, oldValue, onInvalidate);
        // 这次的新值,是下一次的旧值
        oldValue = newValue;
    };
    const effectFn = effect(
        getter, 
        {
            lazy: true, // 注册时不执行getter
            scheduler: function() {
               job();
            }
        }
    );
    if(options.immediate) {
        job();
    }else{
        // 第一次读取,执行getter建立联系,并得到初始化的旧值
        oldValue = effectFn();
    }
}

function traverse(value, seen = new Set()) {
    if(typeof value !== 'object' || value === null || seen.has(value)) {
        return;
    }
    // 数据遍历后,存起来,表示已读取,避免循环引用引起死循环
    seen.add(value);
    // 假设value是个对象,其他数据结构暂不考虑
    for(const key in value){
        traverse(value[key], seen);
    }
    return value;
}

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

let time = 500;
function request(data) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(data);
        }, time);
    });
}

window.onload = function() {
    watch(
        proxyData,
        function(newValue, _, onInvalidate) {
            /*新增: 标记本次回调已过期*/
            let expired = false;
            let flag = newValue.name;
            onInvalidate(() => {
                console.log('expired 过期, ' + flag);
                expired = true;
            });
            request(flag).then((data) => {
                console.log('收到响应,expired: ' , expired);
                /*新增:未过期才赋值 */
                if(!expired){
                    const str = 'name=' + data;
                    console.log('更新html为:' + str);
                    document.body.innerHTML = str;
                }
             });
            time = time - 200;
        }
    );

    proxyData.name = '鼠标键盘';

    setTimeout(() => {
        proxyData.name = '收纳包';
    }, 50);
}