以Tasking的方式思考手写delay函数,以及AbortController的用法思考

153 阅读9分钟

前言

这次依然是源码阅读活动的产物,之前在各种博客中,零零碎碎的看到过delay的函数分析,不同的文字下,尽有不同博主的理解,功能的点各式各样,这次川神刚好组织了一期有关delay的源码阅读...查看源码后依然是不知所谓,delay有这么多的扩展点的嘛,参考川神原文后,深刻阅读,理解,调试一番后,这次使用写tasking的方法,从零开始写一遍delay函数...

川神原文地址 面试官:请手写一个带取消功能的延迟函数,axios 取消功能的原理是什么

1.delay介绍

Delay a promise a specified amount of time

delay延迟函数,根据传入的需要延迟的时间后,再输出数据.这一点是所有不同博客delay的核心思想...听起来那不就是一个延时器操作罢了...当然,这只是一个基础,在这个基本功能条件下,封装了promise返回,并且有非常多的扩展配置,如提前结束并输出数据,配置返回resolve还是reject,使用AbortController中断promise操作并返回reject,自定义setTimeout,clearTimeout.丰富的组合方法,以及函数调用方法,是本次学习的重点....

2.delay原包使用

Install

npm install delay

Usage

普通使用,在传入延时时间后,执行后续代码..

const delay = require('delay')

(async ()=>{
  bar()
  
  await delay(100)
  
  //Executed 100 milliseconds later
  baz()
})()

API

delay(milliseconds, options?) (ps:返回配置为resolve)

Create a promise which resolves after the specified milliseconds.

delay.reject(milliseconds, options?) (ps:返回配置为reject)

Create a promise which rejects after the specified milliseconds.

delay.range(minimum, maximum, options?) (ps:可以取一个范围内的随机延时时间)

Create a promise which resolves after a random amount of milliseconds between minimum and maximum has passed.

delayPromise.clear() (ps:可以提前结束延时)

Clears the delay and settles the promise.

delay.createWithTimers({clearTimeout, setTimeout}) (ps:可以自定义延时的 setTimeout , clearTimeout方法配置)

Creates a new delay instance using the provided functions for clearing and setting timeouts. Useful if you're about to stub timers globally, but you still want to use delay to manage your tests.

参数介绍

  1. milliseconds Type : number 需要延时执行的时间

  2. mininum Type : number 在使用**delay.range()**中传入的随机延时的最小范围

  3. maxinum Type : number 在使用**delay.range()**中传入的随机延时的最大范围

  4. options

  5. value Type : unknow 在调用delaypromise对象返回**(resovle or reject)**的值

  6. signal Type : AbortSignal AbortController的属性,用来关联abort()方法,在delay函数中用来中断promiseerejectError

Advanced usage

  1. 定义延时时间后,,输出的一个值

    const delay = require('delay');

    (async() => { const result = await delay(100, {value: '🦄'});

    // Executed after 100 milliseconds
    console.log(result);
    //=> '🦄'
    

    })();

  2. 是输出为reject

    const delay = require('delay');

    (async () => { try { await delay.reject(100, {value: new Error('🦄')});

    	console.log('This is never executed');
    } catch (error) {
    	// 100 milliseconds later
    	console.log(error);
    	//=> [Error: 🦄]
    }
    

    })();

  3. 清除延时,并提前输出数据

    const delay = require('delay');

    (async () => { const delayedPromise = delay(1000, {value: 'Done'});

    setTimeout(() => {
    	delayedPromise.clear();
    }, 500);
    
    // 500 milliseconds later
    console.log(await delayedPromise);
    //=> 'Done'
    

    })();

  4. 使用AbortSignal中断promise,并返回一个Error.name为'AbortError'的错误

    const delay = require('delay');

    (async () => { const abortController = new AbortController();

    setTimeout(() => {
    	abortController.abort();
    }, 500);
    
    try {
    	await delay(1000, {signal: abortController.signal});
    } catch (error) {
    	// 500 milliseconds later
    	console.log(error.name)
    	//=> 'AbortError'
    }
    

    })();

  5. 配置自定义的 setTimeout,clearTimeout

    const delay = require('delay');

    const customDelay = delay.createWithTimers({clearTimeout, setTimeout});

    (async() => { const result = await customDelay(100, {value: '🦄'});

    // Executed after 100 milliseconds
    console.log(result);
    //=> '🦄'
    

    })();

源码地址

github.com/sindresorhu…

建议使用 github1s.com/sindresorhu… 先查看,之后clone下来运行测试代码

3.Tasking 任务分解及实现

写**"tasking"**是我最近的新看到的学习方法,之前的学习,总是想到哪里,学到哪里,效率很低,最近开始使用写好tasking的方案,在进行代码分析,感觉会好很多...根据源码的功能点,逐一分解,从基础功能到扩展...我这里参考了川神原文的功能分析点,地址在前言部分...

Tsaking 任务分解

  • delay 函数传入延时时间,并返回 Promise 对象;

  • ✅ 可传入需要输出的 value 值,延时后输出该值;

  • ✅ 可控制输出是 resolvereject 状态,源码中会在 delay 函数中添加 delay.reject() ,这里需要注意;

  • ✅ 可传入延时的区间,随机在这个区间内取数,需要单独封装出 range() 方法,先取范围内随机数后,再执行delay 函数,可以通过函数柯里化优化;

  • delay 函数返回的 Promise 对象中,添加了 clear() 方法,用于提前技术延迟,并提前输出数据;

  • ✅ 可传入 AbortController 实例下的 signal 关联本次 promise ,触发实例下的 abort() 方法可提前中止promise 并返回 reject

  • ✅ 可以自定义 setTimeout , clearTimeout ,源码将此方法放在delay函数下,默认通用的

分析好了功能点后,现在就该逐条实现了,每实现一条都回到这里,在 checkbox 上打上

Tsaking 任务实现

  1. delay 函数传入延时时间,并返回 Promise 对象.

    function delay(ms){ return new Promise((resolve,reject)=>{ setTimeout(()=>{ resolve() },ms) }) }

示例 :

(async ()=>{
		await delay(1000)
  	console.log('等待delay执行结束后打印')
})()
  1. 可传入需要输出的 value ,延时后输出该值.

    //这里value放在{} 中是为了后续配置更方便,区分开其他配置 function delay(ms,{ value }){ return new Promise((resolve,reject)=>{ setTimeout(()=>{ resolve(value) },ms) }) }

示例 :

(async ()=>{
		const ret = await delay(1000,{ value :'this is a test' })
  	console.log('等待delay执行结束后打印:'+ ret)
})()
  1. 可控制输出是 resolvereject 状态

    //使用函数柯里化的方式将resolve与reject的结果分离出来,便于后续暴露delay //将willResolve放在{}中,给后续set,clea 留下配置位置 const createDelay = ( { willResolve } ) => (ms,{ value })=>{ return new Promise((resolve,reject)=>{ setTimeout(()=>{ if(willResolve){ resolve(value) }else{ reject(value) } },ms) }) }

    //使用函数对象的方式,将resolve,reject的方法分开 const delay = createDelay({willResolve:true}) delay.reject = createDelay({willResolve:false})

示例 :

(async ()=>{
		try{
    	const ret = await delay.reject(1000,{ value :'this is a reject' })
  		console.log('这个位置不会触发,会去下方的catch')
    }catch(error){
    	console.log(error)
    }
})()
  1. 可传入延时的区间,随机在这个区间内取数,

    //取范围内的随机数 const randomInteger = ( mininum, maxinum )=> Math.floor((Math.random()*(maxinum - mininum + 1) + mininum))

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

    const delay = createDelay({willResolve:true}) delay.reject = createDelay({willResolve:false}) //调用delay,randomInteger作为回调函数取得随机数 //源码中并没有在range后控制输出为resolve还是reject,其实还可进一步优化 delay.range = (mininum,maxinum,options) => delay(randomInteger(mininum,maxinum),options)

示例 :

(async ()=>{
    	const ret = await delay.range(500,,1000,{ value :'this is a range' })
  		console.log('随机延时后打印:'+ re t)
})()
  1. delay 函数返回的 Promise 对象中,添加了 clear() 方法,用于提前技术延迟,并提前输出数据;

    const randomInteger = ( mininum, maxinum )=> Math.floor((Math.random()*(maxinum - mininum + 1) + mininum))

    const createDelay = ( { willResolve } ) => (ms,{ value })=>{ //定义好 延时器的id,还有原本要执行的方法,后续清除延时器,立即执行 let timeoutId let settle //将原本return出去的Promise赋值,给后续添加clear提供方法容器 const delayPromise = new Promise((resolve,reject)=>{ //未清除时的执行方法赋值 settle = ()=>{ if(willResolve){ resolve(value) }else{ reject(value) } } //定时器id赋值 timeoutId = setTimeout(settle,ms) })

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

    return delayPromise }

    const delay = createDelay({willResolve:true}) delay.reject = createDelay({willResolve:false}) delay.range = (mininum,maxinum,options) => delay(randomInteger(mininum,maxinum),options)

示例 :

(async ()=>{
  const delayedPromise = delay(1000, {value: 'this is clear'});

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

	// 延时500后输出
	console.log(await delayedPromise);
	//=> 'this is clear'
})()
  1. 可传入 AbortController 实例下的 signal 关联本次 promise ,触发实例下的 abort() 方法可提前中止promise 并返回 reject

    const randomInteger = ( mininum, maxinum )=> Math.floor((Math.random()*(maxinum - mininum + 1) + mininum))

    //定义好后续抛出的错误 const createAbortError = ()=>{ const error = new Error('Delay aborted') error.name = 'AbortError' return error }

    const createDelay = ( { willResolve } ) => (ms,{ value ,signal={} })=>{ // 在AbortController实例下sign的abort属性一开始false,执行实例的abort()方法后变为true // 这里判断是防止外界已执行的AbortController实例传入,可直接抛出错误 if(signal && signal.abort){ return Promise.reject(createAbortError()) }

    let timeoutId let settle //抛出错误需要Promise的reject方法,后续赋值 let rejectFn

    //下面监听abort事件后,需要中断delay函数,抛出abort错误 const signalListener = ()=>{ clearTimeout(timeoutId) rejectFn(createAbortError()) }

    //如果传入sign但是外界未调用abort()方法中断,那么就要取消监听,不可再被中断 const cleaup = ()=>{ if(signal){ signal.removeEventListener('abort',signalListener) } }

    const delayPromise = new Promise((resolve,reject)=>{
    settle = ()=>{
      //已执行阶段,不可被中断
      cleaup()
    	if(willResolve){
      	resolve(value)
      }else{
      	reject(value)
      }
    }
    
    timeoutId = setTimeout(settle,ms)
    //将reject赋值给rejectFn,用来中断操作
    rejectFn = reject
    

    })

    //传入signal后,开启abort事件监听 if(signal){ signal.addEventListener('abort',signalListener,{once:true}) }

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

    return delayPromise }

    const delay = createDelay({willResolve:true}) delay.reject = createDelay({willResolve:false}) delay.range = (mininum,maxinum,options) => delay(randomInteger(mininum,maxinum),options)

示例 :

(async () => {
  //定义AbortController实例
	const abortController = new AbortController();
  
  //延迟执行AbortController实例的abort方法,用来中断delay函数中的peomise
  //必须在delay中的settle执行之前打断
	setTimeout(() => {
		abortController.abort();
	}, 500);

	try {
    //传入signal与外面的AbortController作为关联
		await delay(1000, {signal: abortController.signal});
	} catch (error) {
		// 500 milliseconds later
		console.log(error.name)
		//=> 'AbortError'
	}
})();

AbortController非常少见,没想到还可以打断promisefetch操作,一个很棒的知识点,通过new AbortController的实例中signal作为关联,监听abort是否执行,未执行前,sign下的abort属性false,执行后为true

AbortController参考博客与文档

developer.mozilla.org/zh-CN/docs/…

www.jianshu.com/p/16335f161…

www.cnblogs.com/yangzhou33/…

  1. 可以自定义 setTimeout , clearTimeout ,源码将此方法放在delay函数下

    const randomInteger = ( mininum, maxinum )=> Math.floor((Math.random()*(maxinum - mininum + 1) + mininum))

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

    // 加入clearTimeout,setTimeout自定义方法 // 这里之前将willResolve放在{}的伏笔就填上了 const createDelay = ({clearTimeout: defaultClear, setTimeout: set, willResolve}) => (ms,{ value ,signal={} })=>{

    if(signal && signal.abort){ return Promise.reject(createAbortError()) }

    let timeoutId let settle let rejectFn //默认还是通用的clearTimeout const clear = defaultClear || clearTimeout;

    const signalListener = ()=>{ clear(timeoutId) rejectFn(createAbortError()) }

    const cleaup = ()=>{ if(signal){ signal.removeEventListener('abort',signalListener) } }

    const delayPromise = new Promise((resolve,reject)=>{
    settle = ()=>{
      cleaup()
    	if(willResolve){
      	resolve(value)
      }else{
      	reject(value)
      }
    }
    
     //默认还是通用的setTimeout
    timeoutId = (set || setTimeout)(settle,ms)
    rejectFn = reject
    

    })

    if(signal){ signal.addEventListener('abort',signalListener,{once:true}) }

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

    return delayPromise }

    //这里将delay抽离,方便自定义的方法放入 const createWithTimers = clearAndSet =>{ const delay = createDelay({...clearAndSet,willResolve:true}) delay.reject = createDelay({...clearAndSet,willResolve:false}) delay.range = (mininum,maxinum,options) => delay(randomInteger(mininum,maxinum),options) return delay }

    const delay = createWithTimers() //将需要自定义的方法createWithTimers放在delay函数对象内 delay.createWithTimers = createWithTimers

示例 :

const customDelay = delay.createWithTimers({clearTimeout, setTimeout});

(async() => {
	const result = await customDelay(1000, {value: 'this is custom func'});
	console.log(result);
	//=> 'this is custom func'
})();

总结

  1. delay 延时函数,简单的延时器应用,在基础上有非常多扩展操作学习.

  2. AbortController 可打断 promisefetch 的操作,用于取消业务请求操作.

  3. delay 源码中各种函数的定义,回调的组合方案也是学习的一个重点,模仿好的编程风格,很重要.

  4. 使用 tasking 任务分解的学习方法,思路很清晰,效率确实提升很多

后记

经过之前很多简单的源码学习,配合川神的文章辅导,现在对阅读源码没有那么吃力,每一篇文章,都是一次对未来大型源码阅读的积累,每一次都有新的编程风格体验...多调试,多写出来,配合好的学习方式,都会有很好的收获,源码阅读就是一个积累的过程,聪明出于勤奋,天才在于积累...卷起来 T_T

原文地址

**www.yuque.com/womaoni-nlw…
**