Promise 原始碼:非同步執行 resolve

cobish發表於2019-03-01

前言

解讀了同步執行 resolve 的程式碼,接下來要看的則是非同步執行 resolve了。非同步總會比同步複雜得多,它不會按照順序執行,所以程式碼會跳來跳去地閱讀。

與同步不同的是,非同步時程式碼有可能會先執行 then 函式,將 then 的回撥函式儲存起來,等到執行 resolve 的時候,再將其取出執行。

new Promise(function (resolve) {
  setTimeout(function () {
    resolve(1);
  }, 1000);
}.then(function (val) {
  console.log(val);
});
複製程式碼

注:本次閱讀的是 then/promise 的 3.0.0 版本,原始碼請戳 這裡

解讀

這一次的解讀會按照以下的一個執行順序來進行:

constructor -> fn --非同步--> then -> resolve(reject) -> then 回撥
複製程式碼

then

從 fn 函式執行後開始,這時來到了 then 函式:

this.then = function(onFulfilled, onRejected) {
  return new Promise(function(resolve, reject) {
    handle({ onFulfilled: onFulfilled, onRejected: onRejected, resolve: resolve, reject: reject })
  })
}
複製程式碼

同樣先不用管返回的 Promise 例項和它的引數,我們只需要知道 onFulfilled 和 onRejected 被作為引數傳遞給了 handle 函式。可以把以上程式碼簡化成以下:

this.then = function(onFulfilled, onRejected) {
  handle({ onFulfilled: onFulfilled, onRejected: onRejected })
}
複製程式碼

handle

來看看 handle 函式是怎麼處理 onFulfilled 函式(即 then 的回撥函式)的。簡化了許多程式碼:

function handle(deferred) {
  if (state === null) {
    deferreds.push(deferred)
    return
  }
}
複製程式碼

state 是建構函式裡定義的一個變數,它主要作用是用來記錄狀態,執行 resolve 成功賦值 true,執行 reject 成功賦值 false,初始為 null。

所以此時還沒呼叫 resolve(reject),state 就為 null。這時就用另一個變數 deferreds 來儲存 handle 傳遞進來的引數,即 then 的回撥函式。

deferreds 是一個陣列,由於我們可以多次呼叫 then 函式,所以需要一個陣列來儲存那些回撥函式。類似於這樣的:

var p = new Promise(function (resolve) {
  setTimeout(function () {
    resolve(1);
  }, 1000);
}

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

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

至此,then 的回撥函式已經被儲存起來了,就等著非同步執行完畢後 resolve 被呼叫了。

resolve

假設非同步執行完畢了,開始呼叫 resolve 函式。

function resolve(newValue) {
  resolve_(newValue)
}

function resolve_(newValue) {
  if (state !== null)
    return
  try {
    state = true
    value = newValue
    finale()
  } catch (e) { reject_(e) }
}
複製程式碼

同樣簡化了一下程式碼,去掉暫時不用到的。state 儲存 resolve 後的狀態,value 儲存 resolve 的引數。然後呼叫 finale 函式。

finale

來到 finale 函式,它將取出之前 deferreds 陣列儲存的 then 回撥函式,再傳給 handle 函式,讓 handle 函式來執行。

function finale() {
  for (var i = 0, len = deferreds.length; i < len; i++)
    handle(deferreds[i])
  deferreds = null
}
複製程式碼

handle

好了,又回到了 handle 函式,這一次程式碼跟剛剛的不一樣了。以下程式碼展示忽略 deferred.resolve 和 deferred.reject 的呼叫,我們的關注點在於 onFulfilled

function handle(deferred) {
  nextTick(function() {
    var cb = state ? deferred.onFulfilled : deferred.onRejected
    if (typeof cb !== `function`){
      (state ? deferred.resolve : deferred.reject)(value)
      return
    }
    var ret
    try {
      ret = cb(value)
    }
    catch (e) {
      deferred.reject(e)
      return
    }
    deferred.resolve(ret)
  })
}
複製程式碼

嗯,是的,這一步跟同步執行 resolve 的最後一步是一樣的。說實話這裡的 nextTick 我也還沒搞懂為啥要用,感覺去掉也不影響使用。

不過不影響整體的閱讀,現在知道這次呼叫 handle 是為了呼叫 onFulfilled 函式,即 then 的回撥函式被執行了。

handle 函式的完整程式碼是這樣的:

function handle(deferred) {
  if (state === null) {
    deferreds.push(deferred)
    return
  }
  nextTick(function() {
    var cb = state ? deferred.onFulfilled : deferred.onRejected
    if (typeof cb !== `function`){
      (state ? deferred.resolve : deferred.reject)(value)
      return
    }
    var ret
    try {
      ret = cb(value)
    }
    catch (e) {
      deferred.reject(e)
      return
    }
    deferred.resolve(ret)
  })
}
複製程式碼

程式碼執行了兩次 handle 函式,它有兩個用處。在 state 為 null 時,它用來儲存回撥函式 onFulfilled。再次呼叫它時,resolve 已被執行,state 被修改成 true,則用來執行 onFulfilled。

總結

Promise 非同步執行程式碼時會比較的繞:

new Promise(function (resolve) {
  setTimeout(function () {
    resolve(1);
  }, 1000);
}.then(function (val) {
  console.log(val);
});
複製程式碼

執行的順序回顧一下:

constructor -> fn --非同步--> then -> resolve(reject) -> then 回撥
複製程式碼

執行程式碼時,會先執行建構函式,然後是傳入建構函式的 fn 函式。緊接著是執行 then 函式,將其回撥函式 onFulfilled 儲存在 deferreds 陣列中。

等到 fn 函式裡的非同步程式碼執行完畢後,呼叫 resolve 函式,將儲存在 deferreds 陣列裡的回撥函式取出執行。

相關文章