Blog

懒癌晚期


Project maintained by VirusPC Hosted on GitHub Pages — Theme by mattgraham

Back Home

Introduction to Asynchronous Programming


Throughout this chapter, examples make extensive use of asynchronous logging setTimeout(console.log, 0, ...params) to demonstrate order of operation and other asynchronous behavior characteristics.

Note: A browser’s console output will often print information about objects that is not otherwise available to the JavaScript runtime (such as the state of a promise).

Fundamental Concept

  1. 顺序(sequntial)执行并发(concurrent)执行 关注的是程序的执行顺序. 顺序执行是指一个任务从执行开始到结束始终占用该进程. 上一个开始执行的任务完成后, 当前任务才能执行, 任务严格按顺序执行. 并发执行是指无论上一个开始执行的任务是否完成, 当前任务都可以开始执行. 并发执行要求这些任务的执行顺序不影响最终结果, 任务轮流执行, 多个任务可以在同一时间段内发生.

  2. 串行(serial)并行(paralell) 关注的是执行任务的能力. 串行是指同一时刻只能执行一个任务. 并行是指同一时刻可以执行多个任务.

  3. 同步(synchronous)异步(asynchronous) 关注的是消息通信机制. 对于消息发送方, 所谓同步,就是在发出一个调用时,在没有得到结果之前, 该调用就不返回。异步则是相反,调用在发出之后,这个调用就直接返回了,所以没有返回结果.

  4. 同步也被称为 阻塞(blocking), 异步也被称为 非阻塞(nonblocking). 阻塞或非阻塞是针对单个进程而言的, 消息的发送方进程和接收方进程区别对待(blocking/nobloking 与 send/receive 组合, 四种, 见操作系统相关书籍), 本文中只针对发送方.

  5. 并行一定可以并发. 异步(非阻塞)是实现并发的手段之一.

Synchronous vs. Asynchronous JavaScript

let x = 3;
setTimeout(() => x = x + 4, 1000);
  1. The need of asynchronous behavior: Asynchronous behavior is borne out of the need to optimize for higher computational throughput in the face of high-latency operations. 比如, 把IO操作做成异步的. 这样当主进程执行到IO语句时, 通知另一个进程执行IO操作, 而自己不等待IO结果就继续执行剩下的语句. 当另一个进程的IO执行完毕后再将结果返回给主进程, 主进程再根据结果执行后续异步相关代码(如果主进程需要结果的话). 这样就避免了主进程一直等待耗时的IO操作, 先去执行与异步行为无关的其他操作.

  2. Drawback: You are generally unable to assert when the system state will change after the callback is scheduled.

  3. 浏览器中一个页面只有一个线程来执行脚本, 是串行的. 浏览器和Node.js提供了一些异步操作的 API, 如setTimeout, XMLHttpRequest#.

  4. JavaScript 是单线程的, 只能有一条线程来执行脚本, 串行 执行. 但是浏览器是多线程的. 所谓的 异步 就是借助浏览器的其他线程实现的. 如 setTimeout() 使用了浏览器的定时器线程, ajax 请求使用了浏览器的 HTTP 请求线程. setTimeout() 将函数丢给浏览器, 使其脱离js运行的主线程, 到达给定时间后再将函数丢回一个队列中, 排队等待主线程执行.

  5. A concurrency model specifies how threads in the the system collaborate to complete the tasks they are are given. Different concurrency models split the tasks in different ways, and the threads may communicate and collaborate in different ways#. JavaScript 的并发模型是基于 event loop 的.

  6. Promise, async/await 并不能将普通代码变成异步的, 它只是用来更方便的组织多个异步行为. async/await 是 generator 的语法糖.

Legacy Asynchronous Programming Patterns

  1. In early versions of the language, an asynchronous operation only supported definition of a callback function to indicate that the asynchronous operation had completed. The full version of the using of an async function as follows:
     asyncfunc(values, success, failure);
    
  2. Serializing asynchronous behavior was a common problem, usually solved by a codebase full of nested callback functions—colloquially referred to as ‘‘callback hell’’. See the following part: Nest Asynchronous Callbacks.

  3. 由于早期 JavaScript 中异步编程只支持简单回调函数的形式, 没有 Promise, async/await, 这就给用户在处理某些场景下的相关问题时带来很大不便. 这些场景包括:

    1. 处理异步操作的返回值
    2. 处理异步操作的错误
    3. 嵌套异步回调

    下面三部分会讲一下传统回调函数是如何处理上述三个场景的以及产生的问题. 前两个场景的问题是,必须在异步行为初始化时就定制好处理结果的回调函数。后一个场景的问题是回调地狱。再往后面的文章会讲Promise是如何解决这两个问题的以及其他特性。

1. Returning Asynchronous Values - success

If the setTimeout returns a useful value, we should wrap the code depends on the value into a callback, and pass the callback to the asynchronous operation.

function double(value, callback) { 
  setTimeout(() => callback(value * 2), 1000);
}
double(3, (x) => console.log(`I was given: ${x}`));

2. Handling Failure - failure

function double(value, success, failure) { 
  setTimeout(() => { 
    try {
      if (typeof value !== 'number') {
        throw 'Must provide number as first argument';
      }
      success(2 * value);
    } catch (e) {
      failure(e);
    }
  }, 1000);
}

const successCallback = (x) => console.log(`Success: ${x}`);
const failureCallback = (e) => console.log(`Success: ${e}`);

double(3, successCallback, failureCallback);  // the callbacks must be defined before there.

This format is already undesirable, as the callbacks must be defined when the asynchronous operation is initialized (the last line). By Promise, we can set the operations latter with then.

3. Nesting Asynchronous Callbacks

This callback strategy does not scale well as code complexity grows. The “callback hell” colloquialism is well-deserved, as JavaScript codebases that were afflicted with such a structure became nearly unmaintainable.

The callback situation is further complicated when access to the asynchronous values have dependencies on other asynchronous values. In the world of callbacks, this necessitates nesting the callbacks:

function double(value, success, failure) {
  setTimeout(() => {
    try {
      if (typeof value !== "number") {
        throw "Must provide number as first argument";
      }
      success(2 * value);  // nesting
    } catch (e) {
      failure(e);
    }
  }, 1000);
}

const successCallback = (x) => {
  double(x, (y) => console.log(`Success: ${y}`)); // nesting
};
const failureCallback = (e) => console.log(`Failure: ${e}`);

double(3, successCallback, failureCallback);

// Success: 12 (printed after roughly 2000ms)

Event Loop

  1. JS 是单线程,event loop 使得 JS 可以处理异步行为。

  2. js异步任务分微任务(micro task)和宏任务(task). 微任务只是普通的js脚本,红任务是真正的异步任务。这可以将异步队列任务划分优先级, 使得微任务可以插队.

  3. 宏任务有: 主线程,UI渲染 (解析DOM, 计算布局, 绘制), UI 交互, I/O, 网络请求(xmlhttprequest, Worker 的创建也需要 fetch 脚本), postMessage, setTimeout, setInterval, nodejs 下的 setImmediate 等.

  4. 微任务有: Promise的 resolvereject, Object.observe (已被Proxy取代), MutationObserver, nodejs 下的 process.nextTick.

  5. 参与event loop的有四部分:主线程,web api, 宏任务队列, 微任务队列。

  6. event loop 中,一次循环称为一次tick。

  7. 过程:
  8. 执行宏任务:主线程(宏任务)正常执行,遇到异步任务先不执行,将他们放入任务队列,直至主线程执行完毕。宏任务和微任务各有各的队列。
  9. 执行微任务:按顺序微任务队列里的任务,直至队列为空。执行过程中可能会产生新的微任务,将他们继续加入微任务队列尾部, 继续执行。
  10. 执行渲染:GUI 线程接管渲染(reflow,repaint 等)。
  11. (第二次tick)执行宏任务:从宏任务队列取出第一个,按第一步执行。
  12. 循环。。。

event loop


Reference: