共计 4252 个字符,预计需要花费 11 分钟才能阅读完成。
单线程是 Javascript 语言最本质的特性之一,Javascript 引擎在运行 js 代码的时候,同一个时间只能执行单个任务。
这种模式的好处是实现起来比较简单,执行环境相对单纯。
坏处是只要有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行。常见的浏览器无响应(假死),往往就是因为某一段 Javascript 代码长时间运行(比如死循环),导致整个页面卡在这个地方,其他任务无法执行。
所以异步编程对 JavaScript 语言太重要。
有些小伙伴可能还不太理解 ” 异步 ”。
所谓的 ” 异步 ”,就是一个任务分成两段,先执行第一段,然后转而执行其他任务,等做好了准备,再回过头执行第二段。
例如,有一个任务是读取文件进行处理,任务的第一段是向操作系统发出请求,要求读取文件。然后,程序执行其他任务,等到操作系统返回文件,再接着执行任务的第二段(处理文件)。这种不连续的执行,就叫做异步。
相应地,连续的执行就叫做同步。由于是连续执行,不能插入其他任务,所以操作系统从硬盘读取文件的这段时间,程序只能干等着。
讲的通俗点:
朱自清的《背影》中,父亲对朱自清说:“我买几个橘子去。你就在此地,不要走动。”
朱自清没有走动,等着买完橘子的父亲一起吃橘子,就叫同步。
如果朱自清没有等父亲,独自走了,那就不能和父亲一起吃橘子,就叫异步。
1、异步编程
我们就以用户注册这个特别常见的场景为例,讲讲异步编程。
第一步,验证用户是否注册
第二步,没有注册,发送验证码
第三步,填写验证码、密码,检验验证码是否正确
这个过程是有一定的顺序的,你必须保证上一步完成,才能顺利进行下一步。
1.1 回调函数
function testRegister(){} // 验证用户是否注册
function sendMessage(){} // 给手机发送验证码 x
function testMessage(){} // 检验验证码是否正确
function doRegister(){ // 开始注册
testRegister(data){if(data===false){// 已注册}else{ // 未注册
sendMessage(data){if(data===true){ // 发送验证码成功
testMessage(data){if(data===true){// 验证码正确}else{// 验证码不正确}
}
}
}
}
}
}
代码中就已经有许多问题,比如杂乱的 if 判断语句、层层嵌套的函数,造成代码的可读性差,难于维护。
另外,如果在层层回调函数中出现异常,调试起来是非常让人奔溃的 —— 由于 try-catch 无法捕获异步的异常,我们只能不断不断的写 debugger 去追踪,简直步步惊心。
这种层层嵌套被称为回调地狱。
1.2 Promise 方式
Promise 就是为了解决回调地狱问题而提出的。它不是新的语法功能,而是一种新的写法,允许将回调函数的嵌套,改成链式调用。采用 Promise,连续读取多个文件,写法如下。
let state=1; // 模拟返回结果
function step1(resolve,reject){console.log('1. 验证用户是否注册');
if(state==1){resolve('未注册');
}else{reject('已注册');
}
}
function step2(resolve,reject){console.log('2. 给手机发送验证码');
if(state==1){resolve('发送成功');
}else{reject('发送失败');
}
}
function step3(resolve,reject){console.log('3. 检验验证码是否正确');
if(state==1){resolve('验证码正确');
}else{reject('验证码不正确');
}
}
new Promise(testRegister).then(function(val){ // 验证用户是否注册
console.log(val);
return new Promise(sendMessage); // 给手机发送验证码
}).then(function(val){console.log(val);
return new Promise(testMessage); // 检验验证码是否正确
}).then(function(val){console.log(val);
return val;
});
回调函数采用了嵌套的方式依次调用 testRegister()、sendMessage() 和 testMessage(),而 Promise 使用 then 将它们链接起来。
相比回调函数而言,Promise 代码可读性更高,代码的执行顺序一目了然。
Promise 的方式虽然解决了回调地狱,但是最大问题是代码冗余,原来的任务被 Promise 包装了一下,不管什么操作,一眼看去都是一堆 then,原来的语义变得很不清楚。代码流程不能很好的表示执行流程。
大家初中学过电路,这个就像电路的串联,如果没学过也没关系,你肯定知道 jquery 有链式操作,这个就很类似链式操作的写法,比较符合我们的思维逻辑。
1.3 async/await 方式
async 语法是对 new Promise 的包装,await 语法是对 then 方法的提炼。
async function doRegister(url) {let data = await testRegister(); // 验证用户是否注册
let data2 = await sendMessage(data); // 给手机发送验证码
let data3 = await testMessage(data2); // 检验验证码是否正确
return data3
}
上面的代码虽然短,但是每一句都极为重要。data 是 await testRegister 的返回结果,data2 又使用了 data 作为 sendMessage 的参数,data3 又使用了 data2 作为 testMessage 的参数。
只要在 doRegister 前面加上关键词 async,在函数内的异步任务前添加 await 声明即可。如果忽略这些额外的关键字,简直就是完完全全的同步写法。
2、async 用法
2.1 返回 Promise 对象
async 函数返回一个 Promise 对象。
async 函数内部 return 语句返回的值,会成为 then 方法回调函数的参数。
async function f() {return 'aaa';}
f().then(v => console.log(v))
//aaa
//Promise {<resolved>: undefined}
2.2 await 命令
正常情况下,await 命令后面是一个 Promise 对象,返回该对象的结果。如果不是 Promise 对象,就直接返回对应的值。
/* 成功情况 */
async function f() {return await 123;}
f().then(value => console.log(value)); // 123
/* 失败情况 */
async function f() {return Promise.reject('error');
}
f().catch(e => console.error(e)); // error
注意事项:
await 命令只能用在 async 函数之中,如果用在普通函数,就会报错。
/* 错误处理 */
function f(db) {let docs = [1, 2, 3];
for(let doc of docs) {await db.push(doc);
}
return db; // Uncaught SyntaxError: Unexpected identifier
}
/* 正确处理 (顺序执行) */
async function f(db) {let docs = [1, 2, 3];
for(let doc of docs) {await db.push(doc);
}
return db;
}
2.3 async 中异常处理
通过使用 async/await,我们就可以配合 try/catch 来捕获异步操作过程中的问题,包括 Promise 中 reject 的数据。
await 后面可能存在 reject,需要进行 try…catch 代码块中
async function f() {
try {await Promise.reject('出错了');
} catch(e) {console.error(e);
}
return Promise.resolve('hello');
}
f().then(v => console.log(v)); // 出错了 hello
3、并联中的 await
async/await 语法确实很简单好用,但也容易使用不当,还要根据具体的业务场景需求来定。
例如我们需要获取一批图片的大小信息:
async function allPicInfo (imgs) {const result = [];
for (const img of imgs) {result.push(await getSize(img));
}
}
代码中的每次 getSize 调用都需要等待上一次调用完成,同样是一种性能浪费,而且花费的时间也长。同样的功能,用这样的方式会更合适:
async function allPicInfo (imgs) {return Promise.all(imgs.map(img => getSize(img)));
}
多个异步操作,如果没有继承关系,最好同时触发。
4、总结
从最早的回调函数,到 Promise 对象,每次都有所改进,但又让人觉得不彻底。它们都有额外的复杂性,都需要理解抽象的底层运行机制。
例如有三个请求需要发生,第三个请求是依赖于第二个请求的结果,第二个请求依赖于第一个请求的结果。若用 ES5 实现会有 3 层的回调,导致代码的横向发展。若用 Promise 实现至少需要 3 个 then,导致代码的纵向发展。然而,async/await 解决了这些问题。
从实现上来看 async/await 是在 生成器、Promise 基础上构建出来的新语法:以生成器实现流程控制,以 Promise 实现异步控制。
但是,不要因此小看 async/await,使用同步的方式写异步代码其实非常强大。
async/await 在语义化、简化代码、错误处理等方面有很多的优势,毕竟用 async/ wait 编写条件代码要简单得多,还可以使用相同的代码结构(众所周知的 try/catch 语句)处理同步和异步错误,所以常被称为 JavaScript 异步编程的终极解决方案,可见其重要性和优势。
希望小伙们在以后的实战项目中,多多练习,才能掌握 async/await 的真正精要。