Promise 的基本使用能够看阮一峰老师的 《ECMAScript 6 入门》。html
咱们来聊点其余的。node
提及 Promise,咱们通常都会从回调或者回调地狱提及,那么使用回调到底会致使哪些很差的地方呢?git
使用回调,咱们颇有可能会将业务代码写成以下这种形式:es6
doA( function(){
doB();
doC( function(){
doD();
} )
doE();
} );
doF();
复制代码
固然这是一种简化的形式,通过一番简单的思考,咱们能够判断出执行的顺序为:github
doA()
doF()
doB()
doC()
doE()
doD()
复制代码
然而在实际的项目中,代码会更加杂乱,为了排查问题,咱们须要绕过不少碍眼的内容,不断的在函数间进行跳转,使得排查问题的难度也在成倍增长。面试
固然之因此致使这个问题,实际上是由于这种嵌套的书写方式跟人线性的思考方式相违和,以致于咱们要多花一些精力去思考真正的执行顺序,嵌套和缩进只是这个思考过程当中转移注意力的细枝末节而已。segmentfault
固然了,与人线性的思考方式相违和,还不是最糟糕的,实际上,咱们还会在代码中加入各类各样的逻辑判断,就好比在上面这个例子中,doD() 必须在 doC() 完成后才能完成,万一 doC() 执行失败了呢?咱们是要重试 doC() 吗?仍是直接转到其余错误处理函数中?当咱们将这些判断都加入到这个流程中,很快代码就会变得很是复杂,以致于没法维护和更新。数组
正常书写代码的时候,咱们理所固然能够控制本身的代码,然而当咱们使用回调的时候,这个回调函数是否能接着执行,其实取决于使用回调的那个 API,就好比:promise
// 回调函数是否被执行取决于 buy 模块
import {buy} from './buy.js';
buy(itemData, function(res) {
console.log(res)
});
复制代码
对于咱们常常会使用的 fetch 这种 API,通常是没有什么问题的,可是若是咱们使用的是第三方的 API 呢?bash
当你调用了第三方的 API,对方是否会由于某个错误致使你传入的回调函数执行了屡次呢?
为了不出现这样的问题,你能够在本身的回调函数中加入判断,但是万一又由于某个错误这个回调函数没有执行呢? 万一这个回调函数有时同步执行有时异步执行呢?
咱们总结一下这些状况:
对于这些状况,你可能都要在回调函数中作些处理,而且每次执行回调函数的时候都要作些处理,这就带来了不少重复的代码。
咱们先看一个简单的回调地狱的示例。
如今要找出一个目录中最大的文件,处理步骤应该是:
fs.readdir
获取目录中的文件列表;fs.stat
获取文件信息代码为:
var fs = require('fs');
var path = require('path');
function findLargest(dir, cb) {
// 读取目录下的全部文件
fs.readdir(dir, function(er, files) {
if (er) return cb(er);
var counter = files.length;
var errored = false;
var stats = [];
files.forEach(function(file, index) {
// 读取文件信息
fs.stat(path.join(dir, file), function(er, stat) {
if (errored) return;
if (er) {
errored = true;
return cb(er);
}
stats[index] = stat;
// 事先算好有多少个文件,读完 1 个文件信息,计数减 1,当为 0 时,说明读取完毕,此时执行最终的比较操做
if (--counter == 0) {
var largest = stats
.filter(function(stat) { return stat.isFile() })
.reduce(function(prev, next) {
if (prev.size > next.size) return prev
return next
})
cb(null, files[stats.indexOf(largest)])
}
})
})
})
}
复制代码
使用方式为:
// 查找当前目录最大的文件
findLargest('./', function(er, filename) {
if (er) return console.error(er)
console.log('largest file was:', filename)
});
复制代码
你能够将以上代码复制到一个好比 index.js
文件,而后执行 node index.js
就能够打印出最大的文件的名称。
看完这个例子,咱们再来聊聊回调地狱的其余问题:
1.难以复用
回调的顺序肯定下来以后,想对其中的某些环节进行复用也很困难,牵一发而动全身。
举个例子,若是你想对 fs.stat
读取文件信息这段代码复用,由于回调中引用了外层的变量,提取出来后还须要对外层的代码进行修改。
2.堆栈信息被断开
咱们知道,JavaScript 引擎维护了一个执行上下文栈,当函数执行的时候,会建立该函数的执行上下文压入栈中,当函数执行完毕后,会将该执行上下文出栈。
若是 A 函数中调用了 B 函数,JavaScript 会先将 A 函数的执行上下文压入栈中,再将 B 函数的执行上下文压入栈中,当 B 函数执行完毕,将 B 函数执行上下文出栈,当 A 函数执行完毕后,将 A 函数执行上下文出栈。
这样的好处在于,咱们若是中断代码执行,能够检索完整的堆栈信息,从中获取任何咱们想获取的信息。
但是异步回调函数并不是如此,好比执行 fs.readdir
的时候,实际上是将回调函数加入任务队列中,代码继续执行,直至主线程完成后,才会从任务队列中选择已经完成的任务,并将其加入栈中,此时栈中只有这一个执行上下文,若是回调报错,也没法获取调用该异步操做时的栈中的信息,不容易断定哪里出现了错误。
此外,由于是异步的缘故,使用 try catch 语句也没法直接捕获错误。
(不过 Promise 并无解决这个问题)
3.借助外层变量
当多个异步计算同时进行,好比这里遍历读取文件信息,因为没法预期完成顺序,必须借助外层做用域的变量,好比这里的 count、errored、stats 等,不只写起来麻烦,并且若是你忽略了文件读取错误时的状况,不记录错误状态,就会接着读取其余文件,形成无谓的浪费。此外外层的变量,也可能被其它同一做用域的函数访问而且修改,容易形成误操做。
之因此单独讲讲回调地狱,实际上是想说嵌套和缩进只是回调地狱的一个梗而已,它致使的问题远非嵌套致使的可读性下降而已。
Promise 使得以上绝大部分的问题都获得了解决。
举个例子:
request(url, function(err, res, body) {
if (err) handleError(err);
fs.writeFile('1.txt', body, function(err) {
request(url2, function(err, res, body) {
if (err) handleError(err)
})
})
});
复制代码
使用 Promise 后:
request(url)
.then(function(result) {
return writeFileAsynv('1.txt', result)
})
.then(function(result) {
return request(url2)
})
.catch(function(e){
handleError(e)
});
复制代码
而对于读取最大文件的那个例子,咱们使用 promise 能够简化为:
var fs = require('fs');
var path = require('path');
var readDir = function(dir) {
return new Promise(function(resolve, reject) {
fs.readdir(dir, function(err, files) {
if (err) reject(err);
resolve(files)
})
})
}
var stat = function(path) {
return new Promise(function(resolve, reject) {
fs.stat(path, function(err, stat) {
if (err) reject(err)
resolve(stat)
})
})
}
function findLargest(dir) {
return readDir(dir)
.then(function(files) {
let promises = files.map(file => stat(path.join(dir, file)))
return Promise.all(promises).then(function(stats) {
return { stats, files }
})
})
.then(data => {
let largest = data.stats
.filter(function(stat) { return stat.isFile() })
.reduce((prev, next) => {
if (prev.size > next.size) return prev
return next
})
return data.files[data.stats.indexOf(largest)]
})
}
复制代码
前面咱们讲到使用第三方回调 API 的时候,可能会遇到以下问题:
对于第一个问题,Promise 只能 resolve 一次,剩下的调用都会被忽略。
对于第二个问题,咱们可使用 Promise.race 函数来解决:
function timeoutPromise(delay) {
return new Promise( function(resolve,reject){
setTimeout( function(){
reject( "Timeout!" );
}, delay );
} );
}
Promise.race( [
foo(),
timeoutPromise( 3000 )
] )
.then(function(){}, function(err){});
复制代码
对于第三个问题,为何有的时候会同步执行有的时候回异步执行呢?
咱们来看个例子:
var cache = {...};
function downloadFile(url) {
if(cache.has(url)) {
// 若是存在cache,这里为同步调用
return Promise.resolve(cache.get(url));
}
return fetch(url).then(file => cache.set(url, file)); // 这里为异步调用
}
console.log('1');
getValue.then(() => console.log('2'));
console.log('3');
复制代码
在这个例子中,有 cahce 的状况下,打印结果为 1 2 3,在没有 cache 的时候,打印结果为 1 3 2。
然而若是将这种同步和异步混用的代码做为内部实现,只暴露接口给外部调用,调用方因为没法判断是究竟是异步仍是同步状态,影响程序的可维护性和可测试性。
简单来讲就是同步和异步共存的状况没法保证程序逻辑的一致性。
然而 Promise 解决了这个问题,咱们来看个例子:
var promise = new Promise(function (resolve){
resolve();
console.log(1);
});
promise.then(function(){
console.log(2);
});
console.log(3);
// 1 3 2
复制代码
即便 promise 对象马上进入 resolved 状态,即同步调用 resolve 函数,then 函数中指定的方法依然是异步进行的。
PromiseA+ 规范也有明确的规定:
实践中要确保 onFulfilled 和 onRejected 方法异步执行,且应该在 then 方法被调用的那一轮事件循环以后的新执行栈中执行。
1.Promise 嵌套
// bad
loadSomething().then(function(something) {
loadAnotherthing().then(function(another) {
DoSomethingOnThem(something, another);
});
});
复制代码
// good
Promise.all([loadSomething(), loadAnotherthing()])
.then(function ([something, another]) {
DoSomethingOnThem(...[something, another]);
});
复制代码
2.断开的 Promise 链
// bad
function anAsyncCall() {
var promise = doSomethingAsync();
promise.then(function() {
somethingComplicated();
});
return promise;
}
复制代码
// good
function anAsyncCall() {
var promise = doSomethingAsync();
return promise.then(function() {
somethingComplicated()
});
}
复制代码
3.混乱的集合
// bad
function workMyCollection(arr) {
var resultArr = [];
function _recursive(idx) {
if (idx >= resultArr.length) return resultArr;
return doSomethingAsync(arr[idx]).then(function(res) {
resultArr.push(res);
return _recursive(idx + 1);
});
}
return _recursive(0);
}
复制代码
你能够写成:
function workMyCollection(arr) {
return Promise.all(arr.map(function(item) {
return doSomethingAsync(item);
}));
}
复制代码
若是你非要以队列的形式执行,你能够写成:
function workMyCollection(arr) {
return arr.reduce(function(promise, item) {
return promise.then(function(result) {
return doSomethingAsyncWithResult(item, result);
});
}, Promise.resolve());
}
复制代码
4.catch
// bad
somethingAync.then(function() {
return somethingElseAsync();
}, function(err) {
handleMyError(err);
});
复制代码
若是 somethingElseAsync 抛出错误,是没法被捕获的。你能够写成:
// good
somethingAsync
.then(function() {
return somethingElseAsync()
})
.then(null, function(err) {
handleMyError(err);
});
复制代码
// good
somethingAsync()
.then(function() {
return somethingElseAsync();
})
.catch(function(err) {
handleMyError(err);
});
复制代码
题目:红灯三秒亮一次,绿灯一秒亮一次,黄灯2秒亮一次;如何让三个灯不断交替重复亮灯?(用 Promse 实现)
三个亮灯函数已经存在:
function red(){
console.log('red');
}
function green(){
console.log('green');
}
function yellow(){
console.log('yellow');
}
复制代码
利用 then 和递归实现:
function red(){
console.log('red');
}
function green(){
console.log('green');
}
function yellow(){
console.log('yellow');
}
var light = function(timmer, cb){
return new Promise(function(resolve, reject) {
setTimeout(function() {
cb();
resolve();
}, timmer);
});
};
var step = function() {
Promise.resolve().then(function(){
return light(3000, red);
}).then(function(){
return light(2000, green);
}).then(function(){
return light(1000, yellow);
}).then(function(){
step();
});
}
step();
复制代码
有的时候,咱们须要将 callback 语法的 API 改形成 Promise 语法,为此咱们须要一个 promisify 的方法。
由于 callback 语法传参比较明确,最后一个参数传入回调函数,回调函数的第一个参数是一个错误信息,若是没有错误,就是 null,因此咱们能够直接写出一个简单的 promisify 方法:
function promisify(original) {
return function (...args) {
return new Promise((resolve, reject) => {
args.push(function callback(err, ...values) {
if (err) {
return reject(err);
}
return resolve(...values)
});
original.call(this, ...args);
});
};
}
复制代码
完整的能够参考 es6-promisif
首先咱们要理解,什么是错误被吃掉,是指错误信息不被打印吗?
并非,举个例子:
throw new Error('error');
console.log(233333);
复制代码
在这种状况下,由于 throw error 的缘故,代码被阻断执行,并不会打印 233333,再举个例子:
const promise = new Promise(null);
console.log(233333);
复制代码
以上代码依然会被阻断执行,这是由于若是经过无效的方式使用 Promise,而且出现了一个错误阻碍了正常 Promise 的构造,结果会获得一个马上跑出的异常,而不是一个被拒绝的 Promise。
然而再举个例子:
let promise = new Promise(() => {
throw new Error('error')
});
console.log(2333333);
复制代码
此次会正常的打印 233333
,说明 Promise 内部的错误不会影响到 Promise 外部的代码,而这种状况咱们就一般称为 “吃掉错误”。
其实这并非 Promise 独有的局限性,try..catch 也是这样,一样会捕获一个异常并简单的吃掉错误。
而正是由于错误被吃掉,Promise 链中的错误很容易被忽略掉,这也是为何会通常推荐在 Promise 链的最后添加一个 catch 函数,由于对于一个没有错误处理函数的 Promise 链,任何错误都会在链中被传播下去,直到你注册了错误处理函数。
Promise 只能有一个完成值或一个拒绝缘由,然而在真实使用的时候,每每须要传递多个值,通常作法都是构造一个对象或数组,而后再传递,then 中得到这个值后,又会进行取值赋值的操做,每次封装和解封都无疑让代码变得笨重。
说真的,并无什么好的方法,建议是使用 ES6 的解构赋值:
Promise.all([Promise.resolve(1), Promise.resolve(2)])
.then(([x, y]) => {
console.log(x, y);
});
复制代码
Promise 一旦新建它就会当即执行,没法中途取消。
当处于 pending 状态时,没法得知目前进展到哪个阶段(刚刚开始仍是即将完成)。
ES6 系列目录地址:github.com/mqyqingfeng…
ES6 系列预计写二十篇左右,旨在加深 ES6 部分知识点的理解,重点讲解块级做用域、标签模板、箭头函数、Symbol、Set、Map 以及 Promise 的模拟实现、模块加载方案、异步处理等内容。
若是有错误或者不严谨的地方,请务必给予指正,十分感谢。若是喜欢或者有所启发,欢迎 star,对做者也是一种鼓励。