寫promise的時候,發現自己應用了樹,連結串列,前度遍歷等方法,覺得對於部分想了解promise機制,並且加深資料結構學習的同學有些許幫助,決定寫一篇文章分享出來。不過文筆實在堪憂,沒有耐心看下去的同學,可以直接看原始碼。原始碼地址 .
promise的狀態是什麼時候變化的 ?
1:通過new Promise()生成的promise的狀態變化過程:
當呼叫resolve時:
情況1:
promise1 = new Promise((resolve) => {
setTimeout(() => {
resolve('promise1');
}, 1000)
})
複製程式碼
情況1: 當引數是一個非promise的時候,1秒後promise的狀態立即變成resolve,並執行then裡面的事件.
情況2:
promise1 = new Promise((resolve) => {
setTimeout(() => {
promise2 = new Promise((resolve, reject) => {
resolve('promise2');
})
resolve(promise2);
}, 1000)
})
複製程式碼
情況2: 當引數是另一個promise的時候,這時promise1的狀態由promise2來決定,什麼時候promise2變化了狀態,promise1的狀態也會相應的變化,並且狀態保持一致.
當呼叫reject時:
這裡與resolve不同的是,reject不管引數是什麼,狀態都會立即變為reject。
2:通過then()或者catch()生成的promise的狀態變化過程
情況1:
promise1 = new Promise((resolve) => {
resolve('promise1');
})
promise2 = promise1.then((data) => {
return 'promise2';
})
複製程式碼
情況1: 當回撥函式裡面直接return一個非promise,和上面的情況1一樣,當前的promise2狀態變為resolve。相當於執行了(resolve('非promise'))
情況2:
promise1 = new Promise((resolve) => {
resolve('promise1');
})
promise2 = promise1.then((data) => {
promise3 = new Promise((resolve, reject) => {
resolve('promise3');
})
return promise3;
})
複製程式碼
情況2: 當回撥函式裡面直接return一個promise3,和上面情況2一樣,當前promise2的狀態依賴於primise3,相當於執行了(resolve(promise3))
情況3:
promise1 = new Promise((resolve) => {
resolve('promise1');
})
promise2 = promise1.then((data) => {
console.log( iamnotundefined );
})
複製程式碼
情況3: 當回撥函式裡面程式碼報錯了,並且沒有被catch到的,當前promise狀態變為reject.(非同步的error程式碼catch不到,不會影響promise狀
態變化)
通過幾個promise例子說明一下
promise1 = new Promise((resolve, reject) => {
setTimeout(()=>{
resolve('promise1_resolve_data');
},1000)
})
console.log(promise1);
promise2 = promise1.then((data) => {
console.log('promise2---', data);
return 'promise2';
})
console.log(promise2);
promise3 = promise1.then((data) => {
console.log('promise3---', data);
return 'promise3';
})
console.log(promise3);
setTimeout(() => {
console.log('--promise1--', promise1);
console.log('--promise2--', promise2);
console.log('--promise3--', promise3);
},3000)
複製程式碼
程式碼執行結果:
依次輸出promise1,promise2,promise3,狀態都是pendding.一秒過後執行relove,promise1狀態變為resolve,值為'promise1_resolve_data'.之後依次執行promise1.then裡面的回撥函式,promise2狀態變為resolve,值為'promise2'.promise3狀態變為resolve,值為'promise3'.
上面程式碼段看出了什麼?
1:當初始化promise1,promise2,promise3後,三個promise的狀態都是pendding.
2:當promise1裡面的resolve執行後,promise1的狀態立即變為resolve,值為resolve函式引數.
3:promise2,promise3都是通過promise1的then方法生成出來的,並且在promose1狀態變為resolve之後也都依次狀態變為了resolve。
通過上面的程式碼可以先得出的結論是:
(1) : 每一個promise的狀態的變化都不是立即就變化得,而是在未來的某一個時刻變化的。這裡可以想到:當我們自己實現的時候,一定要有一個結構去維護著所有promise.
(2) : 什麼結構呢? 這裡可以看出,promise2,promise3都是由promise1的then方法返回的,可以看出這是一個一對多的關係結構,所以這裡的結構一定是一個樹的結構。
(3) : 什麼時候去'裝載'每一個promise和相關的事件呢?很簡單,then和catch方法裡面。
(4) : 什麼時候去'執行'promise狀態變化,相關的事件回撥? resolve,reject裡面。
(5) : 說白了,也就是兩個過程,裝載過程(then,catch),執行過程(resolve,reject)
開始寫程式碼
當去實現一個東東的時候,比如promise,首先要做的是熟悉promise的語法,特性。分析每一個promise之間的關係,然後才能確定一個合適的資料結構去儲存它。前期的結構關係設計合理了,程式碼寫起來也會很容易。
只寫核心程式碼
function PP(){
let promises = new Map(); // 儲存所有的promise例項
let index = 1;
// Promise 建構函式
function App(fn){
this.state = "pendding";
this.id = +index; //每個promise的唯一標識
fn(this.resolve.bind(this), this.reject.bind(this));
}
return App;
}
程式碼很簡單,不做解釋
複製程式碼
前面說到了,promise的實現其實就是兩個過程,裝載和執行. 先說下裝載過程,也就是then() catch()的實現
只寫核心程式碼
App.prototype.then = function(resolve, reject){
let instance = new App(()=>{}); // 生成一個初始狀態的promise,並返回
//把instance和相應的回撥儲存起來
/**
* type: 用來判定這個promise是通過then方法建立的
* instance: promise例項
* callback: 儲存的事件
*/
let item = {
type : 'then',
instance : instance,
callback : length > 1 ? ([{
status : 'resolve',
fn : resolveFn
},{
status : 'reject',
fn : rejectFn
}]) : ([{
status : 'resolve',
fn : resolveFn
}])
}
// 這裡通過map儲存的,兩個promise之間的關係就通過promise的_id相互關聯.
let p_item;
if(p_item = promises.get(this._id)){
p_item.push(item);
}else{
promises.set(this._id,[item])
}
return instance;
}
App.prototype.catch = function(rejectFn){
// 和then差不多
let instance = new app(()=>{});
let item = {
type : 'catch',
instance : instance,
callback : ([{
status : 'reject',
fn : rejectFn
}])
}
let p_item;
if(p_item=promises.get(this._id)){
p_item.push(item);
}else{
promises.set(this._id,[item])
}
return instance;
}
複製程式碼
說下執行的過程 , resolve() , reject()的實現 。
輔助案例: 程式碼 2-1
promise1 = new Promise((resolve, reject) => {
setTimeout(()=>{
resolve("resolve data from promise1");
},1000)
})
promise2 = promise1.then((data) => {
console.log('promise2---', data);
return 'promise2';
})
promise3 = promise2.then((data) => {
console.log('promise3---', data);
return 'promise3';
})
promise4 = promise1.then((data) => {
console.log('promise4---', data);
return 'promise2';
})
promise5 = promise1.catch((data) => {
console.log('promise4---', data);
return 'promise2';
})
複製程式碼
這是程式碼2-1,promise之間的關係圖
執行過程:一秒後執行了resolve方法,當前promise狀態變為resolve.之後拿出與promise1下面的三個promise,分別是promise2,promise4,promise5.之後拿出每一個promise相關的事件,並執行。上面說了,像promise2,promise4,promise5這些通過then或者catch生成的promise,狀態變化過程由返回值來決定。
App.prototype.resolve = function(data){
let ans = null; // 回撥函式的結果
let promise = null; //每一個子節點中的promise例項
let items; //一個節點下面的子節點
//執行mic任務佇列裡面的任務 , 這裡用setTimeout(fn,0)代替
setTimeout(() => {
// 上面說到,這裡做的事就是處理promise的變化。
if(typeof data == 'object' && data!==null && data.__proto__.constructor == app){
// 如果傳入的引數是一個promise物件,這個時候當前的promise的狀態不是立即變化的,而是依賴於傳入的promise也就是data的變化而變化。
// 所以這裡要做的是就是關聯這兩個promise,這裡我用的連結串列
data.previous = this;
}else{
// 這裡也就是上面說的情況1,resolve傳入的引數是一個非promise,這個時候當前promise立即變化,並執行相關的事件回撥.
setTimeout(() => {
this.state = "resolve";
this.value = data;
loadLineToList(this); // (很重要,單獨解釋2)
//拿出當前節點下面的所有子節點
if(items = promise.get(this._id)){
// 這裡以2-1示例程式碼為例,分別拿出promise2,promise4,promise5 .
// 上面promise項裡面的資料結構,分別是 type欄位,instance欄位,callback欄位。在then,或者catch裡面有寫?
// 拿出每一個promise的callback,並執行
for(let i=0;i<items.length;i++){
if(items[i].type == 'then'){
try{
ans = items[i].callback[0].fn(data);
}catch(err){
promise = promises.get(this._id)[i].instance;
promise.reject(err);
continue;
}
}
//這裡已經拿到了事件執行的結果,ans
if(typeof ans == 'object' && ans!==null && ans.__proto__.constructor == app){
ans.previous = promise;
}else{
if(promise){
promise.resolve(ans);
}
}
}
}else{
//下面沒有節點了,出口
return;
}
},0)
}
},0)
}
複製程式碼
程式碼2-2,返回值都是非promise,處理過程如上。接下來說另一種情況,返回值是promise , loadLineToList()這個函式就是用來處理這種情況的
promise1 = new Promise((resolve, reject) => {
setTimeout(()=>{
promise2 = new Promise((resolve) => {
setTimeout(() => {
promise5 = new Promise((resolve) => {
resolve('promise5');
})
promise7 = promise5.then(() => {
})
resolve(promise5);
},1000)
})
console.log('1s');
resolve(promise2);
},1000)
})
promise3 = promise1.then((data) => {
console.log(data);
promise4 = new Promise((resolve) => {
setTimeout(() => {
resolve('promise4');
},1000)
})
return promise4;
})
promise6 = promise3.then((data) => {
console.log(data);
})
setTimeout(() => {
console.log('--promise1--', promise1);
console.log('--promise2--', promise2);
console.log('--promise3--', promise3);
console.log('--promise4--', promise4);
console.log('--promise5--', promise5);
console.log('--promise6--', promise6);
},4000)
複製程式碼
上面是程式碼2-2,promise關係圖,分析出每一個promise之間的關係是重要的,只有明確了關係才能設計出合適的資料結構。上面的程式碼說明了引數是promise的情況 , promise1的變化依賴於promise2, promise2的狀態依賴於promise5. 同樣的,promise3的狀態依賴於promise4. 這裡可以清晰的看出,promise之間的關係是單向的,1對1的,所以用連結串列是合適的。
App.prototype.then程式碼中的data.previous = this;ans.previous = promise;用來建立連結串列的。loadLineToList這個函式用來處理連結串列中promise之前的關係。保持promise1,promise2,promise5狀態一致,並且把promise2,promise5下面的所有promise'移'到promise1的下面。
reject的實現
說reject實現之前,先說明下promise的catch機制。
promise1 = new Promise((resolve, reject) => {
reject('promise1');
})
promise2 = promise1.then(() => {
});
promise4 = promise1.then(() => {
});
promise3 = promise2.catch(() => {
})
複製程式碼
上面程式碼會報一個 Uncaught Error: (in promise) promise1,如果沒有最後的promise3的catch,會報2個Uncaught Error: (in promise) promise1。
promise之間的關係是樹形的,當一個節點狀態變成了reject,那麼一定要在此節點的下面一條線路上,有一個節點去catch這個reject,不然就會報錯。像上面的promise1變成了reject,會向下面的子節點去'發散',promise2沒有catch,那麼promise2的狀態變成reject,並且繼續向下找,promise3catch到了,然後結束。另一條線路,promise4沒有catch到,狀態變為reject,由於下面沒有節點了,也就是沒有catch,所以會抱一個Uncaught Error: (in promise) promise1
說清了catch機制,再去寫reject相關的程式碼就容易了。
App.prototype.reject = function(error){
let promise = null; //子節點
let fn = null; //then or catch的回撥函式
setTimeout(() => {
this.state = "reject";
this.value = error;
loadLineToList(this);
let list = promises.get(this._id);//拿出當前節點下面的所有子節點
//出口,沒有找到,報錯
if(!list || list.length==0){
throw new Error("(in promise) "+error);
}
for(let i=0;i<list.length;i++){
promise = list[i].instance; // 從左的第一個子節點開始
type = list[i].type;
if(type == 'then'){ // 這個promise 是通過p1.then() 出來的 , 但是由於p1是reject , 所以當前promise轉換成reject
//處理then有兩個回撥函式的情況,第一個回撥函式相當於catch
if(list[i].callback.length == 1){
promise.value = error;
promise.reject(error);
continue;
}else{
fn = list[i].callback[1].fn;
}
}
// 拿到catch裡面的fn
if(!fn){
fn = list[i].callback[0].fn;
}
let ans = null; // 回撥函式的結果
// catch回撥函式裡的程式碼,如果程式碼報錯,當前promise變為reject
try{
ans = fn(error);
fn = null;
}catch(err){
promise.reject(err);
continue;
}
promise.value = ans;
if(typeof ans == 'object' && ans!==null && ans.__proto__.constructor == App){
ans.previous = promise;
}else{
if(promise){
promise.resolve(ans);
}
}
}
}, 5)
}
複製程式碼
總結
其實看promise的實現,每一個promise之間的關係是通過樹的結構相互聯絡的。實現也是分為兩個過程,裝載和執行。裝載也就是構建樹的過程,catch和then方法。執行就是通過resolve和reject方法前度遍歷去找出下面的節點,改變每一個promise的狀態,並執行相關的回撥函式。