使用JavaScript编写代码会大量的依赖异步计算,计算那些我们现在不需要但将来某个时候可能需要的值。ES6引入一个新概念,用于更简单的异步任务:promise。
promise对象是对我们现在尚未得到的但将来会得到值的占位符,它是对我们最终能够得知异步计算结果的一种保证。如果我们兑现了我们的承诺,那结果会得到一个值。如果发生了问题,结果则是一个错误。使用promise的一个最佳的例子是从服务器获取数据:我们承若最终会拿到数据,但其实总可能发生错误。
console.log("创建一个简单的Promise");
//通过内置Promise构造函数可以创建一个promise对象,需要向构造函数中传入两个函数,参数:resolve和reject
const ninjaPromise = new Promise((resolve, reject) =>{
//通过调用传入的resolve函数,一个promise将被成功兑现(resolve)(通过调用reject则promise被拒绝)
resolve('Hattori');
});
//在一个promise对象上使用then方法后可以传入两个回调函数,promise成功兑现后会调用第一个回调函数。
ninjaPromise.then(ninjaPro =>{
if (ninjaPro === 'Hattori') {
console.log("We were promised Hattori!");
}
}, err=> {
//出现错误时则调用第二个回调函数
console.log('There should not be an error!');
});
使用新的内置构造函数Promise来创建一个promise需要传入一个函数,在上述代码中是一个箭头函数(当然也可以简单地使用一个函数表达式)。这个函数被称为执行函数(executor function),它包含两个参数resolve和reject。当把两个内置函数:resolve和reject作为参数传入Promise构造函数后,执行函数后立刻调用。我们可以说定调用resolve让承若兑现,也可以当错误发生时手动调用reject。
代码调用Promise对象内置的then方法,我们向这个方法中传入两个回调函数,一个成功回调函数和一个失败回调函数。当承若成功兑现(在promise上调用resolve),前一个回调就会被调用,而当出现错误就会调用后一个回调函数(可以是发生了一个未处理的异常,也可以是在promise上调用了reject)。
上述代码中通过向resolve函数传递参数hattori从而创建了一个承若并立即兑现。因此。我们调用then方法时,首先到达成功状态,回调函数被执行,测试程序输出'We were promised Hattori!',测试通过。现在我们对promise如何工作有了一个总体的概念。
理解简单回调函数所带来的问题
使用异步代码的原因在于不希望在执行长时间任务的时候,应用程序的执行被阻塞(影响用户体验)。当前通过使用回调函数解决这个问题:对长期执行的任务提供一个函数,当任务结束后回调该函数。
例如,从服务器获取JSON文件是一个长时间的任务,在这个任务执行期间我们不希望用户感到应用未响应。因此,我们提供了一个回调函数用于任务结束后调用:
getJSON('data/ninjas.json', function () {
/*Handle results*/
});
长时间任务下发生错误也是很自然的现象。问题就在于当回调函数发生错误时,你无法使用内置构造函数来处理,类似下面使用try-catch的方式:
try{
getJSON('data/ninjas.json', function () {
/*Handle results*/
});
} catch (e) {
console.log('Handle errors');
}
导致这个问题的原因在于,当长时间任务开始运行,调用回调函数的代码一般不会和开始任务中的这段代码位于事件循环的同一步骤。导致的结果就是,错误经常丢失。因此许多函数库定义了各自的报错误规约。例如,在Node.js中,回调函数一般具有两个参数:err和data。当错误在某处发生时,err参数中将会是一个非空的值。这就引起了第一个问题:错误难以处理。
当执行了一个长时间运行的任务后,我们经常希望用来获取的数据来做些什么。这会导致开始另一项长期运行的任务,该任务又去触发另一个长期运行的任务,如此一来导致了互相依赖的一系列异步回调任务。所以可能会出现以下情形:
getJSON('data/ninjas.json', function (err, ninjas) {
getJSON(ninjas[0].location, function (err, locationInfo) {
sendOrder(locationInfo, function (err, status) {
/*Process status*/
});
});
});
你的结果可能就是,至少写一两次类似的结构代码:一堆嵌套的回调函数用来表明需要执行的一些列步骤。还会意识到这样的代码难以理解,向其中再插入几步简直是一种痛苦,增加错误处理也会大大增加代码的复杂度。这就是回调函数的第二个问题:执行连续步骤非常棘手。
有时候得到最终结果的这些步骤并不相互依赖,所以我们不必让他们按顺序执行。为了节约时间可以并行地执行这些任务。所以可以使用jQuery的get方法编写类似代码:
var ninjas, mapInfo, plan;
$.get('data/ninjas.json', function (err, data) {
if(err) {
processError(err);
return;
}
ninjas = data;
actionItemArrived();
});
$.get('data/mapInfo.json', function (err, data) {
if (err) {
processError(err);
return;
}
mapInfo = data;
actionItemArrived();
});
$.get('plan.json', function (err, data) {
if (err) {
processError(err);
return;
}
plan = data;
actionItemArrived();
});
function actionItemArrived() {
if(ninjas != null && mapInfo != null && plan != null) {
console.log("The plan is ready to be set in motion!");
}
}
function processError(err) {
console.log("Error", err);
}
在上面的代码中,由于行动之间互不依赖,所以在捕获地图信息的同时获取计划。只需要关心这两点内容最后能够获取所有数据。我们不知道这些数据的获取顺序,每次获取到一些数据,都检查看看是否是最后一段缺失的数据。最后当所有的数据都获取到了,我们就能立刻开始执行计划了。注意我依然不得不书写很多样板代码仅仅用于并执行多个行动。这导致了回调函数的第三个问题:执行很多并行任务也很棘手。
看过了第一个回调函数问题即错误处理——我们看到了为何我们不能使用语言的基本构造,例如try-catch语句。循环也有类似的问题。
你可以专门写一个函数库来简化处理所有这些问题。但是JavaScript语言的开发者们提供了promise,它是用于处理异步计算的关键方法。
参考《JavaScript忍者秘籍》