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