首页>>前端>>JavaScript->依赖反转+迭代器思想,实现setTimeout面向next编程

依赖反转+迭代器思想,实现setTimeout面向next编程

时间:2023-11-29 本站 点击:0

先举个定时器的例子

每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,ticketsetTimeout满天飞, 不够高雅,我们应该更关心业务的处理;

有多处类似的逻辑,就得重复的写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();});

这里大家都注意到了,createTimeoutGeneratorcreateRequestAnimationFrameGenerator 是关键,是魔法关键,我们来揭开面纱。

createTimeoutGenerator 的背后

因标题太长,应该是createTimeoutGeneratorcreateRequestAnimationFrameGenerator的背后。

createTimeoutGenerator的代码:

其内部构造了一个具有 executecancel属性的对象,然后实例化了一个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

看完了createTimeoutGeneratorcreateRequestAnimationFrameGenerator。 你是不是可以大胆的认为,只要我构造一个对象有executecancel方法,就能弄出一个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, requestAnimationFramePromise, 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

至于后台服务的 expresskoa,大家都熟悉,就不提了。

依赖反转

引用王争设计模式之美里面的话

高层模块(high-level modules)不要依赖低层模块(low-level)。高层模块和低层模块应该通过抽象(abstractions)来互相依赖。除此之外,抽象(abstractions)不要依赖具体实现细节(details),具体实现细节(details)依赖抽象(abstractions)。

所谓高层模块和低层模块的划分,简单来说就是,在调用链上,调用者属于高层,被调用者属于低层。

NextGenerator 就是高层模块,我们编写的具有executecancel属性的对象是低层模块。

NextGenerator 和具有executecancel属性的对象并没有直接的依赖关系,两者都依赖同一个“抽象”。

我们用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怎么算

作者:云的世界


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:/JavaScript/690.html