目录
前言:
JavaScript采用的是单线程模型,单线程模型指的是,JavaScript 只在一个线程上运行。也就是说,JavaScript 同时只能执行一个任务,其他任务都必须在后面排队等待。由于JavaScript采用单线程模型,导致JavaScript的所有网络操作,浏览器事件,都必须是异步执行。
但JavaScript的核心语言特性中没有一个是异步的,那么我们要怎么使用异步编程呢?
回调函数是异步操作最基本的方法。但JavaScript提供了三种重要的语言特性,可以让编写异步代码更容易。
ES6新增的期约(promise)是一种对象,代表了某个异步操作尚不可用的结果。还有关键字async和await它可以简化异步操作,它允许基于期约的异步代码写成同步的形式。最后还有异步迭代器for/await循环。允许在看起来同步的简单循环中操作异步事件流。
同步任务和异步任务
同步任务是那些没有被引擎挂起、在主线程上排队执行的任务。只有前一个任务执行完毕,才能执行后一个任务。
异步任务是那些被引擎放在一边,不进入主线程、而进入任务队列的任务。只有引擎认为某个异步任务可以执行了(比如 Ajax 操作从服务器得到了结果),该任务(采用回调函数的形式)才会进入主线程执行。排在异步任务后面的代码,不用等待异步任务结束会马上运行,也就是说,异步任务不具有“堵塞”效应。
使用回调
同步的回调函数:立即在主线程上执行,不会放入回调队列中,比如:数组遍历相关的回调、promise的executor函数。
异步的回调函数:不会立即执行,会放入回调队列以后再执行。比如:定时器、ajax、promise的成功与失败的回调。
回调:就是函数,可以传给其他函数,在其他函数满足某个条件,或发生某个异步事件的时候调用"回调"这个函数。(是我们没有调用,但是最终还是执行了的函数。)JavaScript的异步编程就是使用回调实现的。
定时器
定时器就是一个简单的异步操作。
使用setTimeout()函数来实现异步操作。setTimeout() 方法用于在指定的毫秒数后调用函数或计算表达式。
setTimeout(函数/表达式,毫秒)第一个参数是一个函数或者表达式,第二个参数是以毫秒为单位的时间间隔,表示在多少时间内调用前一个函数或表达式,setTimeout()只会调用一次指定的回调函数, 如果你只想重复执行可以使用 setInterval() 方法。使用 clearTimeout()可取消由 setTimeout() 方法设置的定时操作。
var myVar;
function myFunction() {
//3秒后执行alertFunc()函数
myVar = setTimeout(alertFunc, 3000);
}
function alertFunc() {
alert("Hello!");
}
// clearTimeout() 函数取消定时器 myVar
function myStopFunction() {
clearTimeout(myVar);
}
事件
客户端的JavaScript编程几乎全是事件驱动的,比如鼠标按下事件,移动鼠标事件等。
事件驱动的JavaScript程序在特定的上下文中为特定类型的事件注册回调函数,而浏览器在指定的事件发生时调用这些函数。这些回调函数也称为事件处理程序或者事件监听器,通过document.addEventListener() 注册。 可以使用 document.removeEventListener() 方法来移除 addEventListener() 方法添加的事件
document.addEventListener(event, function, useCapture)
参数 描述 event 必需。描述事件名称的字符串。
注意: 不要使用 "on" 前缀。例如,使用 "click" 来取代 "onclick"。function 必需。描述了事件触发后执行的函数。
当事件触发时,事件对象会作为第一个参数传入函数。 事件对象的类型取决于特定的事件。例如, "click" 事件属于 MouseEvent(鼠标事件) 对象。useCapture 可选。布尔值,指定事件是否 在捕获或冒泡阶段执行。
可能值:
- true - 事件句柄在捕获阶段执行
- false- 默认。事件句柄在冒泡阶段执行
node的回调
node中也有回调和事件,比如读取文件内容默认的api也是异步的,会在读取内容后调用一个回调函数。具体可见node。要注意的是node使用的是on()方法而不是addEventListener()注册事件监听器。
期约(promise)
期约是什么?
期约是一个对象表示异步操作的结果。期约的结果可能就绪也可能未就绪,没有办法同步取得期约的值,只能要求期约在值就绪时调用一个回调函数。promise就是js中进行异步编程的一种新方案。
回调常遇到无法处理错误的问题,如果一个异步函数抛出异常,则该异常没有办法传播到异步操作的发起者。一个补救的方法时使用回调参数严密跟踪和传播错误并返回值,但是非常麻烦,容易出错,但是期约标准化了异步错误处理。通过期约链可以提供一个让错误正确传播的途径。
什么是期约链呢?
就是以线性then()方法调用链的形式表达一连串异步操作,而无需把每个操作嵌套在前一个操作的回调内部。期约链更容易表达一连串的异步操作。
由于期约具体内容较长,我将单独拿出来写一篇笔记。
具体可见:期约 promise——异步编程新方案_白芸哆的博客-CSDN博客
接下来了解关键字async和await
async和await是ES2017新增的关键字,这两个新关键字极大的简化了期约的使用,允许我们像编写因网络请求或其他异步事件而阻塞的同步代码一样,基于期约的异步代码。
await表达式
await表达式接收一个期约并将其转换成一个返回值或者一个抛出的异常,但我们通常不会使用await来接收一个保存期约的变量,而是把它放在一个会返回期约的函数调用前面。
let response = await fetch("/api/user/profile");
let profile = await response.json();
要注意的是任何使用await的代码本身就是异步的,并不会导致程序阻塞或者在指定期约落定前什么都不做。 因为任何使用await的代码都是异步的,所以它只能在异步函数 async 关键字声明的函数内部使用。
正常情况下,await 命令后面是一个 Promise 对象,返回该对象的结果。如果不是 Promise 对象,就直接返回对应的值。
await针对所跟不同表达式的处理方式:
- Promise 对象:await 会暂停执行,等待 Promise 对象 处理完毕,然后恢复 async 函数的执行并返回解析值。( Promise 对象 执行成功会返回promise成功的值,如果await的promise实例对象失败了,就会抛出异常,需要通过try{..可能出错的代码.}.catch(error){..}来捕获错误。)
- 非 Promise 对象:直接返回对应的值。
async函数
语法
async function name([param[, param[, ... param]]]) { statements }
- name: 函数名称。
- param: 要传递给函数的参数的名称。
- statements: 函数体语句。
把函数声明为 async意味着该函数的返回值将是一个期约,async函数返回一个 Promise 对象,可以使用 then
方法添加回调函数。当函数执行的时候,一旦遇到 await
就会先返回,等到异步操作完成,再接着执行函数体内后面的语句。
async 函数有多种使用形式。
// 函数声明
async function foo() {}
// 函数表达式
const foo = async function () {};
// 对象的方法
let obj = { async foo() {} };
obj.foo().then(...)
// Class 的方法
class Storage {
constructor() {
this.cachePromise = caches.open('avatars');
}
async getAvatar(name) {
const cache = await this.cachePromise;
return cache.match(`/avatars/${name}.jpg`);
}
}
const storage = new Storage();
storage.getAvatar('jake').then(…);
// 箭头函数
const foo = async () => {};
async
函数内部return
语句返回的值,会成为then
方法回调函数的参数。
错误处理
如果await
后面的异步操作出错,那么等同于async
函数返回的 Promise 对象被 reject
。
防止出错的方法,也是将其放在try...catch
代码块之中。
async function f() {
try {
await new Promise(function (resolve, reject) {
throw new Error('出错了');
});
} catch(e) {
}
return await('hello world');
}
如果有多个await
命令,可以统一放在try...catch
结构中。
async function main() {
try {
const val1 = await firstStep();
const val2 = await secondStep(val1);
const val3 = await thirdStep(val1, val2);
console.log('Final: ', val3);
}
catch (err) {
console.error(err);
}
}
要注意的是:await
命令后面的Promise
对象,运行结果可能是rejected(失败的)
,所以最好把await
命令放在try...catch
代码块中。
async function myFunction() {
try {
await somethingThatReturnsAPromise();
} catch (err) {
console.log(err);
}
}
// 另一种写法
async function myFunction() {
await somethingThatReturnsAPromise()
.catch(function (err) {
console.log(err);
});
}
多个await
命令后面的异步操作,如果不存在继发关系,最好让它们同时触发。
let foo = await getFoo();
let bar = await getBar();
上面代码中,getFoo
和getBar
是两个独立的异步操作(即互不依赖),被写成继发关系。这样比较耗时,因为只有getFoo
完成以后,才会执行getBar
,完全可以让它们同时触发。
// 写法一
let [foo, bar] = await Promise.all([getFoo(), getBar()]);
// 写法二
let fooPromise = getFoo();
let barPromise = getBar();
let foo = await fooPromise;
let bar = await barPromise;
上面两种写法,getFoo
和getBar
都是同时触发,这样就会缩短程序的执行时间。
为了理解async的工作原理,我们可以看一下后台发生了什么:
async function f(x){ / *函数体*/ }
把这个函数想象成一个返回期约的包装函数,它包装了你原始函数的函数体:
function f(x){
return new Promise(function(resolve,reject){
try{
resolve((function(X){/*函数体*/})(x));
}
catch(e){
rejext(e);
}
});
}
可以把await、关键字想象成分隔代码体的记号,它们把函数体分割成相对独立的同步代码块。
for/await循环
与常规的await表达式类似的是:for/await循环也是基于期约的。这里的异步迭代器会产生一个期约,而for/await循环等待该期约兑现,将兑现值赋给循环变量,任何在运行循环体,之后再从头开始,从迭代器取得另一个期约并等待这个新期约兑现。
小结
- 期约提供了一种结构化回调函数的新方式,是js中进行异步编程的新方案,期约还支持再then()调用链的末尾用catch()集中处理错误。
- async和await关键字可以让我们以同步代码的形式写出基于期约的异步代码。
- 异步迭代的对象可以再for/await循环中使用。
更多:
异步JavaScript MND:异步 JavaScript - 学习 Web 开发 | MDN
async 推荐学习:ES6 入门教程
Promise 推荐学习: