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,
INTERVAL
.ticket
andsetTimeout
Flying, not elegant enough, we should be more concerned with the handling of business;- There are many similar logic, so it has to be written repeatedly
setTimeout
, lack of reuse; - 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
- Programming for Next
- Rely on reverse
- 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:
- Iterator
- 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:
- This is equal to the context
- 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
cancel
After that,next
It doesn’t trigger the next time. It can only callcontinue
Recovery;- In the execution function, call multiple times
next
It only works once
Based on this, we have several key states
- Waiting, plan requested
- The execution of
- 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:
- Bind to an object
- call
- apply
- Arrow function
- bind
- other
We use bind because it still returns a function and provides more room for manipulation.
Full text code
Source guide:
- The core code is
next
methods
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.
- And here’s the best part
execute
methods
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.
- You can do it yourself
NextFnGenerator
, which provides a relatively high customization capability - The built-in
createRequestAnimationFrameGenerator
.createTimeoutGenerator
.createStepUpGenerator
Out of the box - Both initialization and next can adjust the context and parameters, increasing the flexibility of the call
- Only exposed
start
.cancel
.continue
In accordance with the least known principle
Existing problems:
- What’s the timeout
- How to calculate abnormal
- synchronous
Generator
How 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.