拆開 JavaScript Promise 理解一波再組裝起來
準備好材料、工具,開拆!!!
一個 Promise 的運用:
var firstPromise = new Promise(function(resolve,reject){ setTimeout(function(){ var result = Math.random() <= 0.5 ? 1:0; if(result){ resolve('resolved'); }else{ reject('rejected') } },1000) }) var secondPromise = newPromise(function(resolve,reject){ setTimeout(function(){ var result = Math.random() <= 0.5 ? 1:0; if(result){ resolve('resolved'); }else{ reject('rejected') } },2000) }) firstPromise.then(function(value){ console.log(value); returnsecondPromise; },function(reason){ console.log(reason); return secondPromise; }).then(function(value){ console.log(value); },function(reason){ console.log(reason); }) // 1s後隨機輸出結果 resolved 或者 rejected // 再1s後隨機輸出結果 resolved 或者 rejected
效果如上,在一個 promise 被完成/被拒絕時執行對應的回撥取到非同步結果。
同時,以上程式碼使用 promise 避免了回撥地獄,規範了回撥操作。
接下來,把 promise 拆成幾塊,學習一下怎麼樣的實現過程。
板塊一、Promise 建構函式
建立 promise 物件的建構函式,是創造 promise 的工廠。
基礎要求:Promise 函式僅產生一個物件,避免大量變數的汙染,將該藏好的物件/值藏好,該暴露的暴露;Promise 接收一個函式作為引數,且該函式在構造 promise 物件時被執行;Promise 必須有個 .then 方法(後續方法可自行擴充套件)。
function Promise(fn){ this.then = function(){ }; }
板塊二、初始化過程,處理引數fn
Promise 建構函式引數 fn 中傳入 resolve/reject;Promise 初始化的時候執行 fn 並在 promise 得到最終結果後執行傳入的 resolve/reject ;resolve/reject 函式中執行 promise 中指定的完成/拒絕時回撥函式,並以最終結果作為引數。
function Promise(fn){ // 完成時 function resolve(value) { console.log('value ',value); } // 拒絕時 function reject(reason) { console.log('reason ',reason); } // 執行傳入的fn function init(fn, onResolved, onRejected) { try { fn(function (value) { onResolved(value); }, function (reason) { onRejected(reason); }) } catch (err) { onRejected(err); } } init(fn, resolve, reject); this.then = function(){ }; } var promise = new Promise(function(resolve,reject){ setTimeout(function(){ var result = Math.random() <= 0.5 ? 1:0; if(result){ resolve('resolved') }else{ reject('rejected') } },1000) }) // 1s後隨機輸出 value resolved 或者 reason rejected
板塊三、.then 裡的處理流程
在promise中, .then 將傳入的 resolvedHandle 和 rejectedHandle 函式存入 promise 的 handlers 中作為回撥列表中的一項,在需要的時候(Promise被完成的時候)攜帶最終結果執行。
首先,假設有個非同步操作,而且已經知道回撥函式是什麼,程式碼如下:
var resolvedHandle = function(res){ console.log(res) }; var rejectedHandle = function(err){ console.log(err) }; setTimeout(function(){ var result = Math.random() <= 0.5 ? 1:0; if(result){ resolvedHandle('resolved'); }else{ rejectedHandle('rejected'); } },1000) // 1s後輸出 resolved 或者 rejected
而對於 promise 而言,回撥函式是在 .then 中傳入並且在 promise 中給定義的,並且為了實現鏈式的操作, .then 中必須有返回一個物件,且物件須是一個攜帶 .then 方法的物件或函式或為一個 promise ,才足以繼續執行.then。
// fn 作為初始化Promise時傳入的函式,應該被立即執行並取出其中的呼叫 function Promise(fn) { var $resolveHandle = function (res) { }; var $rejectHandle = function (err) { }; // 執行Promise被完成時函式 function resolve(value) { try { var then = getThen(value); if (then) { init(then.bind(value), resolve, reject); return; }; fulfill(value); } catch (err) { reject(err); } } // 完成時 function fulfill(value) { $resolveHandle(value); $resolveHandle = null; } // 拒絕時 function reject(reason) { $rejectHandle(reason); $rejectHandle = null; } // 執行傳入的fn並執行回撥 function init(fn, onResolved, onRejected) { try { fn(function (value) { onResolved(value); }, function (reason) { onRejected(reason); }) } catch (err) { onRejected(err); } } init(fn, resolve, reject); 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; }; this.then = function (resolveHandle, rejectHandle) { return new Promise(function (resolve, reject) { $resolveHandle = function (result) { resolve(resolveHandle(result)); } $rejectHandle = function (reason) { resolve(rejectHandle(reason)); } }) } } var firstPromise = new Promise(function (resolve, reject) { setTimeout(function () { var result = Math.random() <= 0.5 ? 1 : 0; if (result) { resolve('resolved'); } else { reject('rejected'); } }, 1000); }) var secondPromise = new Promise(function (resolve, reject) { setTimeout(function () { var result = Math.random() <= 0.5 ? 1 : 0; if (result) { resolve('resolved 2'); } else { reject('rejected 2'); } }, 2000); }) firstPromise.then(function (res) { console.log('res ', res); return secondPromise; }, function (err) { console.log('rej ', err); return secondPromise; }).then(function (res) { console.log('res 2 ', res); }, function (err) { console.log('rej 2 ', err); }) // 1s後隨機輸出 res resolved 或者 rej rejected // 又1s後輸出 res 2 resolved 2 或者 rej 2 rejected 2
至此,上面的程式碼基本算是滿足了一個 promise 的實現思路,但離正規軍 promise 實現還存在一段距離
o(╥﹏╥)o...接下去學習吧。
板塊四、Promise/A+規範
由於 Promise/A+規範較長,就不放到文章裡了,給連結吧(中午版是自己翻譯的,有出入的地方還請以英文原版為準)
Promise/A+ 規範 [ 原文 ]
Promise/A+ 規範 [ 譯文 ]
對照promise/A+規範,以上的Promise程式碼還存在問題:
1.promise還需要儲存promise狀態和最終結果,以便後續被多次使用;
2.同一個promise的.then方法中註冊的回撥函式可被多次執行,且回撥函式可以是個列表;
3.事件排程,回撥函式應該在本輪.then方法所在事件佇列結束後被呼叫;
4.捕捉錯誤並做拒絕處理;
更多細節...
繼續改進,最後整改後的程式碼大致是這樣的:
function Promise(fn) { /* state * 0 : pending * 1 : resloved * 2 : rejected */ var state = 0; var value = null; var handlers = []; function fulfill(result) { state = 1; value = result; handlers.forEach(handle); handlers = []; }; function reject(error) { state = 2; value = error; handlers.forEach(handle); handlers = []; }; function resolve(result) { try { var then = getThen(result); if (then) { init(then.bind(result), resolve, reject); return; } fulfill(result); } catch (err) { reject(err); } }; function getThen(value) { var type = typeof value; if (value && (type === 'object' || type === 'function')) { var then = value.then; if (typeof then === 'function') { return then; } } return null; }; function handle(handler) { if (state === 0) { handlers.push(handler); } else { if (typeof handler.onResolved === 'function') { if (state === 1) { handler.onResolved(value); }; if (state === 2) { handler.onRejected(value); }; } } }; // 放到事件佇列最後,在本輪事件執行完後執行 function timeHandle(callback, newValue) { setTimeout(function () { callback(newValue); }, 0) } function init(fn, onResolved, onRejected) { try { fn(function (value) { timeHandle(onResolved, value); }, function (reason) { timeHandle(onRejected, reason); }); } catch (err) { timeHandle(onRejected, err); } }; init(fn, resolve, reject); this.then = function (onResolved, onRejected) { if (!onResolved && !onRejected) { throw new TypeError('One of onResolved or onRejected must be a function.') }; return new Promise(function (resolve, reject) { handle({ onResolved: function (result) { if (typeof onResolved === 'function') { try { resolve(onResolved(result)); } catch (err) { reject(err); } } else { resolve(result); } }, onRejected: function (error) { if (typeof onRejected === 'function') { try { resolve(onRejected(error)); } catch (err) { reject(err); } } else { reject(error); } } }) }) }; } var firstPromise = new Promise(function (resolve, reject) { setTimeout(function () { var result = Math.random() <= 0.5 ? 1 : 0; if (result) { resolve('resolved 1'); } else { reject('rejected 1'); } }, 1000); }) var secondPromise = new Promise(function (resolve, reject) { setTimeout(function () { var result = Math.random() <= 0.5 ? 1 : 0; if (result) { resolve('resolved 2'); } else { reject('rejected 2'); } }, 3000); }) firstPromise.then(function (res) { console.log('res 1 ', res); return secondPromise; }, function (err) { console.log('rej 1 ', err); return secondPromise; }).then(function (res) { console.log('res 2 ', res); }, function (err) { console.log('rej 2 ', err); }) /* * * 1s後輸出 res 1 resolved 1 或者 rej 1 rejected 1 * 2s後輸出 res 2 resolved 2 或者 rej 2 rejected 2 * */
通過板塊一、二、三的知識點,即可大致摸清promise的實現;板塊四加上一些補充和限制,遵循一些規範,提高promise功能的可擴充套件性。
學會了怎麼理解promise,更重要的是學會正確的使用它。
正確使用 Promise
promise 在業務開發中多用來處理非同步或者多層回撥的情況。
基礎使用 Promise MDN 及相關介紹文件中的案例為準,這裡不一一贅述了... 這裡簡單的列出兩個在使用 promise 過程中比較需要注意的點:
1. 不同平臺環境下 Promise 的方法和遵循規則略微有些出入,詳情以各平臺環境下的 Promise 物件為基準。
如 es6 Promise 存在Promise.race,Promise.all等方法,node中則沒有這些方法。
如 瀏覽器 Promise 事件排程走的是 setTimeout,node 走的是 process.nextTick 。(參考 [asap] )
2. Promise 雖可解決回撥操作的規範問題(回撥地獄),但也不能濫用 Promise (可能會佔用過多記憶體)。
promise 解決後的結果會被存於記憶體中,被對應 promise 引用著,將上面的最終程式碼中測試的兩個 promise 改成如下:
var firstPromise = new Promise(function (resolve, reject) { setTimeout(function () { var result = Math.random() <= 0.5 ? 1 : 0; var str = ''; var i = 0, num = 500000; for (; i < num; i++) { str += 'promise ' } if (result) { resolve('resolved 1 : ' + str); } else { reject('rejected 1 : ' + str); } }, 1000); })
則記憶體佔用情況如下:
這些是一些平臺差異或業務需求方面的不同點,對 Promise 核心實現並影響甚微,對 Promise 擴充套件方法有影響,對業務中 Promise 的使用有影響。
參考
1. Promise/implementing
2. Promise 實現程式碼閱讀