iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 11
0
Modern Web

從技術文章深入學習 JavaScript系列 第 14

Day 14 [異步03] 八段代碼徹底掌握Promise

  • 分享至 

  • xImage
  •  

文章選自

作者:艾特老干部

連接:https://juejin.im/post/6844903488695042062

來源:掘金

Promise的立即執行性

let foo = new Promise((resolve, reject) => {
  console.log('我會先執行');
  resolve('成功')
})
console.log('在executor之後');
foo.then(data => {
  console.log(data);
})

https://ithelp.ithome.com.tw/upload/images/20200925/20124350OQUMQleHPv.png

分析

  1. Promsie的executor是同步的

    let foo = new Promise(
        (resolve, reject) => {...} // executor (可以放異步代碼)
    )
    

Promise的三種狀態

var p1 = new Promise(function (resolve, reject) {
  resolve(1);
});
var p2 = new Promise(function (resolve, reject) {
  setTimeout(function () {
    console.log('PromiseP2 Executor')
    resolve(2);
  }, 500);
});
var p3 = new Promise(function (resolve, reject) {
  setTimeout(function () {
    console.log('PromiseP3 Executor')
    reject(3);
  }, 500);
});

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

p1.then(function (value) {
  console.log('p1 then')
  console.log(value);
});
Promise.resolve().then(data => {
  console.log('haha then');
})
p2.then(function (value) {
  console.log(value);
});
p3.catch(function (err) {
  console.log(err);
});


分析

  • 第一階段

    1. 立即執行p1的resolve改變了p1的狀態

    2. 將 PromiseP2 Executor 以及 PromiseP3 Executor 依序放到宏任務隊列裡

    3. 接下來依序打印p1、p2、p3

      附註: 此時p1已經轉換狀態(立即執行了resolve,因此狀態變成resolved)

    4. 接下來將 p2 setTimeout、p3 setTimeout 放到宏任務

    5. 注意這個想法很關鍵,因為then需要偵測到狀態已經改變才會被添加進微任務對列裡面,所以此時只會p1 then 以及haha then會被放到微任務隊列裡

    https://ithelp.ithome.com.tw/upload/images/20200925/20124350iNMow0E2ER.png

    此時的宏任務與微任務:

    https://ithelp.ithome.com.tw/upload/images/20200925/20124350LoBPlQpO53.png

  • 第二階段

    1. 執行p1 then,以及haha then(這裡當然還要注意p1 then裡面有沒有做甚麼其他事,不過此題很明顯沒有所以才直接執行),打印了p1 then、1、haha then

    2. 微任務全部執行完畢,接下來先執行Promise2 Executor,打印出PromiseP2 Executor,並resolve(2),此時因為狀態改變,p2的then因此被放進微任務隊列裡

      注意:每執行完一個宏任務,瀏覽器就會檢查一次微任務隊列是否為空

    https://ithelp.ithome.com.tw/upload/images/20200925/20124350TnyNeiJnSi.png

    此時的宏任務與微任務:

    https://ithelp.ithome.com.tw/upload/images/20200925/20124350pdUh4gkXRL.png

  • 第三階段

    1. 執行p2 then ,打印出2,接下來照剛剛的邏輯跑一次
    2. 因此先打印出PromiseP3 Executor,再打印出3
    3. 最後剩下兩個宏任務,因此打印出剩下的全部(注意,每完成一次宏任務就要檢查微任務是否為空,步過這兩個也沒做甚麼特別的事,因此這邊不再贅述)

    https://ithelp.ithome.com.tw/upload/images/20200925/20124350FWdxPr5zw3.png

    總結:

    這裡主要是想表達Promise實例經過resolve()與reject()回調後會讓狀態改變(但我覺得這是一題很好的EventLoop題目)

    附註: 以上皆是針對瀏覽器的EventLoop

Promise狀態的不可逆

let foo = new Promise((resolve, reject) => {
  resolve(5)
  reject('失敗')
})

console.log(foo);
foo.then(data => {
  console.log(data);
})

https://ithelp.ithome.com.tw/upload/images/20200925/20124350iMv4ToaKE4.png

分析

雖然狀態改變後(resolve) 試圖透過reject再改變他的狀態,但是我們發現打印出來還是resolved

鍊式調用

let foo = new Promise(resolve => {
  resolve(1)
  // 第一個then
}).then(data => {
  console.log(data); // 1
  return data * 2
  // 第二個then
}).then(data => {
  console.log(data); // 2
  // 第三個then
}).then(data => {
  console.log(data); // 因為上一個then沒有返回東西所以是undefined
  return Promise.resolve('resolve');
  // 第四個then
}).then(data => {
  console.log(data); // 上一個返回Promise對象因此輸出resolve
  return Promise.reject('reject');
  // 第五個then
}).then(data => {
  console.log(data);
}, err =>{
  console.log(err); // reject
  // 第六個then
}).then(data => {
  console.log(data, 123456); 
})

https://ithelp.ithome.com.tw/upload/images/20200925/20124350IgYyyo1Qtb.png

分析

  1. then方法會返回一個新的Promise對象,因此可以用鍊式調用(then後面可以接then)

  2. 共有兩個參數,第一個是成功的回調函數,第二個是失敗,只有一個會被調用

  3. 回調函數(不管是then的哪個參數)的返回值有下列幾種可能

    • 返回一個同步的值,或者undefined(當沒有返回一個有效值時,默認返回undefined),

      then方法將返回一個resolved的Promise對象,對象的value即是返回值

    • return另一個Promise,then方法將根據這個Promise的狀態和值創建一個新的Promise對象返回。

Promise then() 回調異步性

之前講過(微任務)

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);
  }
);

分析

這裡我覺得EventLoop講的已經夠多了,所以我不打算在這邊贅述執行的順序是怎樣,我只針對他們輸出的值分析

  • 第一部分

    var p1 = new Promise(function (resolve, reject) {
      foo.bar(); // 根本沒有這種方法,Promise會返回一個錯誤對象
      resolve(1);
      // 注意如果順序有變會改變結果,因為Promise狀態一旦改變就不會再變
      // 所以代碼如果為以下
      // resolve(1)
      // foo.bar()
      // 則底下then會偵測到狀態改變執行第一個回調函數
    });
    
    p1.then(
      function (value) {
        console.log('p1 then value: ' + value);
      },
      function (err) { // 因為接收到rejected狀態,所以then執行第二個參數
        console.log('p1 then err: ' + err); 
      }
    ).then(
      function (value) { // 因為上一個then執行的回調函數( function(err){...} )沒有返回值,因此默認返回resolved的Promise對象,value為undefined
        console.log('p1 then then value: ' + value);
      },
      function (err) {
        console.log('p1 then then err: ' + err);
      }
    );
    

    https://ithelp.ithome.com.tw/upload/images/20200925/20124350YWSr1xgyws.png

  • 第二部分

    var p2 = new Promise(function (resolve, reject) {
        resolve(2);
    });
    
    p2.then(
        function (value) { // 合理執行第一個參數,value為2
            console.log('p2 then value: ' + value);  
            foo.bar(); // 這裡報錯,所以Promise對象狀態改為rejected
        },
        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); // 偵測到Promise對象轉成rejected,將報錯傳進來
            return 1; // 返回1,因此返回Promise對象為resolve,value為1
        }
    ).then(
        function (value) {
            console.log('p2 then then then value: ' + value); // 執行這一行
        },
        function (err) {
            console.log('p2 then then then err: ' + err);
        }
    );
    

    https://ithelp.ithome.com.tw/upload/images/20200925/20124350ROQWC24bQ6.png

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);
})

https://ithelp.ithome.com.tw/upload/images/20200925/20124350vRed9rZ9qX.png

分析

Promise.resolve(...)可以接收一個值或者是一個Promise對像作為參數

  • 參數是普通值,返回一個resolved狀態的Promise對象,對象的value就是這個參數
  • 參數是一個Promise對象,則直接返回這個Promise參數。
  • 第一個part

    var p1 = Promise.resolve(1);
    var p2 = Promise.resolve(p1); // 因為resolve(...) 假如傳入的是Promise對象直接return該對象
    // 所以上面這行等於
    // var p2 = Promise.resolve(1);
    
    var p3 = new Promise(function (resolve, reject) {
      resolve(1);
    });
    var p4 = new Promise(function (resolve, reject) {
      resolve(p1);
    });
    
    console.log(p1 === p2); 
    // 底下全為false非常合理,因為都是p3、p4都是創建新的實例
    console.log(p1 === p3); 
    console.log(p1 === p4);
    console.log(p3 === p4);
    
  • 第二個part

    var p1 = Promise.resolve(1);
    var p2 = Promise.resolve(p1); 
    var p4 = new Promise(function (resolve, reject) {
      resolve(p1);
    });
    
    p4.then(function (value) { // 要先等待resolve改變完成才會放到微任務隊列(所以會比p1、p2晚放進隊列)
      console.log('p4=' + value);
    });
    
    p2.then(function (value) {
      console.log('p2=' + value);
    })
    
    p1.then(function (value) {
      console.log('p1=' + value);
    })
    

    https://ithelp.ithome.com.tw/upload/images/20200925/20124350fgf5S0UjLf.png

    這裡要探討的問題是:為啥p4明明先執行為啥在最後

    主要是因為

    var p4 = new Promise(function (resolve, reject) {
    resolve(p1); // resolve會對p1”拆箱“,獲取p1的狀態和值,但這個過程是異步的
    });
    

resolve vs reject

題目 & 分析

var p1 = new Promise(function (resolve, reject) {
  resolve(Promise.resolve('resolve')); // 返回Promise.resolve('resolve') (異步)
});

var p2 = new Promise(function (resolve, reject) {
  resolve(Promise.reject('reject')); // 返回Promise.reject('reject') (異步)
});

var p3 = new Promise(function (resolve, reject) {
  reject(Promise.resolve('resolve')); // 非異步,直接返回一個rejected的Promise對象,value為裡面那個resolved的Promise對象(也因為非異步因此會讓p3 then比p1、p2更早進入微任務隊列)
});

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

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

p3.then(
  function fulfilled(value) {
    console.log('fulfilled3: ' + value);
  },
  function rejected(err) {
    console.log('rejected3: ' + err);
    // 我這裡將作者的代碼新增了下面這行
    console.log(err)
  }
);

https://ithelp.ithome.com.tw/upload/images/20200925/20124350fOyIbr1eLo.png


上一篇
Day 13 [異步 02] Promise不會??看這裡!!!史上最通俗易懂的Promise!!!
下一篇
Day 15 [EventLoop 02] Eventloop不可怕,可怕的是遇上Promise
系列文
從技術文章深入學習 JavaScript29
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言