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

简介: 本着遇到问题,解决问题,记录方案,思考问题的原则,写一个专栏 从问题到提问, 欢迎大家关注。上一篇专栏的文章是 两个数组数据的高效合并方案。

1.JPG


前言


本着遇到问题,解决问题,记录方案,思考问题的原则,写一个专栏 从问题到提问, 欢迎大家关注。


上一篇专栏的文章是 两个数组数据的高效合并方案


先举个定时器的例子


每1秒执行一次,3次后,停止调用。


const nextFactory = createTimeoutGenerator();
let context = {
    counts: 0
};
nextFactory.start(function (this: any, next: Function) {
    context.counts ++;
    console.log("counts", context.counts);
    if(context.counts > 3){
        nextFactory.cancel();
    }
    next();
}, context);
复制代码

2.JPG


定时器



前端常见三大定时器setTimeout, setInterval, requestAnimationFrame

setInterval的坑不是本文讨论的重点,所以剩下的选择是 setTimeout, requestAnimationFrame


有很多时候,我们需要多次调用定时器,比如验证码倒计时,canvas绘制。 基本都是处理完数据后,进入下一个周期, 我们一起看看例子。


定时器应用


setTimeout


我们用原生代码实现一个60秒倒计时,并支持暂停,继续的功能,来看一看代码: 大概是下面这个样子:


3.JPG

<div class="wrapper">
        <span id="seconds">60</span>
        <div>
            <button id="btnPause">暂停</button>
            <button id="btnContinue">继续</button>
        </div>
    </div>
    <script>
        const secondsEl = document.getElementById("seconds");
        const INTERVAL = 1000;
        let ticket;
        let seconds = 60;
        function setSeconds(val) {
            secondsEl.innerText = val;
        }
        function onTimeout() {
            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. INTERVAL,ticketsetTimeout满天飞, 不够高雅,我们应该更关心业务的处理;
  2. 有多处类似的逻辑,就得重复的写setTimeout,缺少复用;
  3. 语义不好


当然,大家肯定都有自己的封装,我这里要解决的是定时器的封装,与页面和逻辑无关。


我们不妨再看一段代码:


一样的功能,看起来简洁很多,而且语义很清晰。


  • start: 开始
  • cancel: 取消
  • continue: 继续


<div class="wrapper">
        <span id="seconds">60</span>
        <div>
            <button id="btnPause">暂停</button>
            <button id="btnContinue">继续</button>
        </div>
    </div>
    <script src="../dist/index.js"></script>
    <script>
        const nextFactory = createTimeoutGenerator();
        const secondsEl = document.getElementById("seconds");
        let seconds = 60;
        function setSeconds(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绘制的例子,我们每隔一个绘制周期,就把当前的时间戳画在画布上。 大概是这个样子:


4.JPG


同样的,可以暂停和继续。


  • drawTime 绘制时间
  • requestAnimationFrame 启动定时器
  • 两个按钮的点击事件,分别处理暂停和继续


先一起来看看原生JS的基础版本:


<div style="margin: 50px;">
        <canvas id="canvas" height="300" width="300"></canvas>
    </div>
    <div>
        <div>
            <button id="btnPause">暂停</button>
            <button id="btnContinue">继续</button>
        </div>
    </div>
    <script>
        let ticket;
        const canvasEl = document.getElementById("canvas");
        const ctx = canvasEl.getContext("2d");
        ctx.fillStyle = "#f00";
        ctx.fillRect(0, 0, 300, 300);
        function drawTime() {
            ctx.clearRect(0, 0, 300, 300);
            ctx.fillStyle = "#f00";
            ctx.fillRect(0, 0, 300, 300);
            ctx.fillStyle = "#000";
            ctx.font = "bold 20px Arial";
            ctx.fillText(Date.now(), 100, 100);
        }
        function onRequestAnimationFrame() {
            drawTime();
            ticket = requestAnimationFrame(onRequestAnimationFrame);
        }
        ticket = requestAnimationFrame(onRequestAnimationFrame);
        document.getElementById("btnPause").addEventListener("click", () => {
            cancelAnimationFrame(ticket);
        });
        document.getElementById("btnContinue").addEventListener("click", () => {
            requestAnimationFrame(onRequestAnimationFrame);
        });
    </script>
复制代码


问题依旧,我们看看另外一个版本:


const nextFactory = createRequestAnimationFrameGenerator();
    const canvasEl = document.getElementById("canvas");
    const ctx = canvasEl.getContext("2d");
    ctx.fillStyle = "#f00";
    ctx.fillRect(0, 0, 300, 300);
    function drawTime() {
        ctx.clearRect(0, 0, 300, 300);
        ctx.fillStyle = "#f00";
        ctx.fillRect(0, 0, 300, 300);
        ctx.fillStyle = "#000";
        ctx.font = "bold 20px Arial";
        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才是核心。


export function createTimeoutGenerator(interval: number = 1000) {
    const timeoutGenerator = function (cb: Function) {
        let ticket: number;
        function execute() {
            ticket = setTimeout(cb, interval);
        }
        return {
            execute,
            cancel: function () {
                clearTimeout(ticket);
            }
        }
    }
    const factory = new NextGenerator(timeoutGenerator);
    return factory;
}
复制代码


迫不及待打开createRequestAnimationFrameGenerator:

顿然醒悟,妙啊,秒啊。


export function createRequestAnimationFrameGenerator() {
    const requestAnimationFrameGenerator = function (cb: FrameRequestCallback) {
        let ticket: any;
        function execute() {
            ticket = window.requestAnimationFrame(cb);
        }
        return {
            execute,
            cancel: function () {
                cancelAnimationFrame(ticket);
            }
        }
    }
    const factory = new NextGenerator(requestAnimationFrameGenerator);
    return factory
}
复制代码


随心所欲的next


看完了createTimeoutGeneratorcreateRequestAnimationFrameGenerator。 你是不是可以大胆的认为,只要我构造一个对象有executecancel方法,就能弄出一个NextGenerator, 然后嚣张的调用


  • start
  • cancel
  • continue


答案,是的。


我们不妨,现在造一个,时间翻倍的计时器, 第一次 100ms, 第二次200ms, 第二次 400ms, 依着葫芦画瓢:


export function createStepUpGenerator(interval: number = 1000) {
    const stepUpGenerator = function (cb: Function) {
        let ticket: any;
        function execute() {
            interval = interval * 2;
            ticket = setTimeout(cb, interval);
        }
        return {
            execute,
            cancel: function () {
                clearTimeout(ticket);
            }
        }
    }
    const factory = new NextGenerator(stepUpGenerator);
    return factory;
}
复制代码


interval参数为第一次默认的初始值,之后翻倍。 一次执行一下看看结果。

测试代码:


const nextFactory = createStepUpGenerator(100);
let lastTime = Date.now();
nextFactory.start(function (this: any, next, ...args: any[]) {
    const now = Date.now();
    console.log("time:", Date.now());
    console.log("costt time", now - lastTime);
    lastTime = now;
    console.log(" ");
    next();  
})
复制代码

5.JPG


如你所愿,现在你可以为所欲为,你要你想得到,不管是 setTimeout, requestAnimationFramePromise, async/await等等,你都可以用来创造一个属于你自己节拍的定时器。


宏观思路


分析到这,这里说一下思路


  1. 面向next编程
  2. 依赖反转
  3. 组合优先于继承


面向next编程(迭代器)


这个叫,纯属我个人喜欢。 其属于迭代器模式。


我们调用一次后,需要在一定的时机后调用下一次,是不是 next 呢?


前端原生自带的有:

  1. Iterator
  2. Generator


可能有些人记不得了,我贴个Iterator的代码吧:


class RangeIterator {
  constructor(start, stop) {
    this.value = start;
    this.stop = stop;
  }
  [Symbol.iterator]() { return this; }
  next() {
    var value = this.value;
    if (value < this.stop) {
      this.value++;
      return {done: false, value: value};
    }
    return {done: true, value: undefined};
  }
}
function range(start, stop) {
  return new RangeIterator(start, stop);
}
for (var value of range(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就这个抽象


interface Unsubscribe {
    (): void
}
interface CallbackFunction<T = any> {
    (context: T, ...args: any[]): void
}
interface NextFnInfo<T = any> {
    cancel: Unsubscribe
    execute: (next: CallbackFunction<T>) => any
}
复制代码


细心的肯定发现了,其实next函数是还有context和其他参数的,没错。

前面为了简化代码,都去掉了, context就是 start传入的回调函数的this上下文。


const nextFactory = createTimeoutGenerator();
let context = {
    val: 0
};
nextFactory.start(function (this: any, next, ...args: any[]) {
    console.log("this", this);  // this { val: 0 }
    console.log("args", ...args); // args param1 param2
    nextFactory.cancel();
}, context, "param1", "param2")
复制代码


仔细看代码注释:


  1. this 等于 context
  2. param1与 param2被原封不动传递


其实,还有更进一层的信息, next 函数是可以重新传递 context与其他参数的。

再秀一把:


我们执行完毕后,next传递{ a: 10 }作为上下文,下次调用检查a是不是等于10, 如果等于,停止调用。


const nextFactory = createTimeoutGenerator();
let context = {
    val: 0
};
nextFactory.start(function (this: any, next, ...args: any[]) {
    console.log("this", this);  // this { val: 0 }
    console.log("args", ...args); // args param1 param2
    next({ a: 10 }, "param-1", "param-2");
    if (this.a === 10) {
        nextFactory.cancel();
    }
}, context, "param1", "param2")
复制代码

输出结果:

this { val: 0 }
args param1 param2
this { a: 10 }
args param-1 param-2
复制代码


组合优先于继承


实际上,完全可以写一个类,留有一些抽象的方法,然后重写。

但是我个人也是喜欢组合优先于继承的思路。


核心之NextGenerator


状态


我们实现说明一些规则


  1. cancel 之后, next 不会触发下一次, 只能调用continue 恢复;
  2. 执行函数中,多次调用 next 只会生效一次


基于上,我们大致有几种关键状态


  1. 等待中,已经请求计划
  2. 执行中
  3. 取消


缓存参数


通过上面的代码,我们得知,我们是可以传递上下文和参数的,也还可以通过next的参数覆盖的,所以我们要缓存这些参数。


上下文


更改函数的上下文有多种手段:


  1. 绑定到一个对象上
  2. call
  3. apply
  4. 箭头函数
  5. bind
  6. 其他


我们这里采用的是bind,因为其返回的依旧是一个函数,提供了更多的操作空间。


代码全文


源码导读:


  1. 其最核心的代码是就是next方法

其调用了NextFnGenerator实例生成了一个新的对象NextFnInfo的实例,其提供了获取下一次执行计划和取消下一次执行计划的方法。


  1. 其最精彩的是execute方法

其被next方法绑定了上下文,以及传入的所有参数。

这决定了它既能够和NextGenerator实例交互,又能拿到所有的参数,执行回调函数。


一些TS申明:


interface Unsubscribe {
    (): void
}
interface CallbackFunction<T = any> {
    (context: T, ...args: any[]): void
}
interface NextFnInfo<T = any> {
    cancel: Unsubscribe
    execute: (next: CallbackFunction<T>) => any
}
interface NextFnGenerator {
    (...args: any[]): NextFnInfo;
}
enum EnumStatus {
    uninitialized = 0,
    initialized,
    waiting,
    working,
    canceled,
    unkown
}
复制代码


核心类NextGenerator:


export default class NextGenerator<T = any> {
    private status: EnumStatus = EnumStatus.uninitialized;
    private nextInfo!: NextFnInfo;
    // 传入的回调函数
    private cb!: CallbackFunction;
    // 下次回调函数的参数
    private args: any[] = [];
    constructor(private generator: NextFnGenerator) {
        this.status = EnumStatus.initialized;
    }
    private next(...args: any[]) {
        if (this.status === EnumStatus.canceled) {
            return console.warn("current status is canceled, please call continute method to continue");
        }
        if (this.status === EnumStatus.waiting) {
            return console.warn("current status is waiting, please don't multiple call next method");
        }
        if (args.length > 0) {
            this.args = args;
        }
        // this.args[0] context
        const boundFn = this.execute.bind(this, this.cb, ...this.args);
        this.nextInfo = this.generator(boundFn);
        this.status = EnumStatus.waiting;
        this.nextInfo.execute(undefined as any);
    }
    private execute(this: NextGenerator<T>, cb: Function, context: T, ...args: any[]) {
        this.status = EnumStatus.working;
        cb.apply(context, [this.next.bind(this), ...args]);
    }
    cancel() {
        this.status = EnumStatus.canceled;
        if (this.nextInfo && typeof this.nextInfo.cancel === "function") {
            this.nextInfo.cancel();
        }
    }
    start(cb: CallbackFunction, ...args: any[]) {
        if (typeof cb === "function") {
            this.cb = cb;
        }
        if (typeof this.cb !== "function") {
            throw new SyntaxError("param cb must be a function");
        }
        if (args.length > 0) {
            this.args = args;
        }
        this.next();
    }
    continue() {
        this.status = EnumStatus.initialized;
        this.next();
    }
}
复制代码


总结


我们总是写代码,当写了两次或者多次同样的代码,那么就应该停下来思考思考,我们是不是哪里存在问题,有没有优化的空间。


曾今就写过一个简化setTimeout调用的库timeout, 那个时候的眼界和抽象还不够。 解决的问题也很局限。


最开始是想写 面向next编程以及实战的,涉及到太多的东西,比如 redux中间件,koa中间件, express中间件原理和实现等等。


太大了把握不住,那么分而治之,才有了这篇文章。


  1. 可以自己实现NextFnGenerator,提供了比较高的定制能力
  2. 内置了createRequestAnimationFrameGenerator, createTimeoutGenerator, createStepUpGenerator, 开箱即用
  3. 初始化和next都可以调整上下文和参数,增加调用的灵活性
  4. 仅仅暴露 start, cancel, continue, 符合最少知道原则


存在的问题:


  1. 超时了怎么算
  2. 异常了怎么算
  3. 同步的Generator怎么算


写在最后


欢迎关注专栏 从问题到提问 ,一起交流和学习。

写作不易,您的一赞一评就是我前行的动力。

相关文章
|
5月前
|
存储 算法 C语言
二分查找算法的概念、原理、效率以及使用C语言循环和数组的简单实现
二分查找算法的概念、原理、效率以及使用C语言循环和数组的简单实现
|
5月前
|
Java
java实现斐波那契数列(递归、迭代、流)
java实现斐波那契数列(递归、迭代、流)
|
6月前
|
存储 编译器 C++
C++:迭代器的封装思想
C++:迭代器的封装思想
34 0
|
6月前
|
算法 Java 程序员
Java数组全套深入探究——基础知识阶段4、数组的遍历
Java数组全套深入探究——基础知识阶段4、数组的遍历
67 0
|
Python
while循环的妙用
while循环的妙用
89 1
|
前端开发
前端学习案例9-数组遍历方法1-数组方法回顾
前端学习案例9-数组遍历方法1-数组方法回顾
67 0
前端学习案例9-数组遍历方法1-数组方法回顾
|
C++
【c++】:反向迭代器适配器:每天学一点点优秀的思想
【c++】:反向迭代器适配器:每天学一点点优秀的思想
85 0
|
算法 C++
【STL终极奥义❀解耦合思想的实现❀】函数对象、谓词与函数适配器——从for_each、transform、count_if、sort算法源码的角度分析(三)
【STL终极奥义❀解耦合思想的实现❀】函数对象、谓词与函数适配器——从for_each、transform、count_if、sort算法源码的角度分析
130 0
【STL终极奥义❀解耦合思想的实现❀】函数对象、谓词与函数适配器——从for_each、transform、count_if、sort算法源码的角度分析(三)
|
JavaScript 前端开发 中间件
一文彻底搞懂迭代器与生成器函数
参考mdn上解释,迭代器是一个对象,每次调用next方法返回一个{done: false, value: ''},每次调用next返回当前值,直至最后一次调用时返回 {value:undefined,done: true}时结束,无论后面调用next方法都只会返回{value: undefined,done:true}
163 0
一文彻底搞懂迭代器与生成器函数
|
算法 前端开发 JavaScript
【前端算法】定义一个JS函数,反转单向链表
介绍链表与数组的区别,以及它们之间的联系