JavaScript Asynchronous Programming History of Web Front-end Framework Javascript

In the early web applications, when interacting with the background, it is necessary to submit the form form, and then give the user feedback results after the page is refreshed. During the page refresh process, the background will return a piece of HTML code. Most of the content in this HTML is basically the same as the previous page, which will inevitably cause a waste of traffic, and also prolong the response time of the page. It will make people feel that the experience of web applications is not as good as that of client applications.

In 2004, AJAX, or "Asynchronous JavaScript and XML" technology, was born, which improved the experience of Web applications. In 2006, jQuery came out, raising the development experience of web applications to a new level.

Due to the single-threaded feature of the JavaScript language, whether it is the triggering of events or AJAX, asynchronous tasks are triggered through callbacks. If we want to process multiple asynchronous tasks linearly, the following situation will appear in the code:

getUser(token, function (user) {

getClassID(user, function (id) {

getClassName(id, function (name) {

  console.log(name)

})

})

})

We often refer to this kind of code as: "callback hell".

Events and Callbacks

As we all know, the runtime of JavaScript runs on a single thread, and triggers asynchronous tasks based on the event model. There is no need to consider the issue of shared memory locking, and the bound events will be triggered neatly in order. To understand JavaScript's asynchronous tasks, we must first understand JavaScript's event model.

Because it is an asynchronous task, we need to organize a piece of code to run in the future (when the specified time ends or when an event is triggered), we usually put this piece of code in an anonymous function, usually called a callback function.

setTimeout(function () {

// Callback triggered when the specified time ends

}, 800)

window.addEventListener(“resize”, function() {

// Callback triggered when the browser window changes

})

future run

As mentioned above, the operation of the callback function is in the future, which means that the variables used in the callback are not fixed at the callback declaration stage.

for (var i = 0; i < 3; i++) {

setTimeout(function () {

console.log("i =", i)

}, 100)

}

Here three asynchronous tasks are declared in succession, and the result of the variable i will be output after 100 milliseconds. According to the normal logic, the three results of 0, 1, and 2 should be output.

However, this is not the case. This is also the problem we will encounter when we first come into contact with JavaScript, because the actual execution time of the callback function is in the future, so the output value of i is the value at the end of the loop, and the three asynchronous tasks The result is consistent, and three i = 3 will be output.
insert image description here
Students who have experienced this problem generally know that we can solve this problem by closing or redeclaring local variables.

event queue

After the event is bound, all callback functions will be stored, and then during the running process, another thread will schedule and process these asynchronously called callbacks. Once the "trigger" condition is met, the callback function will be placed in the The corresponding event queue (here it is simply understood as a queue, there are actually two event queues: macro task and micro task).

The trigger conditions are generally met in the following situations:

HarmonyOS official strategic cooperation and co-construction —— HarmonyOS technology community

Events triggered by DOM-related operations, such as click, move, out-of-focus, etc.;

IO-related operations, file reading completion, network request end, etc.;

Time-related operations, reaching the agreed time of the scheduled task;

When the above behaviors occur, the previously specified callback function in the code will be put into a task queue. Once the main thread is idle, the tasks in it will be executed one by one according to the first-in-first-out process. When a new event is triggered, it will be put back into the callback, and so on, so this mechanism of JavaScript is usually called the "event loop mechanism".

for (var i = 1; i <= 3; i++) {

const x = i

setTimeout(function () {

console.log(`第${x}个setTimout被执行`)

}, 100)

}

It can be seen that the running sequence satisfies the first-in-first-out feature of the queue, and the first statement is executed first.
insert image description here
thread blocking

Due to the single-threaded characteristics of JavaScript, timers are actually not reliable. When the code encounters a blocking situation, even if the event reaches the trigger time, it will wait until the main thread is idle before running.

const start = Date.now()

setTimeout(function () {

console.log(实际等待时间: ${Date.now() - start}ms)

}, 300)

// The while loop blocks the thread for 800ms

while(Date.now() - start < 800) {}

In the above code, the callback function is triggered after the timer is set for 300ms. If the code is not blocked, it will output the waiting time after 300ms under normal circumstances.

But we haven't added a while loop, and this loop will end after 800ms. The main thread has been blocked here by this loop, causing the callback function to not run normally when the time is up.
insert image description here
Promises

The way of event callback is particularly easy to cause callback hell during the coding process. And Promise provides a more linear way to write asynchronous code, somewhat similar to the pipeline mechanism.

// callback hell

getUser(token, function (user) {

getClassID(user, function (id) {

getClassName(id, function (name) {

  console.log(name)

})

})

})

// Promise

getUser(token).then(function (user) {

return getClassID(user)

}).then(function (id) {

return getClassName(id)

}).then(function (name) {

console.log(name)

}).catch(function (err) {

console.error('request exception', err)

})

Promise has similar implementations in many languages. During the development of JavaScript, the more famous frameworks jQuery and Dojo have also implemented similar implementations. In 2009, the CommonJS specification launched, based on the implementation of Dojo.Deffered, proposed the Promise/A specification. It was also this year that Node.js was born. Many implementations of Node.js are based on the CommonJS specification, and the more familiar one is its modularization scheme.

The Promise object was also implemented in the early Node.js, but in 2010, Ry (the author of Node.js) thought that Promise was a relatively high-level implementation, and the development of Node.js originally relied on the V8 engine, the V8 engine Promise support was not provided natively, so later Node.js modules used the error-first callback style (cb(error, result)).

const fs = require(‘fs’)

// The first parameter is the Error object, if it is not empty, it means an exception occurred

fs.readFile(‘./README.txt’, function (err, buffer) {

if (err !== null) {

return

}

console.log(buffer.toString())

})

This decision also led to the emergence of various Promise libraries in Node.js, the more famous ones being Q.js and Bluebird. Regarding the implementation of Promise, I wrote an article before. If you are interested, you can read it: "Teaching You to Realize Promise".

Before Node.js@8, V8's native Promise implementation had some performance problems, resulting in the performance of native Promise being even inferior to some third-party Promise libraries.
insert image description here
Therefore, in low-version Node.js projects, Promise is often replaced globally:

const Bulebird = require(‘bluebird’)

global.Promise = Bulebird

Generator & co

Generator (generator) is a new function type provided by ES6, which is mainly used to define a function that can iterate itself. A Generator function can be constructed through the syntax of function *. After the function is executed, an iteration (iterator) object will be returned. This object has a next() method. Every time the next() method is called, it will pause before the yield keyword until Call the next() method again.

function * forEach(array) {

const len = array.length

for (let i = 0; i < len; i ++) {

yield i;

}

}

const it = forEach([2, 4, 6])

it.next() // { value: 2, done: false }

it.next() // { value: 4, done: false }

it.next() // { value: 6, done: false }

it.next() // { value: undefined, done: true }

The next() method will return an object, which has two attributes value and done:

value: Indicates the value after yield;

done: Indicates whether the function has been executed;

Since the generator function has the characteristics of interrupt execution, the generator function is regarded as a container of asynchronous operation, and then method of Promise object can be used to hand over the execution right of asynchronous logic, and a Promise object is added after each yeild , the iterator can be executed continuously.

function * gen(token) {

const user = yield getUser(token)

const cId = yield getClassID(user)

const name = yield getClassName(cId)

console.log(name)

}

const g = gen(‘xxxx-token’)

// Execute the value returned by the next method as a Promise object

const { value: promise1 } = g.next()

promise1.then(user => {

// The value passed in the second next method will be accepted by the variable before the first yield keyword in the generator

// The same is true for pushing back, the value of the third next method will be accepted by the variable before the second yield

// Only the value of the first next method will be discarded

const { value: promise2 } = gen.next(user).value

promise2.then(cId => {

const { value: promise3, done } = gen.next(cId).value

// 依次先后传递,直到 next 方法返回的 done 为 true

})

})

Let's abstract the above logic, so that after each Promise object returns normally, it will automatically call next, and let the iterator execute itself until the execution is completed (that is, done is true).

function co(gen, …args) {

const g = gen(…args)

function next(data) {

const { value: promise, done } = g.next(data)

if (done) return promise

promise.then(res => {

  next(res) // 将 promise 的结果传入下一个 yield

})

}

next() // start self-execution

}

co(gen, ‘xxxx-token’)

This is the implementation logic of koa's early core library co, but co has performed some parameter verification and error handling. Adding co to the generator can make the asynchronous process more simple and easy to read, which is definitely a happy thing for developers.

async/await

async/await can be said to be the solution of JavaScript asynchronous transformation. In fact, it is essentially a syntactic sugar of Generator & co. You only need to add async before the asynchronous generator function, and then replace the yield in the generator function with await .

async function fun(token) {

const user = await getUser(token)

const cId = await getClassID(user)

const name = await getClassName(cId)

console.log(name)

}

fun()

The async function has a built-in self-executor, and the await is not limited to a Promise object, which can be any value, and the semantics of async/await are clearer than the yield of the generator, and it can be understood at a glance that this is an asynchronous operation.

Article source: The network copyright belongs to the original author

The above content is not for commercial purposes, if it involves intellectual property issues, please contact the editor, we will deal with it immediately

Guess you like

Origin blog.csdn.net/xuezhangmen/article/details/132017174