单线程是Javascript语言最本质的特性之一,Javascript引擎在运行js代码的时候,同一个时间只能执行单个任务。jquery
这种模式的好处是实现起来比较简单,执行环境相对单纯。编程
坏处是只要有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行。常见的浏览器无响应(假死),每每就是由于某一段Javascript代码长时间运行(好比死循环),致使整个页面卡在这个地方,其余任务没法执行。浏览器
因此异步编程对JavaScript语言过重要。异步
有些小伙伴可能还不太理解"异步"。async
所谓的"异步",就是一个任务分红两段,先执行第一段,而后转而执行其余任务,等作好了准备,再回过头执行第二段。ide
例如,有一个任务是读取文件进行处理,任务的第一段是向操做系统发出请求,要求读取文件。而后,程序执行其余任务,等到操做系统返回文件,再接着执行任务的第二段(处理文件)。这种不连续的执行,就叫作异步。异步编程
相应地,连续的执行就叫作同步。因为是连续执行,不能插入其余任务,因此操做系统从硬盘读取文件的这段时间,程序只能干等着。函数
讲的通俗点:性能
朱自清的《背影》中,父亲对朱自清说 :“我买几个橘子去。你就在此地,不要走动。”url
朱自清没有走动,等着买完橘子的父亲一块儿吃橘子,就叫同步。
若是朱自清没有等父亲,独自走了,那就不能和父亲一块儿吃橘子,就叫异步。
一、异步编程
咱们就以用户注册这个特别常见的场景为例,讲讲异步编程。
第一步,验证用户是否注册
第二步,没有注册,发送验证码
第三步,填写验证码、密码,检验验证码是否正确
这个过程是有必定的顺序的,你必须保证上一步完成,才能顺利进行下一步。
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声明便可。若是忽略这些额外的关键字,简直就是完彻底全的同步写法。
二、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
三、并联中的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))); }
多个异步操做,若是没有继承关系,最好同时触发。
四、总结
从最先的回调函数,到 Promise 对象,每次都有所改进,但又让人以为不完全。它们都有额外的复杂性,都须要理解抽象的底层运行机制。
例若有三个请求须要发生,第三个请求是依赖于第二个请求的结果,第二个请求依赖于第一个请求的结果。若用 ES5实现会有3层的回调,致使代码的横向发展。若用Promise 实现至少须要3个then,致使代码的纵向发展。然而,async/await 解决了这些问题。
从实现上来看 async/await 是在 生成器、Promise 基础上构建出来的新语法:以生成器实现流程控制,以 Promise 实现异步控制。
可是,不要所以小看 async/await,使用同步的方式写异步代码其实很是强大。
async/await 在语义化、简化代码、错误处理等方面有不少的优点,毕竟用async/ wait编写条件代码要简单得多,还可使用相同的代码结构(众所周知的try/catch语句)处理同步和异步错误,因此常被称为JavaScript异步编程的终极解决方案,可见其重要性和优点。
但愿小伙们在之后的实战项目中,多多练习,才能掌握async/await的真正精要。