Promise是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。它由社区最早提出和实现,ES6将其写进了语言标准,统一了用法,原生提供了Promise对象。
如果你以为这是一篇悲伤的爱情故事,不好意思,那你是“被标题”了。
前段时间在用Nodejs开发一个可视化流程工具的过程中,涉及到文件批处理的功能,比如批处理N个样式文件。
我们知道Nodejs读取文件分为阻塞式和非阻塞式2种
阻塞式处理流程如下
假设每个文件的处理时间分别为 T[0,1,2…n],所有文件被处理完的时间为 Time = T[0] + … + T[n],即所有的任务耗时总和。
非阻塞式处理流程如下
假设每个文件的处理时间分别为 T[0,1,2…n],所有文件被处理完的时间为 Time = Maxim(T[0],…,T[n]),即所有的任务中耗时最长的时间。
Node对于IO的快速处理能力,减少整个任务的耗时,我准备使用“非阻塞方式”去读取文件并处理。
为防止演示代码跟业务逻辑耦合,使用延时函数来模拟异步方法,代码如下
"use strict";
let callbacks = [
function(){
setTimeout(() => {
console.log('3000');
},3000);
},
function(){
setTimeout(() => {
console.log('1000');
},1000);
},
function(){
setTimeout(() => {
console.log('4000');
},4000);
}
];
for(let i = 0 ; i < callbacks.length ; i++){
callbacks[i]();
}
如果仅仅是将文件的内容显示出来,并且相互之间无关联,上述的模式已经基本满足需求,但是,假设我们需要在所有文件处理完成之后给用户一个反馈,上述完全无任何逻辑关系的各个回调就不能达到我们的要求。
于是我加了一个计数器,计数器初始值为任务的总数,每当完成一个任务则计数器减1,当计数器为0时,表示所有任务均完成,改造后的代码如下
"use strict";
let callbacks = [
function(){
setTimeout(() => {
console.log('3000');
changeCount();
},3000);
},
function(){
setTimeout(() => {
console.log('1000');
changeCount();
},1000);
},
function(){
setTimeout(() => {
console.log('4000');
changeCount();
},4000);
}
],
count = 3;
function changeCount(){
count--;
if(count <= 0){
console.log('All task completed');
}
}
for(let i = 0 ; i < callbacks.length ; i++){
callbacks[i]();
}
看上去我们的目标基本达到了,有一天,我们要处理雪碧图合并拉,对于单个样式文件而言,当我们把文件中的待合并图片提取出来以后要把图片合并起来,同时根据返回的图片在雪碧图中的位置和本身大小需要对传入的样式文件进行规则修改,那么合并雪碧图和样式修改就成为了一个“阻塞式”操作了,样式的修改依赖于图片合并的结果。不要怕,咱有万能的回调呢,模拟的示例代码如下。
"use strict";
function task(){
readFile();
}
function readFile(){
setTimeout(() => {
console.log('Read file');
sprite();
},1000);
}
function sprite(){
setTimeout(() => {
console.log('Sprite images');
},2000)
}
task();
这里的形式其实就是前端同学们经常说的回调套回调,如果串行任务较多的话,这些回调的嵌套会在代码阅读、问题定位等方面带来一些麻烦。俗称“回调地狱”,但我个人认为少量串行处理这种方式也还不错。
针对这个问题,业界一些好人开发了一些处理异步任务的库,比如async,这也是我在项目中使用过的,它的任务API非常清晰,使用起来也非常方便。但今天它不是主角,感兴趣的童鞋可以自行去搜索学习一下,今天我们想给大家介绍的是ES6的Promise,回到我们的标题“谁的Promise”,OK,是ES6的。
阮一峰老师有一个对ES6的翻译教程,讲的比较详细了,地址,基础的知识我不在赘述,本文重点讲讲如何使用Promise去解决上述问题涉及到的异步管控和串行处理。
先来看上述的场景1,对一堆异步任务进行监控,当所有任务均完成则启动后续处理。
Promise提供了Promise.All这个API,用于将多个Promise包装成一个新的Promise,所有任务均完成以后会执行该Promise的then方法,代码示例如下
"use strict";
let callbacks = [
function(resolve){
setTimeout(() => {
console.log('3000');
resolve({
ret : 1
});
},3000);
},
function(resolve){
setTimeout(() => {
console.log('1000');
resolve({
ret : 1
});
},1000);
},
function(resolve){
setTimeout(() => {
console.log('4000');
resolve({
ret : 1
});
},4000);
}
];
function parallel(tasks){
let promises = [];
for(let i = 0 ; i < tasks.length ; i++){
promises.push(new Promise((resolve,reject) => {
tasks[i](resolve);
}));
}
Promise.all(promises).then(() => {
console.log('Tasks completed');
});
}
parallel(callbacks);
那场景2怎么去处理呢?场景2是一个很典型的瀑布流任务模型,尽管所有的任务都是异步的,但是需要将这些异步操作进行串行化,所有任务列表中的任务按照队列先进先出,顺序执行。这里主要使用了Promise的resolve特性,当一个Promise(p1)它的resolve是另外一个Promise对象(p2)时,则表示p1的执行时机是依赖于p2的结果,当p2正常resolve后,p1开始执行。代码如下:
"use strict";
let callbacks = [
function(resolve){
setTimeout(() => {
console.log('3000');
resolve({
ret : 1
});
},3000);
},
function(){
setTimeout(() => {
console.log('1000');
},1000);
},
function(){
setTimeout(() => {
console.log('4000');
},4000);
}
];
function waterfall(tasks){
let count = tasks.length,
promises = [];
//
promises[0] = new Promise((resolve,reject) => {
tasks[0](resolve);
});
for(let i = 1 ; i < count ; i++){
let tmp = new Promise((resolve,reject) => {
resolve(promises[i-1]);
});
tmp.then((result) => {
tasks[i]();
});
promises.push(tmp);
}
}
waterfall(callbacks);
本文没有对Promise的基本用户进行讲解,大家可从阮老师那篇文章中去详细学习,这里是使用Promise来构建常见的2种任务模式(并行模式、串行模式)进行了简单的模拟实现,具体跟场景结合我相信它依然会大放异彩。
在使用Promise的过程中,需要着重体会的是整个Promise可以理解成一个状态机,执行中(Pending)、完成(resolve)、失败(reject),这种描述不一定对,但也强调你对要执行的任务有一个非常清晰地认识,何时进行状态扭转、传递什么数组到任务流管道中、怎么处理异常情况等等。
最后小结一句,谁用谁知道!Have fun!