瀏覽器中的Javascript 客戶端javascript就是運行在瀏覽器中的javascript,現代的瀏覽器已經有了很好的發展,雖然它是一個應用程式,但完全可以把它看作是一個簡易的操作系統,因為像windows、linux等操作系統提供的文檔存儲、網路調用、繪製圖像等功能在瀏覽器中也同樣都可以得
- 瀏覽器中的Javascript
客戶端javascript就是運行在瀏覽器中的javascript,現代的瀏覽器已經有了很好的發展,雖然它是一個應用程式,但完全可以把它看作是一個簡易的操作系統,因為像windows、linux等操作系統提供的文檔存儲、網路調用、繪製圖像等功能在瀏覽器中也同樣都可以得到支持,所以就像操作系統中的應用程式叫做桌面應用一樣,瀏覽器中可互動的web文檔也叫做web應用。
在web應用中,瀏覽器是javascript的運行環境,這個運行環境不僅包含了javascript解釋器以使js程式可以正確的運行,還為解釋器提供了其他服務的介面來擴展了javascript的功能,比如發起網路調用、DOM編程等。當一個頁面開始載入的時候js解釋器就會啟動,啟動後做的第一件事就是初始化一個全局對象,這個全局對象在不同運行環境中會有不同的標識符表示和內容,在nodeJS中這個全局對象叫global,而在瀏覽器中這個全局對象叫window。一個window對象就代表一個窗體,裡面除了包含javascript語法內置的函數和對象外,還包括瀏覽器為其擴展的功能對象,這些功能對象就是前面說的服務介面,是瀏覽器提供的服務的對象化表示。
- Javascript的執行
解釋器啟動之後就可以執行javascript代碼了,在瀏覽器中觸發javascript代碼運行的情況只有兩種:
- 文檔載入的時候:瀏覽器在載入解析頁面文檔的過程中如果遇到了<script>標簽引用或者包含的js代碼,解釋器就會執行一遍。
- 事件觸發的時候:這種情況是非同步且由事件驅動的,解釋器執行的代碼是前一種情況下為事件註冊的處理函數。
其中第一種情況要尤其註意,當瀏覽器在下載和執行js文件的時候,頁面的解析和渲染是被阻塞的,也就是說頁面的其他有關資源會停止下載(比如css文件、圖片等),同時DOM樹的構建也會暫停,因為js程式的執行是有可能影響到DOM樹的結構的(比如說使用到了document.write方法),DOM樹的解析一暫停,頁面的UI渲染也會被迫暫停。這樣就會產生性能問題,如果js文件過大或者js代碼執行時間過長,瀏覽器等待響應的時間就越久。所以考慮到性能問題,瀏覽器中js腳本應該儘可能的是無阻塞的腳本。
- 無阻塞腳本
由阻塞腳本的阻塞過程可以知道,無阻塞的腳本可以從兩個方面來考慮:不阻塞頁面相關資源的下載和不阻塞頁面UI的渲染。
要是單單實現不阻塞其他資源的下載只有一種方法,就是在<script>標簽中添加defer或者async,這種方法的效果是實現並行下載,也就是說當js文件在下載的時候不會阻塞其他資源的下載,它倆的區別是下載完後js代碼執行的時機,defer會一直推遲到頁面解析完,而async會在js文件下載完後立即執行,但js文件在執行過程中還是會阻塞頁面解析和渲染的。由於瀏覽器對這種方法的支持不是很充分,所以這並不是一個相容性很強的方法。
其他的幾種方法就是既不阻塞下載也不阻塞渲染了,首先是一種討巧的方法,就是把<script>標簽放到<body>底部,因為瀏覽器是一邊解析渲染一邊顯示出來的,等解析到body標簽最底部的時候頁面也就完全在瀏覽器中顯示出來了,這時候再下載和執行js文件基本上不存在阻塞這回事情了,但是要註意的是放到最後面的js文件儘量不要再對DOM進行操作,因為更改了DOM可能會產生迴流或者重繪,迴流和重繪也是非常耗性能的兩個操作,影響用戶的使用體驗。還有一點是asp.net程式要註意的是在webform編程中,如果後臺代碼要用到前臺的js函數,那麼放在body標簽底部的函數是註冊不過去的,後臺只能註冊放在head標簽中的函數。
還有一種方法是創建動態腳本元素,這種方法可以說是做到了非同步無阻塞,在js文件下載和執行的時候都不會阻塞頁面的解析和渲染。該方法需要動態的創建script標簽,並把標簽添加到head元素中,比如
var script = document.createElement('script'); script.type='text/javascript'; script.src='xxxxx.js'; document.getElementsByTagName('head')[0].appendChild(script);
當代碼執行到最後一句的時候js文件開始下載,下載完後立即執行,這整個過程不會阻塞頁面解析和渲染。這是動態創建帶有src屬性的script標簽的時候,還有另外一種動態創建內聯腳本的方式,該方法通過Ajax請求獲取到js腳本內容,再動態創建script標簽把腳本內容註入到script標簽當中。這種方法不是很常用因為有一個缺點就是獲取的js內容只能是同域中的。
一個性能良好的頁面不只是要做到無阻塞腳本,還有很多其他方面要註意的。 其中一條就是不要讓一段js代碼執行太長時間,因為javascript語言中不存在任何線程機制,並且js引擎也是單線程模型的,所以js代碼的執行都是同步的,長時間的占用執行線程會使其他的js代碼無法得到執行。如果必須要計算一個密集的任務,那麼可以把這個任務通過setTimeout或setInterval分離成多個非同步子任務。這裡的同步和非同步看起來似乎有些矛盾,javascript怎麼可能既是單線程同步的又是多線程非同步的呢? 原因在於瀏覽器的機制。
- Javascript單線程與事件的非同步
雖然javascript引擎是單線程的,但是瀏覽器是多線程的,在一個瀏覽器進程中一般有四個線程:javascript引擎線程、渲染引擎線程、瀏覽器事件線程(我也不知道叫什麼)、http請求線程。在javascript引擎線程中有一個執行隊列,裡面的任務是同步的依次執行,而在瀏覽器事件線程中有一個任務隊列,當某個事件觸發時,該事件對應的回掉函數會放到這個任務隊列中,這個任務隊列是一個先進先出結構,當javascript引擎線程中的任務都執行完畢後,會從瀏覽器事件線程中的任務隊列里讀取任務放到javascript引擎線程中的執行隊列里去執行,這個過程是不斷迴圈的,這有個專業術語叫Event Loop。所以就算setTimeout(fun,0)這種看起來是立即執行的函數也並不一定會立即執行的,這個fun函數會先放到任務隊列裡面等待執行隊列中的任務執行完畢才會被執行,所以fun函數的執行會延遲一段時間,這麼樣子的寫法只是改變了fun函數的執行順序。