日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

剖析 Promise 之基础篇

發布時間:2024/7/5 编程问答 24 豆豆
生活随笔 收集整理的這篇文章主要介紹了 剖析 Promise 之基础篇 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

隨著瀏覽器端異步操作復雜程度的日益增加,以及以 Evented I/O 為核心思想的 NodeJS 的持續火爆,Promise、Async 等異步操作封裝由于解決了異步編程上面臨的諸多挑戰,得到了越來越廣泛的應用。本文旨在剖析 Promise 的內部機制,從實現原理層面深入探討,從而達到“知其然且知其所以然”,在使用 Promise 上更加熟練自如。如果你還不太了解 Promise,推薦閱讀下 promisejs.org 的介紹。

是什么

Promise 是一種對異步操作的封裝,可以通過獨立的接口添加在異步操作執行成功、失敗時執行的方法。主流的規范是 Promises/A+。

Promise 較通常的回調、事件/消息,在處理異步操作時具有顯著的優勢。其中最為重要的一點是:Promise 在語義上代表了異步操作的主體。這種準確、清晰的定位極大推動了它在編程中的普及,因為具有單一職責,而且將份內事做到極致的事物總是具有病毒式的傳染力。分離輸入輸出參數、錯誤冒泡、串行/并行控制流等特性都成為 Promise 橫掃異步操作編程領域的重要砝碼,以至于 ES6 都將其收錄,并已在 Chrome、Firefox 等現代瀏覽器中實現。

內部機制

自從看到 Promise 的 API,我對它的實現就充滿了深深的好奇,一直有心窺其究竟。接下來,將首先從最簡單的基礎實現開始,由淺入深的逐步探索,剖析每一個 feature 后面的故事。

為了讓語言上更加準確和簡練,本文做如下約定:

  • Promise:代表由 Promises/A+ 規范所定義的異步操作封裝方式;
  • promise:代表一個 Promise 實例。

基礎實現

為了增加代入感,本文從最為基礎的一個應用實例開始探索:通過異步請求獲取用戶id,然后做一些處理。在平時大家都是習慣用回調或者事件來處理,下面我們看下 Promise 的處理方式:

// 例1function getUserId() {return new Promise(function (resolve) {// 異步請求Y.io('/userid', {on: {success: function (id, res) {resolve(JSON.parse(res).id);}}});}); }getUserId().then(function (id) {// do sth with id });

JS Bin

getUserId 方法返回一個 promise,可以通過它的 then 方法注冊在 promise 異步操作成功時執行的回調。自然、表意的 API,用起來十分順手。

滿足這樣一種使用場景的 Promise 是如何構建的呢?其實并不復雜,下面給出最基礎的實現:

function Promise(fn) {var value = null,deferreds = [];this.then = function (onFulfilled) {deferreds.push(onFulfilled);};function resolve(value) {deferreds.forEach(function (deferred) {deferred(value);});}fn(resolve); }

代碼很短,邏輯也非常清晰:

  • 調用then方法,將想要在 Promise 異步操作成功時執行的回調放入 deferreds 隊列;
  • 創建 Promise 實例時傳入函數被賦予一個函數類型的參數,即 resolve,用以在合適的時機觸發異步操作成功。真正執行的操作是將 deferreds 隊列中的回調一一執行;
  • resolve 接收一個參數,即異步操作返回的結果,方便回調使用。

有時需要注冊多個回調,如果能夠支持 jQuery 那樣的鏈式操作就好了!事實上,這很容易:

this.then = function (onFulfilled) {deferreds.push(onFulfilled);return this; };

這個小改進帶來的好處非常明顯,當真是一個大收益的小創新呢:

// 例2getUserId().then(function (id) {// do sth with id }).then(function (id) {// do sth else with id });

JS Bin

延時

如果 promise 是同步代碼,resolve 會先于 then 執行,這時 deferreds 隊列還空無一物,更嚴重的是,后續注冊的回調再也不會被執行了:

// 例3function getUserId() {return new Promise(function (resolve) {resolve(9876);}); }getUserId().then(function (id) {// do sth with id });

JS Bin

此外,Promises/A+ 規范明確要求回調需要通過異步方式執行,用以保證一致可靠的執行順序。為解決這兩個問題,可以通過 setTimeout 將 resolve 中執行回調的邏輯放置到 JS 任務隊列末尾:

function resolve(value) {setTimeout(function () {deferreds.forEach(function (deferred) {deferred(value);});}, 0); }

引入狀態

Hmm,好像存在一點問題:如果 Promise 異步操作已經成功,之后調用 then 注冊的回調再也不會執行了,而這是不符合我們預期的。

解決這個問題,需要引入規范中所說的 States,即每個 Promise 存在三個互斥狀態:pending、fulfilled、rejected,它們之間的關系是:

經過改進后的代碼:

function Promise(fn) {var state = 'pending',value = null,deferreds = [];this.then = function (onFulfilled) {if (state === 'pending') {deferreds.push(onFulfilled);return this;}onFulfilled(value);return this;};function resolve(newValue) {value = newValue;state = 'fulfilled';setTimeout(function () {deferreds.forEach(function (deferred) {deferred(value);});}, 0);}fn(resolve); }

JS Bin

resolve 執行時,會將狀態設置為 fulfilled,在此之后調用 then 添加的新回調,都會立即執行。

似乎少了點什么,哦,是的,沒有任何地方將 state 設為 rejected,這個問題稍后會聊,方便聚焦在核心代碼上。

串行 Promise

在這一小節,將要探索的是 Promise 的 Killer Feature:串行 Promise,這是最為有趣也最為神秘的一個功能。

串行 Promise 是指在當前 promise 達到 fulfilled 狀態后,即開始進行下一個 promise(后鄰 promise)。例如獲取用戶 id 后,再根據用戶 id 獲取用戶手機號等其他信息,這樣的場景比比皆是:

// 例4getUserId().then(getUserMobileById).then(function (mobile) {// do sth with mobile});function getUserMobileById(id) {return new Promise(function (resolve) {Y.io('/usermobile/' + id, {on: {success: function (i, o) {resolve(JSON.parse(o).mobile);}}});}); }

JS Bin

這個 feature 實現的難點在于:如何銜接當前 promise 和后鄰 promise。

首先對 then 方法進行改造:

this.then = function (onFulfilled) {return new Promise(function (resolve) {handle({onFulfilled: onFulfilled || null,resolve: resolve});}); };function handle(deferred) {if (state === 'pending') {deferreds.push(deferred);return;}var ret = deferred.onFulfilled(value);deferred.resolve(ret); }

then 方法改變很多,這是一段暗藏玄機的代碼:

  • then 方法中,創建了一個新的 Promise 實例,并作為返回值,這類 promise,權且稱作 bridge promise。這是串行 Promise 的基礎。另外,因為返回類型一致,之前的鏈式執行仍然被支持;
  • handle 方法是當前 promise 的內部方法。這一點很重要,看不懂的童鞋可以去補充下閉包的知識。then 方法傳入的形參 onFullfilled,以及創建新 Promise 實例時傳入的 resolve 均被壓入當前 promise 的 deferreds 隊列中。所謂“巧婦難為無米之炊”,而這,正是銜接當前 promise 與后鄰 promise 的“米”之所在。

新增的 handle 方法,相比改造之前的 then 方法,僅增加了一行代碼:

deferred.resolve(ret);

這意味著當前 promise 異步操作成功后執行 handle 方法時,先執行 onFulfilled 方法,然后將其返回值作為實參執行 resolve 方法,而這標志著后鄰 promise 異步操作成功,接力工作就這樣完成啦!

以例 2 代碼為例,串行 Promise 執行流如下:

這就是所謂的串行 Promise?當然不是,這些改造只是為了為最后的沖刺做鋪墊,它們在重構底層實現的同時,兼容了本文之前討論的所有功能。接下來,畫龍點睛之筆–最后一個方法 resolve 是這樣被改造的:

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 () {deferreds.forEach(function (deferred) {handle(deferred);});}, 0); }

啊哈,resolve 方法現在支持傳入的參數是一個 Promise 實例了!以例 4 為例,執行步驟如下:

  • getUserId 生成的 promise (簡稱 getUserId promise)異步操作成功,執行其內部方法 resolve,傳入的參數正是異步操作的結果 userid;
  • 調用 handle 方法處理 deferreds 隊列中的回調:getUserMobileById 方法,生成新的 promise(簡稱 getUserMobileById promise);
  • 執行之前由 getUserId promise 的 then 方法生成的 bridge promise 的 resolve 方法,傳入參數為 getUserMobileById promise。這種情況下,會將該 resolve 方法傳入 getUserMobileById promise 的 then 方法中,并直接返回;
  • 在 getUserMobileById promise 異步操作成功時,執行其 deferreds 中的回調:getUserId bridge promise 的 resolve 方法;
  • 最后,執行 getUserId bridge promise 的后鄰 promise 的 deferreds 中的回調
  • 上述步驟實在有些復雜,主要原因是 bridge promise 的引入。不過正是得益于此,注冊一個返回值也是 promise 的回調,從而實現異步操作串行的機制才得以實現。

    一圖勝千言,下圖描述了例 4 的 Promise 執行流:

    失敗處理

    本節處理之前遺留的 rejected 狀態問題。在異步操作失敗時,標記其狀態為 rejected,并執行注冊的失敗回調:

    // 例5function getUserId() {return new Promise(function (resolve, reject) {// 異步請求Y.io('/userid/1', {on: {success: function (id, res) {var o = JSON.parse(res);if (o.status === 1) {resolve(o.id);} else {// 請求失敗,返回錯誤信息reject(o.errorMsg);}}}});}); }getUserId().then(function (id) {// do sth with id }, function (error) {console.log(error); });

    JS Bin

    有了之前處理 fulfilled 狀態的經驗,支持錯誤處理變得很容易。毫無疑問的是,這將加倍 code base,在注冊回調、處理狀態變更上都要加入新的邏輯:

    function Promise(fn) {var state = 'pending',value = null,deferreds = [];this.then = function (onFulfilled, onRejected) {return new Promise(function (resolve, reject) {handle({onFulfilled: onFulfilled || null,onRejected: onRejected || null,resolve: resolve,reject: reject});});};function handle(deferred) {if (state === 'pending') {deferreds.push(deferred);return;}var cb = state === 'fulfilled' ? deferred.onFulfilled : deferred.onRejected,ret;if (cb === null) {cb = state === 'fulfilled' ? deferred.resolve : deferred.reject;cb(value);return;}ret = cb(value);deferred.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;finale();}function reject(reason) {state = 'rejected';value = reason;finale();}function finale() {setTimeout(function () {deferreds.forEach(function (deferred) {handle(deferred);});}, 0);}fn(resolve, reject); }

    增加了新的 reject 方法,供異步操作失敗時調用,同時抽出了 resolve 和 reject 共用的部分,形成 finale 方法。

    錯誤冒泡是上述代碼已經支持,且非常實用的一個特性。在 handle 中發現沒有指定異步操作失敗的回調時,會直接將 bridge promise 設為 rejected 狀態,如此達成執行后續失敗回調的效果。這有利于簡化串行 Promise 的失敗處理成本,因為一組異步操作往往會對應一個實際功能,失敗處理方法通常是一致的:

    // 例6getUserId().then(getUserMobileById).then(function (mobile) {// do sth else with mobile}, function (error) {// getUserId或者getUerMobileById時出現的錯誤console.log(error);});

    JS Bin

    異常處理

    如果在執行成功回調、失敗回調時代碼出錯怎么辦?對于這類異常,可以使用 try-catch 捕獲錯誤,并將 bridge promise 設為 rejected 狀態。handle 方法改造如下:

    function handle(deferred) {if (state === 'pending') {deferreds.push(deferred);return;}var cb = state === 'fulfilled' ? deferred.onFulfilled : deferred.onRejected,ret;if (cb === null) {cb = state === 'fulfilled' ? deferred.resolve : deferred.reject;cb(value);return;}try {ret = cb(value);deferred.resolve(ret);} catch (e) {deferred.reject(e);} }

    如果在異步操作中,多次執行 resolve 或者 reject 會重復處理后續回調,可以通過內置一個標志位解決。

    總結

    Promise 作為異步操作的一種 Monad,魔幻一般的 API 讓人難以駕馭。本文從簡單的基礎實現起步,逐步添加內置狀態、串行、失敗處理/失敗冒泡、異常處理等關鍵特性,最終達到類似由 Forbes Lindesay 所完成的一個簡單 Promise 實現的效果。在讓我本人更加深刻理解 Promise 魔力之源的同時,希望為各位更加熟練的使用這一實用工具帶來一些幫助。

    預告

    下一篇關于 Promise 的文章中,將重點關注高階應用的一些場景,例如并行 Promise、基于 Promise 的異步操作流封裝、語法糖等。敬請期待。

    參考

    • Introduction to Promises
    • JavaScript Promises … In Wicked Detail
    • A Gentle Introduction to Monads in JavaScript

    總結

    以上是生活随笔為你收集整理的剖析 Promise 之基础篇的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。