摘要:鏈式是指在當前達到狀態后,即開始進行下一個后鄰。在中發現沒有指定異步操作失敗的回調時,會直接將函數返回的,后同設為狀態,如此達成執行后續失敗回調的效果。
原文鏈接
前言前一陣子記錄了promise的一些常規用法,這篇文章再深入一個層次,來分析分析promise的這種規則機制是如何實現的。ps:本文適合已經對promise的用法有所了解的人閱讀,如果對其用法還不是太了解,可以移步我的上一篇博文。
本文的promise源碼是按照Promise/A+規范來編寫的(不想看英文版的移步Promise/A+規范中文翻譯)
引子為了讓大家更容易理解,我們從一個場景開始講解,讓大家一步一步跟著思路思考,相信你一定會更容易看懂。
考慮下面一種獲取用戶id的請求處理
//例1 function getUserId() { return new Promise(function(resolve) { //異步請求 http.get(url, function(results) { resolve(results.id) }) }) } getUserId().then(function(id) { //一些處理 })
getUserId方法返回一個promise,可以通過它的then方法注冊(注意注冊這個詞)在promise異步操作成功時執行的回調。這種執行方式,使得異步調用變得十分順手。
原理剖析那么類似這種功能的Promise怎么實現呢?其實按照上面一句話,實現一個最基礎的雛形還是很easy的。
極簡promise雛形function Promise(fn) { var value = null, callbacks = []; //callbacks為數組,因為可能同時有很多個回調 this.then = function (onFulfilled) { callbacks.push(onFulfilled); }; function resolve(value) { callbacks.forEach(function (callback) { callback(value); }); } fn(resolve); }
上述代碼很簡單,大致的邏輯是這樣的:
調用then方法,將想要在Promise異步操作成功時執行的回調放入callbacks隊列,其實也就是注冊回調函數,可以向觀察者模式方向思考;
創建Promise實例時傳入的函數會被賦予一個函數類型的參數,即resolve,它接收一個參數value,代表異步操作返回的結果,當一步操作執行成功后,用戶會調用resolve方法,這時候其實真正執行的操作是將callbacks隊列中的回調一一執行;
可以結合例1中的代碼來看,首先new Promise時,傳給promise的函數發送異步請求,接著調用promise對象的then屬性,注冊請求成功的回調函數,然后當異步請求發送成功時,調用resolve(results.id)方法, 該方法執行then方法注冊的回調數組。
相信仔細的人應該可以看出來,then方法應該能夠鏈式調用,但是上面的最基礎簡單的版本顯然無法支持鏈式調用。想讓then方法支持鏈式調用,其實也是很簡單的:
this.then = function (onFulfilled) { callbacks.push(onFulfilled); return this; };
see?只要簡單一句話就可以實現類似下面的鏈式調用:
// 例2 getUserId().then(function (id) { // 一些處理 }).then(function (id) { // 一些處理 });加入延時機制
細心的同學應該發現,上述代碼可能還存在一個問題:如果在then方法注冊回調之前,resolve函數就執行了,怎么辦?比如promise內部的函數是同步函數:
// 例3 function getUserId() { return new Promise(function (resolve) { resolve(9876); }); } getUserId().then(function (id) { // 一些處理 });
這顯然是不允許的,Promises/A+規范明確要求回調需要通過異步方式執行,用以保證一致可靠的執行順序。因此我們要加入一些處理,保證在resolve執行之前,then方法已經注冊完所有的回調。我們可以這樣改造下resolve函數:
function resolve(value) { setTimeout(function() { callbacks.forEach(function (callback) { callback(value); }); }, 0) }
上述代碼的思路也很簡單,就是通過setTimeout機制,將resolve中執行回調的邏輯放置到JS任務隊列末尾,以保證在resolve執行時,then方法的回調函數已經注冊完成.
但是,這樣好像還存在一個問題,可以細想一下:如果Promise異步操作已經成功,這時,在異步操作成功之前注冊的回調都會執行,但是在Promise異步操作成功這之后調用的then注冊的回調就再也不會執行了,這顯然不是我們想要的。
加入狀態恩,為了解決上一節拋出的問題,我們必須加入狀態機制,也就是大家熟知的pending、fulfilled、rejected。
Promises/A+規范中的2.1Promise States中明確規定了,pending可以轉化為fulfilled或rejected并且只能轉化一次,也就是說如果pending轉化到fulfilled狀態,那么就不能再轉化到rejected。并且fulfilled和rejected狀態只能由pending轉化而來,兩者之間不能互相轉換。一圖勝千言:
改進后的代碼是這樣的:
function Promise(fn) { var state = "pending", value = null, callbacks = []; this.then = function (onFulfilled) { if (state === "pending") { callbacks.push(onFulfilled); return this; } onFulfilled(value); return this; }; function resolve(newValue) { value = newValue; state = "fulfilled"; setTimeout(function () { callbacks.forEach(function (callback) { callback(value); }); }, 0); } fn(resolve); }
上述代碼的思路是這樣的:resolve執行時,會將狀態設置為fulfilled,在此之后調用then添加的新回調,都會立即執行。
這里沒有任何地方將state設為rejected,為了讓大家聚焦在核心代碼上,這個問題后面會有一小節專門加入。
鏈式Promise那么這里問題又來了,如果用戶再then函數里面注冊的仍然是一個Promise,該如何解決?比如下面的例4:
// 例4 getUserId() .then(getUserJobById) .then(function (job) { // 對job的處理 }); function getUserJobById(id) { return new Promise(function (resolve) { http.get(baseUrl + id, function(job) { resolve(job); }); }); }
這種場景相信用過promise的人都知道會有很多,那么類似這種就是所謂的鏈式Promise。
鏈式Promise是指在當前promise達到fulfilled狀態后,即開始進行下一個promise(后鄰promise)。那么我們如何銜接當前promise和后鄰promise呢?(這是這里的難點)。
其實也不是辣么難,只要在then方法里面return一個promise就好啦。Promises/A+規范中的2.2.7就是這么說噠(微笑臉)~
下面來看看這段暗藏玄機的then方法和resolve方法改造代碼:
function Promise(fn) { var state = "pending", value = null, callbacks = []; this.then = function (onFulfilled) { return new Promise(function (resolve) { handle({ onFulfilled: onFulfilled || null, resolve: resolve }); }); }; function handle(callback) { if (state === "pending") { callbacks.push(callback); return; } //如果then中沒有傳遞任何東西 if(!callback.onFulfilled) { callback.resolve(value); return; } var ret = callback.onFulfilled(value); callback.resolve(ret); } function resolve(newValue) { if (newValue && (typeof newValue === "object" || typeof newValue === "function")) { var then = newValue.then; if (typeof then === "function") { then.call(newValue, resolve); return; } } state = "fulfilled"; value = newValue; setTimeout(function () { callbacks.forEach(function (callback) { handle(callback); }); }, 0); } fn(resolve); }
我們結合例4的代碼,分析下上面的代碼邏輯,為了方便閱讀,我把例4的代碼貼在這里:
// 例4 getUserId() .then(getUserJobById) .then(function (job) { // 對job的處理 }); function getUserJobById(id) { return new Promise(function (resolve) { http.get(baseUrl + id, function(job) { resolve(job); }); }); }
then方法中,創建并返回了新的Promise實例,這是串行Promise的基礎,并且支持鏈式調用。
handle方法是promise內部的方法。then方法傳入的形參onFulfilled以及創建新Promise實例時傳入的resolve均被push到當前promise的callbacks隊列中,這是銜接當前promise和后鄰promise的關鍵所在(這里一定要好好的分析下handle的作用)。
getUserId生成的promise(簡稱getUserId promise)異步操作成功,執行其內部方法resolve,傳入的參數正是異步操作的結果id
調用handle方法處理callbacks隊列中的回調:getUserJobById方法,生成新的promise(getUserJobById promise)
執行之前由getUserId promise的then方法生成的新promise(稱為bridge promise)的resolve方法,傳入參數為getUserJobById promise。這種情況下,會將該resolve方法傳入getUserJobById promise的then方法中,并直接返回。
在getUserJobById promise異步操作成功時,執行其callbacks中的回調:getUserId bridge promise中的resolve方法
最后執行getUserId bridge promise的后鄰promise的callbacks中的回調。
更直白的可以看下面的圖,一圖勝千言(都是根據自己的理解畫出來的,如有不對歡迎指正):
失敗處理在異步操作失敗時,標記其狀態為rejected,并執行注冊的失敗回調:
//例5 function getUserId() { return new Promise(function(resolve) { //異步請求 http.get(url, function(error, results) { if (error) { reject(error); } resolve(results.id) }) }) } getUserId().then(function(id) { //一些處理 }, function(error) { console.log(error) })
有了之前處理fulfilled狀態的經驗,支持錯誤處理變得很容易,只需要在注冊回調、處理狀態變更上都要加入新的邏輯:
function Promise(fn) { var state = "pending", value = null, callbacks = []; this.then = function (onFulfilled, onRejected) { return new Promise(function (resolve, reject) { handle({ onFulfilled: onFulfilled || null, onRejected: onRejected || null, resolve: resolve, reject: reject }); }); }; function handle(callback) { if (state === "pending") { callbacks.push(callback); return; } var cb = state === "fulfilled" ? callback.onFulfilled : callback.onRejected, ret; if (cb === null) { cb = state === "fulfilled" ? callback.resolve : callback.reject; cb(value); return; } ret = cb(value); callback.resolve(ret); } function resolve(newValue) { if (newValue && (typeof newValue === "object" || typeof newValue === "function")) { var then = newValue.then; if (typeof then === "function") { then.call(newValue, resolve, reject); return; } } state = "fulfilled"; value = newValue; execute(); } function reject(reason) { state = "rejected"; value = reason; execute(); } function execute() { setTimeout(function () { callbacks.forEach(function (callback) { handle(callback); }); }, 0); } fn(resolve, reject); }
上述代碼增加了新的reject方法,供異步操作失敗時調用,同時抽出了resolve和reject共用的部分,形成execute方法。
錯誤冒泡是上述代碼已經支持,且非常實用的一個特性。在handle中發現沒有指定異步操作失敗的回調時,會直接將bridge promise(then函數返回的promise,后同)設為rejected狀態,如此達成執行后續失敗回調的效果。這有利于簡化串行Promise的失敗處理成本,因為一組異步操作往往會對應一個實際功能,失敗處理方法通常是一致的:
//例6 getUserId() .then(getUserJobById) .then(function (job) { // 處理job }, function (error) { // getUserId或者getUerJobById時出現的錯誤 console.log(error); });異常處理
細心的同學會想到:如果在執行成功回調、失敗回調時代碼出錯怎么辦?對于這類異常,可以使用try-catch捕獲錯誤,并將bridge promise設為rejected狀態。handle方法改造如下:
function handle(callback) { if (state === "pending") { callbacks.push(callback); return; } var cb = state === "fulfilled" ? callback.onFulfilled : callback.onRejected, ret; if (cb === null) { cb = state === "fulfilled" ? callback.resolve : callback.reject; cb(value); return; } try { ret = cb(value); callback.resolve(ret); } catch (e) { callback.reject(e); } }
如果在異步操作中,多次執行resolve或者reject會重復處理后續回調,可以通過內置一個標志位解決。
總結剛開始看promise源碼的時候總不能很好的理解then和resolve函數的運行機理,但是如果你靜下心來,反過來根據執行promise時的邏輯來推演,就不難理解了。這里一定要注意的點是:promise里面的then函數僅僅是注冊了后續需要執行的代碼,真正的執行是在resolve方法里面執行的,理清了這層,再來分析源碼會省力的多。
現在回顧下Promise的實現過程,其主要使用了設計模式中的觀察者模式:
通過Promise.prototype.then和Promise.prototype.catch方法將觀察者方法注冊到被觀察者Promise對象中,同時返回一個新的Promise對象,以便可以鏈式調用。
被觀察者管理內部pending、fulfilled和rejected的狀態轉變,同時通過構造函數中傳遞的resolve和reject方法以主動觸發狀態轉變和通知觀察者。
參考文獻深入理解 Promise
JavaScript Promises ... In Wicked Detail
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/83085.html
摘要:忍者級別的函數操作對于什么是匿名函數,這里就不做過多介紹了。我們需要知道的是,對于而言,匿名函數是一個很重要且具有邏輯性的特性。通常,匿名函數的使用情況是創建一個供以后使用的函數。 JS 中的遞歸 遞歸, 遞歸基礎, 斐波那契數列, 使用遞歸方式深拷貝, 自定義事件添加 這一次,徹底弄懂 JavaScript 執行機制 本文的目的就是要保證你徹底弄懂javascript的執行機制,如果...
摘要:臆想的針對讀取到的內容進行操作,比如打印文件內容臆想中,讀取文件是有返回值的,將返回值,即文件內容,賦給一個變量,然后決定對讀取到的內容進行相應的操作,例如打印文件中的內容。 臆想的 let fs = require(fs) function readFile(filename){ ... } let content = readFile(config.js) // 針對讀...
摘要:的翻譯文檔由的維護很多人說,阮老師已經有一本關于的書了入門,覺得看看這本書就足夠了。前端的異步解決方案之和異步編程模式在前端開發過程中,顯得越來越重要。為了讓編程更美好,我們就需要引入來降低異步編程的復雜性。 JavaScript Promise 迷你書(中文版) 超詳細介紹promise的gitbook,看完再不會promise...... 本書的目的是以目前還在制定中的ECMASc...
摘要:不兼容問題,本文不予以處理,出門左轉,找谷哥。如果中的回調函數拋出一個錯誤,那么返回的將會成為拒絕狀態,并且將拋出的錯誤作為拒絕狀態的回調函數的參數值。 Promise與async 主要內容: promise基本實現原理 promise 使用中難點(鏈式調用,API基本上返回都是一個新Promise,及參數傳遞) promise 對異常處理 參考: ? 30分鐘,讓你徹底明...
閱讀 3110·2021-11-24 09:39
閱讀 968·2021-09-07 10:20
閱讀 2389·2021-08-23 09:45
閱讀 2255·2021-08-05 10:00
閱讀 566·2019-08-29 16:36
閱讀 833·2019-08-29 11:12
閱讀 2813·2019-08-26 11:34
閱讀 1839·2019-08-26 10:56