JavaScript 异步编程
本文最后更新于:2024年3月18日 凌晨
JavaScript 异步编程
异步的概念
- 异步(Asynchronous, async)是与同步(Synchronous, sync)相对的概念。
- 在我们学习的传统单线程编程中,程序的运行是同步的(同步不意味着所有步骤同时运行,而是指步骤在一个控制流序列中按顺序执行),而异步的概念则是不保证同步的概念,也就是说,一个异步过程的执行将不再与原有的序列有顺序关系。
- 简单来理解就是:同步按你的代码顺序执行,异步不按照代码顺序执行,异步的执行效果更高:
- 以上是关于异步的概念的解释,接下来我们通俗地解释一下异步:异步就是从主线程发射一个子线程来完成任务。

什么时候用异步编程
- 在前端编程中(甚至后端有时也是这样),我们在处理一些简短,快速的操作时,例如计算 1 + 1 的结果,往往在主线程中就可以完成,主线程作为一个线程,不能够同时接受多方面的请求,所以,当一个事件没有结束时,界面将无法处理其他请求。
- 现在有一个按钮,如果我们设置它的
onclick
事件为一个死循环,那么当这个按钮按下,整个网页将失去响应。
- 为了避免这种情况的发生,我们常常用子线程来完成一些可能消耗时间足够长以至于被用户察觉的事情,比如读取一个大文件或者发出一个网络请求,因为子线程独立于主线程,所以即使出现阻塞也不会影响主线程的运行,但是子线程有一个局限:一旦发射了以后就会与主线程失去同步,我们无法确定它的结束,如果结束之后需要处理一些事情,比如处理来自服务器的信息,我们是无法将它合并到主线程中去的。
- 为了解决这个问题,JavaScript 中的异步操作函数往往通过回调函数来实现异步任务的结果处理。
回调函数
- 回调函数就是一个函数,它是在我们启动一个异步任务的时候就告诉它:等你完成了这个任务之后要干什么,这样一来主线程几乎不用关心异步任务的状态了,他自己会善始善终。
1 2 3 4
| function print() { document.getElementById("demo").innerHTML="Test!"; } setTimeout(print, 3000);
|
- 这段程序中的 setTimeout 就是一个消耗时间较长(3 秒)的过程,它的第一个参数是个回调函数,第二个参数是毫秒数,这个函数执行之后会产生一个子线程,子线程会等待 3 秒,然后执行回调函数 “print”,在命令行输出 “Time out”
- 当然,JavaScript 语法十分友好,我们不必单独定义一个函数 print ,我们常常将上面的程序写成:
1 2 3
| setTimeout(function () { document.getElementById("demo").innerHTML="Test!"; }, 3000);
|
- 注意:既然 setTimeout 会在子线程中等待 3 秒,在 setTimeout 函数执行之后主线程并没有停止,所以。
1 2 3 4
| setTimeout(function () { console.log("1"); }, 1000); console.log("2");
|
异步 AJAX
- 除了 setTimeout 函数以外,异步回调广泛应用于 AJAX 编程。
- XMLHttpRequest 常常用于请求来自远程服务器上的 XML 或 JSON 数据,一个标准的 XMLHttpRequest 对象往往包含多个回调:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| var xhr = new XMLHttpRequest();
xhr.onload = function () { document.getElementById("demo").innerHTML=xhr.responseText; }
xhr.onerror = function () { document.getElementById("demo").innerHTML="请求出错"; }
xhr.open("GET", "https://www.test.com/try/ajax/ajax_info.txt", true); xhr.send();
|
- XMLHttpRequest 的 onload 和 onerror 属性都是函数,分别在它请求成功和请求失败时被调用,如果你使用完整的 jQuery 库,也可以更加优雅的使用异步 AJAX:
1 2 3
| $.get("https://www.test.com/try/ajax/demo_test.php",function(data,status){ alert("数据: " + data + "\n状态: " + status); });
|
Promise
构造 Promise
1 2 3
| new Promise(function (resolve, reject) { });
|
- 通过新建一个 Promise 对象好像并没有看出它怎样 “更加优雅地书写复杂的异步任务”,我们之前遇到的异步任务都是一次异步,如果需要多次调用异步函数呢?例如,如果我想分三次输出字符串,第一次间隔 1 秒,第二次间隔 4 秒,第三次间隔 3 秒:
1 2 3 4 5 6 7 8 9
| setTimeout(function () { console.log("First"); setTimeout(function () { console.log("Second"); setTimeout(function () { console.log("Third"); }, 3000); }, 4000); }, 1000);
|
- 这段程序实现了这个功能,但是它是用 “函数瀑布” 来实现的,可想而知,在一个复杂的程序当中,用 “函数瀑布” 实现的程序无论是维护还是异常处理都是一件特别繁琐的事情,而且会让缩进格式变得非常冗赘。
- 现在我们用 Promise 来实现同样的功能:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| new Promise(function (resolve, reject) { setTimeout(function () { console.log("First"); resolve(); }, 1000); }).then(function () { return new Promise(function (resolve, reject) { setTimeout(function () { console.log("Second"); resolve(); }, 4000); }); }).then(function () { setTimeout(function () { console.log("Third"); }, 3000); });
|
- Promise 将嵌套格式的代码变成了顺序格式的代码。
Promise 的使用
- 下面我们通过剖析这段 Promise “计时器” 代码来讲述 Promise 的使用:
- Promise 构造函数只有一个参数,是一个函数,这个函数在构造之后会直接被异步运行,所以我们称之为起始函数,起始函数包含两个参数 resolve 和 reject
- 当 Promise 被构造时,起始函数会被异步执行:
1 2 3
| new Promise(function (resolve, reject) { console.log("Run"); });
|
- 这段程序会直接输出 Run
- resolve 和 reject 都是函数,其中调用 resolve 代表一切正常,reject 是出现异常时所调用的:
1 2 3 4 5 6 7 8 9 10 11 12
| new Promise(function (resolve, reject) { var a = 0; var b = 1; if (b == 0) reject("Diveide zero"); else resolve(a / b); }).then(function (value) { console.log("a / b = " + value); }).catch(function (err) { console.log(err); }).finally(function () { console.log("End"); });
|
- Promise 类有
.then()
, .catch()
和 .finally()
三个方法,这三个方法的参数都是一个函数,.then()
可以将参数中的函数添加到当前 Promise 的正常执行序列,.catch()
则是设定 Promise 的异常处理序列,.finally()
是在 Promise 执行的最后一定会执行的序列, .then()
传入的函数会按顺序依次执行,有任何异常都会直接跳到 catch 序列:
1 2 3 4 5 6 7 8 9 10 11 12
| new Promise(function (resolve, reject) { console.log(1111); resolve(2222); }).then(function (value) { console.log(value); return 3333; }).then(function (value) { console.log(value); throw "An error"; }).catch(function (err) { console.log(err); });
|
resolve()
中可以放置一个参数用于向下一个 then 传递一个值,then 中的函数也可以返回一个值传递给 then,但是,如果 then 中返回的是一个 Promise 对象,那么下一个 then 将相当于对这个返回的 Promise 进行操作,这一点从刚才的计时器的例子中可以看出来。
reject()
参数中一般会传递一个异常给之后的 catch 函数用于处理异常。
- 但是请注意以下两点:
- resolve 和 reject 的作用域只有起始函数,不包括 then 以及其他序列;
- resolve 和 reject 并不能够使起始函数停止运行,别忘了 return
Promise 函数
- 上述的 “计时器” 程序看上去比函数瀑布还要长,所以我们可以将它的核心部分写成一个 Promise 函数:
1 2 3 4 5 6 7 8
| function print(delay, message) { return new Promise(function (resolve, reject) { setTimeout(function () { console.log(message); resolve(); }, delay); }); }
|
1 2 3 4 5
| print(1000, "First").then(function () { return print(4000, "Second"); }).then(function () { print(3000, "Third"); });
|
- 这种返回值为一个 Promise 对象的函数称作 Promise 函数,它常常用于开发基于异步操作的库。
异步函数
- 异步函数(async function)是 ECMAScript 2017 (ECMA-262)标准的规范,几乎被所有浏览器所支持,除了 Internet Explorer
- 在 Promise 中我们编写过一个 Promise 函数:
1 2 3 4 5 6 7 8
| function print(delay, message) { return new Promise(function (resolve, reject) { setTimeout(function () { console.log(message); resolve(); }, delay); }); }
|
1 2 3 4 5
| print(1000, "First").then(function () { return print(4000, "Second"); }).then(function () { print(3000, "Third"); });
|
1 2 3 4 5 6
| async function asyncFunc() { await print(1000, "First"); await print(4000, "Second"); await print(3000, "Third"); } asyncFunc();
|
- 异步函数 async function 中可以使用 await 指令,await 指令后必须跟着一个 Promise,异步函数会在这个 Promise 运行中暂停,直到其运行结束再继续运行。
- 异步函数实际上原理与 Promise 原生 API 的机制是一模一样的,只不过更便于程序员阅读。
- 处理异常的机制将用 try-catch 块实现:
1 2 3 4 5 6 7 8 9 10 11
| async function asyncFunc() { try { await new Promise(function (resolve, reject) { throw "Some error"; }); } catch (err) { console.log(err); } } asyncFunc();
|
- 如果 Promise 有一个正常的返回值,await 语句也会返回它:
1 2 3 4 5 6 7 8 9
| async function asyncFunc() { let value = await new Promise( function (resolve, reject) { resolve("Return value"); } ); console.log(value); } asyncFunc();
|