The history of JavaScript asynchronous programming

Shenfq
中文

Preface

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

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

Due to the single-threaded nature of the JavaScript language, whether it is event triggering 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 call this kind of code: "callback hell".

Events and callbacks

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

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

setTimeout(function () {
  // 在指定时间结束时,触发的回调
}, 800)
window.addEventListener("resize", function() {
  // 当浏览器视窗发生变化时,触发的回调
})

Running in the future

As mentioned earlier, the callback function runs in the future, which means that the variables used in the callback are not fixed during the callback declaration stage.

for (var i = 0; i < 3; i++) {
  setTimeout(function () {
    console.log("i =", i)
  }, 100)
}

Here declares three consecutive asynchronous tasks, 100 milliseconds will output variable after i result, according to the normal output logic should 0、1、2 three results.

However, this is not the case. This is also a problem we encountered when we first started contacting JavaScript. Because the actual running time of the callback function is in the future, the output i the value at the end of the loop, which is the value of the three asynchronous tasks. The results are consistent, and three i = 3 will be output.

Students who have experienced this problem generally know that we can solve this problem by means of closures or redeclaring local variables.

Event queue

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

The trigger conditions are generally met in the following situations:

  1. Events triggered by DOM-related operations, such as clicking, moving, out of focus, etc.;
  2. IO-related operations, file read completion, network request completion, etc.;
  3. Time-related operations arrive at the appointed time of the timed task;

When the above behaviors occur, the callback function specified 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 first-in first-out When a new event is triggered, it will be put back into the callback again, so looping 🔄, 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 meets the first-in-first-out characteristics of the queue, and the first declared first is executed first.

Thread blocking

Due to the single-threaded nature of JavaScript, the timer is 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)

// while循环让线程阻塞 800ms
while(Date.now() - start < 800) {}

In the above code, the 300ms . If the code is not blocked, it will output the waiting time after 300ms

But we haven't added a while loop. This loop will 800ms . The main thread has been blocked by this loop, causing the callback function to not run normally when the time is up.

Promise

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

// 回调地狱
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('请求异常', err)
})

Promises have similar implementations in many languages. During the development of JavaScript, the well-known frameworks jQuery and Dojo have also implemented similar implementations. In 2009, the launch of the CommonJS specification, based on Dojo.Deffered implementation of the proposed Promise/A specification. It was also this year that Node.js turned out. Many implementations of Node.js are based on CommonJS specifications. The more familiar one is its modular solution.

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

const fs = require('fs')
// 第一个参数为 Error 对象,如果不为空,则表示出现异常
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 are Q.js and Bluebird . Regarding the realization of Promise, I have written an article before. If you are interested, you can check it out: "Teach you how to realize Promise" .

Before Node.js@8, V8's native Promise implementation had some performance problems, resulting in the performance of native Promises being inferior to some third-party Promise libraries.

Therefore, in low-version Node.js projects, Promises are often replaced globally:

const Bulebird = require('bluebird')
global.Promise = Bulebird

Generator & co

Generator is a new function type provided by ES6, which is mainly used to define a function that can be iterated by itself. By function * syntax can construct a Generator function, the function will return the implementation of a the Iteration (iterator) object that has a next() method, each call next() method will be in yield pause before the key until you call again next() method .

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 }

next() method returns an object, which has two attributes value and done :

  • value : represents the value after yield
  • done : Indicates whether the function has been executed;

Since the generator function has the characteristic of interrupting execution, the generator function is regarded as an asynchronous operation container, and the then method of the Promise object can return the execution right of the asynchronous logic. A Promise object is added yeild , You can make the iterator execute 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')

// 执行 next 方法返回的 value 为一个 Promise 对象
const { value: promise1 } = g.next()
promise1.then(user => {
  // 传入第二个 next 方法的值,会被生成器中第一个 yield 关键词前面的变量接受
  // 往后推也是如此,第三个 next 方法的值,会被第二个 yield 前面的变量接受
  // 只有第一个 next 方法的值会被抛弃
  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. After each Promise object returns normally, it will automatically call next and let the iterator execute itself until the execution is complete (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() // 开始自执行
}

co(gen, 'xxxx-token')

This is koa early core library co implement the logic, but co carried out some parameters check and error handling. The addition of co to the generator can make the asynchronous process easier to read, which is definitely a joy for developers.

async/await

async/await can be said to become asynchronous JavaScript solutions, in fact, it is essentially a syntactic sugar Generator & co, just precede asynchronous generator function async , and then in the generator function yield replaced await .

async function fun(token) {
  const user = await getUser(token)
  const cId = await getClassID(user)
  const name = await getClassName(cId)
  console.log(name)
}

fun()

async function has a built-in self-executor. At the same time, await , it is not limited to a Promise object and can have any value. Moreover, async/await is semantically clearer than the yield of the generator, and you can understand at a glance that this is an asynchronous operation.

阅读 1.3k

自然醒的笔记本
学习过程中的一些总结和沉淀,欢迎关注公众号「自然醒的笔记本」
3.9k 声望
6.8k 粉丝
0 条评论
3.9k 声望
6.8k 粉丝
文章目录
宣传栏