咱們通常喜歡把異步和同步、並行拿出來比較,我之前的理解老是很模糊,老是生硬地記着「同步就是排隊執行,異步就是一塊兒執行」,如今一看,當初簡直就是傻,因此咱們第一步先把這三個概念搞清楚,我不太喜歡看網上有些博客裏很含糊地說「xxxx是同步,xxxx是異步」,還有舉什麼通俗的例子,其實對不懂的人來講仍是懵逼。javascript
首先咱們要知道這一切的根源都是「Javascript是單線程」,也就是一次只能作一件事,那麼爲何是單線程呢?由於js渲染在瀏覽器上,包含了許多與用戶的交互,若是是多線程,那麼試想一個場景:一個線程在某個DOM上添加內容,而另外一個線程刪除這個DOM,那麼瀏覽器要如何反應呢?這就亂套了。html
單線程下全部的任務都是須要排隊的,而這些任務分爲兩種:同步任務和異步任務,同步任務就是在主線程上排隊執行的任務,只有前一個任務執行完畢,才能執行後一個任務;異步任務指的是,不進入主線程
、而進入任務隊列
(task queue)的任務,只有任務隊列
通知主線程
,某個異步任務能夠執行了,該任務纔會進入主線程
執行。因此說同步執行其實也是一種只有主線程的異步執行。這裏有一個視頻關於異步操做是如何被執行的,講得很是好《what the hack is event loop》,我給你們畫個圖再來理解一下。java
這裏補充說明下不一樣的異步操做添加到任務隊列的時機不一樣,如 onclick, setTimeout, ajax 處理的方式都不一樣,這些異步操做是由瀏覽器內核的 webcore 來執行的,webcore 包含上面提到的3種 webAPI,分別是 DOM Binding、timer、network模塊。
onclick 由瀏覽器內核的 DOM Binding 模塊來處理,當事件觸發的時候,回調函數會當即添加到任務隊列中。
setTimeout 會由瀏覽器內核的 timer 模塊來進行延時處理,當時間到達的時候,纔會將回調函數添加到任務隊列中。
ajax 則會由瀏覽器內核的 network 模塊來處理,在網絡請求完成返回以後,纔將回調添加到任務隊列中。
最後再來講下並行,並行是關於可以同時發生的事情,是一種多線程的運行機制,而無論同步異步都是單線程的。git
這個很好理解,同步下前一個事件執行完了才能執行後一個事件,那麼要是遇到Ajax請求這種耗時很長的,那頁面在這段時間就無法操做了,卡在那兒,更有甚者,萬一這個請求因爲某種緣由一直沒有完成,那頁面就block了,很不友好。github
咱們能夠經過回調函數
、Promise
、生成器
、Async/Await
等來實現異步。
今天咱們先說最基礎的回調函數處理方法來實現,列舉幾個你們熟悉的使用場景,好比:ajax請求、IO操做、定時器。web
ajax(url, function(){ //這就是回調函數 }); setTimeOut(function(){ //回調函數 }, 1000)
回調自己是比較好用的,可是隨着Javascript愈來愈成熟,對於異步編程領域的發展,回調已經不夠用了,體如今如下幾點:ajax
一、大腦處理程序是順序的,對於複雜的回調函數會不易理解,咱們須要一種更同步、更順序的方式來表達異步。
舉例說明:編程
//回調函數實現兩數相加 function add(getX, getY, cb){ var x, y; getX(function(xVal){ x=xVal; if(y!=undefined){ cb(x+y); } }); getY(function(){ y=yVal; if(x!=undefined){ cb(x+y); } }); } add(fetchX, fetchY, function(sum){ console.log(sum); }) //Promise實現兩數相加 function add(xPromise, yPromise){ return Promise.all([xPromise, yPromise]) .then(function(values){ return value[0] + value[1]; }); } //fetchX()、fetchY()返回相應值的Promise add(fetchX(), fetchY()) .then(function(sum){ console.log(sum); })
只看結構是否是Promise的寫法更順序話一些。
二、回調通常會把控制權交給第三方,從而帶來信任問題,好比:瀏覽器
而Promise的特性就有效地解決了這些問題,它是如何解決的呢?網絡
這種顧慮主要是代碼是否會引入類Zalgo效應,也就是一個任務有時會同步完地成,而有時會異步地完成,這將致使竟合狀態。
Promise被定義爲不能受這種顧慮的影響,由於即使是當即完成的Promise(好比 new Promise(function(resolve){ resolve(42); }))也不可能被同步地 監聽。也就是說,但你在Promise上調用then(..)的時候,即使這個Promise已經被解析了,你給then(..)提供的回調也將老是被異步地調用。
當一個Promise被調用時,這個Promise 上的then註冊的回調函數都會在下一個異步時機點上,按順序地,被當即調用。這些回調中的任意一個都沒法影響或延誤對其它回調的調用。
舉例說明:
p.then( function(){ p.then( function(){ console.log( "C" ); } ); console.log( "A" ); } ); p.then( function(){ console.log( "B" ); } ); // A B C
爲何「C」沒有排到「B」的前面?由於由於「C」所處的.then回調函數是在下一個事件循環tick。
這是一個很常見的顧慮。Promise用幾種方式解決它。
首先,當Promise被解析後,在代碼不出錯的狀況下它必定會告知你解析結果。若是代碼有錯誤,歸類於後面的「吞掉錯誤或異常」中。
那若是Promise自己無論怎樣永遠沒有被解析呢?那麼Promise會用Promise.race來解決。
看代碼示例:
// 一個使Promise超時的工具 function timeoutPromise(delay) { return new Promise( function(resolve,reject){ setTimeout( function(){ reject( "Timeout!" ); }, delay ); } ); } // 爲`foo()`設置一個超時 Promise.race( [ foo(), // 嘗試調用`foo()` timeoutPromise( 3000 ) // 給它3秒鐘 ] ) .then( function(){ // `foo(..)`及時地完成了! }, function(err){ // `foo()`不是被拒絕了,就是它沒有及時完成 // 那麼能夠考察`err`來知道是哪一種狀況 } );
正常是調用一次,「過少」就是未被調用,參考上文;「過多」的狀況也很容易理解。Promise的定義方式使得它只能被決議一次,若是出於某種狀況決議了屢次,Promise也只會接受第一次決議,並忽略後續調用。
Promise只會有一個解析結果(完成或拒絕)。若是沒有用一個值明確地解析它,它的值就是undefined,就像JS中常見的那樣。
Promise中異常會被捕獲,而且使這個Promise被拒絕。
舉個例子:
var p = new Promise( function(resolve,reject){ foo.bar(); // `foo`沒有定義,因此這是一個錯誤! resolve( 42 ); // 永遠不會跑到這裏 :( } ); p.then( function fulfilled(){ // 永遠不會跑到這裏 :( }, function rejected(err){ // `err`將是一個來自`foo.bar()`那一行的`TypeError`異常對象 } );
Promise就先說到這裏,關於PromiseAPI及其源碼還有生成器、Async/Await 在後續文章中整理報道。
【寫得很差的地方請大膽吐槽,很是感謝你們帶我進步。】
參考資料:
阮一峯event-loop
王福朋深刻理解javascript異步系列一
你不知道的javascript
你不懂JS: 異步與性能 第三章: Promise(上)