almost 2 years ago

如何實作JavaScript Promise?

本篇文章是我閱讀了A+ Promise implementing的筆記與心得。因為官方講解其實用語很精煉,所以我決定用我自己的話寫一篇容易看得懂的筆記。有任何錯誤歡迎留言指正。

PS: 我在本篇文章中交替使用resolve與議決這兩個詞彙。

建立物件內部變數

//Promise內部有三種狀態

var PENDING = 0;
var FULFILLED = 1;
var REJECTED = 2;

function Promise() {
  // 一開始的狀態是Pending

  var state = PENDING;

  // 一旦Promise被resolve,把成功的value或是失敗的error快取起來

  var value = null;

  // handlers用於儲存 呼叫then或done的後success, failure的handler

  var handlers = [];
}

建立轉換狀態的內部方法fullfill和reject

var PENDING = 0;
var FULFILLED = 1;
var REJECTED = 2;

function Promise() {
  var state = PENDING;
  var value = null;
  var handlers = [];

  function fulfill(result) {
    state = FULFILLED;   //讓狀態改成成功

    value = result;      //快取結果

  }

  function reject(error) {
    state = REJECTED;    //讓狀態改成失敗

    value = error;       //快取錯誤

  }
}

建立更高階轉換狀態的方法resolve

var PENDING = 0;
var FULFILLED = 1;
var REJECTED = 2;

function Promise() {
  var state = PENDING;
  var value = null;
  var handlers = [];

  function fulfill(result) {
    state = FULFILLED;
    value = result;
  }

  function reject(error) {
    state = REJECTED;
    value = error;
  }

  // resolve接受一個單純的值或是一個Promise當作參數。

  // 如果是個一般值,那就轉換狀態

  // 如果是個promise,那就對該promise進行議決,等到議決結果出爐再透過callback轉換狀態。

  function resolve(result) {
    try {
      //試圖取得該物件是否包含then方法,若有,則代表這是一個promise

      var then = getThen(result);   
      if (then) {
        //如果傳入的值是個promise,那麼就透過doResolve先去議決該promise,

        //再根據議決的成功與否callback resolve or reject

        doResolve(then.bind(result), resolve, reject)
        return
      }
      fulfill(result);
    } catch (e) { //在resolve的過程中,如果catch到錯誤會讓該promise reject

      reject(e);
    }
  }
}

getThen

判斷傳入的值是不是promise,如果是的話回傳該promise的then方法,可以注意到檢查方式很鬆散,只是檢查有沒有then方法而已,這種方式可以讓多個不同的promise library彼此相容。

function getThen(value) {
  var t = typeof value;
  if (value && (t === 'object' || t === 'function')) {
    var then = value.then;
    if (typeof then === 'function') {
      return then;
    }
  }
  return null;
}

doResolve

實際進行議決,doResolve 有責任確保傳入的resolve和reject這兩個參數只有其中一個會被呼叫一次

注意doResolve的工作,他會以傳入的fn進行議決,然後再根據議決結果呼叫傳入的onFulfilled或onRejected。並且使用了一個內部變數done來確保onFulfilled或onRejected只會被呼叫一次。

function doResolve(fn, onFulfilled, onRejected) {
  var done = false;
  try {
    fn(function (value) {
      if (done) return
      done = true
      onFulfilled(value)
    }, function (reason) {
      if (done) return
      done = true
      onRejected(reason)
    })
  } catch (ex) {
    if (done) return
    done = true
    onRejected(ex)
  }
}

仔細觀察可以注意到,fn吃兩個參數,一個是成功時的callback,另一個是失敗時的callback,正好對應到Promise建立時的syntax

new Promise(function(resolve, reject) { ... });

建立Promise建構式

var PENDING = 0;
var FULFILLED = 1;
var REJECTED = 2;

function Promise(fn) {  //傳入fn作為參數

  var state = PENDING;
  var value = null;
  var handlers = [];

  function fulfill(result) {
    state = FULFILLED;
    value = result;
  }

  function reject(error) {
    state = REJECTED;
    value = error;
  }

  function resolve(result) {
    try {
      var then = getThen(result);
      if (then) {
        doResolve(then.bind(result), resolve, reject)
        return
      }
      fulfill(result);
    } catch (e) {
      reject(e);
    }
  }

  doResolve(fn, resolve, reject);  //對fn進行議決

}

整個promise基本設定完成後,執行最後一行doResolve,直接對new Promise(xxx)的xxx進行議決,要是議決成功就執行resolve,議決失敗就執行reject。

解釋

為什麼resolve要搞這麼複雜,還要透過doResolve來解決?因為一個promise被議決時有下列兩種情況:

//有一個非同步的promise, 會在一秒鐘之後議決成yeeeee

var yeePromise = new Promise(function(resolve, reject) {
    setTimeout(function() {
    resolve("yeeeee");    //resolve一個value

  }, 1000)
})

//在三秒鐘之後議決yeePromise

var p1 = new Promise(function(resolve, reject) {
  setTimeout(function(){
    resolve(yeePromise);   //resolve一個promise

  }, 3000)
});

如果該promise發現他必須再resolve另一個promise(他具有then方法),那麼就必須繼續取得該promise議決的結果。取得結果的方法是呼叫該promise的then,一旦呼叫then後會有三種情況,沒事、呼叫onFulfilled callback,或是呼叫onRejected callback。

doResolve吃三個參數,fn(要議決的內容), onFulfilled(成功時的callback), onRejected(失敗時的callback),因此我們可以把要fn訂成該promise的then,也就是doResolve(then.bind(result), resolve, reject)。
bind會把執行then時的this綁定到該promise上,因此看起來就像呼叫了該promise的then,如果成功的話就繼續議決(resolve),如果失敗的話就否決(reject)

觀察Promise狀態

我們已經完成所有基本的工作了,現在唯一的問題是,我們沒辦法知道該promise到底有沒有乖乖把任務完成,因此我們需要.then來回報狀態。

但我們先來實作.done吧,因為.done比.then簡單一點

promise.done(onFulfilled, onRejected)

首先我們有幾個需求

  1. 只有onFulfilled或onRejected其中之一會被呼叫
  2. 只會被呼叫一次
  3. 他不會立刻被呼叫,而是會在done return之後之後才會被呼叫(非同步)。
  4. 不管我們的promise在call .done之前被議決或是.done之後,他就是會被呼叫
var PENDING = 0;
var FULFILLED = 1;
var REJECTED = 2;

function Promise(fn) {
  var state = PENDING;
  var value = null;
  var handlers = [];

  function fulfill(result) {
    state = FULFILLED;
    value = result;
    handlers.forEach(handle);   //fulfill時,要執行每一個待執行的handler

    handlers = null;
  }

  function reject(error) {
    state = REJECTED;
    value = error;
    handlers.forEach(handle);  //reject時,要執行每一個待執行的handler

    handlers = null;
  }

  function resolve(result) {
    try {
      var then = getThen(result);
      if (then) {
        doResolve(then.bind(result), resolve, reject)
        return
      }
      fulfill(result);
    } catch (e) {
      reject(e);
    }
  }

  function handle(handler) { 
    if (state === PENDING) {
      handlers.push(handler);
    } else {
      if (state === FULFILLED &&
        typeof handler.onFulfilled === 'function') {
        handler.onFulfilled(value);
      }
      if (state === REJECTED &&
        typeof handler.onRejected === 'function') {
        handler.onRejected(value);
      }
    }
  }

  this.done = function (onFulfilled, onRejected) {
    // ensure we are always asynchronous

    setTimeout(function () {
      handle({
        onFulfilled: onFulfilled,
        onRejected: onRejected
      });
    }, 0);
  }

  doResolve(fn, resolve, reject);
}

.done透過setTimeout來達成非同步的效果,在next Tick之後才根據狀態執行handle看看(晚點再講為什麼要這麼做),然後根據狀態決定要先等待還是進行處理。如此一來,就可以透過傳入.done的callback來讓promise根據狀態決定是否執行任務了。

搞懂.done後再來就是大魔王.then了

Promise.then(onFulfilled, onRejected)

this.then = function (onFulfilled, onRejected) {
  var self = this;
  return new Promise(function (resolve, reject) {
    return self.done(function (result) {
      if (typeof onFulfilled === 'function') {
        try {
          return resolve(onFulfilled(result));
        } catch (ex) {
          return reject(ex);
        }
      } else {
        return resolve(result);
      }
    }, function (error) {
      if (typeof onRejected === 'function') {
        try {
          return resolve(onRejected(error));
        } catch (ex) {
          return reject(ex);
        }
      } else {
        return reject(error);
      }
    });
  });
}

可以看到我們用了一個很漂亮的作法來實作.then,那就.then會回傳一個新的Promise。如此一來你就可以使用Promise Chain來串接).then(cb).then(cb).then(cb)

這裡超級精彩的:.then回傳了一個新的Promise,而這個Promise所答應的事情是「原本Promise的完成(done)」

光講不清楚,我們寫一段簡單的code就知道什麼意思了

//這是一個會在10秒後議決成'yeeeee'的Promise

var yeePromise = new Promise(function(resolve, reject) {
    setTimeout(function() {
    resolve('yeeeee');
  }, 10000)
})
//我們呼叫了.then()

yeePromise.then(function(result) {
    console.log(result);
})

呼叫.then()後, .then()會回傳一個新的Promise,這個Promise會去呼叫yeePromise內部的.done(),而.done會先檢查yeePromise的狀態,發現是Pending,就先把handler放在handlers裡頭,直到5秒後yeePromise被resolve了,他才會執行剛剛保存的handler,最後印出'yeeeee'

為什麼.done內要setTimeout(fn, 0)

這是很重要的問題。請看下列程式碼

var promise = query();  
A();  
promise.then(query);  
B(); 

你預期會發生什麼事?
如果Promise是非同步的話,答案會是A() -> B() -> query()
如果Promise是同步的話,答案會是A()->query()->B()

為了避免讓程式設計師混淆,因此Promise的實作規格規定一定要是非同步的。

注意

另外請注意,.done並不是Promise/A+的實作標準規格,但大多數的標準Library會實作他。

以上就是我的筆記,希望這份筆記能夠讓你簡單的理解Promise是如何實作的。

reference
← 學Vim的啟示 [電影] 革命前夕的破爛摩托車之旅 →
 
comments powered by Disqus