摘要:學習開發,無論是前端開發還是都避免不了要接觸異步編程這個問題就和其它大多數以多線程同步為主的編程語言不同的主要設計是單線程異步模型。由于異步編程可以實現非阻塞的調用效果,引入異步編程自然就是順理成章的事情了。
學習js開發,無論是前端開發還是node.js,都避免不了要接觸異步編程這個問題,就和其它大多數以多線程同步為主的編程語言不同,js的主要設計是單線程異步模型。正因為js天生的與眾不同,才使得它擁有一種獨特的魅力,也給學習者帶來了很多探索的道路。本文就從js的最初設計開始,整理一下js異步編程的發展歷程。
什么是異步在研究js異步之前,先弄清楚異步是什么。異步是和同步相對的概念,同步,指的是一個調用發起后要等待結果返回,返回時候必須拿到返回結果。而異步的調用,發起之后直接返回,返回的時候還沒有結果,也不用等待結果,而調用結果是產生結果后通過被調用者通知調用者來傳遞的。
舉個例子,A想找C,但是不知道C的電話號碼,但是他有B的電話號碼,于是A給B打電話詢問C的電話號碼,B需要查找才能知道C的電話號碼,之后會出現兩種場景看下面兩個場景:
A不掛電話,等到B找到號碼之后直接告訴A
A掛電話,B找到后再給A打電話告訴A
能感受到這兩種情況是不同的吧,前一種就是同步,后一種就是異步。
為什么是異步的先來看js的誕生,JavaScript誕生于1995年,由Brendan Eich設計,最早是在Netscape公司的瀏覽器上實現,用來實現在瀏覽器中處理簡單的表單驗證等用戶交互。至于后來提交到ECMA,形成規范,種種歷史不是這篇文章的重點,提到這些就是想說一點,js的最初設計就是為了瀏覽器的GUI交互。對于圖形化界面處理,引入多線程勢必會帶來各種各樣的同步問題,因此瀏覽器中的js被設計成單線程,還是很容易理解的。但是單線程有一個問題:一旦這個唯一的線程被阻塞就沒辦法工作了--這肯定是不行的。由于異步編程可以實現“非阻塞”的調用效果,引入異步編程自然就是順理成章的事情了。
現在,js的運行環境不限于瀏覽器,還有node.js,node.js設計的最初想法就是設計一個完全由事件驅動,非阻塞式IO實現的服務器運行環境,因為網絡IO請求是一個非常大的性能瓶頸,前期使用其他編程語言都失敗了,就是因為人們固有的同步編程思想,人們更傾向于使用同步設計的API。而js由于最初設計就是全異步的,人們不會有很多不適應,加上V8高性能引擎的出現,才造就了node.js技術的產生。node.js擅長處理IO密集型業務,就得益于事件驅動,非阻塞IO的設計,而這一切都與異步編程密不可分。
js異步原理這是一張簡化的瀏覽器js執行流程圖,nodejs和它不太一樣,但是都有一個隊列
這個隊列就是異步隊列,它是處理異步事件的核心,整個js調用時候,同步任務和其他編程語言一樣,在棧中調用,一旦遇上異步任務,不立刻執行,直接把它放到異步隊列里面,這樣就形成了兩種不同的任務。由于主線程中沒有阻塞,很快就完成,棧中任務邊空之后,就會有一個事件循環,把隊列里面的任務一個一個取出來執行。只要主線程空閑,異步隊列有任務,事件循環就會從隊列中取出任務執行。
說的比較簡單,js執行引擎設計比這復雜的多得多,但是在js的異步實現原理中,事件循環和異步隊列是核心的內容。
異步編程實現異步編程的代碼實現,隨著時間的推移也在逐漸完善,不止是在js中,許多編程語言的使用者都在尋找一種優雅的異步編程代碼書寫方式,下面來看js中的曾出現的幾種重要的實現方式。
最經典的異步編程方式--callback提起異步編程,不能不提的就是回調(callback)的方式了,回調方式是最傳統的異步編程解決方案。首先要知道回調能解決異步問題,但是不代表使用回調就是異步任務了。下面以最常見的網絡請求為例來演示callback是如何處理異步任務的,首先來看一個錯誤的例子:
function getData(url) { const data = $.get(url); return data; } const data = getData("/api/data"); // 錯誤,data為undefined
由于函數getData內部需要執行網絡請求,無法預知結果的返回時機,直接通過同步的方式返回結果是行不通的,正確的寫法是像下面這樣:
function getData(url, callback) { $.get(url, data => { if (data.status === 200) { callback(null, data); } else { callback(data); } }); } getData("/api/data", (err, data) => { if (err) { console.log(err); } else { console.log(data); } });
callback方式利用了函數式編程的特點,把要執行的函數作為參數傳入,由被調用者控制執行時機,確保能夠拿到正確的結果。這種方式初看可能會有點難懂,但是熟悉函數式編程其實很簡單,很好地解決了最基本的異步問題,早期異步編程只能通過這種方式。
然而這種方式會有一個致命的問題,在實際開發中,模型總不會這樣簡單,下面的場景是常有的事:
fun1(data => { // ... fun2(data, result => { // ... fun3(result, () => { // ... }); }); });
整個隨著系統越來越復雜,整個回調函數的層次會逐漸加深,里面再加上復雜的邏輯,代碼編寫維護都將變得十分困難,可讀性幾乎沒有。這被稱為毀掉地獄,一度困擾著開發者,甚至是曾經異步編程最為人詬病的地方。
從地獄中走出來--promise使用回調函數來編程很簡單,但是回調地獄實在是太可怕了,嵌套層級足夠深之后絕對是維護的噩夢,而promise的出現就是解決這一問題的。promise是按照規范實現的一個對象,ES6提供了原生的實現,早期的三方實現也有很多。在此不會去討論promise規范和實現原理,重點來看promise是如何解決異步編程的問題的。
Promise對象代表一個未完成、但預計將來會完成的操作,有三種狀態:
pending:初始值,不是fulfilled,也不是rejected
resolved(也叫fulfilled):代表操作成功
rejected:代表操作失敗
整個promise的狀態只支持兩種轉換:從pending轉變為resolved,或從pending轉變為rejected,一旦轉化發生就會保持這種狀態,不可以再發生變化,狀態發生變化后會觸發then方法。這里比較抽象,我們直接來改造上面的例子:
function getData(url) { return new Promise((resolve, reject) =>{ $.get(url, data => { if (data.status === 200) { reject(data); } else { resolve(data); } }); }); } getData("/api/data").then(data => { console.log(data); }).catch(err => { console.log(err); });
Promise是一個構造函數,它創建一個promise對象,接收一個回調函數作為參數,而回調函數又接收兩個函數做參數,分別代表promise的兩種狀態轉化。resolve回調會使promise由pending轉變為resolved,而reject 回調會使promise由pending轉變為rejected。
當promise變為resolved時候,then方法就會被觸發,在里面可以獲取到resolve的內容,then方法。而一旦promise變為rejected,就會產生一個error。無論是resolve還是reject,都會返回一個新的Promise實例,返回值將作為參數傳入這個新Promise的resolve函數,這樣就可以實現鏈式調用,對于錯誤的處理,系統提供了catch方法,錯誤會一直向后傳遞,總是能被下一個catch捕獲。用promise可以有效地避免回調嵌套的問題,代碼會變成下面的樣子:
fun1().then(data => { // ... return fun2(data); }).then(result => { // ... return fun3(result); }).then(() => { // ... });
整個調用過程變的很清晰,可維護性可擴展性都會大大增強,promise是一種非常重要的異步編程方式,它改變了以往的思維方式,也是后面新方式產生的重要基礎。
轉換思維--generatorpromise的寫法是最好的嗎,鏈式調用相比回調函數而言卻是可維護性增加了不少,但是和同步編程相比,異步看起來不是那么和諧,而generator的出現帶來了另一種思路。
generator是ES對協程的實現,協程指的是函數并不是整個執行下去的,一個函數執行到一半可以移交執行權,等到可以的時候再獲得執行權,這種方式最大的特點就是同步的思維,除了控制執行的yield命令之外,整體看起來和同步編程感覺幾乎一樣,下面來看一下這種方式的寫法:
function getDataPromise(url) { return new Promise((resolve, reject) =>{ $.get(url, data => { if (data.status === 200) { reject(data); } else { resolve(data); } }); }); } function *getDataGen(url) { yield getDataPromise(url); } const g = getDataGen("/api/data"); g.next();
generator與普通函數的區別就是前面多一個*,不過這不是重點,重點是generator里面可以使用yield關鍵字來表示暫停,它接收一個promise對象,返回promise的結果并且停在此處等待,不是一次性執行完。generator執行后會返回一個iterator,iterator里面有一個next方法,每次調用next方法,generator都會向下執行,直到遇上yield,返回結果是一個對象,里面有一個value屬性,值為當前yield返回結果,done屬性代表整個generator是否執行完畢。generator的出現使得像同步一樣編寫異步代碼成為可能,下面是使用generator改造后的結果:
* fun() { const data = yield fun1(); // ... const result = yield fun2(data); // ... yield fun3(result); // ... } const g = fun(); g.next(); g.next(); g.next(); g.next();
在generator的編寫過程中,我們還需要手動控制執行過程,而實際上這是可以自動實現的,接下來的一種新語法的產生使得異步編程真的和同步一樣容易了。
新時代的寫法--async,await異步編程的最高境界,就是根本不用關心它是不是異步。在最新的ES中,終于有了這種激動人心的語法了。async函數的寫法和generator幾乎相同,把*換成async關鍵字,把yield換成await即可。async函數內部自帶generator執行器,我們不再需要手動控制執行了,現在來看最終的寫法:
function getDataPromise(url) { return new Promise((resolve, reject) =>{ $.get(url, data => { if (data.status === 200) { reject(data); } else { resolve(data); } }); }); } async function getData(url) { return await getDataPromise(url); } const data = await getData(url);
除了多了關鍵字,剩下的和同步的編碼方式完全相同,對于異常捕獲也可以采取同步的try-catch方式,對于再復雜的場景也不會邏輯混亂了:
* fun() { const data = await fun1(); // ... const result = await fun2(data); // ... return await fun3(result); // ... } fun()
現在回去看回調函數的寫法,感覺好像換了一個世界。這種語法比較新,在不支持的環境要使用babel轉譯。
寫在最后在js中,異步編程是一個長久的話題,很慶幸現在有這么好用的async和await,不過promise原理,回調函數都是要懂的,很重要的內容,弄清楚異步編程模式,算是掃清了學習js尤其是node.js路上最大的障礙了。
尊重原創,轉載分享前請先知悉作者,也歡迎指出錯誤不足共同交流,更多內容歡迎關注作者博客點擊這里
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/92607.html
摘要:是與服務器交換數據并更新部分網頁的藝術,在不重新加載整個頁面的情況下。對象是的核心,所有現代瀏覽器均支持對象和使用。用于在后臺與服務器交換數據。及時有效地幫助學員解決疑難問題,提高學員的學習積極性。 Asynchronous JavaScript and XML(異步的 JavaScript 和 XML)。 AJAX...
摘要:基于以太坊項目,以太坊團隊目前運營了一個公開的區塊鏈平臺以太坊網絡。主要特點以太坊區塊鏈底層也是一個類似比特幣網絡的網絡平臺,智能合約運行在網絡中的以太坊虛擬機里。以太坊采用交易作為執行操作的最小單位。 以太坊將比特幣針對數字交易的功能進一步進行了拓展,面向更為復雜和靈活的應用場景,支持了智能合約這一重要特性。 以太坊項目簡介 以太坊:項目最初的目標是打造以個智能合約的平臺,該平臺支持...
摘要:本次主要分享關于上一篇區域的學習。區域為的核心部分,它的結構如下為了便于梳理思路,以上代碼省略了細節,只保留了輪廓脈絡。最終暴露給開發者的如下圖所示這里只分析了區域的結構,下一次會深入到函數語句粒度。 本次主要分享關于上一篇區域2的學習。區域2為Zepto的核心部分,它的結構如下 var Zepto = (function() { var $, zepto = {}; fu...
摘要:確認需要移除的長度,最小為,確認原數組變化后的新長度按照索引位置,獲取長度的新數組,用于返回對原數組進行替換新元素。 array.splice(start,deleteCount,item1,item2....) splice方法從array移除n個元素(大于或等于0),并且可以用新的item替換被移除的元素。參數start是從數組array中移除元素的最開始位置(數組的索引,正負數表...
閱讀 2810·2019-08-30 15:55
閱讀 2857·2019-08-30 15:53
閱讀 2294·2019-08-26 13:47
閱讀 2557·2019-08-26 13:43
閱讀 3154·2019-08-26 13:33
閱讀 2802·2019-08-26 11:53
閱讀 1796·2019-08-23 18:35
閱讀 798·2019-08-23 17:16