之前看了一些關(guān)于作用域的文章和書,可是都漸漸淡忘了,這里我在重新復習作用域之前,先去了解一下js引擎編譯的大致過程,來幫助我加深對js的理解.
渲染引擎
瀏覽器的核心是兩部分:渲染引擎和javascript解釋器(引擎)得糜;不同的瀏覽器有不同的渲染引擎,他的主要作用是生成網(wǎng)頁,通常分成四個階段,因為圖片看起來更加直觀,所以把內(nèi)容放在圖片里.
javascript虛擬機(引擎)
-
js是解釋型語言,有一定的優(yōu)缺點:
-
早期,瀏覽器內(nèi)部對js的處理過程:
-
即時編譯
為了提高運行速度,現(xiàn)代瀏覽器改為采用'即時編譯'(just in time compiler,縮寫JIT),即字節(jié)碼只在運行時編譯,用到哪行,就編譯哪一行,并且把編譯結(jié)果緩存,通常,一個程序被經(jīng)常用到的,只是其中一小部分的代碼,有了緩存的編譯結(jié)果,整個程序的運行速度就會顯著提升.
有的瀏覽器索性省略了字節(jié)碼的翻譯步驟,直接編譯成機器碼,比如CHROME瀏覽器的v8引擎.
- 字節(jié)碼不能直接運行,而是運行在一個虛擬機之上,一般也把虛擬機稱為'javascript引擎'.因為js運行時未必有字節(jié)碼,所以js虛擬機并不完全基于字節(jié)碼,而是部分基于源碼,即只要有可能,就通過JIT編譯器直接把源碼編譯成機器碼運行,省略字節(jié)碼步驟,這一點與其他采用虛擬機的語言不盡相同,這樣做的目的,是為了盡可能地優(yōu)化代碼,提高性能.
script標簽的工作原理
正常的網(wǎng)頁加載流程:
為了避免發(fā)生阻塞效應,較好的做法是將script標簽都放在底部,而不是頭部.這樣即使遇到腳本失去響應,網(wǎng)頁主體的渲染也已經(jīng)完成了,用戶至少可以看到內(nèi)容,而不是面對一張空白的頁面.
如果某些腳本代碼非常重要,一定放在頁面頭部的話,最好將代碼嵌入頁面,而不是鏈接外部腳本文件,這樣能縮短加載時間
將腳本放在網(wǎng)頁尾部加載還有一個好處,在DOM結(jié)構(gòu)生成之前就調(diào)用DOM,js會報錯,如果腳本都在網(wǎng)頁尾部加載,就不存在這個問題,因為這時DOM肯定已經(jīng)生成了.
defer屬性
為了解決腳本文件下載阻塞網(wǎng)頁渲染的問題,一個方法是加入defer屬性
-
defer屬性的作用是,告訴瀏覽器,等到DOM加載完成后,再執(zhí)行指定腳本
-
有了defer屬性,瀏覽器下載腳本文件的時候,不會阻塞頁面渲染.下載的腳本文件在DOMContentLoaded事件觸發(fā)前執(zhí)行(即剛剛讀取完</html>標簽),而且可以保證執(zhí)行順序就是他們在頁面上出現(xiàn)的順序
-
對于內(nèi)置而不是鏈接外部腳本的script標簽,以及動態(tài)生成的script標簽,defer屬性不起作用
async
-
解決'阻塞效應'的另一個方法是加入async屬性
-
async屬性可以保證腳本下載的同時,瀏覽器繼續(xù)渲染,
-
需要注意的是,一旦采用這個屬性,就無法保證腳本的執(zhí)行順序,哪個腳本先下載結(jié)束,就先執(zhí)行那個.使用async屬性的腳本文件中,不應該使用document.write方法
重流和重繪(此部分為轉(zhuǎn)載)
渲染樹轉(zhuǎn)換為網(wǎng)頁布局,成為'布局流';布局顯示到頁面的這個過程,稱為'繪制' 他們都具有阻塞效應,并且會耗費很多時間和計算資源
頁面生成后,腳本操作和樣式表操作,都會觸發(fā)重流和重繪,用戶的互動,比如設(shè)置了鼠標懸停效果,頁面滾動,在輸入框中輸入文本,改變窗口大小.
重流和重繪并不一定一起發(fā)生,重流必然導致重繪,重繪不一定需要重流.比如改變元素的顏色,只會導致重繪,而不會導致重流.改變元素的布局,則會導致重流和重繪.
大多情況下,瀏覽器會智能判斷,將重流和重繪只限制到相關(guān)的子樹上面,最小化所耗費的代價,而不會全局生成網(wǎng)頁
-
作為開發(fā)者,應該盡量設(shè)法降低重繪的次數(shù)和成本.比如,盡量不要變動高層的DOM元素,而以底層DOM元素的變動代替,再比如,重繪table布局和flex布局,開銷都比較大.
var foo = document.getElementById(‘foobar’); foo.style.color = ‘blue’; foo.style.marginTop = ‘30px’;
上面的代碼只會導致一次重繪,因為瀏覽器會累積DOM 變動,然后一次性執(zhí)行.
下面的代碼則會導致兩次重繪:var foo = document.getElementById(‘foobar’); foo.style.color = ‘blue’; var margin = parseInt(foo.style.marginTop); foo.style.marginTop = (margin + 10) + ‘px’;
優(yōu)化技巧:
- 讀取DOM或者寫入DOM,盡量寫在一起,不要混雜;
- 緩存DOM信息
- 不要一項一項的改變樣式,而是使用CSS class一次性改變樣式
- 使用document fragment操作DOM
- 動畫時使用absolute定位或fixed定位,這樣可以減少對其他元素的影響
- 只在必要時才顯示元素
- 使用window.requestAnimationFrame(),因為它可以把代碼推遲到下一次重流時執(zhí)行,而不是立即要求頁面重流
- 使用虛擬DOM(virtual DOM)庫
// 重繪代價高 function doubleHeight(element) { var currentHeight = element.clientHeight; element.style.height = (currentHeight * 2) + ‘px’; } all_my_elements.forEach(doubleHeight); // 重繪代價低 function doubleHeight(element) { var currentHeight = element.clientHeight; window.requestAnimationFrame(function () { element.style.height = (currentHeight * 2) + ‘px’; }); } all_my_elements.forEach(doubleHeight);
腳本的動態(tài)嵌入(此部分為轉(zhuǎn)載)
['1.js', '2.js'].forEach(function(src) {
var script = document.createElement('script');
script.src = src;
document.head.appendChild(script);
});
這種方法的好處是,動態(tài)生成script標簽不會阻塞頁面渲染,也就不會造成瀏覽器假死,但是問題在于,這種方法無法保證腳本的執(zhí)行順序,哪個腳本文件先下載完成,就先執(zhí)行哪個,
如果想避免這個問題,可以設(shè)置async屬性為false
['1.js', '2.js'].forEach(function(src) {
var script = document.createElement('script');
script.src = src;
script.async = false;
document.head.appendChild(script);
});
在這段代碼之后加載的腳本,要等待2.js執(zhí)行完成后再執(zhí)行
(function() {
var script,
scripts = document.getElementsByTagName('script')[0];
function load(url) {
script = document.createElement('script');
script.async = true;
script.src = url;
scripts.parentNode.insertBefore(script, scripts);
}
load('//apis.google.com/js/plusone.js');
load('//platform.twitter.com/widgets.js');
load('//s.thirdpartywidget.com/widget.js');
}());
此外,動態(tài)嵌入還有一個地方需要注意,動態(tài)嵌入必須等到CSS文件加載完成后,才會去下載外部腳本文件,靜態(tài)加載就不存在這個問題,script標簽指定的外部腳本文件,都是與css文件同時并發(fā)下載的.
單線程模型
-
js采用的是單線程模型
-
一次只能運行一個任務,其他任務需要等待前一個任務完成才能工作.
-
為什么不用多線程呢?
- 原因是不想瀏覽器變得復雜,因為多線程需要共享資源,且有可能修改彼此的運行結(jié)果
-
-
h5允許多線程,但是子線程完全受主線程控制,且不得操作DOM,所以這個新標準并沒
- 有改變js單線程的本質(zhì).
-
存在的問題.
容易造成瀏覽器假死狀態(tài);
-
js設(shè)計者意識到,CPU完全可以不管IO設(shè)備,掛起處于等待中的任務,先運行排在后面的任務,等到IO設(shè)備返回了結(jié)果,再回過頭,把掛起的任務繼續(xù)執(zhí)行下去,這種機制就是js內(nèi)部采用的Event Loop
Event Loop
-
Event Loop,指的是一種內(nèi)部循環(huán),用來排列和處理事件,以及執(zhí)行函數(shù).是一種程序結(jié)果,用于等待和發(fā)送信息和事件.
-
所有任務可以分成兩種,一種是同步任務,一種是異步任務,
同步任務,在主線程上排隊執(zhí)行的任務,只有前一個任務執(zhí)行完畢,才能執(zhí)行后一個任務,
異步任務,不進入主線程,而進入任務隊列的任務.只有任務隊列通知主線程,某個異步任務可以執(zhí)行了,該任務才會進入主線程執(zhí)行
下圖上面淺藍色底為同步任務,圖下淺紅色底為異步任務萌腿。
-
同步模式和異步模式
異步模式主線程可以運行更多的任務,提高了效率