全栈工程师,编程爱好者
['prɒmɪs]
n. 许诺,允诺;希望
The Promise object is used for asynchronous computations.
A Promise represents a value which may be available now, or in the future, or never.
--MDN
Promise 对象用于异步计算。
一个 Promise 表示一个现在、将来或永不可能可用的值。
--MDN 中文
假设你去到一家饭店,自己找座坐下了,然后招呼服务员拿菜单来。
服务员说:"对不起,我是'同步'服务员,我要服务完这张桌子才能招呼你。"
你是不是很想抽ta?
那一桌人明明已经吃上了,你只是想要菜单,这么小的一个动作,服务员却要你等到别人的一个大动作完成。
这就是"同步"的问题:
顺序交付的工作1234,必须按照1234的顺序完成。
异步,则是将耗时很长的 A 交付的工作交给系统之后,就去继续做 B 交付的工作。等到系统完成之后,再通过回调或者事件,继续做 A 剩下的工作。
从观察者的角度,看起来 AB 工作的完成顺序,和交付他们的时间顺序无关,所以叫"异步"。
对异步的依赖进一步加剧了……
稍有不慎,就会踏入“回调地狱”。
假设需求:
遍历目录,找出最大的一个文件。
function findLargest(dir, callback) {
fs.readdir(dir, function (err, files) {
if (err) return callback(err);
let count = files.length; // [1]
let errored = false;
let stats = [];
files.forEach( file => {
fs.stat(path.join(dir, file), (err, stat) => {
if (errored) return; // [2]
if (err) {
errored = true;
return callback(err);
}
stats.push(stat); // [3]
if (--count === 0) { // [4]
let largest = stats
.filter(function (stat) { return stat.isFile(); })
.reduce(function (prev, next) {
if (prev.size > next.size) return prev;
return next;
});
callback(null, files[stats.indexOf(largest)]);
}
});
});
});
}
findLargest('./path/to/dir', function (err, filename) {
if (err) return console.error(err);
console.log('largest file was:', filename);
});
回调有四个问题:
return
和 throw
有没有一种方案,既能保留异步在无阻塞上的优势,又能让我们写代码写的更舒服呢?
社区经过长时间探索,最终总结出:
new Promise(
/* 执行器 executor */
function (resolve, reject) {
// 一段耗时很长的异步操作
resolve(); // 数据处理完成
reject(); // 数据处理出错
}
)
.then(function A() {
// 成功,下一步
}, function B() {
// 失败,做相应处理
});
pending
[待定] 初始状态fulfilled
[实现] 操作成功rejected
[被否决] 操作失败.then()
执行后续步骤。接下来,看一个简单的范例
./sample/timeout.js
再看一个
./sample/timeout2.js
假如一个 Promise 已经完成了,再 .then()
会怎样?
./sample/fulfilled-then.js
假如在 .then()
的函数里面不返回新的 Promise,会怎样?
./sample/timeout3.js
.then()
.then()
接受两个函数作为参数,分别代表 fulfilled
和 rejected
.then()
返回一个新的 Promise 实例,所以它可以链式调用.then()
根据其最终状态,选择特定的状态响应函数执行.then()
会在新 Promise 状态改变之后执行.then()
.then()
里有 .then()
的情况因为 .then()
返回的还是 Promise 实例。
会等里面的 .then()
执行完,在执行外面的。
对于我们来说,此时最好将其展开,会更好读。
./sample/nested-then.js
问题:下面的四种 promises 的区别是什么
// #1
doSomething().then(function () {
return doSomethingElse();
});
// #2
doSomething().then(function () {
doSomethingElse();
});
// #3
doSomething().then(doSomethingElse());
// #4
doSomething().then(doSomethingElse);
doSomething()
.then(function () {
return doSomethingElse();
})
.then(finalHandler);
答案:
doSomething
|-----------|
doSomethingElse(undefined)
|------------|
finalHandler(resultOfDoSomethingElse)
|------------|
doSomething()
.then(function () {
doSomethingElse();
})
.then(finalHandler);
答案:
doSomething
|-----------------|
doSomethingElse(undefined)
|------------------|
finalHandler(undefined)
|------------------|
doSomething()
.then(doSomethingElse())
.then(finalHandler);
答案:
doSomething
|-----------------|
doSomethingElse(undefined)
|---------------------------------|
finalHandler(resultOfDoSomething)
|------------------|
doSomething()
.then(doSomethingElse)
.then(finalHandler);
答案:
doSomething
|-----------|
doSomethingElse(resultOfDoSomething)
|------------|
finalHandler(resultOfDoSomethingElse)
|------------------|
注:以上4道题及答案均来自
We have a problem with promises
Promise 会自动捕获内部异常,并交给 rejected
响应函数处理。
./sample/error.js
./sample/error-reject.js
通常有两种做法:
reject('错误信息')
.then(null, message => {})
throw new Error('错误信息')
.catch( message => {})
我推荐使用第二种,更加清晰,更加好读,并且可以捕获前面的错误。
来看一个稍微复杂一些的,捕获错误的范例:
.catch()
+ .then()
./sample/catch-then.js
注意:强烈建议在所有队列最后都加上
.catch()
,以避免漏掉错误处理造成意想不到的问题。
doSomething()
.doAnotherThing()
.doMoreThing()
.catch( err => {
console.log(err);
});
Promise.all()
Promise.all([p1, p2, p3, ....])
用于将多个 Promise 实例,包装成一个新的 Promise 实例。
./sample/all.js
Promise.all()
最常见就是和 .map()
连用。
我们改造一下前面的例子。
./sample/map.js
有时候我们不希望所有动作一起发生,而是按照一定顺序,逐个进行。
let promise = doSomething();
promise = promise.then(doSomethingElse);
promise = promise.then(doSomethingElse2);
promise = promise.then(doSomethingElse3);
....
.forEach()
function queue(things) {
let promise = Promise.resolve();
things.forEach( thing => {
promise = promise.then( () => {
return new Promise( resolve => {
doThing(thing, () => {
resolve();
});
});
});
});
return promise;
}
queue(['lots', 'of', 'things', ....]);
.reduce()
function queue(things) {
return things.reduce( (promise, thing) => {
return promise.then( () => {
return new Promise( resolve => {
doThing(thing, () => {
resolve();
});
});
});
}, Promise.resolve());
}
queue(['lots', 'of', 'things', ....]);
两个常见错误:
....
things.forEach( thing => {
promise.then( () => {
return new Promise( resolve => {
doThing(thing, () => {
resolve();
});
});
});
});
....
没有把 .then()
产生的新 Promise 实例赋给 promise
,没有生成队列。
function queue(things) {
return things.reduce( (promise, thing) => {
let step = new Promise( resolve => {
doThing(thing, () => {
resolve();
});
});
return promise.then( step );
}, Promise.resolve());
}
Promise 实例创建之后,会立刻运行执行器代码,所以这个也无法达成队列的效果。
假设需求:
开发一个爬虫,抓取某网站。
let url = ['http://blog.meathill.com/'];
function fetchAll(urls) {
return urls.reduce((promise, url) => {
return promise.then( () => {
return fetch(url);
});
}, Promise.resolve());
}
function fetch(url) {
return spider.fetch(url)
.then( content => {
return saveOrOther(content);
})
.then( content => {
let links = spider.findLinks(content);
return fetchAll(links);
});
}
fetchAll(url);
关于 Generator 的详情,请参阅相关文档。
简而言之,
Generator 可以在执行中中断,并等待唤起。
let generator = function* (urls) {
let loaded = [];
while (urls.length > 0) {
let url = urls.unshift();
yield spider.fetch(url)
.then( content => {
loaded.push(url);
return saveOrOther(content);
})
.then( content => {
let links = spider.findLinks(content);
links = _.without(links, loaded);
urls = urls.concat(links);
});
}
return 'over';
};
function fetch(urls) {
let iterator = generator();
function next() {
let result = iterator.next();
if (result.done) {
return result.value;
}
let promise = iterator.next().value;
promise.then(next);
}
next();
}
let urls = ['http://blog.meathill.com'];
fetch(urls);
Promise.resolve()
fulfilled
的 Promise 实例fulfuilled
响应函数会得到这个参数thenable
,立刻执行它的 .then()
./sample/resolve.js
Promise.reject()
Promise.reject()
会返回一个状态为 rejected
的 Promise 实例。
Promise.reject()
不认 thenable
./sample/reject.js
Promise.race()
Promise.race()
功能类似 Promise.all()
,不过它是有一个完成就算完成。
./sample/race.js
把回调包装成 Promise 是最常见的应用。
它有两个显而易见的好处:
./sample/wrap.js
假设需求:用户点击按钮,弹出确认窗体,用户确认和取消有不同的处理。
样式问题,不能使用 window.confirm()
。
// 弹出窗体
let confirm = popupManager.confirm('您确定么?');
confirm.promise
.then(() => {
// do confirm staff
})
.catch(() => {
// do cancel staff
});
// 窗体的构造函数
class Confirm {
constructor() {
this.promise = new Promise( (resolve, reject) => {
this.confirmButton.onClick = resolve;
this.cancelBUtton.onClick = reject;
});
}
}
jQuery 已经实现了 Promise。参见 jqXHR
$.ajax(url, {
dataType: 'json'
})
.then(json => {
// 该干啥干啥
});
如果你需要在 IE 中使用 Promise,有两个选择:
Fetch API 是 XMLHttpRequest 的现代化替代方案,它更强大,也更友好。
它直接返回一个 Promise 实例。
fetch('some.json')
.then( response => {
return response.json();
})
.then( json => {
// do something with the json
})
.catch( err => {
console.log(err);
});
放手用吧,少年!
相对于传统的回调模式,Promise 有着巨大的进步,值得我们学习和使用。
return/try/catch
的问题ES2017 新增运算符,增加新的语言元素,赋予 JavaScript 以顺序手法编写异步脚本的能力。
return/try/catch
。function resolveAfter2Seconds(x) {
return new Promise(resolve => {
setTimeout(() => {
resolve(x);
}, 2000);
});
}
async function f1() {
var x = await resolveAfter2Seconds(10);
console.log(x); // 10
}
f1();
具体的内容请参考 MDN async 文档 和 await 文档
这是我犯过的一些错误,希望成为大家前车之鉴。
.resolve()
.reject()
不会自动 return
。.resolve()
.reject()
throw err
才会改变状态,.then()
不需要。.resolve()
只会返回一个值,返回多个值请用数组或对象。