先举个定时器的例子
每1秒执行一次,3次后,停止调用。
constnextFactory=createTimeoutGenerator();letcontext={counts:0};nextFactory.start(function(this:any,next:Function){context.counts++;console.log("counts",context.counts);if(context.counts>3){nextFactory.cancel();}next();},context);
定时器
前端常见三大定时器setTimeout
, setInterval
, requestAnimationFrame
setInterval
的坑不是本文讨论的重点,所以剩下的选择是 setTimeout
, requestAnimationFrame
。
有很多时候,我们需要多次调用定时器,比如验证码倒计时,canvas绘制。 基本都是处理完数据后,进入下一个周期, 我们一起看看例子。
定时器应用
setTimeout
我们用原生代码实现一个60秒倒计时,并支持暂停,继续的功能,来看一看代码: 大概是下面这个样子:
<divclass="wrapper"><spanid="seconds">60</span><div><buttonid="btnPause">暂停</button><buttonid="btnContinue">继续</button></div></div><script>constsecondsEl=document.getElementById("seconds");constINTERVAL=1000;letticket;letseconds=60;functionsetSeconds(val){secondsEl.innerText=val;}functiononTimeout(){seconds--;setSeconds(seconds);ticket=setTimeout(onTimeout,INTERVAL);}ticket=setTimeout(onTimeout,INTERVAL);document.getElementById("btnPause").addEventListener("click",()=>{clearTimeout(ticket);});document.getElementById("btnContinue").addEventListener("click",()=>{ticket=setTimeout(onTimeout,INTERVAL);});</script>
有没有,什么问题? 我觉得有,
INTERVAL
,ticket
和setTimeout
满天飞, 不够高雅,我们应该更关心业务的处理;
有多处类似的逻辑,就得重复的写setTimeout
,缺少复用;
语义不好
当然,大家肯定都有自己的封装,我这里要解决的是定时器的封装,与页面和逻辑无关。
我们不妨再看一段代码: 一样的功能,看起来简洁很多,而且语义很清晰。
start: 开始
cancel: 取消
continue: 继续
<divclass="wrapper"><spanid="seconds">60</span><div><buttonid="btnPause">暂停</button><buttonid="btnContinue">继续</button></div></div><scriptsrc="../dist/index.js"></script><script>constnextFactory=createTimeoutGenerator();constsecondsEl=document.getElementById("seconds");letseconds=60;functionsetSeconds(val){secondsEl.innerText=val;};nextFactory.start(function(next){seconds--;setSeconds(seconds);next();});document.getElementById("btnPause").addEventListener("click",()=>{nextFactory.cancel();});document.getElementById("btnContinue").addEventListener("click",()=>{nextFactory.continue();});</script>
requestAnimationFrame
再一起来看一个canvas绘制的例子,我们每隔一个绘制周期,就把当前的时间戳画在画布上。 大概是这个样子:
同样的,可以暂停和继续。
drawTime 绘制时间
requestAnimationFrame 启动定时器
两个按钮的点击事件,分别处理暂停和继续
先一起来看看原生JS的基础版本:
<divstyle="margin:50px;"><canvasid="canvas"height="300"width="300"></canvas></div><div><div><buttonid="btnPause">暂停</button><buttonid="btnContinue">继续</button></div></div><script>letticket;constcanvasEl=document.getElementById("canvas");constctx=canvasEl.getContext("2d");ctx.fillStyle="#f00";ctx.fillRect(0,0,300,300);functiondrawTime(){ctx.clearRect(0,0,300,300);ctx.fillStyle="#f00";ctx.fillRect(0,0,300,300);ctx.fillStyle="#000";ctx.font="bold20pxArial";ctx.fillText(Date.now(),100,100);}functiononRequestAnimationFrame(){drawTime();ticket=requestAnimationFrame(onRequestAnimationFrame);}ticket=requestAnimationFrame(onRequestAnimationFrame);document.getElementById("btnPause").addEventListener("click",()=>{cancelAnimationFrame(ticket);});document.getElementById("btnContinue").addEventListener("click",()=>{requestAnimationFrame(onRequestAnimationFrame);});</script>
问题依旧,我们看看另外一个版本:
constnextFactory=createRequestAnimationFrameGenerator();constcanvasEl=document.getElementById("canvas");constctx=canvasEl.getContext("2d");ctx.fillStyle="#f00";ctx.fillRect(0,0,300,300);functiondrawTime(){ctx.clearRect(0,0,300,300);ctx.fillStyle="#f00";ctx.fillRect(0,0,300,300);ctx.fillStyle="#000";ctx.font="bold20pxArial";ctx.fillText(Date.now(),100,100);}nextFactory.start((next)=>{drawTime();next();});document.getElementById("btnPause").addEventListener("click",()=>{nextFactory.cancel();});document.getElementById("btnContinue").addEventListener("click",()=>{nextFactory.continue();});
这里大家都注意到了,createTimeoutGenerator
与createRequestAnimationFrameGenerator
是关键,是魔法关键,我们来揭开面纱。
createTimeoutGenerator 的背后
因标题太长,应该是createTimeoutGenerator
与createRequestAnimationFrameGenerator
的背后。
createTimeoutGenerator
的代码:
其内部构造了一个具有 execute
与cancel
属性的对象,然后实例化了一个NextGenerator
, 也就是说,NextGenerator
才是核心。
exportfunctioncreateTimeoutGenerator(interval:number=1000){consttimeoutGenerator=function(cb:Function){letticket:number;functionexecute(){ticket=setTimeout(cb,interval);}return{execute,cancel:function(){clearTimeout(ticket);}}}constfactory=newNextGenerator(timeoutGenerator);returnfactory;}
迫不及待打开createRequestAnimationFrameGenerator
:
顿然醒悟,妙啊,秒啊。
exportfunctioncreateRequestAnimationFrameGenerator(){constrequestAnimationFrameGenerator=function(cb:FrameRequestCallback){letticket:any;functionexecute(){ticket=window.requestAnimationFrame(cb);}return{execute,cancel:function(){cancelAnimationFrame(ticket);}}}constfactory=newNextGenerator(requestAnimationFrameGenerator);returnfactory}
随心所欲的next
看完了createTimeoutGenerator
与createRequestAnimationFrameGenerator
。 你是不是可以大胆的认为,只要我构造一个对象有execute
与cancel
方法,就能弄出一个NextGenerator
, 然后嚣张的调用
start
cancel
continue
答案,是的。
我们不妨,现在造一个,时间翻倍的计时器, 第一次 100ms, 第二次200ms, 第二次 400ms, 依着葫芦画瓢:
exportfunctioncreateStepUpGenerator(interval:number=1000){conststepUpGenerator=function(cb:Function){letticket:any;functionexecute(){interval=interval*2;ticket=setTimeout(cb,interval);}return{execute,cancel:function(){clearTimeout(ticket);}}}constfactory=newNextGenerator(stepUpGenerator);returnfactory;}
interval
参数为第一次默认的初始值,之后翻倍。 一次执行一下看看结果。 测试代码:
constnextFactory=createStepUpGenerator(100);letlastTime=Date.now();nextFactory.start(function(this:any,next,...args:any[]){constnow=Date.now();console.log("time:",Date.now());console.log("costttime",now-lastTime);lastTime=now;console.log("");next();})
如你所愿,现在你可以为所欲为,你要你想得到,不管是 setTimeout
, requestAnimationFrame
, Promise
, async/await
等等,你都可以用来创造一个属于你自己节拍的定时器。
宏观思路
分析到这,这里说一下思路
面向next编程
依赖反转
组合优先于继承
面向next编程(迭代器)
这个叫,纯属我个人喜欢。 其属于迭代器模式。
我们调用一次后,需要在一定的时机后调用下一次,是不是 next
呢?
前端原生自带的有:
Iterator
Generator
可能有些人记不得了,我贴个Iterator
的代码吧:
classRangeIterator{constructor(start,stop){this.value=start;this.stop=stop;}[Symbol.iterator](){returnthis;}next(){varvalue=this.value;if(value<this.stop){this.value++;return{done:false,value:value};}return{done:true,value:undefined};}}functionrange(start,stop){returnnewRangeIterator(start,stop);}for(varvalueofrange(0,3)){console.log(value);//0,1,2}
前端框架 redux
的中间件,是不是也有那个next
。
至于后台服务的 express
与koa
,大家都熟悉,就不提了。
依赖反转
引用王争设计模式之美里面的话
高层模块(high-level modules)不要依赖低层模块(low-level)。高层模块和低层模块应该通过抽象(abstractions)来互相依赖。除此之外,抽象(abstractions)不要依赖具体实现细节(details),具体实现细节(details)依赖抽象(abstractions)。
所谓高层模块和低层模块的划分,简单来说就是,在调用链上,调用者属于高层,被调用者属于低层。
NextGenerator 就是高层模块,我们编写的具有execute
和cancel
属性的对象是低层模块。
NextGenerator 和具有execute
和cancel
属性的对象并没有直接的依赖关系,两者都依赖同一个“抽象”。
我们用TS来描述一下这个抽象: NextFnInfo
就这个抽象
<divclass="wrapper"><spanid="seconds">60</span><div><buttonid="btnPause">暂停</button><buttonid="btnContinue">继续</button></div></div><script>constsecondsEl=document.getElementById("seconds");constINTERVAL=1000;letticket;letseconds=60;functionsetSeconds(val){secondsEl.innerText=val;}functiononTimeout(){seconds--;setSeconds(seconds);ticket=setTimeout(onTimeout,INTERVAL);}ticket=setTimeout(onTimeout,INTERVAL);document.getElementById("btnPause").addEventListener("click",()=>{clearTimeout(ticket);});document.getElementById("btnContinue").addEventListener("click",()=>{ticket=setTimeout(onTimeout,INTERVAL);});</script>0
细心的肯定发现了,其实next
函数是还有context
和其他参数的,没错。
前面为了简化代码,都去掉了, context就是 start传入的回调函数的this
上下文。
<divclass="wrapper"><spanid="seconds">60</span><div><buttonid="btnPause">暂停</button><buttonid="btnContinue">继续</button></div></div><script>constsecondsEl=document.getElementById("seconds");constINTERVAL=1000;letticket;letseconds=60;functionsetSeconds(val){secondsEl.innerText=val;}functiononTimeout(){seconds--;setSeconds(seconds);ticket=setTimeout(onTimeout,INTERVAL);}ticket=setTimeout(onTimeout,INTERVAL);document.getElementById("btnPause").addEventListener("click",()=>{clearTimeout(ticket);});document.getElementById("btnContinue").addEventListener("click",()=>{ticket=setTimeout(onTimeout,INTERVAL);});</script>1
仔细看代码注释:
this 等于 context
param1与 param2被原封不动传递
其实,还有更进一层的信息, next
函数是可以重新传递 context与其他参数的。
再秀一把: 我们执行完毕后,next传递{ a: 10 }
作为上下文,下次调用检查a是不是等于10, 如果等于,停止调用。
<divclass="wrapper"><spanid="seconds">60</span><div><buttonid="btnPause">暂停</button><buttonid="btnContinue">继续</button></div></div><script>constsecondsEl=document.getElementById("seconds");constINTERVAL=1000;letticket;letseconds=60;functionsetSeconds(val){secondsEl.innerText=val;}functiononTimeout(){seconds--;setSeconds(seconds);ticket=setTimeout(onTimeout,INTERVAL);}ticket=setTimeout(onTimeout,INTERVAL);document.getElementById("btnPause").addEventListener("click",()=>{clearTimeout(ticket);});document.getElementById("btnContinue").addEventListener("click",()=>{ticket=setTimeout(onTimeout,INTERVAL);});</script>2
输出结果:
<divclass="wrapper"><spanid="seconds">60</span><div><buttonid="btnPause">暂停</button><buttonid="btnContinue">继续</button></div></div><script>constsecondsEl=document.getElementById("seconds");constINTERVAL=1000;letticket;letseconds=60;functionsetSeconds(val){secondsEl.innerText=val;}functiononTimeout(){seconds--;setSeconds(seconds);ticket=setTimeout(onTimeout,INTERVAL);}ticket=setTimeout(onTimeout,INTERVAL);document.getElementById("btnPause").addEventListener("click",()=>{clearTimeout(ticket);});document.getElementById("btnContinue").addEventListener("click",()=>{ticket=setTimeout(onTimeout,INTERVAL);});</script>3
组合优先于继承
实际上,完全可以写一个类,留有一些抽象的方法,然后重写。 但是我个人也是喜欢组合优先于继承的思路。
核心之NextGenerator
状态
我们实现说明一些规则
cancel
之后, next
不会触发下一次, 只能调用continue
恢复;
执行函数中,多次调用 next
只会生效一次
基于上,我们大致有几种关键状态
等待中,已经请求计划
执行中
取消
缓存参数
通过上面的代码,我们得知,我们是可以传递上下文和参数的,也还可以通过next
的参数覆盖的,所以我们要缓存这些参数。
上下文
更改函数的上下文有多种手段:
绑定到一个对象上
call
apply
箭头函数
bind
其他
我们这里采用的是bind,因为其返回的依旧是一个函数,提供了更多的操作空间。
代码全文
源码导读:
其最核心的代码是就是next
方法
其调用了NextFnGenerator
实例生成了一个新的对象NextFnInfo
的实例,其提供了获取下一次执行计划和取消下一次执行计划的方法。
其最精彩的是execute
方法
其被next
方法绑定了上下文,以及传入的所有参数。 这决定了它既能够和NextGenerator
实例交互,又能拿到所有的参数,执行回调函数。
一些TS申明:
<divclass="wrapper"><spanid="seconds">60</span><div><buttonid="btnPause">暂停</button><buttonid="btnContinue">继续</button></div></div><script>constsecondsEl=document.getElementById("seconds");constINTERVAL=1000;letticket;letseconds=60;functionsetSeconds(val){secondsEl.innerText=val;}functiononTimeout(){seconds--;setSeconds(seconds);ticket=setTimeout(onTimeout,INTERVAL);}ticket=setTimeout(onTimeout,INTERVAL);document.getElementById("btnPause").addEventListener("click",()=>{clearTimeout(ticket);});document.getElementById("btnContinue").addEventListener("click",()=>{ticket=setTimeout(onTimeout,INTERVAL);});</script>0interfaceNextFnGenerator{(...args:any[]):NextFnInfo;}enumEnumStatus{uninitialized=0,initialized,waiting,working,canceled,unkown}
核心类NextGenerator:
<divclass="wrapper"><spanid="seconds">60</span><div><buttonid="btnPause">暂停</button><buttonid="btnContinue">继续</button></div></div><script>constsecondsEl=document.getElementById("seconds");constINTERVAL=1000;letticket;letseconds=60;functionsetSeconds(val){secondsEl.innerText=val;}functiononTimeout(){seconds--;setSeconds(seconds);ticket=setTimeout(onTimeout,INTERVAL);}ticket=setTimeout(onTimeout,INTERVAL);document.getElementById("btnPause").addEventListener("click",()=>{clearTimeout(ticket);});document.getElementById("btnContinue").addEventListener("click",()=>{ticket=setTimeout(onTimeout,INTERVAL);});</script>5
总结
我们总是写代码,当写了两次或者多次同样的代码,那么就应该停下来思考思考,我们是不是哪里存在问题,有没有优化的空间。
曾今就写过一个简化setTimeout调用的库timeout, 那个时候的眼界和抽象还不够。 解决的问题也很局限。
最开始是想写 面向next编程以及实战的,涉及到太多的东西,比如 redux中间件,koa中间件, express中间件原理和实现等等。
太大了把握不住,那么分而治之,才有了这篇文章。
可以自己实现NextFnGenerator
,提供了比较高的定制能力
内置了createRequestAnimationFrameGenerator
, createTimeoutGenerator
, createStepUpGenerator
, 开箱即用
初始化和next都可以调整上下文和参数,增加调用的灵活性
仅仅暴露 start
, cancel
, continue
, 符合最少知道原则
存在的问题:
超时了怎么算
异常了怎么算
同步的Generator
怎么算
作者:云的世界