如何让异步接口同时支持 callback 和 promise异步编程

如何把 Callback 接口包装成 Promise 接口 - 推酷
如何把 Callback 接口包装成 Promise 接口
前端开发尤其 Node.js 开发中,经常要调用一些异步接口,如:文件操作、网络数据读取。而这些接口默认情况下往往是通过 Callback 方式提供的,即:最后一个参数传入一个回调函数,当出现异常时,将错误信息作为第一个参数传给回调函数,如果正常,第一个参数为 null,后面的参数为对应其他的值。
var fs = require(&fs&);
fs.readFile(&foo.json&, &utf8&, function(err, content){
//异常情况
//正常情况
当这种写法遇上比较复杂的逻辑时,就很容易出现
的问题。为此,开发者也积极寻找对应的解决方案,如:Promise、ES6 Generator + co + Promise、ES2016 草案里的 async functions 等。
这几种方案也是慢慢的在进化,视图更好的处理 callback hell 的问题。但这几种方案一致的依赖基础方式都是 Promise,这也是为什么 Promise 并没有引入新的语法但也写进了 ES6 规范的一个大的原因。甚至现在一些新的接口(如:
)直接返回 Promise。
然后对异步接口的处理方式都依赖 Promise,那么下面就来说下如何将 Callback 接口变成 Promise 接口。
Callback 接口变成 Promise 接口
其实 Callback 接口变成 Promise 接口非常简单,包括现在也有很多库都有类似的方法可以转换,如:
bluebird 模块里有 promisify 方法
es6-promisify 模块
ThinkJS 里的 promisify 方法
由于 Callback 接口的参数方式是固定的,所以很容易变成 Promise 接口,如:
let promisify = (fn, receiver) =& {
return (...args) =& {
return new Promise((resolve, reject) =& {
fn.apply(receiver, [...args, (err, res) =& {
return err ? reject(err) : resolve(res);
几行代码基本就搞定了对 Callback 接口对 Promise 的转换,当然上面的代码是用 ES6 代码写的。用 ES5 写的话可以类似下面这样:
var promisify = function promisify(fn, receiver) {
return function () {
for (var _len = arguments.length, args = Array(_len), _key = 0; _key & _ _key++) {
args[_key] = arguments[_key];
return new Promise(function (resolve, reject) {
fn.apply(receiver, [].concat(args, [function (err, res) {
return err ? reject(err) : resolve(res);
这样一个函数,那么把 Callback 接口变成 Promise 接口就非常简单了,如:
var fs = require(&fs&);
var readFilePromise = promisify(fs.readFile, fs); //包装为 Promise 接口
readFilePromise(&foo.json&, &utf8&).then(function(content){
//正常情况
}).catch(function(err){
//异常情况
有了快速转换的方法后,就不用去找模块对应的 Promise 版本的模块了。
有些设计不合理的接口可能会传递多个值给回调函数,如:
var fn = function(foo, callback){
if(success){
callback(null, content1, content2);
callback(err);
上面的代码在正常情况下会传递 2 个参数给回调函数,由于 Promise resolve 的时候只能传入一个值,所以这种接口变成 Promise 接口后是无法获取到 content2 数据的。
对于这种情况只能手工来包装了,同时顺便鄙视下设计这个接口的人。
有些人担心大量使用 Promise 会引起性能的下降,这个事情在当初 Node.js 设计接口时也争吵了很久,有时候易用性和性能本来就是有些互斥的。
其实可以使用高性能的 Promise 库来提高性能,如:bluebird。简单对比测试发现,blurbird 的性能是 V8 里内置的 Promise 3 倍左右(bluebird 的优化方式见
可以通过下面的方式替换调内置的 Promise:
global.Promise = require(&bluebird&);
如果项目里用了 Babel 编译 ES6 代码的话,可以用下面的方式替换:
//Babel 编译时会把 Promise 编译为 Babel 依赖的 Promise
require(&babel-runtime/core-js/promise&).default = require(&bluebird&);
global.Promise = require(&bluebird&);
已发表评论数()
请填写推刊名
描述不能大于100个字符!
权限设置: 公开
仅自己可见
正文不准确
标题不准确
排版有问题
主题不准确
没有分页内容
图片无法显示
视频无法显示
与原文不一致【转】JavaScript 异步进化史
来源:博客园
JS 中最基础的异步调用方式是 callback,它将回调函数 callback 传给异步 API,由浏览器或 Node 在异步完成后,通知 JS 引擎调用 callback。对于简单的异步操作,用 callback 实现,是够用的。但随着负责交互页面和 Node 出现,callback 方案的弊端开始浮现出来。 Promise 规范孕育而生,并被纳入 ES6 的规范中。后来 ES7 又在 Promise 的基础上将 async 函数纳入标准。此为 JavaScript 异步进化史。


通常,代码是由上往下依次执行的。如果有多个任务,就必需排队,前一个任务完成,后一个任务才会执行。这种执行模式称之为:同步(synchronous)。新手容易把计算机用语中的同步,和日常用语中的同步弄混淆。如,“把文件同步到云端”中的同步,指的是“使...保持一致”。而在计算机中,同步指的是任务从上往下依次执行的模式。比如:

A();
B();
C();

在这段代码中,A、B、C是三个不同的函数,每个函数都是一个不相关的任务。在同步模式,计算机会先执行 A 任务,再执行 B 任务,最后执行 C 任务。在大部分情况,同步模式都没问题。但是如果 B 任务是一个耗时很长的网络请求,而 C 任务恰好是展现新页面,就会导致网页卡顿。
更好解决方案是,将 B 任务分成两个部分。一部分立即执行网络请求的任务,另一部分在请求回来后的执行任务。这种一部分立即执行,另一部分在未来执行的模式称为异步。

A();
// 在现在发送请求 
ajax('url1',function B() {
// 在未来某个时刻执行
})
C();
// 执行顺序 A =& C =& B

实际上,JS 引擎并没有直接处理网络请求的任务,它只是调用了浏览器的网络请求接口,由浏览器发送网络请求并监听返回的数据。JavaScript 异步能力的本质是浏览器或 Node 的多线程能力。
callback
未来执行的函数通常也叫 callback。使用 callback 的异步模式,解决了阻塞的问题,但是也带来了一些其他问题。在最开始,我们的函数是从上往下书写的,也是从上往下执行的,这种“线性”模式,非常符合我们的思维习惯,但是现在却被 callback 打断了!在上面一段代码中,现在它跳过 B 任务先执行了 C任务!这种异步“非线性”的代码会比同步“线性”的代码,更难阅读,因此也更容易滋生 BUG。
试着判断下面这段代码的执行顺序,你会对“非线性”代码比“线性”代码更难以阅读,体会更深。

A();

ajax('url1', function(){
B();

ajax('url2', function(){

});
E();
// A =& E =& B =& D =& C

这段代码中,从上往下执行的顺序被 Callback 打乱了。我们的阅读代码视线是A =& B =& C =& D =& E,但是执行顺序却是A =& E =& B =& D =& C,这就是非线性代码带来的糟糕之处。
通过将ajax后面执行的任务提前,可以更容易看懂代码的执行顺序。虽然代码因为嵌套看起来不美观,但现在的执行顺序却是从上到下的“线性”方式。这种技巧在写多重嵌套的代码时,是非常有用的。

A();
E();

ajax('url1', function(){
D();

ajax('url2', function(){

});
// A =& E =& B =& D =& C

上一段代码只有处理了成功回调,并没处理异常回调。接下来,把异常处理回调加上,再来讨论代码“线性”执行的问题。

A();

ajax('url1', function(){
B();

ajax('url2', function(){
},function(){

},function(){

});

加上异常处理回调后,url1的成功回调函数 B 和异常回调函数 E,被分开了。这种“非线性”的情况又出现了。
在 node 中,为了解决的异常回调导致的“非线性”的问题,制定了错误优先的策略。node 中 callback 的第一个参数,专门用于判断是否发生异常 

A();

get('url1', function(error){
if(error){
}else {
get('url2', function(error){
if(error){
}else{
}
});

到此,callback 引起的“非线性”问题基本得到解决。遗憾的是,使用 callback 嵌套,一层层if else和回调函数,一旦嵌套层数多起来,阅读起来不是很方便。此外,callback 一旦出现异常,只能在当前回调函数内部处理异常。
promise
在 JavaScript 的异步进化史中,涌现出一系列解决 callback 弊端的库,而 Promise 成为了最终的胜者,并成功地被引入了 ES6 中。它将提供了一个更好的“线性”书写方式,并解决了异步异常只能在当前回调中被捕获的问题。
Promise 就像一个中介,它承诺会将一个可信任的异步结果返回。首先 Promise 和异步接口签订一个协议,成功时,调用resolve函数通知 Promise,异常时,调用reject通知 Promise。另一方面 Promise 和 callback 也签订一个协议,由 Promise 在将来返回可信任的值给then和catch中注册的 callback。

// 创建一个 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 一定会被异步调用,且只会被调用一次。

var promise1 = new Promise(function (resolve) {
// 可能由于某些原因导致同步调用
resolve('B');
});
// promise依旧会异步执行
promise1.then(function(value){
console.log(value)
});
console.log('A');
// A B (先 A 后 B)


var promise2 = new Promise(function (resolve) {
// 成功回调被通知了2次
setTimeout(function(){
resolve();
},0)
});
// promise只会调用一次
promise2.then(function(){
console.log('A')
});
// A (只有一个)

var promise3 = new Promise(function (resolve,reject) {
// 成功回调先被通知,又通知了失败回调
setTimeout(function(){
resolve();
reject();
},0)
});
// promise只会调用成功回调
promise3.then(function(){
console.log('A')
}).catch(function(){
console.log('B')
});
// A(只有A)

View Code
介绍完 Promise 的特性后,来看看它如何利用链式调用,解决异步代码可读性的问题的。

var fetch = function(url){
// 返回一个新的 Promise 实例
return new Promise(function (resolve,reject) {
ajax(url,resolve,reject);
});
}

A();
fetch('url1').then(function(){
// 返回一个新的 Promise 实例
return fetch('url2');
}).catch(function(){
// 异常的时候也可以返回一个新的 Promise 实例
return fetch('url2');
// 使用链式写法调用这个新的 Promise 实例的 then 方法

}).then(function() {
// 继续返回一个新的 Promise 实例...
})
// A B C ...

View Code
如此反复,不断返回一个 Promise 对象,再采用链式调用的方式不断地调用。使 Promise 摆脱了 callback 层层嵌套的问题和异步代码“非线性”执行的问题。
Promise 解决的另外一个难点是 callback 只能捕获当前错误异常。Promise 和 callback 不同,每个 callback 只能知道自己的报错情况,但 Promise 代理着所有的 callback,所有 callback 的报错,都可以由 Promise 统一处理。所以,可以通过catch来捕获之前未捕获的异常。
Promise 解决了 callback 的异步调用问题,但 Promise 并没有摆脱 callback,它只是将 callback 放到一个可以信任的中间机构,这个中间机构去链接我们的代码和异步接口。

异步(async)函数是 ES7 的一个新的特性,它结合了 Promise,让我们摆脱 callback 的束缚,直接用类同步的“线性”方式,写异步函数。
声明异步函数,只需在普通函数前添加一个关键字 async 即可,如async function main(){} 。在异步函数中,可以使用await关键字,表示等待后面表达式的执行结果,一般后面的表达式是 Promise 实例。

async function main{
// timer 是在上一个例子中定义的
var value = await timer(100);
console.log(value); // done (100ms 后返回 done)
}

main();

异步函数和普通函数一样调用 main() 。调用后,会立即执行异步函数中的第一行代码 var value = await timer(100) 。等到异步执行完成后,才会执行下一行代码。
除此之外,异步函数和其他函数基本类似,它使用try...catch来捕捉异常。也可以传入参数。但不要在异步函数中使用return来返回值。

var
timer = new Promise(function create(resolve,reject) {
if(typeof delay !== 'number'){
reject(new Error('type error'));
setTimeout(resolve,delay,'done');
});

async function main(delay){
var value1 = await timer(delay);
var value2 = await timer('');
var value3 = await timer(delay);
}catch(err){
console.error(err);
// Error: type error
at create (&anonymous&:5:14)
at timer (&anonymous&:3:10)
at A (&anonymous&:12:10)
}
}
main(0);

View Code
异步函数也可以被当作值,传入普通函数和异步函数中执行。但是在异步函数中,使用异步函数时要注意,如果不使用await,异步函数会被同步执行。

async function main(delay){
var value1 = await timer(delay);
console.log('A')
}

async function doAsync(main){
main(0);
console.log('B')
}

doAsync(main);
// B A

这个时候打印出来的值是 B A。说明 doAsync 函数并没有等待 main 的异步执行完毕就执行了 console。如果要让 console 在 main 的异步执行完毕后才执行,我们需要在main前添加关键字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

由于异步函数采用类同步的书写方法,所以在处理多个并发请求,新手可能会像下面一样书写。这样会导致url2的请求必需等到url1的请求回来后才会发送。

var fetch = function (url) {
return new Promise(function (resolve,reject) {
ajax(url,resolve,reject);
});
}

async function main(){
var value1 = await fetch('url1');
var value2 = await fetch('url2');
conosle.log(value1,value2);
}catch(err){
console.error(err)
}
}

main();

View Code
使用Promise.all的方法来解决这个问题。Promise.all用于将多个Promise实例,包装成一个新的 Promis e实例,当所有的 Promise 成功后才会触发Promise.all的resolve函数,当有一个失败,则立即调用Promise.all的reject函数。

var fetch = function (url) {
return new Promise(function (resolve,reject) {
ajax(url,resolve,reject);
});
}

async function main(){
var arrValue = await Promise.all[fetch('url1'),fetch('url2')];
conosle.log(arrValue[0],arrValue[1]);
}catch(err){
console.error(err)
}
}

main();

View Code
目前使用 Babel 已经支持 ES7 异步函数的转码了,大家可以在自己的项目中开始尝试。
免责声明:本站部分内容、图片、文字、视频等来自于互联网,仅供大家学习与交流。相关内容如涉嫌侵犯您的知识产权或其他合法权益,请向本站发送有效通知,我们会及时处理。反馈邮箱&&&&。
学生服务号
在线咨询,奖学金返现,名师点评,等你来互动}

我要回帖

更多关于 promise 异步转同步 的文章

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信