八段程式碼徹底掌握 Promise

艾特老幹部發表於2017-07-25

1.Promise的立即執行性

var p = new Promise(function(resolve, reject){
  console.log("create a promise");
  resolve("success");
});

console.log("after new Promise");

p.then(function(value){
  console.log(value);
});
複製程式碼

控制檯輸出:

"create a promise"
"after new Promise"
"success"
複製程式碼

Promise物件表示未來某個將要發生的事件,但在建立(new)Promise時,作為Promise引數傳入的函式是會被立即執行的,只是其中執行的程式碼可以是非同步程式碼。有些同學會認為,當Promise物件呼叫then方法時,Promise接收的函式才會執行,這是錯誤的。因此,程式碼中"create a promise"先於"after new Promise"輸出。

2.Promise 三種狀態

var p1 = new Promise(function(resolve,reject){
  resolve(1);
});
var p2 = new Promise(function(resolve,reject){
  setTimeout(function(){
    resolve(2);  
  }, 500);      
});
var p3 = new Promise(function(resolve,reject){
  setTimeout(function(){
    reject(3);  
  }, 500);      
});

console.log(p1);
console.log(p2);
console.log(p3);
setTimeout(function(){
  console.log(p2);
}, 1000);
setTimeout(function(){
  console.log(p3);
}, 1000);

p1.then(function(value){
  console.log(value);
});
p2.then(function(value){
  console.log(value);
});
p3.catch(function(err){
  console.log(err);
});
複製程式碼

控制檯輸出:

Promise {[[PromiseStatus]]: "resolved", [[PromiseValue]]: 1}
Promise {[[PromiseStatus]]: "pending", [[PromiseValue]]: undefined}
Promise {[[PromiseStatus]]: "pending", [[PromiseValue]]: undefined}
1
2
3
Promise {[[PromiseStatus]]: "resolved", [[PromiseValue]]: 2}
Promise {[[PromiseStatus]]: "rejected", [[PromiseValue]]: 3}
複製程式碼

Promise的內部實現是一個狀態機。Promise有三種狀態:pending,resolved,rejected。當Promise剛建立完成時,處於pending狀態;當Promise中的函式引數執行了resolve後,Promise由pending狀態變成resolved狀態;如果在Promise的函式引數中執行的不是resolve方法,而是reject方法,那麼Promise會由pending狀態變成rejected狀態。

p2、p3剛建立完成時,控制檯輸出的這兩臺Promise都處於pending狀態,但為什麼p1是resolved狀態呢? 這是因為p1 的函式引數中執行的是一段同步程式碼,Promise剛建立完成,resolve方法就已經被呼叫了,因而緊跟著的輸出顯示p1是resolved狀態。我們通過兩個setTimeout函式,延遲1s後再次輸出p2、p3的狀態,此時p2、p3已經執行完成,狀態分別變成resolved和rejected。

3.Promise 狀態的不可逆性

var p1 = new Promise(function(resolve, reject){
  resolve("success1");
  resolve("success2");
});

var p2 = new Promise(function(resolve, reject){
  resolve("success");
  reject("reject");
});

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

p2.then(function(value){
  console.log(value);
});
複製程式碼

控制檯輸出:

"success1"
"success"
複製程式碼

Promise狀態的一旦變成resolved或rejected時,Promise的狀態和值就固定下來了,不論你後續再怎麼呼叫resolve或reject方法,都不能改變它的狀態和值。因此,p1中resolve("success2")並不能將p1的值更改為success2,p2中reject("reject")也不能將p2的狀態由resolved改變為rejected.

4.鏈式呼叫

var p = new Promise(function(resolve, reject){
  resolve(1);
});
p.then(function(value){               //第一個then
  console.log(value);
  return value*2;
}).then(function(value){              //第二個then
  console.log(value);
}).then(function(value){              //第三個then
  console.log(value);
  return Promise.resolve('resolve'); 
}).then(function(value){              //第四個then
  console.log(value);
  return Promise.reject('reject');
}).then(function(value){              //第五個then
  console.log('resolve: '+ value);
}, function(err){
  console.log('reject: ' + err);
})
複製程式碼

控制檯輸出:

1
2
undefined
"resolve"
"reject: reject"
複製程式碼

Promise物件的then方法返回一個新的Promise物件,因此可以通過鏈式呼叫then方法。then方法接收兩個函式作為引數,第一個引數是Promise執行成功時的回撥,第二個引數是Promise執行失敗時的回撥。兩個函式只會有一個被呼叫,函式的返回值將被用作建立then返回的Promise物件。這兩個引數的返回值可以是以下三種情況中的一種:

  • return 一個同步的值 ,或者 undefined(當沒有返回一個有效值時,預設返回undefined),then方法將返回一個resolved狀態的Promise物件,Promise物件的值就是這個返回值。
  • return 另一個 Promise,then方法將根據這個Promise的狀態和值建立一個新的Promise物件返回。
  • throw 一個同步異常,then方法將返回一個rejected狀態的Promise, 值是該異常。

根據以上分析,程式碼中第一個then會返回一個值為2(1*2),狀態為resolved的Promise物件,於是第二個then輸出的值是2。第二個then中沒有返回值,因此將返回預設的undefined,於是在第三個then中輸出undefined。第三個then和第四個then中分別返回一個狀態是resolved的Promise和一個狀態是rejected的Promise,依次由第四個then中成功的回撥函式和第五個then中失敗的回撥函式處理。

5.Promise then() 回撥非同步性

var p = new Promise(function(resolve, reject){
  resolve("success");
});

p.then(function(value){
  console.log(value);
});

console.log("which one is called first ?");
複製程式碼

控制檯輸出:

"which one is called first ?"
"success"
複製程式碼

Promise接收的函式引數是同步執行的,但then方法中的回撥函式執行則是非同步的,因此,"success"會在後面輸出。

6.Promise 中的異常

var p1 = new Promise( function(resolve,reject){
  foo.bar();
  resolve( 1 );	  
});

p1.then(
  function(value){
    console.log('p1 then value: ' + value);
  },
  function(err){
    console.log('p1 then err: ' + err);
  }
).then(
  function(value){
    console.log('p1 then then value: '+value);
  },
  function(err){
    console.log('p1 then then err: ' + err);
  }
);

var p2 = new Promise(function(resolve,reject){
  resolve( 2 );	
});

p2.then(
  function(value){
    console.log('p2 then value: ' + value);
    foo.bar();
  }, 
  function(err){
    console.log('p2 then err: ' + err);
  }
).then(
  function(value){
    console.log('p2 then then value: ' + value);
  },
  function(err){
    console.log('p2 then then err: ' + err);
    return 1;
  }
).then(
  function(value){
    console.log('p2 then then then value: ' + value);
  },
  function(err){
    console.log('p2 then then then err: ' + err);
  }
);
複製程式碼

控制檯輸出:

p1 then err: ReferenceError: foo is not defined
p2 then value: 2
p1 then then value: undefined
p2 then then err: ReferenceError: foo is not defined
p2 then then then value: 1
複製程式碼

Promise中的異常由then引數中第二個回撥函式(Promise執行失敗的回撥)處理,異常資訊將作為Promise的值。異常一旦得到處理,then返回的後續Promise物件將恢復正常,並會被Promise執行成功的回撥函式處理。另外,需要注意p1、p2 多級then的回撥函式是交替執行的 ,這正是由Promise then回撥的非同步性決定的。

7.Promise.resolve()

var p1 = Promise.resolve( 1 );
var p2 = Promise.resolve( p1 );
var p3 = new Promise(function(resolve, reject){
  resolve(1);
});
var p4 = new Promise(function(resolve, reject){
  resolve(p1);
});

console.log(p1 === p2); 
console.log(p1 === p3);
console.log(p1 === p4);
console.log(p3 === p4);

p4.then(function(value){
  console.log('p4=' + value);
});

p2.then(function(value){
  console.log('p2=' + value);
})

p1.then(function(value){
  console.log('p1=' + value);
})
複製程式碼

控制檯輸出:

true
false
false
false
p2=1
p1=1
p4=1
複製程式碼

Promise.resolve(...)可以接收一個值或者是一個Promise物件作為引數。當引數是普通值時,它返回一個resolved狀態的Promise物件,物件的值就是這個引數;當引數是一個Promise物件時,它直接返回這個Promise引數。因此,p1 === p2。但通過new的方式建立的Promise物件都是一個新的物件,因此後面的三個比較結果都是false。另外,為什麼p4的then最先呼叫,但在控制檯上是最後輸出結果的呢?因為p4的resolve中接收的引數是一個Promise物件p1,resolve會對p1”拆箱“,獲取p1的狀態和值,但這個過程是非同步的,可參考下一節。

8.resolve vs reject

var p1 = new Promise(function(resolve, reject){
  resolve(Promise.resolve('resolve'));
});

var p2 = new Promise(function(resolve, reject){
  resolve(Promise.reject('reject'));
});

var p3 = new Promise(function(resolve, reject){
  reject(Promise.resolve('resolve'));
});

p1.then(
  function fulfilled(value){
    console.log('fulfilled: ' + value);
  }, 
  function rejected(err){
    console.log('rejected: ' + err);
  }
);

p2.then(
  function fulfilled(value){
    console.log('fulfilled: ' + value);
  }, 
  function rejected(err){
    console.log('rejected: ' + err);
  }
);

p3.then(
  function fulfilled(value){
    console.log('fulfilled: ' + value);
  }, 
  function rejected(err){
    console.log('rejected: ' + err);
  }
);
複製程式碼

控制檯輸出:

p3 rejected: [object Promise]
p1 fulfilled: resolve
p2 rejected: reject
複製程式碼

Promise回撥函式中的第一個引數resolve,會對Promise執行"拆箱"動作。即當resolve的引數是一個Promise物件時,resolve會"拆箱"獲取這個Promise物件的狀態和值,但這個過程是非同步的。p1"拆箱"後,獲取到Promise物件的狀態是resolved,因此fulfilled回撥被執行;p2"拆箱"後,獲取到Promise物件的狀態是rejected,因此rejected回撥被執行。但Promise回撥函式中的第二個引數reject不具備”拆箱“的能力,reject的引數會直接傳遞給then方法中的rejected回撥。因此,即使p3 reject接收了一個resolved狀態的Promise,then方法中被呼叫的依然是rejected,並且引數就是reject接收到的Promise物件。


歡迎關注我的公眾號:老幹部的大前端,領取21本大前端精選書籍!

八段程式碼徹底掌握 Promise

相關文章