摘要:签订协议的两方分别是异步接口和。在异步函数中,使用异常捕获的方案,代替了的异常捕获的方案。需要注意的是,在异步函数中使异步函数用时要使用,不然异步函会被同步执行。
同步与异步
通常,代码是由上往下依次执行的。如果有多个任务,就必需排队,前一个任务完成,后一个任务才会执行。这种执行模式称之为: 同步(synchronous) 。新手容易把计算机用语中的同步,和日常用语中的同步弄混淆。如,“把文件同步到云端”中的同步,指的是“使...保持一致”。而在计算机中,同步指的是任务从上往下依次执行的模式。比如:
例 1
A(); B(); C();
在上述代码中,A、B、C 是三个不同的函数,每个函数都是一个不相关的任务。在同步模式下,计算机会先执行 A 任务,再执行 B 任务,最后执行 C 任务。在大部分情况,同步模式都没问题。但是如果 B 任务是一个耗时很长网络的请求,而 C 任务恰好是展现新页面,B 与 C 没有依赖关系。这就会导致网页卡顿的现象。有一种解决方案,将 B 放在 C 后面去执行,但唯一有些不足的是,B 的网络请求会迟一些再发送。
还有另一种更完美解决方案,将 B 任务分成的两个部分。一部分是,立即执行网络请求的任务;另一部分是,在请求数据回来后执行的任务。这种一部分在立即执行,另一部分在未来执行的模式称为 异步(asynchronous) 。伪代码如下:
例 2
A(); // 在现在发送请求 ajax("url1",function B() { // 在未来某个时刻执行 }) C(); // 执行顺序 A => C => B
实际上,JavaScript 引擎先执行了调用了浏览器的网络请求接口的任务(一部分任务),再由浏览器发送网络请求并监听请求返回(这个任务不由 JavaScript 引擎执行,而是浏览器);等请求放回后,浏览器再通知 JavaScript 引擎,开始执行回调函数中的任务(另一部分)。JavaScript 异步能力的本质是浏览器或 Node 的多线程能力。
callback未来执行的函数通常也叫 callback。使用 callback 的异步模式,解决了阻塞的问题,但是也带了一些其他问题。在最开始,我们的函数是从上往下书写的,也是从上往下执行的,这非常符合我们的思维习惯,但是现在却被 callback 打断了!在上面一段代码中,它跳过 B 任务,先执行了 C任务!这种异步“非线性”的代码会比同步“线性”的代码,更难阅读,因此也更容易滋生 BUG。
试着判断下面这段代码的执行顺序,你会对“非线性”代码比“线性”代码更难以阅读,体会更深。
例 3
A(); ajax("url1", function(){ B(); ajax("url2", function(){ C(); } D(); }); E(); // 下面是答案,你猜对了吗? // A => E => B => D => C
在例 3 中,我们的阅读代码视线是 A => B => C => D => E ,但是执行顺序却是 A => E => B => D => C 。从上往下执行的顺序被 Callback 打乱了,这就是非线性代码带来的糟糕之处。
上面的例子中,我们可以通过将 ajax 后面执行的任务 E 和 任务 D 提前,来进行代码优化。这种技巧在写多重嵌套的代码时,是非常有用的。改进后,如下。
例 4
A(); E(); ajax("url1", function(){ B(); D(); ajax("url2", function(){ C(); } }); // 稍作优化,代码更容易看懂 // A => E => B => D => C
在例 4 中,只有处理了成功回调,并没处理异常回调。接下来,把异常处理回调加上,再来讨论代码“线性”执行的问题。
例 5
A(); ajax("url1", function(){ B(); ajax("url2", function(){ C(); },function(){ D(); }); },function(){ E(); });
例 5 中,加上异常处理回调后,url1 的成功回调函数 B 和异常回调函数 E,被分开了。这种“非线性”的情况又出现了。
在 node 中,为了解决的异常处理“非线性”的问题,制定了错误优先的策略。node 中 callback 的第一个参数,专门用于判断是否发生异常。
例 6
A(); get("url1", function(error){ if(error){ E(); }else { B(); get("url2", function(error){ if(error){ D(); }else{ C(); } }); } });
到此,callback 引起的“非线性”问题基本得到解决。遗憾的是,一旦嵌套层数多起来,阅读起来还不是很方便。此外,callback 一旦出现异常,只能在当前回调内部处理异常,并没有一个整体的异常触底方案。
promise在 JavaScript 的异步进化史中,涌现出一系列解决 callback 弊端的库,而 Promise 成为了最终的胜者,并成功地被引入了 ES6 中。它将提供了一个更好的“线性”书写方式,并解决了异步异常只能在当前回调中捕获的问题。
Promise 就像一个中介,它承诺会将一个可信任的异步结果返回。签订协议的两方分别是异步接口和 callback。首先 Promise 和异步接口签订一个协议,成功时,调用 resolve 函数通知 Promise,异常时,调用 reject 通知 Promise。另一方面 Promise 和 callback 也签订一个协议,当异步接口的 resolve 或 reject 被调用时,由 Promise 返回可信任的值给 then 和 catch 中注册的 callback。
一个最简单的 promise 示例如下:
例 7
// 创建一个 Promise 实例(异步接口和 Promise 签订协议) var promise = new Promise(function (resolve,reject) { ajax("url",resolve,reject); }); // 调用实例的 then catch 方法 (成功回调、异常回调与 Promise 签订协议) promise.then(function(value) { // success }).catch(function (error) { // error })
Promise 是个非常不错的中介,它只返回可信的信息给 callback。怎么理解可信的概念呢?准确的讲,就是 callback 一定会被异步调用,且只会调用一次。比如在使用第三方库的时候,由于某些原因,(假的)“异步”接口不可靠,它执行了同步代码,而没有进入异步逻辑,如例 8。
例 8
var promise1 = new Promise(function (resolve) { // 由于某些原因导致“异步”接口,被同步执行了 if (true ){ // 同步代码 resolve("B"); } else { // 异步代码 setTimeout(function(){ resolve("B"); },0) } }); // promise依旧会异步执行 promise1.then(function(value){ console.log(value) }); console.log("A"); // A => B (先 A 后 B)
再比如,由于某些原因,异步接口不可靠,resolve 或 reject 被执行了两次。但 Promise 只会通知 callback ,第一次异步接口返回的结果。如例 9:
例 9
var promise2 = new Promise(function (resolve) { // resolve 被执行了 2 次 setTimeout(function(){ resolve("第一次"); },0) setTimeout(function(){ resolve("第二次"); },0) }); // 但 callback 只会被调用一次, promise2.then(function(msg){ console.log(msg) // "第一次" console.log("A") }); // A (只有一个)
介绍完 Promise 的特性后,来看看它如何利用链式调用,解决 callback 模式下,异步代码可读性的问题。链式调用指的是:函数 return 一个可以继续执行的对象,该对象可以继续调用,并且 return 另一个可以继续执行的对象,如此反复达到不断调用的结果。如例 10:
例 10
// return 一个可以继续执行的 Promise 对象 var fetch = function(url){ return new Promise(function (resolve,reject) { ajax(url,resolve,reject); }); } A(); fetch("url1").then(function(){ B(); // 返回一个新的 Promise 实例 return fetch("url2"); }).catch(function(){ C(); // 异常的时候也可以返回一个新的 Promise 实例 return fetch("url2"); // 使用链式写法调用这个新的 Promise 实例的 then 方法 }).then(function() { // 可以继续 return,也可以不继续 return,结束链式调用 D(); }) // A B C D (顺序执行)
如此反复,不断返回一个 Promise 对象,使 Promise 摆脱了 callback 层层嵌套的问题和异步代码“非线性”执行的问题。
另外,Promise 还解决了一个难点,callback 只能捕获当前错误异常。Promise 和 callback 不同,每个 callback 只能知道自己的报错情况,但 Promise 代理着所有的 callback,所有 callback 的报错,都可以由 Promise 统一处理。所以,可以通过在最后设置一个 catch 来捕获之前未捕获异常。
Promise 解决 callback 的异步调用问题,但 Promise 并没有摆脱 callback,它只是将 callback 放到一个可以信任的中间机构,这个中间机构去链接 callback 和异步接口。此外,链式调用的写法并不是非常优雅。接下来介绍的异步(async)函数方案,会给出一个更好的解决方案。
异步(async)函数异步(async)函数是 ES7 的一个新的特性,它结合了 Promise,让我们摆脱 callback 的束缚,直接用“同步”方式,写异步函数。注意,这里的同步指的是写法同步,但实际依旧是异步执行的。
声明异步函数,只需在普通函数前添加一个关键字 async 即可,如:
async function main(){}
在异步函数中,可以使用 await 关键字,表示等待后面表达式的执行结果,再往下继续执行。表达式一般都是 Promise 实例。如,例 11:
例 11
var timer = function (delay) { return new Promise(function create(resolve,reject) { if(typeof delay !== "number"){ reject(new Error("type error")); } setTimeout(resolve,delay,"done"); }); } async function main{ var value = await timer(100); // 不会立刻执行,等待 100ms 后才开始执行 console.log(value); // done } main();
异步函数和普通函数的调用方式一样,最先执行 main() 函数。之后,会立即执行 timer(100) 函数。等到( await )后面的 promise 函数( timer(100) )返回结果后,程序才会执行下一行代码。
异步函数和普通函数写法基本类似,除了前面提到的声明方式类似和调用方式一样之外,它也可以使用 try...catch 来捕捉异常,也可以传入参数。但在异步函数中使用 return 是没有作用的,这和普通的 callback 函数 return 没有作用是一样原因。callback 或者异步函数是多带带放在 JavaScript 栈(stack)中执行的,这时同步代码已经执行完毕。
在异步函数中,使用 try...catch 异常捕获的方案,代替了 Promise catch 的异常捕获的方案。示例如下:
例 12
async function main(delay){ try{ // timer 在例 11 中有过声明 var value1 = await timer(delay); var value2 = await timer(""); var value3 = await timer(delay); }catch(err){ console.error(err); // Error: type error // at create (:5:14) // at timer ( :3:10) // at A ( :12:10) } } main(0);
更神奇的是,异步函数也遵循,“函数是第一公民”的准则。也可以当作值,传入普通函数和异步函数中执行。需要注意的是,在异步函数中使异步函数用时要使用 await,不然异步函会被同步执行。例子如下:
例 12
async function doAsync(delay){ // timer 在例 11 中有过声明 var value1 = await timer(delay); console.log("A") } async function main(main){ doAsync(0); console.log("B") } main(main); // B A
这个时候打印出来的值是 B A。说明 doAsync 函数中的 await timer(delay) 并被同步执行了。如果要正确异步地执行 doAsync 函数,需要该函数之前添加 await 关键字,如下:
async function main(delay){ var value1 = await timer(delay); console.log("A") } async function doAsync(main){ await main(0); console.log("B") } doAsync(main); // A B
由于异步函数采用类同步的书写方法,所以在处理多个并发请求,新手可能会像下面一样书写:
例 13
var fetch = function (url) { return new Promise(function (resolve,reject) { ajax(url,resolve,reject); }); } async function main(){ try{ var value1 = await fetch("url1"); var value2 = await fetch("url2"); conosle.log(value1,value2); }catch(err){ console.error(err) } } main();
但这样会导致 url2 的请求必需等到 url1 的请求回来后才会发送。如果 url1 与 url2 没有相互的依赖关系,将这两个请求同时发送实现的效果会更好。
Promise.all 的方法,可以很好的处理并发请求。Promise.all 接受将多个 Promise 实例为参数,并将这些参数包装成一个新的 Promise 实例。这样,Promise.all 中所有的请求会第一时间发送出去;在所有的请求成功回来后才会触发 Promise.all 的 resolve 函数;当有一个请求失败,则立即调用 Promise.all 的 reject 函数。
var fetch = function (url) { return new Promise(function (resolve, reject) { ajax(url, resolve, reject); }); } async function main(){ try{ var arrValue = await Promise.all[fetch("url1"),fetch("url2")]; conosle.log(arrValue[0], arrValue[1]); }catch(err){ console.error(err) } } main();
最后对异步函数的内容做个小结:
声明: async function main(){}
异步函数逻辑:可以使用 await
调用: main()
捕获异常: try...catch
传入参数: main("第一个参数")
return:不生效
异步函数作为参数传入其他函数:可以
处理并发逻辑:Promise.all
目前使用最新的 Chrome/node 已经支持 ES7 异步函数的写法了,另外也可以通过 Babel 以将异步函数转义为 ES5 的语法执行。大家可以自己动手试试,使用异步函数,用类同步的方式,书写异步代码。
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/90840.html
摘要:闭包利用的,其实就是作用域嵌套情况下,内部作用域可以访问外部作用域这一特性。之所以要将闭包和垃圾回收策略联系在一起,是因为这涉及到闭包的一些重要特性,如变量暂时存储和内存泄漏。因为匿名函数回调的闭包实际引用的是变量,而非变量的值。 本文旨在总结在js学习过程中,对闭包的思考,理解,以及一些反思,如有不足,还请大家指教 闭包---closure 闭包是js中比较特殊的一个概念,其特殊之处...
摘要:但是的的出现碉堡的新朋友,我们可以轻松写出同步风格的代码同时又拥有异步机制,可以说是目前最简单,最优雅,最佳的解决方案了。不敢说这一定是终极的解决方案,但确实是目前最优雅的解决方案 一、异步解决方案的进化史 JavaScript的异步操作一直是个麻烦事,所以不断有人提出它的各种解决方案。可以追溯到最早的回调函数(ajax老朋友),到Promise(不算新的朋友),再到ES6的Gener...
摘要:作者珂珂沪江前端开发工程师本文为原创文章,有不当之处欢迎指出。只对未来发生的事情做出两种基本情况的应对成功和失败。在异步转同步这条道路上,只是一个出彩的点,他还尚有一些缺陷和不足,并不是我们最终的解决方案。 作者:珂珂 (沪江前端开发工程师)本文为原创文章,有不当之处欢迎指出。转载请标明出处。 一个新事物的产生必然是有其历史原因的。为了更好的以同步的方式写异步的代码,人们在JS上操碎了...
摘要:然而,临近规范发布时,有建议提及未来的版本号切换为编年制,比如用同来指代在年末前被定稿的所有版本。总得来说就是版本号不再那么重要了,开始变得更像一个万古长青的活标准。 你不知道的JS(下卷)ES6与之未来 第一章:ES的今与明 在你想深入这本书之前,你应该对(在读此书时)JavaScript的最近标准掌握熟练,也就是ES5(专业来说是ES 5.1)。在此,我们决定全方面地谈论关于将近的...
摘要:异步编程一般用来调取接口拉数据。通过我描述的篇幅,就知道异步编程比同步编程麻烦许多。远古时期,异步编程是通过回调函数来解决的。 半理解系列--Promise的进化史 学过js的都知道,程序有同步编程和异步编程之分,同步就好比流水线,一步一个脚印,做完上个任务才做下一个,异步编程好比客服,客服接了一个电话,收到了一个任务,然后把任务交给另外的人来处理,同时,继续接听下一个电话,等到另外的...
阅读 875·2021-11-16 11:45
阅读 2104·2021-10-09 09:44
阅读 1319·2019-08-30 14:03
阅读 1076·2019-08-26 18:28
阅读 3309·2019-08-26 13:50
阅读 1692·2019-08-23 18:38
阅读 3439·2019-08-23 18:22
阅读 3559·2019-08-23 15:27