新聞中心
之前在網上看到一道Promise執(zhí)行順序的題目——打印以下程序的輸出:

成都網站制作、成都網站建設的開發(fā),更需要了解用戶,從用戶角度來建設網站,獲得較好的用戶體驗。創(chuàng)新互聯公司多年互聯網經驗,見的多,溝通容易、能幫助客戶提出的運營建議。作為成都一家網絡公司,打造的就是網站建設產品直銷的概念。選擇創(chuàng)新互聯公司,不只是建站,我們把建站作為產品,不斷的更新、完善,讓每位來訪用戶感受到浩方產品的價值服務。
你所需要的網站建設服務,我們均能行業(yè)靠前的水平為你提供.標準是產品質量的保證,主要從事成都網站制作、成都網站設計、外貿營銷網站建設、企業(yè)網站建設、手機網站制作設計、網頁設計、成都品牌網站建設、網頁制作、做網站、建網站。創(chuàng)新互聯建站擁有實力堅強的技術研發(fā)團隊及素養(yǎng)的視覺設計專才。
目前創(chuàng)新互聯已為數千家的企業(yè)提供了網站建設、域名、網頁空間、網站運營、企業(yè)網站設計、江華網站維護等服務,公司將堅持客戶導向、應用為本的策略,正道將秉承"和諧、參與、激情"的文化,與客戶和合作伙伴齊心協力一起成長,共同發(fā)展。
創(chuàng)新互聯專注于企業(yè)全網整合營銷推廣、網站重做改版、普陀網站定制設計、自適應品牌網站建設、H5高端網站建設、商城網站開發(fā)、集團公司官網建設、外貿網站建設、高端網站制作、響應式網頁設計等建站業(yè)務,價格優(yōu)惠性價比高,為普陀等各大城市提供網站開發(fā)制作服務。
超過10多年行業(yè)經驗,技術領先,服務至上的經營模式,全靠網絡和口碑獲得客戶,為自己降低成本,也就是為客戶降低成本。到目前業(yè)務范圍包括了:成都網站建設、網站建設,成都網站推廣,成都網站優(yōu)化,整體網絡托管,重慶小程序開發(fā)公司,微信開發(fā),成都App定制開發(fā),同時也可以讓客戶的網站和網絡營銷和我們一樣獲得訂單和生意!
成都創(chuàng)新互聯是一家專注于成都網站設計、網站制作、外貿營銷網站建設與策劃設計,馬邊彝族網站建設哪家好?成都創(chuàng)新互聯做網站,專注于網站建設十年,網設計領域的專業(yè)建站公司;建站業(yè)務涵蓋:馬邊彝族等地區(qū)。馬邊彝族做網站價格咨詢:13518219792
成都創(chuàng)新互聯成都網站建設按需定制網站,是成都網站設計公司,為成都集裝箱提供網站建設服務,有成熟的網站定制合作流程,提供網站定制設計服務:原型圖制作、網站創(chuàng)意設計、前端HTML5制作、后臺程序開發(fā)等。成都網站制作熱線:13518219792
創(chuàng)新互聯服務項目包括山西網站建設、山西網站制作、山西網頁制作以及山西網絡營銷策劃等。多年來,我們專注于互聯網行業(yè),利用自身積累的技術優(yōu)勢、行業(yè)經驗、深度合作伙伴關系等,向廣大中小型企業(yè)、政府機構等提供互聯網行業(yè)的解決方案,山西網站推廣取得了明顯的社會效益與經濟效益。目前,我們服務的客戶以成都為中心已經輻射到山西省份的部分城市,未來相信會繼續(xù)擴大服務區(qū)域并繼續(xù)獲得客戶的支持與信任!
網站建設哪家好,找創(chuàng)新互聯建站!專注于網頁設計、網站建設、微信開發(fā)、重慶小程序開發(fā)、集團企業(yè)網站建設等服務項目。為回饋新老客戶創(chuàng)新互聯還提供了梨樹免費建站歡迎大家使用!
成都創(chuàng)新互聯公司2013年至今,先為杜集等服務建站,杜集等地企業(yè),進行企業(yè)商務咨詢服務。為杜集企業(yè)網站制作PC+手機+微官網三網同步一站式服務解決您的所有建站問題。
贛縣ssl適用于網站、小程序/APP、API接口等需要進行數據傳輸應用場景,ssl證書未來市場廣闊!成為創(chuàng)新互聯的ssl證書銷售渠道,可以享受市場價格4-6折優(yōu)惠!如果有意向歡迎電話聯系或者加微信:18982081108(備注:SSL證書合作)期待與您的合作!
- new Promise(resolve => {
- console.log(1);
- resolve(3);
- }).then(num => {
- console.log(num)
- });
- console.log(2)
這道題的輸出是123,為什么不是132呢?因為我一直理解Promise是沒有異步功能,它只是幫忙解決異步回調的問題,實質上是和回調是一樣的,所以如果按照這個想法,resolve之后應該會立刻then。但實際上并不是。難道用了setTimeout?
如果在promise里面再加一個promise:
- new Promise(resolve => {
- console.log(1);
- resolve(3);
- Promise.resolve().then(()=> console.log(4))
- }).then(num => {
- console.log(num)
- });
- console.log(2)
執(zhí)行順序是1243,第二個Promise的順序會比***個的早,所以直觀來看也是比較奇怪,這是為什么呢?
Promise的實現有很多庫,有jQuery的deferred,還有很多提供polyfill的,如es6-promise,lie等,它們的實現都基于Promise/A+標準,這也是ES6的Promise采用的。
為了回答上面題目的執(zhí)行順序問題,必須得理解Promise是怎么實現的,所以得看那些庫是怎么實現的,特別是我錯誤地認為不存在的Promise的異步是怎么實現的,因為***一行的console.log(2)它并不是***執(zhí)行的,那么必定有某些類似于setTimeout的異步機制讓上面同步的代碼在異步執(zhí)行,所以它才能在代碼執(zhí)行完了之后才執(zhí)行。
當然我們不只是為了解答一道題,主要還是借此了解Promise的內部機制。讀者如果有時間有興趣可以自行分析,然后再回過頭來比較一下本文的分析?;蛘吣憧梢愿旅娴乃悸?,操起鼠標和鍵盤和我一起干。
這里使用lie的庫,相對于es6-promise來說代碼更容易看懂,先npm install一下:
- npm install lie
讓代碼在瀏覽器端運行,準備以下html:
其中index.js的內容為:
- console.log(Promise);
- new Promise(resolve => {
- console.log(1);
- resolve(3);
- Promise.resolve().then(()=> console.log(4))
- }).then(num => {
- console.log(num)
- });
- console.log(2);
把Promise打印一下,確認已經把原生的那個覆蓋了,對比如下:
因為原生的Promise我們是打不了斷點的,所以才需要借助一個第三方的庫。
我們在第4行的resolve(3)那里打個斷點進去看一下resolve是怎么執(zhí)行的,層層進去,***的函數是這個:
我們發(fā)現,這個函數好像沒干啥,它就是設置了下self的state狀態(tài)為FULFILLED(完成),并且把結果outcome設置為調resolve傳進來的值,這里是3,如果resolve傳來是一個Promise的話就會進入到上圖187行的Promise鏈處理,這里我們不考慮這種情況。這里的self是指向一個Promise對象:
它主要有3個屬性——outcome、queue、state,其中outcome是resolve傳進來的結果,state是Promise的狀態(tài),在第83行的代碼可以查到Promise的狀態(tài)總共有3種:
- var REJECTED = ['REJECTED'];
- var FULFILLED = ['FULFILLED'];
- var PENDING = ['PENDING'];
Rejected失敗,fulfilled成功,pending還在處理中,在緊接著89行的Promise的構造函數可以看到,state初始化的狀態(tài)為pending:
- function Promise(resolver) {
- if (typeof resolver !== 'function') {
- throw new TypeError('resolver must be a function');
- }
- this.state = PENDING;
- this.queue = [];
- this.outcome = void 0;
- if (resolver !== INTERNAL) {
- safelyResolveThenable(this, resolver);
- }
- }
并且在右邊的調用??梢钥吹剑瑀esolver是由Promise的構造函數觸發(fā)執(zhí)行的,即當你new Promise的時候就會執(zhí)行傳參的函數,如下圖所示:
傳進來的函數支持兩個參數,分別是resolve和reject回調:
傳進來的函數支持兩個參數,分別是resolve和reject回調:
- let resolver = function(resolve, reject) {
- if (success) resolve();
- else reject();
- };
- new Promise(resolver);
這兩個函數是Promise內部定義,但是要在你的函數里調一下它的函數,告訴它什么時候成功了,什么時候失敗了,這樣它才能繼續(xù)下一步的操作。所以這兩個函數參數是傳進來的,它們是Promise的回調函數。Promise是怎么定義和傳遞這兩個函數的呢?還是在剛剛那個斷點的位置,但是我們改變一下右邊調用棧顯示的位置:
上圖執(zhí)行的thenable函數就是我們傳給它的resolver,然后傳遞onSuccess和onError,分別是我們在resolver里面寫的resolve和reject這兩個參數。如果我們調了它的resolve即onSuccess函數,它就會調236行的handlers.resolve就到了我們***次打斷點的那張圖,這里再放一次:
然后去設置當前Promise對象的state,outcome等屬性。這里沒有進入到193行的while循環(huán)里,因為queue是空的。這個地方下文會繼續(xù)提到。
接著,我們在then那里打個斷點進去看一下:
then又做了些什么工作呢?如下圖所示:
then可以傳兩個參數,分別為成功回調和失敗回調。我們給它傳了一個成功回調,即上圖劃線的地方。并且由于在resolver里面已經把state置成fulfilled完成態(tài)了,所以它會執(zhí)行unwrap函數,并傳遞成功回調、以及resolve給的結果outcome(還有一個參數promise,主要是用于返回,形成then鏈)。
unwrap函數是這樣實現的:
在167行執(zhí)行then里傳給Promise的成功回調,并傳遞結果outcome。
這段代碼是包在一個immediate函數里的,這里就是解決Promise異步問題的關鍵了。并且我們在node_modules目錄里面,也發(fā)現了lie使用了immediate庫,它可以實現一個nextTick的功能,即在當前代碼邏輯單元同步執(zhí)行完了之后立刻執(zhí)行,相當于setTimeout 0,但是它又不是直接用setTimeout 0實現的。
我們重點來看一下它是怎么實現一個nextTick的功能的。immediate里面會調一個scheduleDrain(drain是排水的意思):
- function immediate(task) {
- // 這個判斷先忽略
- if (queue.push(task) === 1 && !draining) {
- scheduleDrain();
- }
- }
實現邏輯在這個scheduleDrain,它是這么實現的:
- var Mutation = global.MutationObserver || global.WebKitMutationObserver;
- var scheduleDrain = null;
- {
- // 瀏覽器環(huán)境,IE11以上支持
- if (Mutation) {
- // ...
- }
- // Node.js環(huán)境
- else if (!global.setImmediate && typeof global.MessageChannel !== 'undefined')
- }
- // 低瀏覽器版本解決方案
- else if ('document' in global && 'onreadystatechange' in global.document.createElement('script')) {
- }
- // ***實在沒辦法了,用最次的setTimeout
- else {
- scheduleDrain = function () {
- setTimeout(nextTick, 0);
- };
- }
- }
它會有一個兼容性判斷,優(yōu)先使用MutationObserver,然后是使用script標簽的方式,這種到IE6都支持,***啥都不行就用setTimeout 0.
我們主要看一下Mutation的方式是怎么實現的,MDN上有介紹這個MutationObserver的用法,可以用它來監(jiān)聽DOM結點的變化,如增刪、屬性變化等。Immediate是這么實現的:
- if (Mutation) {
- var called = 0;
- var observer = new Mutation(nextTick);
- var element = global.document.createTextNode('');
- // 監(jiān)聽節(jié)點的data屬性的變化
- observer.observe(element, {
- characterData: true
- });
- scheduleDrain = function () {
- // 讓data屬性發(fā)生變化,在0/1之間不斷切換,
- // 進而觸發(fā)observer執(zhí)行nextTick函數
- element.data = (called = ++called % 2);
- };
- }
使用nextTick回調注冊一個observer觀察者,然后創(chuàng)建一個DOM節(jié)點element,成為observer的觀察對象,觀察它的data屬性。當需要執(zhí)行nextTick函數的時候,就調一下scheduleDrain改變data屬性,就會觸發(fā)觀察者的回調nextTick。它是異步執(zhí)行的,在當前代碼單元執(zhí)行完之后立刻之行,但又是在setTimeout 0之前執(zhí)行的,也就是說,以下代碼,***行的5是***輸出的:
- setTimeout(()=> console.log(5), 0);
- new Promise(resolve => {
- console.log(1);
- resolve(3);
- // Promise.resolve().then(()=> console.log(4))
- }).then(num => {
- console.log(num)
- });
- console.log(2);
這個時候,我們就可以回答為什么上面代碼的輸出順序是123,而不是132了。***點可以肯定的是1是***輸出的,因為new一個Promise之后,傳給它的resolver同步執(zhí)行,所以1***打印。執(zhí)行了resolve(3)之后,就會把當前Promiser對象的state改成完成態(tài),并記錄結果outcome。然后跳出來執(zhí)行then,把傳給then的成功回調給immediate在nextTick執(zhí)行,而nextTick是使用Mutation異步執(zhí)行的,所以3會在2之后輸出。
如果在promise里面再寫一個promsie的話,由于里面的promise的then要比外面的promise的then先執(zhí)行,也就是說它的nextTick更先注冊,所以4是在3之前輸出。
這樣基本上就解釋了Promise的執(zhí)行順序的問題。但是我們還沒說它的nextTick是怎么實現的,上面代碼在執(zhí)行immediate的時候把成功回調push到一個全局的數組queue里面,而nextTick是把這些回調按順序執(zhí)行,如下代碼所示:
- function nextTick() {
- draining = true;
- var i, oldQueue;
- var len = queue.length;
- while (len) {
- oldQueue = queue;
- // 把queue清空
- queue = [];
- i = -1;
- // 執(zhí)行當前所有回調
- while (++i < len) {
- oldQueue[i]();
- }
- len = queue.length;
- }
- draining = false;
- }
它會先把排水的變量draining設置成true,然后處理完成之后再設置成false,我們再回顧一下剛剛執(zhí)行immediate的判斷:
- function immediate(task) {
- if (queue.push(task) === 1 && !draining) {
- scheduleDrain();
- }
- }
由于JS是單線程的,所以我覺得這個draining的變量判斷好像沒有太大的必要。另外一個判斷,當queue為空時,push一個變量進來,這個時候queue只有1個元素,返回值就為1。所以如果之前已經push過了,那么這里就不用再觸發(fā)nextTick,因為***次的push會把所有queue回調元素都執(zhí)行的,只要保證后面的操作有被push到這個queue里面就好了。所以這個判斷是一個優(yōu)化。
另外,es6-promise的核心代碼是一樣的,只是它把immediate函數改成asap(as soon as possible),它也是優(yōu)先使用Mutation.
還有一個問題,上面說的resolver的代碼是同步,但是我們經常用Promise是用在異步的情況,resolve是異步調的,不是像上面同步調的,如:
- let resolver = function(resolve) {
- setTimeout(() => {
- // 異步調用resolve
- resolve();
- }, 2000);
- // resolver執(zhí)行完了還沒執(zhí)行resolve
- };
- new Promise(resolver).then(num => console.log(num));
這個時候,同步執(zhí)行完resolver,但還沒執(zhí)行resolve,所以在執(zhí)行then的時候這個Promise的state還是pending的,就會走到134的代碼(剛剛執(zhí)行的是132行的unwrap):
它會創(chuàng)建一個QueueItem然后放到當前Promise對象的queue屬性里面(注意這里的queue和上面說的immediate里全局的queue是兩個不同的變量)。然后異步執(zhí)行結束調用resolve,這個時候queue不為空了:
就會執(zhí)行queue隊列里面的成功回調。因為then是可以then多次的,所以成功回調可能會有多個。它也是調用immediate,在nextTick的時候執(zhí)行的。
也就是說如果是同步resolve的,是通過MutationObserver/Setimeout 0之類的方式在當前的代碼單元執(zhí)行完之后立刻執(zhí)行成功回調;而如果是異步resolve的,是先把成功回調放到當前Promise對象的一個隊列里面,等到異步結束了執(zhí)行resolve的時候再用同樣的方式在nextTick調用成功回調。
我們還沒說失敗的回調,但大體是相似的。
【本文是專欄作者“人人網FED”的原創(chuàng)稿件,轉載請通過聯系原作者獲取授權】
戳這里,看該作者更多好文
當前標題:從一道Promise執(zhí)行順序的題目看Promise實現
本文鏈接:http://fisionsoft.com.cn/article/cdeieed.html


咨詢
建站咨詢
