摘要:如果我們只有一個異步操作,用回調函數來處理是完全沒有任何問題的。事件監聽使用事件監聽的方式番禺廣州上述代碼需要實現一個事件監聽器。只處理對象廣州番禺函數將函數的自動執行器,改在語言層面提供,不暴露給用戶。
概論
由于 JavaScript 是一門單線程執行的語言,所以在我們處理耗時較長的任務時,異步編程就顯得尤為重要。
js 處理異步操作最傳統的方式是回調函數,基本上所有的異步操作都可以用回調函數來處理;
為了使代碼更優雅,人們又想到了用事件監聽、發布/訂閱模式和 Promise 等來處理異步操作;
之后在 ES2015 語言標準中終于引入了Promise,從此瀏覽器原生支持 Promise ;
此外,ES2015 中的生成器generator因其中斷/恢復執行和傳值等優秀功能也被人們用于異步處理;
之后,ES2017 語言標準又引入了更優秀的異步處理方法async/await......
為了更直觀地發現這些異步處理方式的優勢和不足,我們將分別使用不同的方式解決同一個異步問題。
問題:假設我們需要用原生 XMLHttpRequest 獲取兩個 json 數據 —— 首先異步獲取廣州的天氣,等成功后再異步獲取番禺的天氣,最后一起輸出獲取到的兩個 json 數據。
前提:假設我們已經了解了Promise,generator和async。
我們首先用最傳統的回調函數來處理:
var xhr1 = new XMLHttpRequest(); xhr1.open("GET", "https://www.apiopen.top/weatherApi?city=廣州"); xhr1.send(); xhr1.onreadystatechange = function() { if(this.readyState !== 4) return; if(this.status === 200) { data1 = JSON.parse(this.response); var xhr2 = new XMLHttpRequest(); xhr2.open("GET", "https://www.apiopen.top/weatherApi?city=番禺"); xhr2.send(); xhr2.onreadystatechange = function() { if(this.readyState !== 4) return; if(this.status === 200) { data2 = JSON.parse(this.response); console.log(data1, data2); } } } };
優點:簡單、方便、實用。
缺點:易形成回調函數地獄。如果我們只有一個異步操作,用回調函數來處理是完全沒有任何問題的。如果我們在回調函數中再嵌套一個回調函數,問題也不大。但是如果我們要嵌套很多個回調函數,問題就很大了,因為多個異步操作形成了強耦合,代碼將亂作一團,無法管理。這種情況被稱為"回調函數地獄"(callback hell)。
使用事件監聽的方式:
var events = new Events(); events.addEvent("done", function(data1) { var xhr = new XMLHttpRequest(); xhr.open("GET", "https://www.apiopen.top/weatherApi?city=番禺"); xhr.send(); xhr.onreadystatechange = function() { if(this.readyState !== 4) return; if(this.status === 200) { data1 = JSON.parse(data1); var data2 = JSON.parse(this.response); console.log(data1, data2); } } }); var xhr = new XMLHttpRequest(); xhr.open("GET", "https://www.apiopen.top/weatherApi?city=廣州"); xhr.send(); xhr.onreadystatechange = function() { if(this.readyState !== 4) return; if(this.status === 200) { events.fireEvent("done", this.response); } };
上述代碼需要實現一個事件監聽器 Events。
優點:與回調函數相比,事件監聽方式實現了代碼的解耦,將兩個回調函數分離了開來,更方便進行代碼的管理。
缺點:使用起來不方便,每次都要手動地綁定和觸發事件。
而發布/訂閱模式與其類似,就不多說了。
使用 ES6 Promise 的方式:
new Promise(function(resolve, reject) { const xhr = new XMLHttpRequest(); xhr.open("GET", "https://www.apiopen.top/weatherApi?city=廣州"); xhr.send(); xhr.onreadystatechange = function() { if(this.readyState !== 4) return; if(this.status === 200) return resolve(this.response); reject(this.statusText); }; }).then(function(value) { const xhr = new XMLHttpRequest(); xhr.open("GET", "https://www.apiopen.top/weatherApi?city=番禺"); xhr.send(); xhr.onreadystatechange = function() { if(this.readyState !== 4) return; if(this.status === 200) { const data1 = JSON.parse(value); const data2 = JSON.parse(this.response); console.log(data1, data2); } }; });
優點:使用Promise的方式,我們成功地將回調函數嵌套調用變成了鏈式調用,與前兩種方式相比邏輯更強,執行順序更清楚。
缺點:代碼冗余,異步操作都被包裹在Promise構造函數和then方法中,主體代碼不明顯,語義變得不清楚。
接下來,我們使用 generator 和回調函數來實現。
首先用一個 generator function 封裝異步操作的邏輯代碼:
function* gen() { const data1 = yield getJSON_TH("https://www.apiopen.top/weatherApi?city=廣州"); const data2 = yield getJSON_TH("https://www.apiopen.top/weatherApi?city=番禺"); console.log(data1, data2); }
看了這段代碼,是不是感覺它很直觀、很優雅。實際上,除去星號和yield關鍵字,這段代碼就變得和同步代碼一樣了。
當然,只有這個 gen 函數是沒有用的,直接執行它只會得到一個generator對象。我們需要用它返回的 generator 對象來恢復/暫停 gen 函數的執行,同時傳遞數據到 gen 函數中。
用getJSON_TH函數封裝異步操作的主體代碼:
function getJSON_TH(url) { return function(fn) { const xhr = new XMLHttpRequest(); xhr.open("GET", url); xhr.responseType = "json"; xhr.setRequestHeader("Accept", "application/json"); xhr.send(); xhr.onreadystatechange = function() { if(this.readyState !== 4) return; let err, data; if(this.status === 200) { data = this.response; } else { err = new Error(this.statusText); } fn(err, data); } } }
有的同學可能覺得直接給getJSON_TH函數傳入 url 和 fn 兩個參數不就行了嗎,為什么非要返回一個函數。其實這正是奧妙所在,getJSON_TH函數返回的函數是一個Thunk函數,它只接收一個回調函數作為參數。通過Thunk函數或者說Thunk函數的回調函數,我們可以在 gen 函數外部向其內部傳入數據,同時恢復 gen 函數的執行。在 node.js 中,我們可以通過 Thunkify 模塊將帶回調參數的函數轉化為 Thunk 函數。
接下來,我們手動執行 gen 函數:
const g = gen(); g.next().value((err, data) => { if(err) return g.throw(err); g.next(data).value((err, data) => { if(err) return g.throw(err); g.next(data); }) });
其中,g.next().value 就是 gen 函數中yield輸出的值,也就是我們之前提到的Thunk函數,我們在它的回調函數中,通過 g.next(data) 方法將 data 傳給 gen 函數中的 data1,并且恢復 gen 函數的執行(將 gen 函數的執行上下文再次壓入調用棧中)。
方便起見,我們還可以將自動執行 gen 函數的操作封裝起來:
function run(gen) { const g = gen(); function next(err, data) { if(err) return g.throw(err); const res = g.next(data); if(res.done) return; res.value(next); } next(); } run(gen);
優點:generator 方式使得異步操作很接近同步操作,十分的簡潔明了。另外,gen 執行 yield 語句時,只是將執行上下文暫時彈出,并不會銷毀,這使得上下文狀態被保存。
缺點:流程管理不方便,需要一個執行器來執行 generator 函數。
除了Thunk函數,我們還可以借助Promise對象來執行 generator 函數。
同樣優雅的邏輯代碼:
function* gen() { const data1 = yield getJSON_PM("https://www.apiopen.top/weatherApi?city=廣州"); const data2 = yield getJSON_PM("https://www.apiopen.top/weatherApi?city=番禺"); console.log(data1, data2); }
getJSON_PM函數返回一個 Promise 對象:
function getJSON_PM(url) { return new Promise((resolve, rejext) => { const xhr = new XMLHttpRequest(); xhr.open("GET", url); xhr.responseType = "json"; xhr.setRequestHeader("Accept", "application/json"); xhr.send(); xhr.onreadystatechange = function() { if(this.readyState !== 4) return; if(this.status === 200) return resolve(this.response); reject(new Error(this.statusText)); }; }); }
手動執行 generator 函數:
const g = gen(); g.next().value.then(data => { g.next(data).value.then(data => g.next(data), err => g.throw(err)); }, err => g.throw(err));
自動執行 generator 函數:
function run(gen) { const g = gen(); function next(data) { const res = g.next(data); if(res.done) return; res.value.then(next); } next(); } run(gen);generator + co 模塊
node.js 中的co模塊是一個用來自動執行generator函數的模塊,它的入口是一個co(gen)函數,它預期接收一個 generator 對象或者 generator 函數作為參數,返回一個Promise對象。
在參數 gen 函數中,yield語句預期接收一個 generator 對象,generator 函數,thunk 函數,Promise 對象,數組或者對象。co模塊的主要實現原理是將 yield 接收的值統一轉換成一個Promise對象,然后用類似上述 generator + Promise 的方法來自動執行 generator 函數。
下面是我根據 node.js co 模塊源碼修改的 es6 co 模塊,讓它更適合自己使用:
https://github.com/lyl123321/...
yield接收thunk函數:
import co from "./co.mjs" function* gen() { const data1 = yield getJSON_TH("https://www.apiopen.top/weatherApi?city=廣州"); const data2 = yield getJSON_TH("https://www.apiopen.top/weatherApi?city=番禺"); console.log(data1, data2); } co(gen);
yield接收Promise對象:
function* gen() { const data1 = yield getJSON_PM("https://www.apiopen.top/weatherApi?city=廣州"); const data2 = yield getJSON_PM("https://www.apiopen.top/weatherApi?city=番禺"); console.log(data1, data2); } co(gen);async/await
async函數是generator函數的語法糖,它相對于一個自帶執行器(如 co 模塊)的generator函數。
async函數中的await關鍵字預期接收一個Promise對象,如果不是 Promise 對象則返回原值,這使得它的適用性比 co 執行器更廣。
async函數返回一個Promise對象,這點與 co 執行器一樣,這使得async函數比返回generator對象的generator函數更實用。如果 async 函數順利執行完,則返回的 Promise 對象狀態變為 fulfilled,且 value 值為 async 函數中 return 關鍵字的返回值;如果 async 函數執行時遇到錯誤且沒有在 async 內部捕獲錯誤,則返回的 Promise 對象狀態變為 rejected,且 reason 值為 async 函數中的錯誤。
await只處理Promise對象:
async function azc() { const data1 = await getJSON_PM("https://www.apiopen.top/weatherApi?city=廣州"); const data2 = await getJSON_PM("https://www.apiopen.top/weatherApi?city=番禺"); console.log(data1, data2); } azc();
async函數將generator函數的自動執行器,改在語言層面提供,不暴露給用戶。
async function fn(args) { // ... }
相當于:
function fn(args) { return exec(function* () { // ... }); }
優點:最簡潔,最符合語義,最接近同步代碼,最適合處理多個 Promise 異步操作。相比 generator 方式,async 方式省掉了自動執行器,減少了代碼量。
缺點:js 語言自帶的 async 執行器功能性可能沒有 co 模塊等執行器強。你可以根據自己的需求定義自己的 generator 函數執行器。
參考鏈接:
http://es6.ruanyifeng.com/#do...
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/103044.html
摘要:雖然這個模式運行效果很不錯,但是如果嵌套了太多的回調函數,就會陷入回調地獄。當需要跟蹤多個回調函數的時候,回調函數的局限性就體現出來了,非常好的改進了這些情況。 JavaScript引擎是基于單線程 (Single-threaded) 事件循環的概念構建的,同一時刻只允許一個代碼塊在執行,所以需要跟蹤即將運行的代碼,那些代碼被放在一個任務隊列 (job queue) 中,每當一段代碼準...
摘要:臆想的針對讀取到的內容進行操作,比如打印文件內容臆想中,讀取文件是有返回值的,將返回值,即文件內容,賦給一個變量,然后決定對讀取到的內容進行相應的操作,例如打印文件中的內容。 臆想的 let fs = require(fs) function readFile(filename){ ... } let content = readFile(config.js) // 針對讀...
摘要:本文給大家介紹的是相比于其他框架更靈活的配置方式,大家可以根據自己的項目需要選擇合適的方式。標簽的方式下面我們看一個例子當為時渲染我們可以看到這種路由配置方式使用標簽,然后根據找到對應的映射。 路由的概念 路由的作用就是將url和函數進行映射,在單頁面應用中路由是必不可少的部分,路由配置就是一組指令,用來告訴router如何匹配url,以及對應的函數映射,即執行對應的代碼。 react...
摘要:事件中屬性等于。響應的狀態為或者。同步在上會產生頁面假死的問題。表示聲明的變量未初始化,轉換為數值時為。但并非所有瀏覽器都支持事件捕獲。它由兩部分構成函數,以及創建該函數的環境。 1 介紹JavaScript的基本數據類型Number、String 、Boolean 、Null、Undefined Object 是 JavaScript 中所有對象的父對象數據封裝類對象:Object、...
摘要:參與任何數值計算的結構都是,而且。。面向人類的理性事物,而不是機器信號。達到無刷新效果。的工作原理總是指向一個對象,具體是運行時基于函數的執行環境動態綁定的,而非函數被聲明時的環境。原型對象上有一個屬性,該屬性指向的就是構造函數。 1.JS面向對象的理解 面向對象的三大特點:繼承、封裝、多態 1、JS中通過prototype實現原型繼承 2、JS對象可以通過對象冒充,實現多重繼承, 3...
閱讀 3069·2023-04-25 16:50
閱讀 903·2021-11-25 09:43
閱讀 3512·2021-09-26 10:11
閱讀 2517·2019-08-26 13:28
閱讀 2530·2019-08-26 13:23
閱讀 2418·2019-08-26 11:53
閱讀 3566·2019-08-23 18:19
閱讀 2987·2019-08-23 16:27