Javascript的事件模型和Promise實現


1. Javascript的運行時模型——事件循環 
JS的運行時是個單線程的運行時,它不像其他編程語言,比如C++,Java,C#這些可以進行多線程操作的語言。當它執行一個函數時,它只會一條路走到黑,不會在當前函數結束之前去調用其他的函數(除非當前函數主動調用其他函數)。它也不用擔心會有其他線程打擾它,因為它的運行時只有一個線程。如果你還記得一些計算機原理的話,這種運行時只有一個棧,設計起來相當的簡單。

一條路走到黑的設計很棒,因為它足夠簡單,但是又是誰決定哪個函數從開始進入棧內執行呢?答案是JS的運行時還有一個事件等待隊列與棧搭配,每當運行棧為空時(也就是當前函數運行結束),JS的運行時就從當前的事件隊列中取出一個消息處理,執行與這個消息相關聯的函數。這種行為可以用以下代碼來說明:

1 while (eventQueue.waitForMessage()) {
2     let event = eventQueue.pop();
3     let handler = event.handler;
4     handler();                      //執行事件關聯的函數
5     context.scheduler.schedule();   //讓調度器處理一下其他事務
6 }

有了運行棧和事件隊列之后,我們的Javascript運行時已經初具雛形。不過Javascript中的變量都是對象,它們的大小通常很大,可不是一個小小的棧能放下的,如果我們熟悉C++,就會知道一般在C++中我們只在棧中存儲基本類型(int, bool等)和指針,而指針所指的位置是內存堆中的一個地址,這也是JS的對象的存儲地點。下面這張圖可以形象地解釋一下JS運行時的模型。

 

2. 事件循環模型的優點和缺點 
先說優點。除了實現上的簡單,Javascript的最大優點就是完全異步,永不阻塞。這句話可能有點令人迷糊,一個單線程的運行時怎么完全異步,永不阻塞?實際上雖然JS運行時單線程,但是瀏覽器是個多進程多線程的環境,這一個點在后端也一樣,雖然Node是個單線程JS運行時,但是后端還有其他進程和線程配合Node一起完成響應操作。

以瀏覽器打開IndexedDB為例,當你執行indexedDB.open()的之后,當前的Javascript運行棧就結束了,JS可以處理其他事件的關聯函數,所以JS不會阻塞。那原來的open操作交給誰了呢?瀏覽器會調用其他線程接管這個打開數據庫的過程,當返回時,瀏覽器會在JS運行時的事件隊列中添加一個打開成功或者打開失敗的事件,同時將你當時添加的回調函數關聯到事件。

再說缺點。我們都知道JS的調用函數只會一條路走到黑,而且沒有正常的方法能打斷這一過程,如果這一路恰好比較長(比如進行了大量的數學運算),就會使JS進入一種類阻塞的狀態,頁面會無法響應。等等!剛說JS永不阻塞,這里怎么又冒出一個類阻塞呢?這時因為我們所說的阻塞一般都是指IO阻塞,也就是CPU等IO結束的過程,這種情況在JS中可以永遠不會發生(注意這里是可以,不是一定,某些IO操作是有同步的API可以調用的)。所謂類阻塞狀態呢,就是在執行CPU密集型任務,這是一種不可避免的過程。那為什么這種情況下頁面會沒有響應呢?這時因為瀏覽器雖然會把事件放入事件隊列里,但是由於前一個函數還沒執行完,頁面響應事件關聯的函數得不到執行,自然頁面會表現出不響應的狀態。

 

3. Promise的實現 
Promise是JS處理回調的一種方式,也是利用JS的事件循環模型的一個編程范式包裝,也是ES7中await async的基礎。如果說回調是對JS事件模型最直接最拙劣的實現的話,Promise至少給回調加了件衣服,使它不再那么難看。但本質上講,Promise還是描述JS事件循環模型的一種工具。下面給個例子來說明它和JS事件模型的聯系。

 1 let getUrlAsync = (url) => {
 2     let promise = new Promise((resolve, reject) => {
 3         const xhr = new XMLHttpRequest();
 4         xhr.open('GET', url);
 5         xhr.onload = () => resolve(xhr.responseText);
 6         xhr.onerror = () => reject(xhr.statusText);
 7     });
 8     return promise;
 9 };
10 getUrlAsync('http://exaple.com/text/11111')
11     .then(res => console.log(res))
12     .catch(error => console.log(error));

當調用getUrlAsync時,JS運行時做以下事情: 
1. 會創建一個Promise對象,此時還是在getUrlAsync的棧幀里; 
2. 然后創建一個XMLHttpRequest對象,此時還是在getUrlAsync的棧幀里; 
3. 調用XMLHttpRequestopen方法,此時瀏覽器其他線程接管open過程,JS無需等待open結束; 
4. 給xhronload事件關聯一個處理函數(委托),注意此時該事件並沒有進入事件隊列; 
5. 給xhronerror事件關聯一個處理函數(委托),同樣此時該事件沒有進入運行時的事件隊列; 
6. 傳入res => console.log(res)來具體化第4步中的委托; 
7. 傳入error => console.log(error)來具體化第5部中的委托,此時當前的運行棧就退出了,運行時將處理其他事件。 
在某一個時刻,瀏覽器控制的open方法返回,它會在JS運行時的事件隊列中添加一個事件,比如onload 
8. JS運行時循環到onload事件,並找到它的關聯處理函數,在這個例子中就是res => console.log(res),並運行這個函數。

 

 

 

最后吐槽一個博客園,還不支持Markdown嗎?默認編輯器真的好難用。。。。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM