【若川视野 x 源码共读】第18期 | delay 带取消功能的延迟函数

286 阅读5分钟

本文参加了由公众号@若川视野 ****发起的每周源码共读活动, 点击了解详情一起参与。

有时我们会遇到延迟一段时间再执行的场景,你一定能想到最简单的解决方案:setTimeout。但是如果在延迟的基础上增加像“一定范围内随机时间”,“提前触发”,“可以失败”,“可以取消”等需求时,就不是单单setTimeout就能搞定的。本文分析了delay的源码,带你一步一步的实现了一个功能完善的延迟函数。

1.学习准备工作

代码:

git clone https://github.com/lxchuan12/delay-analysis.git
cd delay-analysis/delay
yarn

文章:

文章: 面试官:请手写一个带取消功能的延迟函数,axios 取消功能的原理是什么
地址: https://juejin.cn/post/7042461373904715812#heading-24

文章:学习 axios 源码整体架构,打造属于自己的请求库
地址: https://juejin.cn/post/6844904019987529735#heading-26

源码共读群友(前舟小哥)文章:https://juejin.cn/post/7092588620007079949

2.学习一步一步实现delay

2.1 delay1-3

代码路径参见川哥代码examples\delay1-3

delay1-3的使用:

<body>
    <script src="./index.js">
    </script>
    <script>
        (async() => {
            await delay1(1000);
            console.log('输出这句');
        })();
    </script>
    <script>
        (async() => {
            const result = await delay2(1000, { value: '我是若川' });
            console.log('输出结果', result);
        })();
    </script>
    <script>
        (async() => {
            try{
                const result = await delay3(1000, { value: '我是若川', willResolve: false });
                console.log('永远不会输出这句', result);
            }
            catch(err){
                console.log('输出结果', err);
            }
        })();
    </script>
</body>

delay1-3的实现:

const delay1 = (ms) => {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve();
        }, ms);
    });
}

const delay2 = (ms, { value } = {}) => {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(value);
        }, ms);
    });
}

const delay3 = (ms, {value, willResolve} = {}) => {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            if(willResolve){
                resolve(value);
            }
            else{
                reject(value);
            }
        }, ms);
    });
}

delay1接收参数ms,经过ms毫秒值后resolve;

delay2接收参数ms, 还要一个对象,这个对象的value属性是最后resolve的值;

delay3在第二个对象形式的参数中又加了willResolve参数,此参数传值并且为真值时resolve,否则reject。

运行效果如下图所示:

2.2 delay4

代码路径参见川哥代码 examples\delay4

delay4的使用:

<body>
    <script src="./index.js">
    </script>
    <script>
        (async() => {
            try{
                const result = await delay4.reject(1000, { value: '我是若川', willResolve: true });
                console.log('永远不会输出这句');
            }
            catch(err){
                console.log('输出结果', err);
            }

            const result2 = await delay4.range(10, 20000, { value: '我是range' });
            console.log(result2);
        })();
    </script>
</body>

delay4的实现:

// 随机生成一个整数时间
const randomInteger = (minimum, maximum) => Math.floor((Math.random() * (maximum - minimum + 1)) + minimum);

// createDelay是一个箭头函数接受参数willResolve,返回一个函数
// 这个被返回的函数接受ms毫秒值和一个对象,对象value属性时要resolve的值 (这叫高阶函数)
const createDelay = ({willResolve}) => (ms, {value} = {}) => {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            if(willResolve){
                resolve(value);
            }
            else{
                reject(value);
            }
        }, ms);
    });
}

const createWithTimers = () => {
    // delay是一个函数,在js中除了基本类型一切皆对象
    const delay = createDelay({willResolve: true});
    // 定义delay的reject方法,willResolve固定传值false
    delay.reject = createDelay({willResolve: false});
    // 定义range方法,此函数接受三个参数,前两个参数minimum, maximum传给randomInteger用于生成一个整型时间,
  	// 第三个参数是配置对象,包含value属性
    delay.range = (minimum, maximum, options) => delay(randomInteger(minimum, maximum), options);
    return delay;
}
const delay4 = createWithTimers();

delay4有了range方法可以在一定时间范围内随机获取到结果。运行结果如图:

过一段时间:

2.3 delay5

代码路径参见川哥代码 examples\delay5

delay5使用:

<body>
    <script src="./index.js">
    </script>
    <script>
        (async () => {
            const delayedPromise = delay5(1000, {value: '我是若川'});

            setTimeout(() => {
                delayedPromise.clear();
            }, 300);

            // 300 milliseconds later
            console.log(await delayedPromise);
            //=> '我是若川'
        })();
    </script>
</body>

delay5的定义:

const randomInteger = (minimum, maximum) => Math.floor((Math.random() * (maximum - minimum + 1)) + minimum);

const createDelay = ({willResolve}) => (ms, {value} = {}) => {
  	// 保存setTimeout的id用于清除
    let timeoutId;
    // 持有判断应该resolve还是reject核心逻辑的函数引用
    let settle;
    const delayPromise = new Promise((resolve, reject) => {
      	//settle函数赋值
        settle = () => {
            if(willResolve){
                resolve(value);
            }
            else{
                reject(value);
            }
        }
        // 记录setTimeout的id用于清除
        timeoutId = setTimeout(settle, ms);
    });
    // Promise对象上定义clear方法
    delayPromise.clear = () => {
        // 清除定义
        clearTimeout(timeoutId);
        timeoutId = null;
        // 调用核心逻辑
        settle();
    };

    return delayPromise;
}

const createWithTimers = () => {
    const delay = createDelay({willResolve: true});
    delay.reject = createDelay({willResolve: false});
    delay.range = (minimum, maximum, options) => delay(randomInteger(minimum, maximum), options);
    return delay;
}
const delay5 = createWithTimers();

delay5通过clear方法,可以取消之前定义的延时时间,让promise提前resolve或者reject。运行效果如图:

不到1秒就立刻输出了~

2.4 delay6

代码路径参见川哥代码 examples\delay6

delay6的使用:

<body>
    <script src="./index.js">
    </script>
    <script>
        (async () => {
            // AbortController 参见 https://developer.mozilla.org/en-US/docs/Web/API/AbortController
            const abortController = new AbortController();

            setTimeout(() => {
                abortController.abort();
            }, 500);

            try {
                // 将abortController的signal属性传给delay6
                await delay6(1000, {signal: abortController.signal});
            } catch (error) {
                // 500 milliseconds later
                console.log(error.name)
                //=> 'AbortError'
            }
        })();
    </script>
</body>

delay6的定义:

// 和之前一样生成整型随机时间
const randomInteger = (minimum, maximum) => Math.floor((Math.random() * (maximum - minimum + 1)) + minimum);

// 创建错误对象的方法
const createAbortError = () => {
	const error = new Error('Delay aborted');
	error.name = 'AbortError';
	return error;
};

// createDelay和之前一样仍然是一个高阶函数
const createDelay = ({
	willResolve
}) => (ms, {
	value,
	signal
} = {}) => {
  // 传signal并且终止,则抛出错误
	if (signal && signal.aborted) {
		return Promise.reject(createAbortError());
	}

	let timeoutId;
	let settle;
  // rejectFn持有对reject回调方法的引用
	let rejectFn;
	const signalListener = () => {
		clearTimeout(timeoutId);
		rejectFn(createAbortError());
	}
  // 事件清除函数
	const cleanup = () => {
		if (signal) {
      // 不在监听signal的abort事件
			signal.removeEventListener('abort', signalListener);
		}
	};
	const delayPromise = new Promise((resolve, reject) => {
    // settle方法中有变化
		settle = () => {
			cleanup();
			if (willResolve) {
          resolve(value);
        } else {
          reject(value);
        }
      };
			// 持有reject函数的引用
      rejectFn = reject;
      timeoutId = setTimeout(settle, ms);
	});

	if (signal) {
    // 如果传signal方法了则监听abort事件,并且只监听一次
		signal.addEventListener('abort', signalListener, {
			once: true
		});
	}
  
	delayPromise.clear = () => {
		clearTimeout(timeoutId);
		timeoutId = null;
		settle();
	};

	return delayPromise;
}

const createWithTimers = () => {
	const delay = createDelay({
		willResolve: true
	});
	delay.reject = createDelay({
		willResolve: false
	});
	delay.range = (minimum, maximum, options) => delay(randomInteger(minimum, maximum), options);
	return delay;
}
const delay6 = createWithTimers();

delay6主要使用了AbortContoler, AbortContoler实例上会有一个signal属性,当AbortContoler实例调用abort方法时,会触发signal的abort事件;把signal传给delay函数,并在其中监听abort事件是否被触发,如果被触发了,则定义一个错误对象,并reject这个错误对象

代码运行效果如下图:

2.5 delay7

代码路径参见川哥代码 examples\delay7

delay7使用:(在川哥原来代码基础上稍有改动)

<body>
    <script src="./index.js">
    </script>
    <script>
        const mySetTimeout = (settle, ms) => {
            console.log('自定义setTimeout')
            setTimeout(settle, ms)
        }
        // 注意createWithTimers方法接收一个对象
        const customDelay = delay7.createWithTimers({clearTimeout, setTimeout: mySetTimeout});

        (async() => {
            const result = await customDelay(100, {value: '我是若川'});

            // Executed after 100 milliseconds
            console.log(result);
            //=> '我是若川'
        })();
    </script>
</body>

delay7的定义:

const randomInteger = (minimum, maximum) => Math.floor((Math.random() * (maximum - minimum + 1)) + minimum);

const createAbortError = () => {
	const error = new Error('Delay aborted');
	error.name = 'AbortError';
	return error;
};

// createDelay 接收参数时候使用了解构,并为参数定义了别名 (defaultClear, set)
const createDelay = ({clearTimeout: defaultClear, setTimeout: set, willResolve}) => (ms, {value, signal} = {}) => {
    if (signal && signal.aborted) {
		return Promise.reject(createAbortError());
	}
    let timeoutId;
    let settle;
    let rejectFn;
    // 如果传了defaultClear则使用,否则使用原生的
    const clear = defaultClear || clearTimeout;

    const signalListener = () => {
        clear(timeoutId);
        rejectFn(createAbortError());
    }
    const cleanup = () => {
		if (signal) {
			signal.removeEventListener('abort', signalListener);
		}
	};
    const delayPromise = new Promise((resolve, reject) => {
        settle = () => {
			cleanup();
			if (willResolve) {
				resolve(value);
			} else {
				reject(value);
			}
		};

        rejectFn = reject;
        // 如果传了set则使用,否则使用原生的
        timeoutId = (set || setTimeout)(settle, ms);
    });
    
    if (signal) {
		signal.addEventListener('abort', signalListener, {once: true});
	}

    delayPromise.clear = () => {
		clear(timeoutId);
		timeoutId = null;
		settle();
	};

    return delayPromise;
}

const createWithTimers = clearAndSet => {
    const delay = createDelay({...clearAndSet, willResolve: true});
    delay.reject = createDelay({...clearAndSet, willResolve: false});
    delay.range = (minimum, maximum, options) => delay(randomInteger(minimum, maximum), options);
    return delay;
}
// 创建delay7
const delay7 = createWithTimers();
// 棒一个方法,可以接收自定义的setTimeout和clearTimeout
delay7.createWithTimers = createWithTimers;

deay7可以传递 clearTimeout, setTimeout ,这就是delay7的最终实现。运行效果如下图所示:

3.学习收获

1.了解delay的实现思路

2.了解AbortController基本知识

3.了解高阶函数在源码中的用法

4.了解解构赋值并起别名在源码中的用法

5.了解axios取消请求的实现原理

学完本期源码,您可以思考如下问题:

1.如何使Promise延迟执行?

2.如何随机生成一定范围内的整数?

3.你在项目中如何使用高阶函数和闭包的?

4.是否了解AbortController?

5.如何取消axios请求?

6.谈谈axios取消请求的原理?

7.简述delay的实现思路?