javascript 進階之 - Promise

haokur發表於2018-06-03

引言

promise 主要解決了回撥地域, 也就是巢狀太深的 callback, 而採用鏈式方式. 如:

// 普通方式
$.get({
    url:'url',
    success:function(){
        $.get({
            url:'url2',
            success:function(){
                $.get({
                    url:'url3',
                    success:function(){
                        // ...
                    }
                })
            }
        })
    }
})

// promise , 假如 $.get 支援 promise 方式
$.get({
    url:'url1'
})
.then(res=>{
    return $.get({
        url :'url2'
    })
})
.then(res=>{
    console.log('ok')
})
.catch(()=>{
    console.log('error')
})

Promise 物件

  • 構造 Promise 例項時, 引數為一個函式
var p1 = new Promise(function(resolve,reject){
    console.log('promise start')
})

// Promise 內部類似
function Promise(fn){
    var resolve = function(){}
    var reject = function(){}
    fn(resolve,reject);
    return this;
}
  • then, 收集 resolve 後要執行的回撥 ; catch , 收集 reject 後要執行的回撥
var p1 = new Promise(function(resolve,reject){
    console.log('promise start')
})

p1.then(function(){
    console.log(1)
})
.then(function(){
    console.log(2)
})

// Promise 內部類似
function Promise(fn){
    var resolve = ()=>{
        this.thenList.forEach((fn)=>{
            fn();
        })
    }
    var reject = ()=>{
        this.catchList.forEach((fn)=>{
            fn();
        })
    }

    this.thenList = [] ;
    this.catchList = [];

    fn(resolve,reject);

    this.then = function(callback){
        this.thenList.push(callback);
        return this;
    }

    this.catch = function(callback){
        this.catchList.push(callback) ;
        return this;
    }

    return this;
}

實踐發現 , 並不能列印出 1 , 因為 resolve 的執行, 是早於 then 的呼叫, 這個時候的 thenList 還是個空陣列. 所以修改一下, 先讓 then 執行. 也就是利用事件迴圈的原理.

function Promise(fn){
    var resolve = ()=>{
        setTimeout(()=>{
            this.thenList.forEach((fn) => {
              fn();
            })
        },0)
    }; 

    var reject = ()=>{
        setTimeout(()=>{
            this.catchList.forEach((fn) => {
              fn();
            })
        },0)
    }; 

    // 其他不變

}
  • 而還有一個要注意的點就是 , then 的時候, 是可以返回一個新的 Promise 物件, 打斷當前鏈條的. 而返回其他非 Promise 對 then 的鏈式無影響.

如下, 並未在返回新的 Promise 未打斷原 Promise 的 then 鏈式:

    var p1 = new Promise(function (resolve, reject) {
      console.log('promise start')
      resolve();
    })
      .then(function () {
        console.log(1)
        return new Promise(function (resolve, reject) {
          reject();
        })
      })
      .then(function () {
        console.log(2)
      })
      .catch(function () {
        console.log('error')
      })
// 應該列印 : promise start , 1 , error

可以通過對每次的 then 和 catch 做返回判斷, 如果返回的是 Promise 物件, 則停止之前的 then 和 catch 執行, 將剩餘未執行的拼在返回的 Promise 的物件的原有 thenList 和 catchList 後面 .
這裡順便也加上引數和狀態.

   function Promise(fn) {
      this.status = 'Pending';

      var resolve = (...args) => {
        this.status = 'Resolved';
        setTimeout(() => {
          for (var i = 0; i < this.thenList.length; i++) {
            var result = this.thenList[i](...args);
            if (result instanceof Promise) {
              result.thenList = [...result.thenList, ...this.thenList.slice(i + 1)]
              result.catchList = [...result.catchList, ...this.catchList]
              break;
            }
          }
        })
      }

      var reject = (...args) => {
        this.status = 'Rejected';
        setTimeout(() => {
          for (var i = 0; i < this.catchList.length; i++) {
            var result = this.catchList[i](...args);
            if (result instanceof Promise) {
              result.thenList = [...result.thenList, ...this.thenList]
              result.catchList = [...result.catchList, ...this.catchList.slice(i + 1)]
              break;
            }
          }
        })
      }

      this.thenList = [];
      this.catchList = [];

      fn(resolve, reject);

      this.then = function (callback) {
        this.thenList.push(callback);
        return this;
      }

      this.catch = function (callback) {
        this.catchList.push(callback);
        return this;
      }

      return this;
    }

需要特別說明的是: 這裡雖然借用 setTimeout 實現 , 但 promise 和 setTimeout 在事件迴圈中的表現還是有差異的 , promise 是生成微任務 jobs, 而 setTimeout 則是生成巨集任務 , 也就是 task , 每次的 task 是一個任務 , 一個完整的任務包括 jobs .

async / await

async 和 await 只是 promise 的語法糖, 其核心還是 promise .
既然是糖, 吃多了對牙口不好.

  • await 替代 promise 的 then 的寫法

使用 await 可以省略 then 的鏈式寫法 , 但是 await 必須在 async “裝飾” 的函式體內. 也就是 await 不能寫在全域性作用域內.

// 常規方式新建 promise 物件
var p1 = new Promise((resolve,reject)=>{
    setTimeout(()=>{ resolve('hello') },5000)
})

// 常規 promise then 呼叫
p1.then(result =>{ console.log('normal',result) })

// await 方式全域性作用域呼叫
// var result = await p1; // 報錯 Uncaught SyntaxError: await is only valid in async function

// await 正常呼叫 => 在一個 async 修飾的函式內
async function test(){
    console.log('async start')
    var result = await p1;
    console.log('await',result)
}
test();
console.log('end');

// 列印 async start , end ,  normal hello , await hello

從上述例子, 可以得出以下幾點:
1. await 等的是一個 promise 的 resolve 的結果
2. await 只能在一個 async 修飾的函式體內
3. await 時, 不會堵塞 await 所在的 async 的函式體之外的 js 執行

所以通俗 await 做了哪些事

// 常規寫法
var result ;
p1.then(res=>{
    result = res
    console.log(result);
})

// await 寫法
var result = await p1;
console.log(result);

還有就是 await 可以不只是等 promise , 也可以等一個同步函式

function sayHi(){
    return 'hi';
}

async function test(){
    var result = await sayHi(); 
    console.log(result); 
}
test();
console.log('end');

// 列印結果: end , hi

可以看出, 雖然 await 的是一個同步執行, 直接返回了 hi , 但是卻是在 end 後面輸出; 由此可以大膽猜測 , await 的作用就是把它後面的程式碼都放到了 promise 的 then 中

  • async 修飾一個函式返回 promise 物件
// 常規建立 promise 物件
var p1 = new Promise((resolve,reject)=>{
    resolve('hello');
})

// async 建立 promise 物件
// 1. 無返回 , 則是 resolve(undefined)
var p2 = (async function b(){
})();

// 2. 有返回, 則是 resolve(返回值)
var p3 = (async function c(){
    return 'hi';
})();

// 呼叫
p1.then(res=>{console.log(res)});
p2.then(res=>{console.log(res)});
p3.then(res=>{console.log(res)});

console.log('end');
// 列印 : end , hello , undefined , hi

注意:
1. async 用來 “修飾” 函式, 但該函式並不是 promise , 而是該函式執行後, 返回一個 promise 物件.
2. async “修飾” 的函式有返回值, 則作為 resolve 的引數返回, 無返回值或者無返回, 則 undefined 作為 resolve 的結果返回.

async 和 promise 的 resolve 的一點差別

// 常規 promise 的延時返回
var p1 = new Promise((resolve,reject)=>{
    setTimeout(()=>{ resolve('hi') },5000);
})

// await 如果照常規寫法返回
var p2 = (async function b(){
    setTimeout(()=>{ return ('hi')},5000)
})();

p1.then(res=>{ console.log(res) }); // 5 秒後列印 hi
p2.then(res=>{ console.log(res) }); // undefined

// 列印結果: undefined , 5秒後列印 hi

// 那麼怎麼實現 async 中的 延遲效果? 也就是說 d 函式需要等 (await) 5秒, 再返回出去
var p3 = (async function d(){
    await wait(5000);
    return 'hi';
})();

function wait(s){
    return new Promise((resolve,reject)=>{
        setTimeout(()=>{ resolve() },s)
    })
}

p3.then(res=>{ console.log(res) }); 

那麼, 從上面可以看出什麼端倪?
1. async “修飾” 的函式, 也還是一個同步函式, 也就是它急切地完成, 急切地需要一個結果, 急切地包裝成一個 promise 物件並返回. 所有 p2 他不會等非同步的定時器走完, 返回 ‘hi’ , 才結束一個函式. 直接跳過非同步事件, 走完整個函式, 找到返回值包裝返回.
2. async 並不能代替 new Promise 做事情, 主要作用是用來 , 框定一部分非同步程式碼 (await) 邏輯 ,使得其按 promise 的 then 方式順序執行, 又不至於影響到範圍之外的同步程式碼的執行.

  • await 和 async 總結

    1. await 只能存在 async 修飾的函式體內
    2. await 後面的程式碼, 都將等待 await 的 promise 的 resolve , 也就是堵塞的.
    3. async 單獨用處不大, 主要用來配合 await .

目前綜合來看 , await 和 async 的組合的主要作用, 是解決了 then 的存在和 then 的冗長的鏈式呼叫. 然後通過區域性變數接收 promise 的 then 的結果, 方便後面的呼叫, 避免了 多個 then 需要傳參的尷尬.

await 的錯誤捕獲 和 Promise.all 等後面專門總結一下.

總結

以上 Promise 模擬實現, 並非 js 內建實現的 Promise , 只是用簡單的程式碼模擬 , 強調 Promise 其中的幾個特性.

  1. Promise 是一個函式.
  2. Promise 的引數是一個函式 ( fn ), 且在生成 Promise 例項時, 這個函式會立即執行.
  3. Promise 的 then 和 catch 的引數也是函式, 在生成 Promise 例項時, 並不會直接執行函式 , 但是會掛載在 Promise 例項上 .
  4. Promise 傳入的 fn 執行 resolve 或 reject 時 , 這時候的 Promise 例項必然已經是初始化完成, 即 then 和 catch 都已經掛載完畢.
  5. 第 4 條 , 也就是說明 resolve 和 reject 觸發 的 then 和 catch 在同步程式碼執行之後 . 而且實際上是在 setTimeout 之前.
  6. 在 then 和 catch 中可以返回一個新的 Promise , 打斷之前 Promise 鏈.

研究新的事物, 可以從兩個方面出發:

  1. 用舊有的已經掌握的知識來推導新知識, 以及建立聯絡
  2. 控制變數法, 每次只研究其中一小部分, 其他保持不變.

相關參考

相關文章