ES6学习笔记10-Promise与异步编程

  |  

前言

JS最强大的一方面就是它能轻易地处理异步编程。但是随着越来越多的程序使用异步编程,事件与回调函数已不足以支持开发者的所有需求了,因此Promise被提出


51



异步编程的背景

事件模型

当用户点击一个按钮或按下键盘上的一个键时,一个事件——例如onclick——就会被出发。该事件可能会对此交互进行响应,从而将一个新的宏任务添加到宏任务队列的尾部。这就是JS异步编程的最基本形式。事件处理程序代码直到时间发生才会被执行,此时他会拥有上下文。

1
2
3
4
let button = document.getElementById('btn');
button.onclick = function(event) {
console.log('clicked');
}

优点:可以很好的工作于简单的交互

缺点:对于多个分离的异步调用串联在一起会很麻烦,因为必须追踪每个事件的事件对象。此外还得确保所有的事件处理程序都能在事件第一次触发之前被绑定完毕

综上,事件只适合于处理响应式交互或类似的低频功能,而在面对更复杂的需求时他就不够灵活了。

回调模式

当Node.js被创建时,它通过普及回调函数编程模式提升了异步编程模型。回调函数模式类似于事件模型,因为异步代码也会在后面的一个时间点才执行。不同之处在于需要调用的函数(回调函数)作为参数传入。

1
2
3
4
5
6
readFile('example.txt', function(err, contents) {
if(err) {
throw err;
}
console.log(contents);
})

这个例子使用了Node.js惯例,即错误优先(error-first)的回调函数风格。

回调函数模式要比事件模型灵活的多,因为使用回调函数串联多个调用会相对容易。

1
2
3
4
5
6
7
8
9
10
11
readFile('example.txt', function(err, contents) {
if(err) {
throw err;
}
writeFile('example.txt', function(err) {
if(err) {
throw err;
}
console.log('File was written!');
});
});

这种模式运作的相当好,但是我们还是能迅速的察觉到其中的缺陷,那就是可能会嵌套过多的的回调函数。

而且要实现诸如让两个异步操作并行运行,并且在它们都结束后提醒这样更复杂的问题时,回调函数也有些力有不逮。

Promise基础

Promise的生命周期

每个Promise都会经历一个短暂的生命周期:

初始为挂起状态(pending),这表示异步操作尚未结束。一个挂起的Promise被认为是未决定的

一旦异步操作结束,Promise就会并认为是已经决定的,并且会进入到下列两个可能的状态之一:

  • 已完成(fulfilled):Promise的异步操作以成功结束
  • 已拒绝(rejected):Promise的异步操作未成功结束,可能是一个错误,或由其它原因导致的

Promise对象内部的[[PromiseState]]属性会被设置为’pending’、’fulfilled’或’rejected’,以反映Promise的状态。改属性并未在Promise对象上被暴露出来,因此你无法以编程的方式判断Promise到底处于哪种状态。不过你可以使用then()方法在Promise的状态改变时执行一些特定的操作。

then()方法在所有的Promise上都存在,并且接受两个参数。第一个参数是Promise被完成时要调用的函数,第二个参数则是Promise被拒绝是要调用的函数。传递给then的两个参数都是可选的。

Promise也具有要给catch()方法,其行为等同于只传递拒绝处理函数给then。

1
2
3
4
5
6
7
promise.then(null, function(err) {
console.log(err.message);
});
// 等同于
promise.catch(function(err) {
console.log(err.message)
})

即使完成处理函数或拒绝处理函数在Promise已经处理之后才添加进任务队列,他们仍会被执行

1
2
3
4
5
6
7
let promsie = readFile('example.txt');
promise.then(function(contents) {
console.log(contents);
promise.then(function(contents) {
console.log(contents);
})
})

创建未决定的Promise

新的Promise使用Promise构造器来创建。此构造器接收单个参数:一个被称为执行器的函数,包含初始化Promise的代码。该执行器会被传递两个名为resolve与reject函数作为参数。resolve函数在执行器中调用,表示执行器成功完成执行,并可使用resolve往之后then中Promise被完成时要调用的函数传递参数。reject函数则表明执行器的菜哦做失败,同样可以往then中第二个参数的函数传递参数。

这里我们使用Promise实现上例中的readFile函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
let fs = require('fs');
function readFile(filename) {
return new Promise(function(resolve, reject) {
//触发异步操作
fs.readFile(filename, { encoding: 'utf-8'}, function(err, contents) {
//检查错误
if(err) {
//向then第二个参数传递错误信息参数
reject(err);
return;
}
resolve(contents);
});
});
}
let promise = readFile('example.txt');
promise.then(function(contents) {
console.log(contents);
promise.then(function(contents) {
console.log('第二次输出');
console.log(contents);
})
}, function(err) {
console.error(err.message);
})

在此例中,Node.js原生的fs.readFile()异步调用被包装在一个Promise中。执行器要么传递错误对象给reject()函数,要么传递文件内容给resolve()函数。

要记住执行器会在readFile()被调用时立即运行。当resolve()或reject()在执行器内部被调用时,一个宏任务被添加到宏任务队列中

创建已经决定的Promise

使用Promise构造器就是最好的创建未决的Promise的最好方式。但是若你想让一个Promise代表一个已知的值,那么特意调用一个构造器并没有意义。相反可以使用下列方法来创建已经决定的Promise

使用Promise.resolve()

Promise.resolve()方法接收单个参数并返回一个处于完成态的Promise,并且你可以向Promise添加一个或多个完成处理函数来提取这个参数值

1
2
3
4
let promise = Promise.resolve(42);
promise.then(function(value) {
console.log(value); // 42
})

使用Promise.reject()

同样可以使用Promise.reject()方法来创建要给已拒绝的Promise。

1
2
3
4
let promise = Promise.reject(42);
promise.catch(function(value) {
console.log(vlaue); // 42
})

任何附加到这个Promise的拒绝处理函数都将会被调用,而完成处理函数则不会执行。

非Promise的Thenable

Promise.resolve()与Promise.reject()都能接受非Promise的thenable作为参数。当传入了非Promise的thenable时,这些方法会创建一个新的Promise,此Promise会在then()函数之后被调用。

当一个对象拥有一个能接受resolve与reject参数的then()方法,该对象就会被认为是一个非Promise的thenable。

之所以能接受thenable,是因为在Promise被引入ES6之前,许多库都使用thenable,所以引入这种方式向下兼容

1
2
3
4
5
6
7
8
9
let thenable = {
then(resolve, reject) {
resolve(42);
}
};
let p1 = Promise.resolve(thenable);
p1.then(function(value) {
console.log(value); // 42
})

执行器错误

如果执行器内部抛出错误,那么Promise的拒绝处理函数就会被调用

1
2
3
4
5
6
let promise = new Promise(function(resolve, reject) {
throw new Erro('Explosion!');
});
promise.catch(function(erro) {
console.log(err.message); // Explosion!
})

执行器处理程序捕捉抛出的任何错误。但是执行器内部抛出的错误仅当存在拒绝处理函数是才会被报告。如果没有定义拒绝处理函数,这个错误就会被隐瞒。这也是开发者早期使用Promise时存在的一个问题。但JS环境通过提供钩子来捕捉被拒绝的Promise,从而解决了这个问题

全局的Promise拒绝处理

Promise最有争议的方面之一就是:当一个Promise被拒绝时若缺少拒绝处理函数,就会静默失败。有人认为这是规范中最大的缺陷,因为这使得JS语言所有组成部分中唯一不让错误清晰可见的。

由于Promise的本质,判断一个Promise的拒绝是否被处理并不直观。因为无论Promise是否已经被解决,你都可以在任何时候调用then或catch来定义它的解决函数。

Node.js的拒绝处理

在Node.js中,process对象上存在两个关联到Promise的拒绝处理的事件:

  • unhandledRejection:当一个Promise被拒绝而在事件循环的一个轮次中没有任何拒绝处理函数被调用,该事件就会被触发
  • rejectionHandled:若一个Promise被拒绝并在事件循环的一个轮次之后才有拒绝处理函数被调用,该时间就会被触发

这两个事件旨在共同帮助识别已被拒绝但未曾被处理的promise

unhandledRejection事件的回调函数接受两个参数,拒绝原因(常常是一个错误)以及已被拒绝的Promise

1
2
3
4
5
6
7
8
9
10
let rejected1;
let rejected2;

process.on('unhandledRejection', (reason, rejected) => {
console.log(reason);
console.log(rejected);
})

rejected1 = Promise.reject(new Error('Explosion1'));
rejected2 = Promise.reject(new Error('Explosion2'));

当在一个事件循环轮次中有一个已拒绝当未被处理的promise,回调函数就会执行一次,所以我们可以在unhandledRejection事件中定义被拒绝的promise的默认处理函数

rejectionHandled事件处理函数只有一个参数,及已被拒绝的Promise

1
2
3
4
5
6
7
8
9
10
let rejected;
process.on('rejectionHandled', (promise) => {
console.log(promise);
})
rejected = Promise.reject(new Error("Explosion"));
setTimeout(function() {
rejected.catch(function(value) {
console.log(value); // "Explosion!"
});
}, 1000);

这里我们使用setTimeout函数来将拒绝处理函数放到下一轮事件循环中来调用,这样rejectionHandled事件就会在拒绝处理函数调用时触发。这里需要特意指出,并不是一定要在下一轮事件循环中调用拒绝处理函数才会触发rejectionHandled事件,而是只要在Promise拒绝的那一轮之后的任何轮次中都可以。

结合这两个事件,我们可以构造一个列表来持续追踪那些未被处理的拒绝。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let possiblyUnhandledRejections = new Map();
process.on('unhandledRejection', function(reason,promise) {
possiblyUnhandledRejections.set(promise, reason);
});
process.on('rejectionHandled', function(promise) {
possiblyUnhandledRejections.delete(promise);
})
setInterval(function() {
possiblyUnhandledRejections.forEach(function(reason, promise) {
console.log(reason.message ? reason.message : reason);
handleRejection(promise, reason);
})
possiblyUnhandledRejections.clear();
}, 60000);

这里我们定义一个Map结构来存储在每一轮事件循环中被拒绝了但未被处理的Promise,作为键名,将导致拒绝的理由作为键值。而如果在之后的事件循环中,被拒绝的Promise得到了处理,就将其从Map中删除。在一分钟后,循环Map中存放的值,输出其中Promise被拒绝的理由,并统一处理这些Promise,然后清空map。

浏览器的拒绝处理

浏览器同样能触发两个事件来帮助识别未处理的拒绝。这两个事件会被window对象触发,并完全等效于Node.js中相关事件

  • unhandledrejection
  • rejectionhandled

而浏览器与Node.js的不同在于时间处理函数的参数,Node.js是分离的,而浏览器的事件处理函数则只接收到包含下列属性的一个对象:

  • type:事件的名称(unhandledrejection或rejectionhandled)
  • promise:被拒绝的Promise对象
  • reason:Promise被拒绝的理由
1
2
3
4
5
6
7
8
9
10
11
12
let rejected;
window.addEventListener('unhandledrejection', function(event) {
console.log(event.type);
console.log(event.reason.message);
console.log(rejected === event.promise);
})
window.addEventListener('rejectionhandled', function(event) {
console.log(event.type);
console.log(event.reason.message);
console.log(rejected === event.promise);
})
rejected = Promise.reject(new Error('Explosion'));

串联Promise

每次对then或catch的调用实际上创建并返回另一个Promise,仅当前一个Promise被完成或拒绝时,后一个Promise才会被决议

1
2
3
4
5
6
7
8
9
10
let p1 = new Promise(function(resolve, reject) {
resolve(42);
})

p1.then(function(value) {
console.log(value);
return 43
}).then(function(value) {
console.log(value)
})

而then和catch返回的Promise是什么状态的取决于两者的内部操作。

捕获错误

Promise链允许你捕获前一个Promise的完成或拒绝处理函数中发生的错误,及这是then或catch返回的Promise是rejected状态的。

1
2
3
4
5
6
7
8
9
let p1 = new Promise(function(resolve, reject) {
resolve(42);
})
p1.then(function(value) {
console.log(value);
throw new Error('Explosion!');
}).catch(function(error) {
console.log(error.message);
})

在Promise链中返回值

Promise链的另一重要方面是能从一个Promise传递数据给下一个Promise的能力。可以通过指定完成处理函数的返回值,以便沿着一个链继续传递数据。这时then和catch返回的Promise就是fulfilled状态的。

1
2
3
4
5
6
7
8
9
let p1 = new Promise(function(resolve, reject) {
reject(42);
})
p1.catch(function(value) {
console.log(value);
return value + 1;
}).then(function(value) {
console.log(value);
})

在Promise链中返回Promise

1
2
3
4
5
6
7
8
9
10
11
12
13
let p1 = new Promise(function(resolve, reject) {
resolve(42);
});
p1.then(function(value) {
console.log(value); // 42
// 创建一个新的 promise
let p2 = new Promise(function(resolve, reject) {
resolve(43);
});
return p2
}).then(function(value) {
console.log(value); // 43
});

响应多个Promise

目前我们所有的例子在同一时刻都只响应一个Promise。那么当我们想同时监视多个Promise的进程,以便决定下一步行动。这时候我们可以使用ES6提供的两个方法Promise.all()和Promise.race()

Promise.all()方法

Promise.all()方法接收单个可迭代对象(如数组)作为参数,并返回要给Promise。这个可迭代对象的元素都是Promise,只有在它们都完成后,所返回的Promise才会被完成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let p1 = new Promise(function(resolve, reject) {
resolve(41);
});
let p2 = new Promise(function(resolve, reject) {
resolve(42);
});
let p3 = new Promise(function(resolve, reject) {
resolve(43);
});
let p4 = Promise.all([p1, p2, p3]);

p4.then(function(value) {
console.log(Array.isArray(value));
console.log(value[0]);
console.log(value[1]);
console.log(value[2]);
})

传递给p4的完成处理函数的结果是一个包含每个决议值的数组,这些值的存储顺序保持了待决议的Promise的顺序(与完成的先后顺序无关)。

若传递给Promise.all()的任意Promise被拒绝了,那么方法所返回的Promise就会立刻被拒绝,而不必等其它Promise被决议。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let p1 = new Promise(function(resolve, reject) {
resolve(41);
});
let p2 = new Promise(function(resolve, reject) {
reject(42);
});
let p3 = new Promise(function(resolve, reject) {
resolve(43);
});
let p4 = Promise.all([p1, p2, p3]);

p4.catch(function(value) {
console.log(Array.isArray(value)); //false
console.log(value); // 42
})

Promise.race()方法

Promise.rase()提供了监视多个Promise的要给稍微不同的方法。此方法也接受一个包含需要监视的Promise的可迭代对象,并返回要给新的Promise,但一旦来源Promise中有一个被解决或被拒绝,所返回的Promise就会立刻被解决或被拒绝。

1
2
3
4
5
6
7
8
9
10
11
12
13
let p1 = Promise.resolve(41);
let p2 = new Promise(function(resolve, reject) {
resolve(42);
});
let p3 = new Promise(function(resolve, reject) {
resolve(43);
});
let p4 = Promise.race([p1, p2, p3]);

p4.then(function(value) {
console.log(Array.isArray(value)); //false
console.log(value); // 41
})

p1是一个直接被创建的已完成的Promise,显然快于p2和p3

继承Promise

与其他内置类型一样,我们可以将一个Promise用作派生类的基类。这允许我们自定义变异的Promise,在内置的Promise的基础上扩展功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class MyPromise extends Promise {
//这里不定义constructor,使用默认构造器
success(resolve, reject) {
return this.then(resolve, reject);
}
failure(reject) {
return this.catch(reject)
}
}
let promise = new MyPromise(function(resolve, reject) {
resolve(42);
})
promise.success(function(value) {
console.log(value); // 42
}).failure(function(value) {
console.log(value);
});

在此例中,MyPromise 从 Promise 上派生出来,并拥有两个附加方法。 success() 方法模
拟了 resolve() , failure() 方法则模拟了 reject() 。每个附加方法都使用了 this 来调用它所模拟的方法。

派生的 Promise 函数与内置的Promise 几乎一样,除了可以随你需要调用 success() 与 failure() 。由于静态方法被继承了, MyPromise.resolve() 方法、 MyPromise.reject() 方法、MyPromise.race() 方法与 MyPromise.all() 方法在派生的 Promise 上都可用。

后两个方法的行为等同于内置的方法,但前两个方法则有轻微的不同。MyPromise.resolve() 与 MyPromise.reject() 都会返回 MyPromise 的一个实例,无视传递进来的值的类型,这是由于这两个方法使用了 Symbol.species 属性来决定需要
返回的 Promise 的类型。

若传递内置 Promise 给这两个方法,将会被决议或被拒绝,并且会返回一个新的 MyPromise ,以便绑定完成或拒绝处理函数。例如:

1
2
3
4
5
6
7
8
let p1 = new Promise(function(resolve, reject) {
resolve(42);
});
let p2 = MyPromise.resolve(p1);
p2.success(function(value) {
console.log(value); // 42
});
console.log(p2 instanceof MyPromise); // true

异步任务的运行

之前我们有尝试过用迭代器来实现异步,相当的麻烦,但是引入Promise以后,就能有所简化。这里我们来写一个异步读取文件的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
let fs = require("fs");
function run(taskDef) {
// 创建迭代器
let task = taskDef();
// 启动任务
let result = task.next();
// 递归使用函数来进行迭代
(function step() {
// 如果还有更多要做的
console.log(result);
//这里输出的Promise都是pending,因该跟事件循环这一轮文件读取未完成有关
//具体我也没研究每一轮,太复杂了
if (!result.done) {
// 决议一个 Promise ,让任务处理变简单
let promise = Promise.resolve(result.value);
promise.then(function(value) {
console.log(value)
result = task.next();
step();
}).catch(function(error) {
result = task.throw(error);
step();
});
}
}());
}
// 定义一个函数来配合任务运行器使用
function readFile(filename) {
return new Promise(function(resolve, reject) {
fs.readFile(filename,'utf-8', function(err, contents) {
if (err) {
reject(err);
} else {
resolve(contents);
}
});
});
}
// 运行一个任务
run(function*() {
let contents1 = yield readFile("example1.txt");
let contents2 = yield readFile("example2.txt")
});

首先我们定义一个readFile文件,用来返回一个Promise。然后我们定义要给通用的run函数来执行生成器创建一个迭代器。它调用task.next来启动任务。并使用立即执行函数,来判断迭代器中的任务是否都完成了。未完成,则调用Promise.resolve来预防未正确返回promise对象。这里的value就是我们读取的文件内容。然后调用task.next启动下一个任务,直到异步任务全部完成

文章目录
  1. 1. 异步编程的背景
    1. 1.1. 事件模型
    2. 1.2. 回调模式
  2. 2. Promise基础
    1. 2.1. Promise的生命周期
    2. 2.2. 创建未决定的Promise
    3. 2.3. 创建已经决定的Promise
      1. 2.3.1. 使用Promise.resolve()
      2. 2.3.2. 使用Promise.reject()
      3. 2.3.3. 非Promise的Thenable
    4. 2.4. 执行器错误
  3. 3. 全局的Promise拒绝处理
    1. 3.1. Node.js的拒绝处理
    2. 3.2. 浏览器的拒绝处理
  4. 4. 串联Promise
    1. 4.1. 捕获错误
    2. 4.2. 在Promise链中返回值
    3. 4.3. 在Promise链中返回Promise
  5. 5. 响应多个Promise
    1. 5.1. Promise.all()方法
    2. 5.2. Promise.race()方法
  6. 6. 继承Promise
  7. 7. 异步任务的运行
|