摘要:接下來我們看下三類異步編程的實現。事件監聽事件發布訂閱事件監聽是一種非常常見的異步編程模式,它是一種典型的邏輯分離方式,對代碼解耦很有用處。
一、 一道面試題
前段時間面試,考察比較多的是js異步編程方面的相關知識點,如今,正好輪到自己分享技術,所以想把js異步編程學習下,做個總結。
下面這個demo 概括了大多數面試過程中遇到的問題:
for(var i = 0; i < 3; i++) { setTimeout(function() { console.log("timeout" + i); }) } new Promise(function(resolve) { console.log("promise1"); for(var i = 0; i < 1000; i++) { i == 99 && resolve(); } console.log("promise2"); }).then(function() { console.log("then1"); }) console.log("global1");
通過驗證可以得知這個demo的結果為:
可是為什么會是這樣的結果,我們可能需要先了解下下面兩個知識點
二、 二個前提知識點 2.1 瀏覽器內核的多線程瀏覽器的內核是多線程的,他們在內核的控制下互相配合以保持同步,一個瀏覽器至少實現三個常駐的線程:javascript引擎線程,GUI渲染線程,瀏覽器事件觸發線程。
1)js引擎,基于事件驅動單線程執行的,js引擎一直等待著任務隊列中任務的到來,然后加以處理,瀏覽器無論什么時候都只有一個JS線程在運行JS程序。
2)GUI線程,當界面需要重繪或由于某種操作引發回流時,該線程就會執行。它和JS引擎是互斥的。
3)瀏覽器事件觸發線程,當一個事件被觸發時,該線程會把事件添加到待處理隊列的隊尾,等待js引擎的處理,這些事件可來自JavaScript引擎當前執行的代碼塊如,setTimeOut, 也可以來自瀏覽器內核的其他線程如鼠標點擊,AJAX異步請求等,但由于JS的單線程關系,所有這些事件都得排隊等待JS引擎處理。
1)任務隊列又分為macro-task(宏任務)與micro-task(微任務),
在最新標準中,它們被分別稱為task與jobs。
2)macro-task大概包括:script(整體代碼), setTimeout, setInterval, setImmediate, I/O, UI rendering。
3)micro-task【先執行】大概包括: process.nextTick, Promise, Object.observe(已廢棄), MutationObserver(html5新特性)
setTimeout/Promise等我們稱之為任務源。而進入任務隊列的是他們指定的具體執行任務。
事件循環的順序,決定了JavaScript代碼的執行順序。它從script(整體代碼)開始第一次循環。之后全局上下文進入函數調用棧。直到調用棧清空(只剩全局),然后執行所有的micro-task。當所有可執行的micro-task執行完畢之后。循環再次從macro-task開始,找到其中一個任務隊列執行完畢,然后再執行所有的macro-task,這樣一直循環下去。
通過這個事件循環的順序,我們就知道,為什么上面提到的面試題為什么是這樣的輸出結果了。
接下來我們看下三類異步編程的實現。
demo1:
// 一個簡單的封裝 function want() { console.log("這是你想要執行的代碼"); } function fn(want) { console.log("這里表示執行了一大堆各種代碼"); // 其他代碼執行完畢,最后執行回調函數 want && want(); } fn(want);
demo2:
//callback hell doSomethingAsync1(function(){ doSomethingAsync2(function(){ doSomethingAsync3(function(){ doSomethingAsync4(function(){ doSomethingAsync5(function(){ // code... }); }); }); }); });
可以發現一個問題,在回調函數嵌套層數不深的情況下,代碼還算容易理解和維護,一旦嵌套層數加深,就會出現“回調金字塔”的問題,就像demo2那樣,如果這里面的每個回調函數中又包含了很多業務邏輯的話,整個代碼塊就會變得非常復雜。從邏輯正確性的角度來說,上面這幾種回調函數的寫法沒有任何問題,但是隨著業務邏輯的增加和趨于復雜,這種寫法的缺點馬上就會暴露出來,想要維護它們實在是太痛苦了,這就是“回調地獄(callback hell)”。
回調函數還有一個問題就是我們在回調函數之外無法捕獲到回調函數中的異常,一般我們用try catch來捕捉異常,我們嘗試下捕捉回調中的異常
可以看到,不能捕捉到callback中的異常。
事件監聽是一種非常常見的異步編程模式,它是一種典型的邏輯分離方式,對代碼解耦很有用處。通常情況下,我們需要考慮哪些部分是不變的,哪些是容易變化的,把不變的部分封裝在組件內部,供外部調用,需要自定義的部分暴露在外部處理。從某種意義上說,事件的設計就是組件的接口設計。
1)jQuery事件監聽
$("#btn").on("myEvent", function(e) { console.log("There is my Event"); }); $("#btn").trigger("myEvent");
2)發布/訂閱模式
var PubSub = function(){ this.handlers = {}; }; PubSub.prototype.subscribe = function(eventType, handler) { if (!(eventType in this.handlers)) { this.handlers[eventType] = []; } this.handlers[eventType].push(handler); //添加事件監聽器 return this;//返回上下文環境以實現鏈式調用 }; PubSub.prototype.publish = function(eventType) { var _args = Array.prototype.slice.call(arguments, 1); for (var i = 0, _handlers = this.handlers[eventType]; i < _handlers.length; i++) { _handlers[i].apply(this, _args);//遍歷事件監聽器 } return this; }; var event = new PubSub;//構造PubSub實例 event.subscribe("list", function(msg) { console.log(msg); }); event.publish("list", {data: ["one,", "two"]}); //Object {data: Array[2]}
這種模式實現的異步編程,本質上還是通過回調函數實現的,所以3.1中提到的回調嵌套和無法捕捉異常的問題還是存在的,接下來我們看ES6提供的Promise對象,是否解決這兩個問題。
3.3 Promise對象ES 6中原生提供了Promise對象,Promise對象代表了某個未來才會知道結果的事件(一般是一個異步操作),并且這個事件對外提供了統一的API,可供進一步處理。
使用Promise對象可以用同步操作的流程寫法來表達異步操作,避免了層層嵌套的異步回調,代碼也更加清晰易懂,方便維護,也可以捕捉異常。
一個簡單例子:
function fn(num) { return new Promise(function(resolve, reject) { if (typeof num == "number") { resolve(); } else { reject(); } }) .then(function() { console.log("參數是一個number值"); }) .then(null, function() { console.log("參數不是一個number值"); }) } fn("haha"); fn(1234);
為什么Promise 可以這樣實現異步編程,在這我們簡單分析下Promise實現過程:
1)極簡Promise雛形
// 極簡promise雛形 function Promise(fn) { var value = null, callbacks = []; //callbacks為數組,因為可能同時有很多個回調 this.then = function (onFulfilled) { callbacks.push(onFulfilled); }; function resolve(value) { callbacks.forEach(function (callback) { callback(value); }); } fn(resolve); }
如果promise內部的函數是同步函數,我們要加入一些處理,保證在resolve執行之前,then方法已經注冊完所有的回調;
通過setTimeout機制,將resolve中執行回調的邏輯放置到JS任務隊列末尾,以保證在resolve執行時,then方法的回調函數已經注冊完成.
2)加入延時處理
// 極簡promise雛形,加入延時處理 function Promise(fn) { var value = null, callbacks = []; //callbacks為數組,因為可能同時有很多個回調 this.then = function (onFulfilled) { callbacks.push(onFulfilled); }; function resolve(value) { setTimeout(function() { callbacks.forEach(function (callback) { callback(value); }); }, 0) } fn(resolve); }
如果Promise異步操作已經成功,這時,在異步操作成功之前注冊的回調都會執行,但是在Promise異步操作成功這之后調用的then注冊的回調就再也不會執行了,這顯然不是我們想要的
3)加入狀態判斷
// 極簡promise雛形,加狀態判斷 function Promise(fn) { var state = "pending", value = null, callbacks = []; this.then = function (onFulfilled) { if (state === "pending") { callbacks.push(onFulfilled); return this; } onFulfilled(value); return this; }; function resolve(newValue) { value = newValue; state = "fulfilled"; setTimeout(function () { callbacks.forEach(function (callback) { callback(value); }); }, 0); } fn(resolve); }
4)鏈式promise
// 極簡promise雛形,鏈式promise function Promise(fn) { var state = "pending", value = null, callbacks = []; this.then = function (onFulfilled) { return new Promise(function (resolve) { handle({ onFulfilled: onFulfilled || null, resolve: resolve }); }); }; function handle(callback) { if (state === "pending") { callbacks.push(callback); return; } //如果then中沒有傳遞任何東西 if(!callback.onResolved) { callback.resolve(value); return; } var ret = callback.onFulfilled(value); callback.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); return; } } state = "fulfilled"; value = newValue; setTimeout(function () { callbacks.forEach(function (callback) { handle(callback); }); }, 0); } fn(resolve); }四、四個擴展點 4.1 Promise常用的應用場景:ajax
利用Promise的知識,對ajax進行一個簡單的封裝。看看會是什么樣子:
//demo3 promise封裝ajax var url = "https://hq.tigerbrokers.com/fundamental/finance_calendar/getType/2017-02-26/2017-06-10"; function getJSON(url) { return new Promise(function(resolve, reject) { var XHR = new XMLHttpRequest(); XHR.open("GET", url, true); XHR.send(); XHR.onreadystatechange = function() { if (XHR.readyState == 4) { if (XHR.status == 200) { try { var response = JSON.parse(XHR.responseText); resolve(response); } catch (e) { reject(e); } } else { reject(new Error(XHR.statusText)); } } } }) } getJSON(url).then(resp => console.log(resp));
除了串行執行若干異步任務外,Promise還可以并行執行異步任務。
當有一個ajax請求,它的參數需要另外2個甚至更多請求都有返回結果之后才能確定,那么這個時候,就需要用到Promise.all來幫助我們應對這個場景。
4.2 Promise.allPromise.all接收一個Promise對象組成的數組作為參數,當這個數組所有的Promise對象狀態都變成resolved或者rejected的時候,它才會去調用then方法。
// demo4 promise.all var url = "https://hq.tigerbrokers.com/fundamental/finance_calendar/getType/2017-02-26/2017-06-10"; var url1 = "https://hq.tigerbrokers.com/fundamental/finance_calendar/getType/2017-03-26/2017-06-10"; function renderAll() { return Promise.all([getJSON(url), getJSON(url1)]); } renderAll().then(function(value) { console.log(value); //將得到一個數組,里面是兩個接口返回的值 })
結果:
有些時候,多個異步任務是為了容錯。比如,同時向兩個URL讀取用戶的個人信息,只需要獲得先返回的結果即可。這種情況下,用Promise.race()實現。
4.3 Promise.race與Promise.all相似的是,Promise.race都是以一個Promise對象組成的數組作為參數,不同的是,只要當數組中的其中一個Promsie狀態變成resolved或者rejected時,就可以調用.then方法了
// demo5 promise.race function renderRace() { return Promise.race([getJSON(url), getJSON(url1)]); } renderRace().then(function(value) { console.log(value); })
這里then()傳的value值將是接口返回比較快的接口數據,另外一個接口仍在繼續執行,但執行結果將被丟棄。
結果:
4.4 Generator 函數Generator函數是協程在ES 6中的實現,最大特點就是可以交出函數的執行權(暫停執行)。
注意:在node中需要開啟--harmony選項來啟用Generator函數。
整個Generator函數就是一個封裝的異步任務,或者說是異步任務的容器。異步操作需要暫停的地方,都用yield語句注明。
看個簡單的例子:
function* gen(x){ var y = yield x + 2; return y; } var g = gen(1); var r1 = g.next(); // { value: 3, done: false } console.log(r1); var r2 = g.next() // { value: undefined, done: true } console.log(r2);
需要注意的是Generator函數的函數名前面有一個"*"。
上述代碼中,調用Generator函數,會返回一個內部指針(即遍歷器)g,這是Generator函數和一般函數不同的地方,調用它不會返回結果,而是一個指針對象。調用指針g的next方法,會移動內部指針,指向第一個遇到的yield語句,上例就是執行到x+2為止。
換言之,next方法的作用是分階段執行Generator函數。每次調用next方法,會返回一個對象,表示當前階段的信息(value屬性和done屬性)。value屬性是yield語句后面表達式的值,表示當前階段的值;done屬性是一個布爾值,表示Generator函數是否執行完畢,即是否還有下一個階段。
對Generator函數,只有一個感性認知,沒有實踐過,所以就先介紹到這了,后面還有ES7新的知識點async await,看了下網上的資料,理解得還不夠,希望后面自己接觸得更多再來這里補上,未完待續...
參考資料:
1) http://www.jianshu.com/p/12b9...
2) http://www.jianshu.com/p/fe5f...
3) https://mengera88.github.io/2...
4) http://www.cnblogs.com/nullcc...
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/84905.html
摘要:為此決定自研一個富文本編輯器。例如當要轉化的對象有環存在時子節點屬性賦值了父節點的引用,為了關于函數式編程的思考作者李英杰,美團金融前端團隊成員。只有正確使用作用域,才能使用優秀的設計模式,幫助你規避副作用。 JavaScript 專題之惰性函數 JavaScript 專題系列第十五篇,講解惰性函數 需求 我們現在需要寫一個 foo 函數,這個函數返回首次調用時的 Date 對象,注意...
摘要:從最開始的到封裝后的都在試圖解決異步編程過程中的問題。為了讓編程更美好,我們就需要引入來降低異步編程的復雜性。寫一個符合規范并可配合使用的寫一個符合規范并可配合使用的理解的工作原理采用回調函數來處理異步編程。 JavaScript怎么使用循環代替(異步)遞歸 問題描述 在開發過程中,遇到一個需求:在系統初始化時通過http獲取一個第三方服務器端的列表,第三方服務器提供了一個接口,可通過...
閱讀 860·2021-11-25 09:44
閱讀 1063·2021-11-19 09:40
閱讀 7062·2021-09-07 10:23
閱讀 1975·2019-08-28 17:51
閱讀 1106·2019-08-26 10:59
閱讀 1928·2019-08-26 10:25
閱讀 3131·2019-08-23 18:22
閱讀 865·2019-08-23 16:58