漫話JavaScript與非同步·第二話——Promise:一諾千金

来源:http://www.cnblogs.com/leegent/archive/2017/05/12/6846664.html
-Advertisement-
Play Games

一、難以掌控的回調 我在第一話中介紹了非同步的概念、事件迴圈、以及JS編程中可能的3種非同步情況(用戶交互、I/O、定時器)。在編寫非同步操作代碼時,最直接、也是每個JSer最先接觸的寫法一定是回調函數(callback),比如下麵這位段代碼: Ajax請求是一種I/O操作,往往需要較長時間來完成,為了不 ...


一、難以掌控的回調

我在第一話中介紹了非同步的概念、事件迴圈、以及JS編程中可能的3種非同步情況(用戶交互、I/O、定時器)。在編寫非同步操作代碼時,最直接、也是每個JSer最先接觸的寫法一定是回調函數(callback),比如下麵這位段代碼:

ajax('www.someurl.com', function(res) {
    doSomething();
    ...
});

Ajax請求是一種I/O操作,往往需要較長時間來完成,為了不阻塞單線程的JS程式,故設計為非同步操作。此處,將一個匿名函數作為參數傳給ajax,意思是“這個匿名函數先放你那兒,但暫不執行,須在收到response之後,再回過頭來調用這個函數”,因此這個匿名函數也被稱為“回調”。這樣的寫法相信每個JSer都再熟悉不過了,但仔細想想,這種寫法可能有什麼問題?

問題就出在“控制反轉”。

匿名函數的代碼,完完全全是我寫的。但是,這段代碼何時被調用、調用幾次、調用時傳入什麼參數……等等,我卻無法掌握;而本來是被我所調用的ajax函數,竟堂而皇之地接管了我的代碼,回調的控制權旁落到了寫ajax函數的那家伙手裡——控制被反轉了。

很多情況下,“那家伙”是個非常可信的機構或公司(比如Google的Chrome團隊)、或是比你我牛得多的天才程式員,因此可以放心地把回調交給他。但也有很多情況下,事情並非如此:假如你在開發一個電商網站的代碼,把“刷一次信用卡”的回調傳給一個第三方庫,而那個庫很不巧地在某種特殊情況下把這個回調調用了5次,那麼,你的老闆可能不得不做好準備,在電話中親自安撫怒氣衝衝的顧客。而且,即使換一個第三方合作伙伴,就能保證不再出類似的問題嗎?

換句話說,我們無法100%信任接管回調的第三方(當然,那個“第三方”也可能是自己)。

另一個問題是,非同步操作本質上是無法保證完成時間的,因此,當多個非同步操作需要按先後順序依次執行、並且後面的步驟依賴於前面步驟的返回結果時,如果用回調的寫法,就只能把後一個的步驟硬編碼在前一個步驟的回調中,整個操作流程形成一個嵌一個的回調金字塔,再加上異常處理和多分支等情況,口味更加酸爽:

ajax(url, function (res){
    ajax(res.url, function(res) {
        ajax(res.url, function(res) {
            if (res.status == '1') {
                ajax(res.url, function(res) {
                ...
                }
            }
            else if (res.status == '2') {
                ajax(url2, function(res) {
                ...
            }
            ...
        }
    }
}
);

這樣的流程是極其脆弱的,而且包含大量重覆卻無法復用的代碼,體驗非常糟心。

面對越來越複雜的業務場景,簡單的回調已經越來越力不從心,更好的解決方案在哪兒呢?

二、事件訂閱模式的啟示

也許我們可以嘗試換一種模式:不是把回調的控制權交出去,而是讓非同步操作在返回時觸發一個事件,通知主線程非同步操作的結果,隨後主線程根據預先的設定執行事件相應的回調,這就是“事件訂閱模式”。在這種模式下,本來要被反轉的回調控制權又被反轉回來了,因此稱為“反控制反轉”。偽代碼如下:

on('ajax_return', function(val) {
    doSomething();
});

ajax(url, function(res) { emitEvent('ajax_return', res); });

on()是假想的用於註冊事件回調的函數,emitEvent()是假想的用於觸發事件的函數。

這種模式解決了控制反轉的問題,而且用ES5也能輕鬆實現。但是,它還沒有很好地解決非同步流程的問題——總不能為每一個非同步操作都單獨註冊一個事件吧?無論如何,事件訂閱模式給我們提供了十分有益的啟示,接下來上場的主角正是以這種模式為基礎設計的。

三、理解Promise的姿勢

Promise是一種範式,專治非同步操作的各種疑難雜症。本節不打算逐一介紹Promise的API,而是著重探求其設計思想,由此學習其正確的使用方法。

第一,Promise基於事件訂閱模式。我們知道,Promise有三種狀態:未決議、決議、拒絕。從未決議變化到決議或拒絕,就相當於觸發了一個匿名事件,使得通過then方法註冊的fulfilled或rejected回調被調用,實現了反控制反轉。

第二,Promise“只能決議一次”的特性,使得“裸回調”和不可信的thenable對象都可以包裝為可信的Promise對象。示例代碼如下:

// 例1.將ajax函數的返回結果Promise化
let p1 = new Promise((resolve, reject) => {
    ajax(url, function(res) {
        if (res.error) reject(res.error);
        resolve(res);
    });
});


// 例2.將不規範的thenable對象Promise化
let obj = {
    then: function(cb, errcb) {
        cb(1);
        cb(2);  // 不合規範的用法!
        errcb('evil laugh');
    }
};

let p2 = new Promise((resolve, reject) => {
    obj.then(resolve, reject);
});
// 或寫成如下語法糖
let p2 = Promise.resolve(obj);

例1中,傳給ajax的匿名函數不知道會被調用幾次,然而由於Promise的特性,保證了只有第一次調用會使Promise的狀態發生決議,之後的調用都被直接忽略。

例2中,obj對象有一個then方法,接受兩個函數作為參數,所以它是一個thenable對象;但是其內部的代碼卻完全不符合Promise規範——"fulfilled"被調用了兩次,"rejected"也在resolve時被調用,完全是亂來嘛!但是,只要把它包裝成p2,那就沒有問題了——resolve(1)順利執行,resolve(2)和reject('evil laugh')被直接忽略。

第三,then方法註冊的回調一定會被非同步調用,比如:

console.log('A');
Promise.resolve('B').then(console.log);
console.log('C');

執行結果是 A C B。

這是為了將現在值(同步)和未來值(非同步)歸一化,避免出現Zalgo現象(指同一個操作既可能同步返回也可能非同步返回,比如緩存命中則同步返回、未命中則非同步返回)。

再看一段代碼:

setTimeout(function(){console.log('A');}, 0);
setTimeout(function(){console.log('B');}, 0);
Promise.resolve('C').then(console.log);
Promise.resolve('D').then(console.log); console.log(
'E');

執行結果為 E C D A B。

原因在於,Promise的then回調實現非同步不是用setTimeout(.., 0),而是用一種叫做Job Queue(任務隊列)的專門機制。傳統的setTimeout(.., 0)把回調放在Event Loop的末尾,作為一個新的event老老實實排隊;而Job Queue是Event Loop中每個event後面掛著的一個隊列,往這個隊列里插入回調,可以搶在下個event之前執行,相當於“插隊”,因此Promise一旦決議,可以以最快的速度(在當前同步代碼執行完之後,立刻)調用回調,沒有別的非同步能夠搶在前面(除了另一個Promise)!

第四,then方法會返回一個新的Promise,以fulfilled回調為其resolve,以rejected回調為其reject,因此連續調用then方法可以構成一條Promise鏈。由於鏈上的Promise決議有先後順序(別忘了,每一步都是非同步的),因此可以用來控制非同步操作的順序。當然,一般情況下同步操作就不要強行非同步化了,我見過p.then(res=>res.text).then(...)這樣的代碼,除了增加程式複雜度以外好像沒什麼用處。。。

 

從以上幾點可以看出,Promise是一種非常強大的模式,對於非同步操作中可能遇到的信任問題、硬編碼流程問題等,都設計了相應的機制來加以剋服,試著正確地瞭解它、使用它,你一定能體會到它的好處,從而愛不釋手。但是,探尋更優雅的非同步操作方法的任務,還沒有結束……

 

推薦閱讀:《你不知道的JavaScript·中捲》第二部分:非同步和性能


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 1 #include 2 #include 3 #include 4 #include 5 #include 6 7 using namespace std; 8 9 class STSubject; 10 11 //觀察者 12 class STObserver 13 { 14 public: 1... ...
  • 今天大風大雨, 但心情還行, 繼續擼DOTA 狀態模式(state pattern)定義 當一個對象的內在狀態改變時允許改變其行為, 這個對象看起來像是改變了其類 這在我們開發的業務中太常見了, 角色許可權的管理, 給予不同的許可權, 不同角色的操作就會發生改變 該類圖中包含三個角色 Context: ...
  • 看到游戲, 就來勁了, 職責鏈模式詳解 職責鏈模式(chain of responsibility)的定義 為了避免請求的發送者和接受者之間的耦合關係, 使多個接收對象都用機會處理請求. 將這些對象連成一條鏈, 並沿著這條鏈傳遞請求, 直到有一個對象處理它為止 職責鏈模式解決的問題 客戶端與具體的處 ...
  • 1.children()方法:$('div').children() 遍歷查找div元素的所有子元素節點 2.next()方法:$('div').next() 查找div元素後相鄰的同級元素但非所有同級元素 [相關方法] (1)nextAll()方法:$('div').nextAll() 查找div ...
  • $ionicHistory 定義:當用戶通過導航欄切換視圖頁面的時候,ionicHistory起到跟蹤視圖的作用,類似的瀏覽器的行為方式,一個ionic應用程式能夠保持以前的視圖,當前視圖,和前視圖(如果有一個)。然而,一個典型的Web瀏覽器只跟蹤一個歷史堆棧在一個線性的方式。不同於傳統的瀏覽器環境 ...
  • <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.o ...
  • 組件中的路由 `` 無參數 與router1.x不同點 在vue router2.x中使用 帶參數 在vue devtools中可查看到所有的某個路由的所有信息: 路由的重定向 路由的重定向需要我們在路由文件中配置: 註意:從圖中看出,雖然路由發生了跳轉,但是後面跟的參數並沒有發生變化。 組件內的導 ...
  • for...in遍歷拿到的x是鍵(下標)。而for...of遍歷拿到的x是值,但在對象中會提示不是一個迭代器報錯。例子如下: let x; let a = ['A','B','C']; let b = {name: '劉德華',age: '18'}; console.log(a.length); f ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...