Promise是JavaScript异步操作解决方案。介绍Promise之前,先对异步操作做一个详细介绍。

JavaScript的异步执行

概述

Javascript语言的执行环境是”单线程”(single thread)。所谓”单线程”,就是指一次只能完成一件任务。如果有多个任务,就必须排队,前面一个任务完成,再执行后面一个任务。

这种模式的好处是实现起来比较简单,执行环境相对单纯;坏处是只要有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行。常见的浏览器无响应(假死),往往就是因为某一段Javascript代码长时间运行(比如死循环),导致整个页面卡在这个地方,其他任务无法执行。

JavaScript语言本身并不慢,慢的是读写外部数据,比如等待Ajax请求返回结果。这个时候,如果对方服务器迟迟没有响应,或者网络不通畅,就会导致脚本的长时间停滞。

为了解决这个问题,Javascript语言将任务的执行模式分成两种:同步(Synchronous)和异步(Asynchronous)。”同步模式”就是传统做法,后一个任务等待前一个任务结束,然后再执行,程序的执行顺序与任务的排列顺序是一致的、同步的。这往往用于一些简单的、快速的、不涉及读写的操作。

“异步模式”则完全不同,每一个任务分成两段,第一段代码包含对外部数据的请求,第二段代码被写成一个回调函数,包含了对外部数据的处理。第一段代码执行完,不是立刻执行第二段代码,而是将程序的执行权交给第二个任务。等到外部数据返回了,再由系统通知执行第二段代码。所以,程序的执行顺序与任务的排列顺序是不一致的、异步的。

以下总结了”异步模式”编程的几种方法,理解它们可以让你写出结构更合理、性能更出色、维护更方便的JavaScript程序。

回调函数

回调函数是异步编程最基本的方法。

假定有两个函数f1和f2,后者等待前者的执行结果。

 f1();
f2();

上面代码中,f2必须要等到f1执行完,才能执行。

如果f1是一个很耗时的任务,可以考虑改写f1,把f2写成f1的回调函数。

 function f1(callback) {
setTimeout(function () {
// f1的任务代码
// ...
callback();
}, 0);
}

执行代码就变成下面这样。

 f1(f2);

采用这种方式,我们把同步操作变成了异步操作,setTimeout(fn, 0)fn放到下一轮事件循环执行。f1不会堵塞程序运行,相当于先执行程序的主要逻辑,将耗时的操作推迟执行。

回调函数的优点是简单、容易理解和部署,缺点是不利于代码的阅读和维护,各个部分之间高度耦合(Coupling),使得程序结构混乱、流程难以追踪(尤其是回调函数嵌套的情况),而且每个任务只能指定一个回调函数。

事件监听

另一种思路是采用事件驱动模式。任务的执行不取决于代码的顺序,而取决于某个事件是否发生。

还是以f1f2为例。首先,为f1绑定一个事件(这里采用的jQuery的写法)。

 f1.on('done', f2);

上面这行代码的意思是,当f1发生done事件,就执行f2。然后,对f1进行改写:

 function f1(){
setTimeout(function () {
// f1的任务代码
f1.trigger('done');
}, 1000);
}

上面代码中,f1.trigger('done')表示,执行完成后,立即触发done事件,从而开始执行f2

这种方法的优点是比较容易理解,可以绑定多个事件,每个事件可以指定多个回调函数,而且可以”去耦合“(Decoupling),有利于实现模块化。缺点是整个程序都要变成事件驱动型,运行流程会变得很不清晰。

发布/订阅

“事件”完全可以理解成”信号”,如果存在一个”信号中心”,某个任务执行完成,就向信号中心”发布”(publish)一个信号,其他任务可以向信号中心”订阅”(subscribe)这个信号,从而知道什么时候自己可以开始执行。这就叫做”发布/订阅模式“(publish-subscribe pattern),又称”观察者模式“(observer pattern)。

这个模式有多种实现,下面采用的是Ben Alman的Tiny Pub/Sub,这是jQuery的一个插件。

首先,f2向”信号中心”jQuery订阅”done”信号。

 jQuery.subscribe("done", f2);

然后,f1进行如下改写:

 function f1(){
setTimeout(function () {
// f1的任务代码
jQuery.publish("done");
}, 1000);
}

jQuery.publish(“done”)的意思是,f1执行完成后,向”信号中心”jQuery发布”done”信号,从而引发f2的执行。

f2完成执行后,也可以取消订阅(unsubscribe)。

 jQuery.unsubscribe("done", f2);

这种方法的性质与”事件监听”类似,但是明显优于后者。因为我们可以通过查看”消息中心”,了解存在多少信号、每个信号有多少订阅者,从而监控程序的运行。

异步操作的流程控制

如果有多个异步操作,就存在一个流程控制的问题:确定操作执行的顺序,以后如何保证遵守这种顺序。

 function async(arg, callback) {
console.log('参数为 ' + arg +' , 1秒后返回结果');
setTimeout(function() { callback(arg * 2); }, 1000);
}

上面代码的async函数是一个异步任务,非常耗时,每次执行需要1秒才能完成,然后再调用回调函数。

如果有6个这样的异步任务,需要全部完成后,才能执行下一步的final函数。

 function final(value) {
console.log('完成: ', value);
}

请问应该如何安排操作流程?

 async(1, function(value){
async(value, function(value){
async(value, function(value){
async(value, function(value){
async(value, function(value){
async(value, final);
});
});
});
});
});

上面代码采用6个回调函数的嵌套,不仅写起来麻烦,容易出错,而且难以维护。

串行执行

我们可以编写一个流程控制函数,让它来控制异步任务,一个任务完成以后,再执行另一个。这就叫串行执行。

 var items = [ 1, 2, 3, 4, 5, 6 ];
var results = [];
function series(item) {
if(item) {
async( item, function(result) {
results.push(result);
return series(items.shift());
});
} else {
return final(results);
}
}
series(items.shift());

上面代码中,函数series就是串行函数,它会依次执行异步任务,所有任务都完成后,才会执行final函数。items数组保存每一个异步任务的参数,results数组保存每一个异步任务的运行结果。

并行执行

流程控制函数也可以是并行执行,即所有异步任务同时执行,等到全部完成以后,才执行final函数。

 var items = [ 1, 2, 3, 4, 5, 6 ];
var results = []; items.forEach(function(item) {
async(item, function(result){
results.push(result);
if(results.length == items.length) {
final(results);
}
})
});

上面代码中,forEach方法会同时发起6个异步任务,等到它们全部完成以后,才会执行final函数。

并行执行的好处是效率较高,比起串行执行一次只能执行一个任务,较为节约时间。但是问题在于如果并行的任务较多,很容易耗尽系统资源,拖慢运行速度。因此有了第三种流程控制方式。

并行与串行的结合

所谓并行与串行的结合,就是设置一个门槛,每次最多只能并行执行n个异步任务。这样就避免了过分占用系统资源。

 var items = [ 1, 2, 3, 4, 5, 6 ];
var results = [];
var running = 0;
var limit = 2; function launcher() {
while(running < limit && items.length > 0) {
var item = items.shift();
async(item, function(result) {
results.push(result);
running--;
if(items.length > 0) {
launcher();
} else if(running == 0) {
final();
}
});
running++;
}
} launcher();

上面代码中,最多只能同时运行两个异步任务。变量running记录当前正在运行的任务数,只要低于门槛值,就再启动一个新的任务,如果等于0,就表示所有任务都执行完了,这时就执行final函数。

Promise对象

简介

Promise对象是CommonJS工作组提出的一种规范,目的是为异步操作提供统一接口

那么,什么是Promises?

首先,它是一个对象,也就是说与其他JavaScript对象的用法,没有什么两样;其次,它起到代理作用(proxy),充当异步操作与回调函数之间的中介。它使得异步操作具备同步操作的接口,使得程序具备正常的同步运行的流程,回调函数不必再一层层嵌套。

简单说,它的思想是,每一个异步任务立刻返回一个Promise对象,由于是立刻返回,所以可以采用同步操作的流程。这个Promises对象有一个then方法,允许指定回调函数,在异步任务完成后调用。

比如,异步操作f1返回一个Promise对象,它的回调函数f2写法如下。

 (new Promise(f1)).then(f2);

这种写法对于多层嵌套的回调函数尤其方便。

 // 传统写法
step1(function (value1) {
step2(value1, function(value2) {
step3(value2, function(value3) {
step4(value3, function(value4) {
// ...
});
});
});
}); // Promises的写法
(new Promise(step1))
.then(step2)
.then(step3)
.then(step4);

从上面代码可以看到,采用Promises接口以后,程序流程变得非常清楚,十分易读。

注意,为了便于理解,上面代码的Promise对象的生成格式,做了简化,真正的语法请参照下文。

总的来说,传统的回调函数写法使得代码混成一团,变得横向发展而不是向下发展。Promises规范就是为了解决这个问题而提出的,目标是使用正常的程序流程(同步),来处理异步操作。它先返回一个Promise对象,后面的操作以同步的方式,寄存在这个对象上面。等到异步操作有了结果,再执行前期寄放在它上面的其他操作。

Promises原本只是社区提出的一个构想,一些外部函数库率先实现了这个功能。ECMAScript 6将其写入语言标准,因此目前JavaScript语言原生支持Promise对象。

Promise接口

前面说过,Promise接口的基本思想是,异步任务返回一个Promise对象。

Promise对象只有三种状态。

  • 异步操作“未完成”(pending)
  • 异步操作“已完成”(resolved,又称fulfilled)
  • 异步操作“失败”(rejected)

这三种的状态的变化途径只有两种。

  • 异步操作从“未完成”到“已完成”
  • 异步操作从“未完成”到“失败”。

这种变化只能发生一次,一旦当前状态变为“已完成”或“失败”,就意味着不会再有新的状态变化了。因此,Promise对象的最终结果只有两种。

  • 异步操作成功,Promise对象传回一个值,状态变为resolved
  • 异步操作失败,Promise对象抛出一个错误,状态变为rejected

Promise对象使用then方法添加回调函数。then方法可以接受两个回调函数,第一个是异步操作成功时(变为resolved状态)时的回调函数,第二个是异步操作失败(变为rejected)时的回调函数(可以省略)。一旦状态改变,就调用相应的回调函数。

 // po是一个Promise对象
po.then(
console.log,
console.error
);

上面代码中,Promise对象po使用then方法绑定两个回调函数:操作成功时的回调函数console.log,操作失败时的回调函数console.error(可以省略)。这两个函数都接受异步操作传回的值作为参数。

then方法可以链式使用。

 po
.then(step1)
.then(step2)
.then(step3)
.then(
console.log,
console.error
);

上面代码中,po的状态一旦变为resolved,就依次调用后面每一个then指定的回调函数,每一步都必须等到前一步完成,才会执行。最后一个then方法的回调函数console.logconsole.error,用法上有一点重要的区别。console.log只显示回调函数step3的返回值,而console.error可以显示step1step2step3之中任意一个发生的错误。也就是说,假定step1操作失败,抛出一个错误,这时step2step3都不会再执行了(因为它们是操作成功的回调函数,而不是操作失败的回调函数)。Promises对象开始寻找,接下来第一个操作失败时的回调函数,在上面代码中是console.error。这就是说,Promises对象的错误有传递性。

 try {
var v1 = step1(po);
var v2 = step2(v1);
var v3 = step3(v2);
console.log(v3);
} catch (error) {
console.error(error);
}

Promise对象的生成

ES6提供了原生的Promise构造函数,用来生成Promise实例。

下面代码创造了一个Promise实例。

从同步的角度看,上面的代码大致等同于下面的形式。

 var promise = new Promise(function(resolve, reject) {
// 异步操作的代码 if (/* 异步操作成功 */){
resolve(value);
} else {
reject(error);
}
});

Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolvereject。它们是两个函数,由JavaScript引擎提供,不用自己部署。

resolve函数的作用是,将Promise对象的状态从“未完成”变为“成功”(即从Pending变为Resolved),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去;reject函数的作用是,将Promise对象的状态从“未完成”变为“失败”(即从Pending变为Rejected),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。

Promise实例生成以后,可以用then方法分别指定Resolved状态和Reject状态的回调函数。

 po.then(function(value) {
// success
}, function(value) {
// failure
});

用法辨析

Promise的用法,简单说就是一句话:使用then方法添加回调函数。但是,不同的写法有一些细微的差别,请看下面四种写法,它们的差别在哪里?

 // 写法一
doSomething().then(function () {
return doSomethingElse();
}); // 写法二
doSomething().then(function () {
doSomethingElse();
}); // 写法三
doSomething().then(doSomethingElse()); // 写法四
doSomething().then(doSomethingElse);

为了便于讲解,下面这四种写法都再用then方法接一个回调函数finalHandler。写法一的finalHandler回调函数的参数,是doSomethingElse函数的运行结果。

 doSomething().then(function () {
return doSomethingElse();
}).then(finalHandler);

写法二的finalHandler回调函数的参数是undefined

 doSomething().then(function () {
doSomethingElse();
return;
}).then(finalHandler);

写法三的finalHandler回调函数的参数,是doSomethingElse函数返回的回调函数的运行结果。

 doSomething().then(doSomethingElse())
.then(finalHandler);

写法四与写法一只有一个差别,那就是doSomethingElse会接收到doSomething()返回的结果。

 doSomething().then(doSomethingElse)
.then(finalHandler);

Promise的应用

加载图片

我们可以把图片的加载写成一个Promise对象。

 var preloadImage = function (path) {
return new Promise(function (resolve, reject) {
var image = new Image();
image.onload = resolve;
image.onerror = reject;
image.src = path;
});
};

Ajax操作

Ajax操作是典型的异步操作,传统上往往写成下面这样。

 function search(term, onload, onerror) {
var xhr, results, url;
url = 'http://example.com/search?q=' + term; xhr = new XMLHttpRequest();
xhr.open('GET', url, true); xhr.onload = function (e) {
if (this.status === 200) {
results = JSON.parse(this.responseText);
onload(results);
}
};
xhr.onerror = function (e) {
onerror(e);
}; xhr.send();
} search("Hello World", console.log, console.error);

如果使用Promise对象,就可以写成下面这样。

 function search(term) {
var url = 'http://example.com/search?q=' + term;
var xhr = new XMLHttpRequest();
var result; var p = new Promise(function (resolve, reject) {
xhr.open('GET', url, true);
xhr.onload = function (e) {
if (this.status === 200) {
result = JSON.parse(this.responseText);
resolve(result);
}
};
xhr.onerror = function (e) {
reject(e);
};
xhr.send();
}); return p;
} search("Hello World").then(console.log, console.error);

加载图片的例子,也可以用Ajax操作完成。

 function imgLoad(url) {
return new Promise(function(resolve, reject) {
var request = new XMLHttpRequest();
request.open('GET', url);
request.responseType = 'blob';
request.onload = function() {
if (request.status === 200) {
resolve(request.response);
} else {
reject(new Error('图片加载失败:' + request.statusText));
}
};
request.onerror = function() {
reject(new Error('发生网络错误'));
};
request.send();
});
}

注意点

1)一般来说,不要在then方法里面定义 Reject 状态的回调函数(即then的第二个参数),总是使用catch方法。

// bad
promise
.then(function(data) {
// success
}, function(err) {
// error
}); // good
promise
.then(function(data) { //cb
// success
})
.catch(function(err) {
// error
});

2)Promise 对象后面要跟catch方法
一般总是建议,Promise 对象后面要跟catch方法,这样可以处理 Promise 内部发生的错误。catch方法返回的还是一个 Promise 对象,因此后面还可以接着调用then方法。
代码运行完catch方法指定的回调函数,会接着运行后面那个then方法指定的回调函数。如果没有报错,则会跳过catch方法。此时,要是最后的then方法里面报错,就与前面的catch无关了。

const someAsyncThing = function() {
return new Promise(function(resolve, reject) {
// 下面一行会报错,因为x没有声明
resolve(x + );
});
}; someAsyncThing()
.catch(function(error) {
console.log('oh no', error);
})
.then(function() {
console.log('carry on');
});
// oh no [ReferenceError: x is not defined]
// carry on

代码中,第二个catch方法用来捕获前一个catch方法抛出的错误。

someAsyncThing().then(function() {
return someOtherAsyncThing();
}).catch(function(error) {
console.log('oh no', error);
// 下面一行会报错,因为y没有声明
y + ;
}).catch(function(error) {
console.log('carry on', error);
});
// oh no [ReferenceError: x is not defined]
// carry on [ReferenceError: y is not defined]

小结

Promise对象的优点在于,让回调函数变成了规范的链式写法,程序流程可以看得很清楚。它的一整套接口,可以实现许多强大的功能,比如为多个异步操作部署一个回调函数、为多个回调函数中抛出的错误统一指定处理方法等等。

而且,它还有一个前面三种方法都没有的好处:如果一个任务已经完成,再添加回调函数,该回调函数会立即执行(即你可以做同步操作)。所以,你不用担心是否错过了某个事件或信号。这种方法的缺点就是,编写和理解都相对比较难。

Promise解决回调地狱的更多相关文章

  1. async + promise 解决回调地狱

    // 解决异步回调地狱的方案: async + promise async function writeFile() {   // 打开文件   const fd = await new Promis ...

  2. 基于PROMISE解决回调地狱问题

    回调地狱问题: 在使用JavaScript时,为了实现某些逻辑经常会写出层层嵌套的回调函数,如果嵌套过多,会极大影响代码可读性和逻辑,这种情况也被成为回调地狱.比如说你要把一个函数 A 作为回调函数, ...

  3. Promise解决回调地狱(多层调用问题)

    Promise # Promise 是异步编程的一种解决方案:从语法上讲,promise是一个对象,从它可以获取异步操作的消息:从本意上讲,它是承诺,承诺它过一段时间会给你一个结果.promise有三 ...

  4. JS之用ES6 Promise解决回调地狱(这里以小程序为例)

    首先 写一个请求的方法,如: /** * 银行窗口 * 你需要给我提供相关的相关参数我帮你提交到服务器上 * 我会给你一个等待区的编号给你 你去等待区等待,我处理完成会去等待区通知你 * @param ...

  5. ES6(promise)_解决回调地狱初体验

    一.前言 通过这个例子对promise解决回调地狱问题有一个初步理解. 二.主要内容 1.回调地狱:如下图所示,一个回调函数里面嵌套一个回调函数,这样的代码可读性较低也比较恶心 2.下面用一个简单的例 ...

  6. promise对象解决回调地狱

    先放一张图片感受一下回调地狱 看起来是真的很让人头痛的东西 而现在我这里使用promise对象来解决回调地狱 采用链式的 then,可以指定一组按照次序调用的回调函数. 这时,前一个 then 里的一 ...

  7. Promise,async/await解决回调地狱

    先说一下async的用法,它作为一个关键字放到函数前面,用于表示函数是一个异步函数,因为async就是异步的意思, 异步函数也就意味着该函数的执行不会阻塞后面代码的执行. 写一个async 函数 as ...

  8. 【JavaScript】 使用Async 和 Promise 完美解决回调地狱

    很久以前就学习过Async和Promise,但总是一知半解的. 今天在写NodeJS的时候,发现好多第三方库使用回调,这样在实际操作中会出现多重回调,这就是传说中的JS回调地狱. 举个例子 有一个方法 ...

  9. 使用ES6的Promise完美解决回调地狱

    相信经常使用ajax的前端小伙伴,都会遇到这样的困境:一个接口的参数会需要使用另一个接口获取. 年轻的前端可能会用同步去解决(笑~),因为我也这么干过,但是极度影响性能和用户体验. 正常的前端会把接口 ...

随机推荐

  1. Eclipse之向前快进,向后快退

    在已经写好的代码上进行修改,存在代码快需要向前快进,向后快退的情况. 选中代码块,然后右击,有Shift Right, Shift Left

  2. 国外AI界牛人主页 及资源链接

    感觉 好博客要收集,还是贴在自己空间里难忘!!! 原文链接:http://blog.csdn.net/hitwengqi/article/details/7907366 http://people.c ...

  3. jQuery插件的怎么写

    对于jQuery之前一直用,也看到过别人写的插件,直到最近才想着学习怎么写自己的jQuery插件,今天看了网上的一些资料,发现其实很简单的. 先看一个简单的jQuery插件的例子 <script ...

  4. texi格式文件的读取

    使用texi2html可以将texi格式的文件转换成html格式的文件. sudo apt-get install texi2html 在对应目录下 texi2html filename.texi 或 ...

  5. handyJson的技术内核

    1.swift对象内存模型: 2.指针操作: 3.协议.泛型.扩展: 4.kvc: 1是所有实现的基础,没有内存对象(类)模型,后面的一切都我从谈起. 在1的基础上使用2进行对象模型信息的提取和转换. ...

  6. [iOS Reverse]logify日志追踪,锁定注入口-控制台查看

    前言 logify是theos的一个组件,路径是: /opt/theos/bin/logify.pl 我们还是以微信红包为例子,根据[iOS Hacking]运行时分析cycript得到的入口文件: ...

  7. java 循环document 通用替换某个字符串或特殊字符

    document 生成xml时 报错 XML-20100: (Fatal Error) Expected ';'.  查了半天发现是 特殊字符 & 不能直接转出,需要进行转换,因为是通用方法很 ...

  8. day34-3 类和对象小知识

    目录 属性查找顺序 类与对象的绑定方法 类与数据类型 对象的高度整合 属性查找顺序 属性查找顺序:先从对象自身查找,对象没有就去类中查找,类中没有则报错 class Student: name = ' ...

  9. 用doxygen风格注释代码生成文档

    目录 用doxygen风格注释代码生成文档 1. 说明 2. 具体操作 2.1 生成头部注释 2.2 安装doxygen 2.3 工程配置 3. 总结 用doxygen风格注释代码生成文档 1. 说明 ...

  10. Travel Card

    Travel Card time limit per test 2 seconds memory limit per test 256 megabytes input standard input o ...