现代JS中的流程控制:详解Callbacks 、Promises 、Async/Await

时间:2022-12-29 20:51:41

JavaScript经常声称是_异步_。那是什么意思?它如何影响发展?近年来这种方法有何变化?

请思考以下代码:


result1 = doSomething1();
result2 = doSomething2(result1);

大多数语言都处理每一行同步。第一行运行并返回结果。第二行在第一行完成后运行无论需要多长时间。

单线程处理

JavaScript在单个处理线程上运行。在浏览器选项卡中执行时,其他所有内容都会停止,因为在并行线程上不会发生对页面DOM的更改;将一个线程重定向到另一个URL而另一个线程尝试追加子节点是危险的。

这对用户来说是显而易见。例如,JavaScript检测到按钮单击,运行计算并更新DOM。完成后,浏览器可以*处理队列中的下一个项目。

(旁注:其他语言如PHP也使用单个线程,但可以由多线程服务器(如Apache)管理。同时对同一个PHP运行时页面的两个请求可以启动两个运行隔离的实例的线程。)

使用回调进行异步

单线程引发了一个问题。当JavaScript调用“慢”进程(例如浏览器中的Ajax请求或服务器上的数据库操作)时会发生什么?这个操作可能需要几秒钟 - 甚至几分钟。浏览器在等待响应时会被锁定。在服务器上,Node.js应用程序将无法进一步处理用户请求。

解决方案是异步处理。而不是等待完成,一个进程被告知在结果准备好时调用另一个函数。这称为callback,它作为参数传递给任何异步函数。例如:


doSomethingAsync(callback1);
console.log('finished'); // call when doSomethingAsync completes
function callback1(error) {
if (!error) console.log('doSomethingAsync complete');
}

doSomethingAsync()接受一个回调函数作为参数(只传递对该函数的引用,因此几乎没有开销)。doSomethingAsync()需要多长时间并不重要;我们所知道的是callback1()将在未来的某个时刻执行。控制台将显示:


finished
doSomethingAsync complete

回调地狱

通常,回调只能由一个异步函数调用。因此可以使用简洁的匿名内联函数:


doSomethingAsync(error => {
if (!error) console.log('doSomethingAsync complete');
});

通过嵌套回调函数,可以串行完成一系列两个或多个异步调用。例如:


async1((err, res) => {
if (!err) async2(res, (err, res) => {
if (!err) async3(res, (err, res) => {
console.log('async1, async2, async3 complete.');
});
});
});

不幸的是,这引入了回调地狱 - 一个臭名昭着的概念(http://callbackhell.com/) !代码难以阅读,并且在添加错误处理逻辑时会变得更糟。

回调地狱在客户端编码中相对较少。如果您正在进行Ajax调用,更新DOM并等待动画完成,它可以深入两到三个级别,但它通常仍然可以管理。

操作系统或服务器进程的情况不同。Node.js API调用可以接收文件上载,更新多个数据库表,写入日志,并在发送响应之前进行进一步的API调用。

Promises

ES2015(ES6)推出了Promises。回调仍然可以使用,但Promises提供了更清晰的语法chains异步命令,因此它们可以串行运行(更多相关内容)。

要启用基于Promise的执行,必须更改基于异步回调的函数,以便它们立即返回Promise对象。该promises对象在将来的某个时刻运行两个函数之一(作为参数传递):

  • resolve :处理成功完成时运行的回调函数
  • reject :发生故障时运行的可选回调函数。

在下面的示例中,数据库API提供了一个接受回调函数的connect()方法。外部asyncDBconnect()函数立即返回一个新的Promise,并在建立连接或失败后运行resolve()或reject():


const db = require('database'); // connect to database
function asyncDBconnect(param) { return new Promise((resolve, reject) => { db.connect(param, (err, connection) => {
if (err) reject(err);
else resolve(connection);
}); }); }

Node.js 8.0+提供了util.promisify()实用程序,将基于回调的函数转换为基于Promise的替代方法。有几个条件:

  1. 将回调作为最后一个参数传递给异步函数
  2. 回调函数必须指向一个错误,后跟一个值参数。

例子:


// Node.js: promisify fs.readFile
const
util = require('util'),
fs = require('fs'),
readFileAsync = util.promisify(fs.readFile); readFileAsync('file.txt');

各种客户端库也提供promisify选项,但您可以自己创建几个:


// promisify a callback function passed as the last parameter
// the callback function must accept (err, data) parameters
function promisify(fn) {
return function() {
return new Promise(
(resolve, reject) => fn(
...Array.from(arguments),
(err, data) => err ? reject(err) : resolve(data)
)
);
}
} // example
function wait(time, callback) {
setTimeout(() => { callback(null, 'done'); }, time);
} const asyncWait = promisify(wait); ayscWait(1000);

异步链

任何返回Promise的东西都可以启动.then()方法中定义的一系列异步函数调用。每个都传递了上一个解决方案的结果:


asyncDBconnect('http://localhost:1234')
.then(asyncGetSession) // passed result of asyncDBconnect
.then(asyncGetUser) // passed result of asyncGetSession
.then(asyncLogAccess) // passed result of asyncGetUser
.then(result => { // non-asynchronous function
console.log('complete'); // (passed result of asyncLogAccess)
return result; // (result passed to next .then())
})
.catch(err => { // called on any reject
console.log('error', err);
});

同步函数也可以在.then()块中执行。返回的值将传递给下一个.then()(如果有)。

.catch()方法定义了在触发任何先前拒绝时调用的函数。此时,不会再运行.then()方法。您可以在整个链中使用多个.catch()方法来捕获不同的错误。

ES2018引入了一个.finally()方法,无论结果如何都运行任何最终逻辑 - 例如,清理,关闭数据库连接等。目前仅支持Chrome和Firefox,但技术委员会39已发布了 .finally() polyfill.


function doSomething() {
doSomething1()
.then(doSomething2)
.then(doSomething3)
.catch(err => {
console.log(err);
})
.finally(() => {
// tidy-up here!
});
}

使用Promise.all()进行多个异步调用

Promise .then()方法一个接一个地运行异步函数。如果顺序无关紧要 - 例如,初始化不相关的组件 - 同时启动所有异步函数并在最后(最慢)函数运行解析时结束更快。

这可以通过Promise.all()来实现。它接受一组函数并返回另一个Promise。例如:


Promise.all([ async1, async2, async3 ])
.then(values => { // array of resolved values
console.log(values); // (in same order as function array)
return values;
})
.catch(err => { // called on any reject
console.log('error', err);
});

如果任何一个异步函数调用失败,则Promise.all()立即终止。

使用Promise.race的多个异步调用()

Promise.race()与Promise.all()类似,只是它会在first Promise解析或拒绝后立即解析或拒绝。只有最快的基于Promise的异步函数才能完成:


Promise.race([ async1, async2, async3 ])
.then(value => { // single value
console.log(value);
return value;
})
.catch(err => { // called on any reject
console.log('error', err);
});

但是有什么别的问题吗?

Promises 减少了回调地狱但引入了别的问题。

教程经常没有提到_整个Promise链是异步的。使用一系列promise的任何函数都应返回自己的Promise或在最终的.then(),. catch()或.finally()方法中运行回调函数。

学习基础知识至关重要。

更多的关于Promises的资源:

Async/Await

Promises 可能令人生畏,因此ES2017引入了async and await。 虽然它可能只是语法糖,它使Promise更完善,你可以完全避免.then()链。 考虑下面的基于Promise的示例:


function connect() { return new Promise((resolve, reject) => { asyncDBconnect('http://localhost:1234')
.then(asyncGetSession)
.then(asyncGetUser)
.then(asyncLogAccess)
.then(result => resolve(result))
.catch(err => reject(err)) });
} // run connect (self-executing function)
(() => {
connect();
.then(result => console.log(result))
.catch(err => console.log(err))
})();

用这个重写一下async/await:

  • 外部函数必须以async语句开头
  • 对异步的基于Promise的函数的调用必须在await之前,以确保在下一个命令执行之前完成处理。

async function connect() { try {
const
connection = await asyncDBconnect('http://localhost:1234'),
session = await asyncGetSession(connection),
user = await asyncGetUser(session),
log = await asyncLogAccess(user); return log;
}
catch (e) {
console.log('error', err);
return null;
} } // run connect (self-executing async function)
(async () => { await connect(); })();

await有效地使每个调用看起来好像是同步的,而不是阻止JavaScript的单个处理线程。 此外,异步函数总是返回一个Promise,因此它们可以被其他异步函数调用。

async/await 代码可能不会更短,但有相当大的好处:

1、语法更清晰。括号更少,错误更少。

2、调试更容易。可以在任何await语句上设置断点。
3、错误处理更好。try / catch块可以与同步代码一样使用。

4、支持很好。它在所有浏览器(IE和Opera Mini除外)和Node 7.6+中都得到了支持。

但是并非所有都是完美的......

切勿滥用async/await

async / await仍然依赖于Promises,它最终依赖于回调。你需要了解Promises是如何工作的,并且没有Promise.all()和Promise.race()的直接等价物。并且不要忘记Promise.all(),它比使用一系列不相关的await命令更有效。

同步循环中的异步等待

在某些时候,您将尝试调用异步函数中的同步循环。例如:


async function process(array) {
for (let i of array) {
await doSomething(i);
}
}

它不会起作用。这也不会:


async function process(array) {
array.forEach(async i => {
await doSomething(i);
});
}

循环本身保持同步,并且总是在它们的内部异步操作之前完成。

ES2018引入了异步迭代器,它与常规迭代器一样,但next()方法返回Promise。因此,await关键字可以与for循环一起用于串行运行异步操作。例如:


async function process(array) {
for await (let i of array) {
doSomething(i);
}
}

但是,在实现异步迭代器之前,最好将数组项映射到异步函数并使用Promise.all()运行它们。例如:


const
todo = ['a', 'b', 'c'],
alltodo = todo.map(async (v, i) => {
console.log('iteration', i);
await processSomething(v);
}); await Promise.all(alltodo);

这具有并行运行任务的好处,但是不可能将一次迭代的结果传递给另一次迭代,并且映射大型数组可能在性能消耗上是很昂贵。

try/catch 有哪些问题了?

如果省略任何await失败的try / catch,async函数将以静默方式退出。如果您有一组很长的异步await命令,则可能需要多个try / catch块。

一种替代方案是高阶函数,它捕获错误,因此try / catch块变得不必要(thanks to @wesbos for the suggestion):


async function connect() { const
connection = await asyncDBconnect('http://localhost:1234'),
session = await asyncGetSession(connection),
user = await asyncGetUser(session),
log = await asyncLogAccess(user); return true;
} // higher-order function to catch errors
function catchErrors(fn) {
return function (...args) {
return fn(...args).catch(err => {
console.log('ERROR', err);
});
}
} (async () => {
await catchErrors(connect)();
})();

但是,在应用程序必须以与其他错误不同的方式对某些错误做出反应的情况下,此选项可能不实用。

尽管有一些陷阱,async / await是JavaScript的一个优雅补充。更多资源:

JavaScript 旅程

异步编程是一项在JavaScript中无法避免的挑战。回调在大多数应用程序中都是必不可少的,但它很容易陷入深层嵌套的函数中。

Promises 抽象回调,但有许多语法陷阱。 转换现有函数可能是一件苦差事,而.then()链仍然看起来很混乱。

幸运的是,async / await提供了清晰度。代码看起来是同步的,但它不能独占单个处理线程。它将改变你编写JavaScript的方式!

(译者注:Craig Buckler讲解JavaScript的文章都还不错,基本是用一些比较通俗的语言和代码事例讲解了JavaScript的一些特性和一些语法可能出现的问题。感兴趣的朋友可以看一下(https://www.sitepoint.com/aut...))

来源:https://segmentfault.com/a/1190000016143319

现代JS中的流程控制:详解Callbacks 、Promises 、Async/Await的更多相关文章

  1. js中鼠标滚轮事件详解

    js中鼠标滚轮事件详解   (以下内容部分内容参考了http://adomas.org/javascript-mouse-wheel/ ) 之前js 仿Photoshop鼠标滚轮控制输入框取值中已使用 ...

  2. JS中的event 对象详解

    JS中的event 对象详解   JS的event对象 Event属性和方法:1. type:事件的类型,如onlick中的click:2. srcElement/target:事件源,就是发生事件的 ...

  3. js中中括号,大括号使用详解

    js中中括号,大括号使用详解 一.总结 一句话总结:{ } 是一个对象,[ ] 是一个数组 1.js大括号{}表示什么意思? 对象 { } 大括号,表示定义一个对象,大部分情况下要有成对的属性和值,或 ...

  4. js键盘事件全面控制详解

      js键盘事件全面控制 主要分四个部分第一部分:浏览器的按键事件第二部分:兼容浏览器第三部分:代码实现和优化第四部分:总结 第一部分:浏览器的按键事件 用js实现键盘记录,要关注浏览器的三种按键事件 ...

  5. js键盘事件全面控制详解【转】

    js键盘事件全面控制 主要分四个部分第一部分:浏览器的按键事件第二部分:兼容浏览器第三部分:代码实现和优化第四部分:总结 第一部分:浏览器的按键事件 用js实现键盘记录,要关注浏览器的三种按键事件类型 ...

  6. Vue.js中学习使用Vuex详解

    在SPA单页面组件的开发中 Vue的vuex和React的Redux 都统称为同一状态管理,个人的理解是全局状态管理更合适:简单的理解就是你在state中定义了一个数据之后,你可以在所在项目中的任何一 ...

  7. JS中的this对象详解

    JS中this关键字很常见,但是它似乎变幻莫测,让人抓狂.这篇文章就来揭示其中的奥秘. 借助阮一峰老师的话:它代表函数运行时,自动生成的一个内部对象,只能在函数内部使用.这句话看似平常,可是要非常注意 ...

  8. JS中的this用法详解

    随着对js的深入学习和使用,你会发现它里面包含了很多令人困惑的机制,比如对象.闭包.原型链继承等等,而这其中肯定包含令你现在或者曾经费解的this,如果你不把心一横,花点时间还真不明白这个this的用 ...

  9. js 中中括号,大括号使用详解

    一.{ } 大括号,表示定义一个对象,大部分情况下要有成对的属性和值,或是函数.如:var LangShen = {"Name":"Langshen",&quo ...

随机推荐

  1. Webstorm常用的快捷键

    WS的常用操作: 常用快捷键(Keymap/Eclipse): 复制当前行: Ctrl+Alt+↓ 向上/下移动当前行: Alt+↑/↓ 删除当前行: Ctrl+D 注释/取消当前行: Ctrl+/ ...

  2. dell 交换机 双链路冗余

    公司海外机房引入2G带宽,是由2个电口绑定实现的.因业务需要扩容到3G,在绑定端口扩展性不太好,因此直接上10G光纤模块. 机房技术人员建议,2g老线路不撤做备份,3g新线路在线使用.使用STP协议实 ...

  3. 使用mitmf 来绕过HSTS站点抓取登陆明文

    使用mitmf 来绕过HSTS站点抓取登陆明文 HSTS简介 HSTS是HTTP Strict Transport Security的缩写,即:"HTTP严格安全传输".当浏览器第 ...

  4. 11th day

    今天MySQL数据库的基本知识就学完了,明天开始做小项目什么的,有点小激动啊... <?php // 定义$sql语句执行函数 function my_query($sql){ $result ...

  5. 对LockWindowUpdate与GetDCEx的理解(以前不知道还可以锁住刷新)

    MSDN如是说:The LockWindowUpdate function disables or enables drawing in the specified window. Only one  ...

  6. html中window对象top 、self 、parent 等属性

    window对象用法: http://www.w3school.com.cn/htmldom/dom_obj_window.asp top 属性返回最顶层的先辈窗口. 该属性返回对一个*窗口的只读引 ...

  7. linux系统安装iprouter

    在上文中将mpls编译进了linux内核,现在需要安装iprouter,安装过程如下: 1) 下载两个文件iproute2-2.6.39.tar.gz和iproute2-v2.6.39-mpls.pa ...

  8. Zabbix监控之迁移zabbix server

    abbix监控中有时会根据需要对zabbix服务器进行迁移,zabbix迁移是非常简单的,因为zabbix的前端所有的操作都存在zabbix数据库里.所以zabbix迁移只需对zabbix库中相应的表 ...

  9. 缓存cache介绍

    1.  为何要用缓存.缓存的目的是为了什么?(https://my.oschina.net/u/3378039/blog/2986697) 一个程序的瓶颈在于数据库,内存的速度远远大于硬盘的速度,当我 ...

  10. django 不能访问静态资源的解决办法

    最近在中文win10下使用python的django搭建web测试服务器,发现一个诡异的现象,正常配置好django的模型,视图和模板, 1.setting.py内容如下: ""& ...