This article has participated in the good article call order activity, click to see: back end, big front end double track submission, 20,000 yuan prize pool for you to challenge!

preface

In line with the principle of encountering problems, solving problems, recording solutions and thinking about problems, I will write a column from problems to questions. Welcome your attention.

My last column was about an efficient way to merge two arrays of data.

Let’s take an example of a timer

The call is executed once every second. After three times, the call is stopped.

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);
Copy the code

The timer

There are three common front-end timers: setTimeout, setInterval, and requestAnimationFrame

The pit for setInterval is not the focus of this article, so the remaining option is setTimeout, requestAnimationFrame.

Many times, we need to call the timer multiple times, such as captcha countdown, Canvas drawing. Basically, after you’ve processed the data, you go to the next cycle, so let’s look at an example.

Timer application

setTimeout

We use native code to implement a 60 second countdown and support pause and continue. Take a look at the code:

 <div class="wrapper">
        <span id="seconds">60</span>
        <div>
            <button id="btnPause">suspended</button>
            <button id="btnContinue">Continue to</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>
Copy the code

Yes, yes? I think there is,

  1. INTERVAL.ticketandsetTimeoutFlying, not elegant enough, we should be more concerned with the handling of business;
  2. There are many similar logic, so it has to be written repeatedlysetTimeout, lack of reuse;
  3. Semantics is bad

Of course, everyone has their own encapsulation, I want to solve here is the timer encapsulation, has nothing to do with page and logic.

Let’s look at one more piece of code: same functionality, looks much cleaner, and the semantics are clear.

  • Start: start
  • Cancel: cancel
  • Continue, continue to
    <div class="wrapper">
        <span id="seconds">60</span>
        <div>
            <button id="btnPause">suspended</button>
            <button id="btnContinue">Continue to</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>
Copy the code

requestAnimationFrame

Let’s look at a canvas drawing example. We draw the current timestamp on the canvas every other drawing cycle. It looks something like this:

Again, you can pause and continue.

  • DrawTime drawTime
  • RequestAnimationFrame Starts the timer
  • Two button click events, one for pause and one for continue

Let’s take a look at the base version of native JS:

    <div style="margin: 50px;">
        <canvas id="canvas" height="300" width="300"></canvas>
    </div>
    <div>
        <div>
            <button id="btnPause">suspended</button>
            <button id="btnContinue">Continue to</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>
Copy the code

As usual, let’s look at another version:

    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();
    });

Copy the code

Here everyone noticed that createTimeoutGenerator and createRequestAnimationFrameGenerator is the key, is the magic key, we come to the veil.

CreateTimeoutGenerator behind

Because of the title is too long, should be createTimeoutGenerator and createRequestAnimationFrameGenerator behind.

Code for createTimeoutGenerator:

Internally, an object is constructed with execute and cancel properties, and then a NextGenerator is instantiated. That is, NextGenerator is the core.

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;
}
Copy the code

Can’t wait to open the createRequestAnimationFrameGenerator:

Suddenly woke up, wonderful, seconds ah.

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
}
Copy the code

Next at will

After reading the createTimeoutGenerator createRequestAnimationFrameGenerator. Would you be so bold as to think that if I construct an object with execute and cancel methods, that I can make a NextGenerator and then call it like crazy

  • start
  • cancel
  • continue

The answer, yes.

Let’s make a timer that doubles in time, 100ms for the first time, 200ms for the second time, 400ms for the second time, and follow the gourds:

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;
}
Copy the code

The interval parameter is the default initial value for the first time and then doubled. Let’s do it at a time and see what happens. Test code:

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();  
})
Copy the code

As you wish, now you can do whatever you want, whether it’s setTimeout, requestAnimationFrame, Promise, async/await, etc. You can use it to create a timer with your own beat.

Macro way of thinking

So here’s the idea

  1. Programming for Next
  2. Rely on reverse
  3. Composition takes precedence over inheritance

Programming for Next (iterators)

It’s called, it’s my personal favorite. It belongs to the iterator pattern.

After we call it once, we need to call it again after a certain amount of time, is it next?

Front-end native with:

  1. Iterator
  2. Generator

For those of you who don’t remember, let me post the Iterator code:

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
}
Copy the code

Front-end framework redux middleware, is there that next.

As for the back-end service Express and KOA, everyone is familiar with, let’s not mention.

Rely on reverse

To quote the beauty of wang’s design pattern

Do not rely on low-level modules. High-level and low-level modules should rely on each other through abstractions. In addition to this, abstractions should not be dependent on implementation details as details depend on abstractions.

The so-called division of high-level module and low-level module, simply put, in the call chain, the caller belongs to the high-level, the called belongs to the low-level.

NextGenerator is the high-level module, and the objects we write with execute and cancel properties are the low-level modules.

There is no direct dependency between NextGenerator and objects with execute and cancel properties; both depend on the same “abstraction.”

Let’s use TS to describe this abstraction: NextFnInfo is the abstraction

interface Unsubscribe {
    (): void
}

interfaceCallbackFunction<T = any> { (context: T, ... args:any[]) :void
}

interface NextFnInfo<T = any> {
    cancel: Unsubscribe
    execute: (next: CallbackFunction<T>) = > any
}
Copy the code

You must have noticed that the next function does have a context and other parameters, yes.

This is the this context of the callback function that is passed in to start.

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")
Copy the code

Look closely at the code comment:

  1. This is equal to the context
  2. Param1 and param2 are passed unchanged

As a further step, the next function can repass the context and other parameters.

Another demonstration: after we finish, next passes {a: 10} as context, and the next call checks to see if a is equal to 10, and if so, stops the call.

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")
Copy the code

Output results:

this { val: 0 }
args param1 param2
this { a: 10 }
args param-1 param-2
Copy the code

Composition takes precedence over inheritance

In fact, it’s perfectly possible to write a class, leave some abstract methods, and override them. But I personally like the idea of combination over inheritance.

The core of NextGenerator

state

We implement some rules

  1. cancelAfter that,nextIt doesn’t trigger the next time. It can only callcontinueRecovery;
  2. In the execution function, call multiple timesnextIt only works once

Based on this, we have several key states

  1. Waiting, plan requested
  2. The execution of
  3. cancel

Cache parameters

From the above code, we know that we can pass the context and parameters, which can also be overridden by the next parameter, so we need to cache these parameters.

context

There are several ways to change the context of a function:

  1. Bind to an object
  2. call
  3. apply
  4. Arrow function
  5. bind
  6. other

We use bind because it still returns a function and provides more room for manipulation.

Full text code

Source guide:

  1. The core code isnextmethods

It calls the NextFnGenerator instance to generate a new instance of the object NextFnInfo that provides methods to get the next execution plan and cancel the next execution plan.

  1. And here’s the best partexecutemethods

It is bound by the next method to the context, as well as any parameters passed in. This allows it to interact with the NextGenerator instance, take all the parameters, and execute the callback function.

Some TS statements:

interface Unsubscribe {
    (): void
}

interfaceCallbackFunction<T = any> { (context: T, ... args:any[]) :void
}

interface NextFnInfo<T = any> {
    cancel: Unsubscribe
    execute: (next: CallbackFunction<T>) = > any
}

interfaceNextFnGenerator { (... args:any[]): NextFnInfo;
}

enum EnumStatus {
    uninitialized = 0,
    initialized,
    waiting,
    working,
    canceled,
    unkown
}
Copy the code

Core class NextGenerator:

export default class NextGenerator<T = any> {

    private status: EnumStatus = EnumStatus.uninitialized;
    privatenextInfo! : NextFnInfo;// The callback function passed in
    privatecb! : CallbackFunction;// The next callback function parameter
    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(); }}Copy the code

conclusion

We write code all the time, and when we write the same code twice or more times, it’s time to stop and think, is there a problem, is there room for improvement?

I’ve written a library called Timeout that simplifies setTimeout calls, but at the time the vision and abstraction weren’t enough. The problems are limited.

At the beginning, I wanted to write programming and actual combat for next, involving too many things, such as redux middleware, KOA middleware, Express middleware principle and implementation, and so on.

Too big to grasp, so divide and conquer, there is this article.

  1. You can do it yourselfNextFnGenerator, which provides a relatively high customization capability
  2. The built-increateRequestAnimationFrameGenerator.createTimeoutGenerator.createStepUpGeneratorOut of the box
  3. Both initialization and next can adjust the context and parameters, increasing the flexibility of the call
  4. Only exposedstart.cancel.continueIn accordance with the least known principle

Existing problems:

  1. What’s the timeout
  2. How to calculate abnormal
  3. synchronousGeneratorHow to calculate

Write in the last

Welcome to the column from questions to questions, exchange and learn together.

Writing is not easy, your praise and comment is my motivation.