前言自己着手准备写这篇文章的初衷是觉得如果想要更深入的理解 JS,异步编程则是必须要跨过的一道坎。由于这里面涉及到的东西很多也很广,在初学 JS 的时候可能无法完整的理解这一概念,即使在现在来看还是有很多自己没有接触和理解到的知识点,但是为了跨过这道坎,我仍然愿意鼓起勇气用我已经掌握的部分知识尽全力讲述一下 JS 中的异步编程。如果我所讲的一些概念或术语有错误,请读者向我指出问题所在,我会立即纠正更改。
同步与异步我们知道无论是在浏览器端还是在服务器 ( Node ) 端,JS 的执行都是在单线程下进行的。我们以浏览器中的 JS 执行线程为例,在这个线程中 JS 引擎会创建执行上下文栈,之后我们的代码就会作为执行上下文 ( 全局、函数、eval ) 像一系列任务一样在执行上下文栈中按照后进先出 ( LIFO ) 的方式依次执行。而同步最大的特性就是会阻塞后面任务的执行,比如此时 JS 正在执行大量的计算,这个时候就会使线程阻塞从而导致页面渲染加载不连贯 ( 在浏览器端的 Event Loop 中每次执行栈中的任务执行完毕后都会去检查并执行事件队列里面的任务直到队列中的任务为空,而事件队列中的任务又分为微队列与宏队列,当微队列中的任务执行完后才会去执行宏队列中的任务,而在微队列任务执行完到宏队列任务开始之前浏览器的 GUI 线程会执行一次页面渲染 ( UI rendering ),这也就解释了为什么在执行栈中进行大量的计算时会阻塞页面的渲染 ) 。
与同步相对的异步则可以理解为在异步操作完成后所要做的任务,它们通常以回调函数或者 Promise 的形式被放入事件队列,再由事件循环 ( Event Loop ) 机制在每次轮询时检查异步操作是否完成,若完成则按事件队列里面的执行规则来依次执行相应的任务。也正是得益于事件循环机制的存在,才使得异步任务不会像同步任务那样完全阻塞 JS 执行线程。
异步操作一般包括 网络请求 、文件读取 、数据库处理
异步任务一般包括 setTimout / setInterval 、Promise 、requestAnimationFrame ( 浏览器独有 ) 、setImmediate ( Node 独有 ) 、process.nextTick ( Node 独有 ) 、etc ...
注意: 在浏览器端与在 Node 端的 Event Loop 机制是有所不同的,下面给出的两张图简要阐述了在不同环境下事件循环的运行机制,由于 Event Loop 不是本文内容的重点,但是 JS 异步编程又是建立在它的基础之上的,故在下面给出相应的阅读链接,希望能够帮助到有需要的读者。
浏览器端
![]()
Node 端
![]()
阅读链接
为异步而生的 JS 语法回望历史,在最近几年里 ECMAScript 标准几乎每年都有版本的更新,也正是因为有像 ES6 这种在语言特性上大版本的更新,到了现今的 8102 年, JS 中的异步编程相对于那个只有回调函数的远古时代有了很大的进步。下面我将介绍 callback 、Promise 、generator 、async / await 的基本用法以及如何在异步编程中使用它们。
callback回调函数并不算是 JS 中的语法但它却是解决异步编程问题中最常用的一种方法,所以在这里有必要提出来,下面举一个例子,大家看一眼就懂。
const foo = function (x, y, cb) { setTimeout(() => { cb(x + y) }, 2000)}// 使用 thunk 函数,有点函数柯里化的味道,在最后处理 callback。const thunkify = function (fn) { return function () { let args = Array.from(arguments) return function (cb) { fn.apply(null, [...args, cb]) } }}let fooThunkory = thunkify(foo)let fooThunk1 = fooThunkory(2, 8)let fooThunk2 = fooThunkory(4, 16)fooThunk1((sum) => { console.log(sum) // 10})fooThunk2((sum) => { console.log(sum) // 20})复制代码Promise在 ES6 没有发布之前,作为异步编程主力军的回调函数一直被人诟病,其原因有太多比如回调地狱、代码执行顺序难以追踪、后期因代码变得十分复杂导致无法维护和更新等,而 Promise 的出现在很大程度上改变了之前的窘境。话不多说先直接上代码提前感受下它的魅力,然后我再总结下自己认为在 Promise 中很重要的几个点。
const foo = function () { let args = [...arguments] let cb = args.pop() setTimeout(() => { cb(...args) }, 2000)}const promisify = function (fn) { return function () { let args = [...arguments] return function (cb) { return new Promise((resolve, reject) => { fn.apply(null, [...args, resolve, reject, cb]) }) } }}const callback = function (x, y, isAdd, resolve, reject) { if (isAdd) { resolve(x + y) } else { reject('Add is not allowed.') }}let promisory = promisify(foo)let p1 = promisory(4, 16, false)let p2 = promisory(2, 8, true)p1(callback).then((sum) => { console.log(sum)}, (err) => { console.error(err) // Add is not allowed.}).finally(() => { console.log('Triggered once the promise is settled.')})p2(callback).then((sum) => { console.log(sum) // 10 return 'evil |
|