Promise的诞生与Javascript中异步编程息息相关,js中异步编程主要指的是setTimout/setInterval、DOM事件机制、ajax,通过传入回调函数实现控制反转。异步编程为js带来强大灵活性的同时,也带来了嵌套回调的问题。详细来说主要有两点,第一嵌套太深代码可读性太差,第二并行逻辑必须串行执行。
1 request = function(url, cb, eb) {
2 var xhr = new XMLHttpRequest();
3 xhr.onreadystatechange = function() {
4 if (xhr.readyState === 4) {
5 if ((xhr.status >=200 && xhr.status < 300) || xhr.status === 304) {
6 cb(xhr.responseText);
7 } else {
8 eb(new Error({
9 message: xhr.status
10 }));
11 }
12 }
13 };
14 xhr.open('get', url, true);
15 xhr.send(null);
16 }
这个例子中程序要依次处理data1、data2、data3,嵌套太多可读性太差
1 //回调函数嵌套过深
2 request('data1.json', function(data1){
3 console.log(data1);//处理data1
4 request('data2.json', function(data2) {
5 console.log(data2);//处理data2
6 request('data3.json', function(data3) {
7 console.log(data3);//处理data3
8
9 alert('success');
10 }, function(err) {
11 console.error(err);
12 });
13 }, function(err) {
14 console.error(err);
15 });
16 }, function(err) {
17 console.error(err);
18 });
这个例子中程序需要请求data1、data2、data3数据,得到三个数据后才进行下一步处理。数据并不需要串行请求,但我们的代码却需要串行执行,增加了等待时间。
1 //并行逻辑串行执行
2 request('data1', function(data1) {
3 request('data2', function(data2) {
4 request('data3', function(data3) {
5 console.log(data1, data2, data3);//处理全部数据
6
7 alert('success');
8 }, function(err) {
9 console.error(err);
10 });
11 }, function(err) {
12 console.error(err);
13 });
14 }, function(err) {
15 console.error(err);
16 });
Promise机制
Promise机制便是上述问题的一种解决方案。与他相关的规范有PromiseA和PromiseA+,PromiseA中对Promise进行了整体描述,PromiseA+对A进行了补充,在then函数的行为方面进行了更加详尽的阐述。
PromiseA+规范
A promise represents the eventual result of an asynchronous operation.
一个promise代表了一个异步操作的最终结果
The primary way of interacting with a promise is through its then
method, which registers callbacks to receive either a promise’s eventual value or the reason why the promise cannot be fulfilled.跟promise交互的主要方式是通过他的then方法来注册回调函数去接收promise的最终结果值或者是promise不能完成的原因。
我们可以简单总结一下规范。每个promise都有三个状态:pending(默认)、fulfilled(完成)、rejected(失败);默认状态可以转变为完成态或失败态,完成态与失败态之间无法相互转换,转变的过程是不可逆的,转变一旦完成promise对象就不能被修改。通过promise提供的then函数注册onFulfill(成功回调)、onReject(失败回调)、onProgres(进度回调)来与promise交互。Then函数返回一个promise对象(称为promise2,前者成为promise1),promise2受promise1状态的影响,具体请查看A+规范。
上两个规范中并没有说明promise的状态如何改变,大部分前端框架中使用Deferred来改变promise的状态(resolve()、reject())。二者关系请看下图。
这里根据规范,我们实现一下promise
1 Promise = function() {
2 this.queue = [];
3 this.value = null;
4 this.status = 'pending';// pending fulfilled rejected
5 };
6
7 Promise.prototype.getQueue = function() {
8 return this.queue;
9 };
10 Promise.prototype.getStatus = function() {
11 return this.status;
12 };
13 Promise.prototype.setStatus = function(s, value) {
14 if (s === 'fulfilled' || s === 'rejected') {
15 this.status = s;
16 this.value = value || null;
17 this.queue = [];
18 var freezeObject = Object.freeze || function(){};
19 freezeObject(this);// promise的状态是不可逆的
20 } else {
21 throw new Error({
22 message: "doesn't support status: " + s
23 });
24 }
25 };
26 Promise.prototype.isFulfilled = function() {
27 return this.status === 'fulfilled';
28 };
29 Promise.prototype.isRejected = function() {
30 return this.status === 'rejected';
31 }
32 Promise.prototype.isPending = function() {
33 return this.status === 'pending';
34 }
35 Promise.prototype.then = function(onFulfilled, onRejected) {
36 var handler = {
37 'fulfilled': onFulfilled,
38 'rejected': onRejected
39 };
40 handler.deferred = new Deferred();
41
42 if (!this.isPending()) {//这里允许先改变promise状态后添加回调
43 utils.procedure(this.status, handler, this.value);
44 } else {
45 this.queue.push(handler);//then may be called multiple times on the same promise;规范2.2.6
46 }
47 return handler.deferred.promise;//then must return a promise;规范2.2.7
48 };
49
50 var utils = (function(){
51 var makeSignaler = function(deferred, type) {
52 return function(result) {
53 transition(deferred, type, result);
54 }
55 };
56
57 var procedure = function(type, handler, result) {
58 var func = handler[type];
59 var def = handler.deferred;
60
61 if (func) {
62 try {
63 var newResult = func(result);
64 if (newResult && typeof newResult.then === 'function') {//thenable
65 // 此种写法存在闭包容易造成内存泄露,我们通过高阶函数解决
66 // newResult.then(function(data) {
67 // def.resolve(data);
68 // }, function(err) {
69 // def.reject(err);
70 // });
71 //PromiseA+规范,x代表newResult,promise代表def.promise
72 //If x is a promise, adopt its state [3.4]:
73 //If x is pending, promise must remain pending until x is fulfilled or rejected.
74 //If/when x is fulfilled, fulfill promise with the same value.
75 //If/when x is rejected, reject promise with the same reason.
76 newResult.then(makeSignaler(def, 'fulfilled'), makeSignaler(def, 'rejected'));//此处的本质是利用了异步闭包
77 } else {
78 transition(def, type, newResult);
79 }
80 } catch(err) {
81 transition(def, 'rejected', err);
82 }
83 } else {
84 transition(def, type, result);
85 }
86 };
87
88 var transition = function(deferred, type, result) {
89 if (type === 'fulfilled') {
90 deferred.resolve(result);
91 } else if (type === 'rejected') {
92 deferred.reject(result);
93 } else if (type !== 'pending') {
94 throw new Error({
95 'message': "doesn't support type: " + type
96 });
97 }
98 };
99
100 return {
101 'procedure': procedure
102 }
103 })();
104
105 Deferred = function() {
106 this.promise = new Promise();
107 };
108
109 Deferred.prototype.resolve = function(result) {
110 if (!this.promise.isPending()) {
111 return;
112 }
113
114 var queue = this.promise.getQueue();
115 for (var i = 0, len = queue.length; i < len; i++) {
116 utils.procedure('fulfilled', queue[i], result);
117 }
118 this.promise.setStatus('fulfilled', result);
119 };
120
121 Deferred.prototype.reject = function(err) {
122 if (!this.promise.isPending()) {
123 return;
124 }
125
126 var queue = this.promise.getQueue();
127 for (var i = 0, len = queue.length; i < len; i++) {
128 utils.procedure('rejected', queue[i], err);
129 }
130 this.promise.setStatus('rejected', err);
131 }
通过Promise机制我们的编程方式可以变成这样:
1 request = function(url) {
2 var def = new Deferred();
3
4 var xhr = new XMLHttpRequest();
5 xhr.onreadystatechange = function() {
6 if (xhr.readyState === 4) {
7 if ((xhr.status >=200 && xhr.status < 300) || xhr.status === 304) {
8 def.resolve(xhr.responseText)
9 } else {//简化ajax,没有提供错误回调
10 def.reject(new Error({
11 message: xhr.status
12 }));
13 }
14 }
15 };
16 xhr.open('get', url, true);
17 xhr.send(null);
18
19 return def.promise;
20 }
21
22 request('data1.json').then(function(data1) {
23 console.log(data1);//处理data1
24 return request('data2.json');
25 }).then(function(data2) {
26 console.log(data2);//处理data2
27 return request('data3.json');
28 }, function(err) {
29 console.error(err);
30 }).then(function(data3) {
31 console.log(data3);
32 alert('success');
33 }, function(err) {
34 console.error(err);
35 });
对于并行逻辑串行执行问题我们可以这样解决
1 //所有异步操作都完成时,进入完成态,
2 //其中一项异步操作失败则进入失败态
3 all = function(requestArray) {
4 // var some = Array.prototype.some;
5 var def = new Deferred();
6 var results = [];
7 var total = 0;
8 requestArray.some(function(r, idx) {
9 //为数组中每一项注册回调函数
10 r.then(function(data) {
11 if (def.promise.isPending()) {
12 total++;
13 results[idx] = data;
14
15 if (total === requestArray.length) {
16 def.resolve(results);
17 }
18 }
19 }, function(err) {
20 def.reject(err);
21 });
22 //如果不是等待状态则停止,比如requestArray[0]失败的话,剩下数组则不用继续注册
23 return !def.promise.isPending();
24 });
25
26 return def.promise;
27 }
28
29 all(
30 [request('data1.json'),
31 request('data2.json'),
32 request('data3.json')]
33 ).then(
34 function(results){
35 console.log(results);// 处理data1,data2,data3
36 alert('success');
37 }, function(err) {
38 console.error(err);
39 });
以下是几个测试案例
1 //链式调用
2 var p1 = new Deferred();
3 p1.promise.then(function(result) {
4 console.log('resolve: ', result);
5 return result;
6 }, function(err) {
7 console.log('reject: ', err);
8 return err;
9 }).then(function(result) {
10 console.log('resolve2: ', result);
11 return result;
12 }, function(err) {
13 console.log('reject2: ', err);
14 return err;
15 }).then(function(result) {
16 console.log('resolve3: ', result);
17 return result;
18 }, function(err) {
19 console.log('reject3: ', err);
20 return err;
21 });
22 p1.resolve('success');
23 //p1.reject('failed');
24 p1.promise.then(function(result) {
25 console.log('after resolve: ', result);
26 return result;
27 }, function(err) {
28 console.log('after reject: ', err);
29 return err;
30 }).then(function(result) {
31 console.log('after resolve2: ', result);
32 return result;
33 }, function(err) {
34 console.log('after reject2: ', err);
35 return err;
36 }).then(function(result) {
37 console.log('after resolve2: ', result);
38 return result;
39 }, function(err) {
40 console.log('after reject2: ', err);
41 return err;
42 });
43
44 //串行异步
45 var p2 = new Deferred();
46 p2.promise.then(function(result) {
47 var def = new Deferred();
48 setTimeout(function(){
49 console.log('resolve: ', result);
50 def.resolve(result);
51 })
52 return def.promise;
53 }, function(err) {
54 console.log('reject: ', err);
55 return err;
56 }).then(function(result) {
57 var def = new Deferred();
58 setTimeout(function(){
59 console.log('resolve2: ', result);
60 def.reject(result);
61 })
62 return def.promise;
63 }, function(err) {
64 console.log('reject2: ', err);
65 return err;
66 }).then(function(result) {
67 console.log('resolve3: ', result);
68 return result;
69 }, function(err) {
70 console.log('reject3: ', err);
71 return err;
72 });
73 p2.resolve('success');
74
75 //并行异步
76 var p1 = function(){
77 var def = new Deferred();
78 setTimeout(function() {
79 console.log('p1 success');
80 def.resolve('p1 success');
81 }, 20);
82
83 return def.promise;
84 }
85 var p2 = function(){
86 var def = new Deferred();
87 setTimeout(function() {
88 console.log('p2 failed');
89 def.reject('p2 failed');
90 }, 10);
91
92 return def.promise;
93 }
94
95 var p3 = function(){
96 var def = new Deferred();
97 setTimeout(function() {
98 console.log('p3 success');
99 def.resolve('p3 success');
100 }, 15);
101
102 return def.promise;
103 }
104
105 all([p1(), p2(), p3()]).then(function(results) {
106 console.log(results);
107 }, function(err) {
108 console.error(err);
109 });
Promise优点
对比使用Promise前后我们可以发现,传统异步编程通过嵌套回调函数的方式,等待异步操作结束后再执行下一步操作。过多的嵌套导致意大利面条式的代码,可读性差、耦合度高、扩展性低。通过Promise机制,扁平化的代码机构,大大提高了代码可读性;用同步编程的方式来编写异步代码,保存线性的代码逻辑,极大的降低了代码耦合性而提高了程序的可扩展性。
参考文章: