Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537 Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537
擊右上方紅色按鈕關注“小鄭搞碼事”,每天都能學到知識,搞懂一個問題!
先來看這段代碼,大家看表示什么意思。
一眼看過去,非常熟悉,通常我們會這樣理解:這段代碼表示1秒后,會執行setTimeout里面那個函數。
然而,這種解釋并不準確, 那應該怎么理解呢? 我們先來理解JavaScript的遠行機制。
本文最后我會給出JavaScript的運行機制完整圖。但在這之前,我想先來給大家解釋幾個問題。
JavaScript設計的初衷是用在瀏覽器中, 那么,我們來想象一下,如果JavaScript是多線程的話。
必然可以有兩個進程,process1和process2,那么這兩個進程可以同時對同一個DOM進行操作。如果這個時候,一個進程要刪除這個DOM,另一個進程要編輯這個DOM。啟不是矛盾嘛。
所以,這樣應該更好理解,JS為什么是單線程了。
單線程為什么需要異步呢?
JavaScript如果不存在異步,而是自上而下執行,這樣的話,假如上一行解析時間很長,那么下面的代碼直接就會被阻塞。這種現象對于用戶來說,意味著"卡死"。嚴重影響用戶流失,這樣解釋好理解吧,所以JavaScript需要異步處理。
JavaScript竟然需要異步,那么它是如何實現異步的呢?
JavaScript是通過事件循環(event loop)來實現的,事件循環機制也就是今天要說的JavaScript運行機制。
(一)同步任務和異步任務
來看一段代碼:
首先這段代碼輸出結果是啥?
輸出:1 3 2
其中setTimeout需要延遲一段時間才去執行,這類代碼就是異步代碼。
看到這個結果,所以通常我們都這么理解JS的執行原理:
第一,判斷JS是同步還是異步,同步進入主線程,異步則進入event table。
第二,異步任務在event table中注冊函數,當滿足觸發條件后,被推入event queue(事件隊列)。
第三,同步任務進入主線程后一直執行,直到主線程空閑,才會去event queue中查看是否有可執行的異步任務,如果有就推入主線程。
按到這個邏輯,上面這段實例代碼,是不是就很好理解了。1,3是同步任務進入主要線程,自上而下執行,2是異步任務,滿足觸發條件后,推入事件隊列,等待主線程有空時調用。
(二)宏任務(macro-task)和微任務(micro-task)
然而,按照同步和異步任務來理解JS的運行機制似乎并不準確。
來看一段代碼。看看它的輸出順序。
上面這段代碼,按同步和異步的理解,輸出結果是:2,4,1,3。因為2,4是同步任務,按順序在主線程自上而下執行,而1,3是異步任務,按順序在主線程有空后自先而后執行。
可事實輸出并不是這個結果,而是這樣的:2,4,3,1。為什么呢?來理解一下宏任務和微任務。
寵任務:包括整體script代碼,setTimeout,setInterval。
微任務:Promise,process.nextTick。
來看原理圖:
嗯,對,這就是JS的運行機制。也就是事件循環。解釋一下:
第一,執行一個宏任務(主線程的同步script代碼),過程中如果遇到微任務,就將其放到微任務的事件隊列里。
第二,當前宏任務執行完成后,會查微任務的事件隊列,將將全部的微任務依次執行完,再去依次執行宏任務事件隊列。
上面代碼中promise的then是一微任務,因此它的執行在setTimeout之前。
需要注意的是:在node環境下,process.nextTick的優先級高于promise。也就是可以簡單理解為,在宏任務結束后會先執行微任務隊列中的nextTickQueue部分,然后才會執行微任務中的promise部分。
所以最后總結一下,對于文章一開頭提到的那段代碼,我們可以準確的理解為:
1秒后,setTimeout里的函數會被推入event queue,而event queue(事件隊列)里的任務,只有在主線程空閑時才會執行。也就是需要同時滿足兩個條件(1)1秒后。(2)主線程必須空閑,這樣1秒后才會執行該函數。
現在,關于JavaScript的運行機制,大家應該都理解了,有問題歡迎留言。
事跟我說他用jQuery取不到頁面上隱藏元素input的值,他的html頁面大概內容如下。
<!DOCTYPE html>
<html lang="zh">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<script type="text/javascript" src="jslib/jquery-1.11.2.min.js"></script>
<title>淺談Html頁面內容執行順序</title>
<script type="text/javascript">
var userId = $('#hiddenUserId').val();
var contextPath = $('#hiddenContextPath').val();
var userName = $('#hiddenUserName').val();
</script>
</head>
<body>
<input type="hidden" id="hiddenUserId" value="101" />
<input type="hidden" id="hiddenContextPath" value="/web" />
<input type="hidden" id="hiddenUserName" value="小明" />
</body>
</html>
頁面中的JS腳本在head中,JS腳本要讀取的input在body中。瀏覽器對html頁面內容的加載是順序加載,也就是在html頁面中前面先加載,因此當加載到JS腳本時,input還沒有加載到瀏覽器中。JS是一種解釋性的腳本,也是從上而下順序執行,由于這段JS代碼是立即執行的,所以當JS在執行的時候,讀取不到input的值。
最直接的修改方法是把JS放到網頁的最下面執行。
<!DOCTYPE html>
<html lang="zh">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<script type="text/javascript" src="jslib/jquery-1.11.2.min.js"></script>
<title>淺談Html頁面內容執行順序</title>
</head>
<body>
<input type="hidden" id="hiddenUserId" value="101" />
<input type="hidden" id="hiddenContextPath" value="/web" />
<input type="hidden" id="hiddenUserName" value="小明" />
<script type="text/javascript">
var userId = $('#hiddenUserId').val();
var contextPath = $('#hiddenContextPath').val();
var userName = $('#hiddenUserName').val();
</script>
</body>
</html>
把JS放到網頁的最下面,這樣在JS執行的時候,網頁內容都已經加載完畢。把JS放在網頁的最下面方法并不是最好的解決方法,大部分情況JS并不是總能放在網頁的最下面。這時可以用window的onload事件,onload事件在整個頁面都加載完成后才觸發,可以把JS腳本放在onload里面執行。不同瀏覽器onload事件添加方式也不一樣。
IE下事件:
window.attachEvent('onload', function(){
var userId = $('#hiddenUserId').val();
var contextPath = $('#hiddenContextPath').val();
var userName = $('#hiddenUserName').val();
});
Chrome/Firefox等DOM標準事件:
window.addEventListener('load', function(){
var userId = $('#hiddenUserId').val();
var contextPath = $('#hiddenContextPath').val();
var userName = $('#hiddenUserName').val();
});
由于不同瀏覽器的事件添加方式不一樣,jQuery為我們提供了通用的初始化方法,該方法在頁面加載完成時觸發。
$(function(){
var userId = $('#hiddenUserId').val();
var contextPath = $('#hiddenContextPath').val();
var userName = $('#hiddenUserName').val();
});
上面方法本質就是添加onload監聽事件。
最終修改后的頁面
瀏覽器的“心”,說的就是瀏覽器的內核。在研究瀏覽器微觀的運行機制之前,我們首先要對瀏覽器內核有一個宏觀的把握。
許多工程師因為業務需要,免不了需要去處理不同瀏覽器下代碼渲染結果的差異性。這些差異性正是因為瀏覽器內核的不同而導致的——瀏覽器內核決定了瀏覽器解釋網頁語法的方式。
瀏覽器內核可以分成兩部分:渲染引擎(Layout Engine 或者 Rendering Engine)和 JS 引擎。早期渲染引擎和 JS 引擎并沒有十分明確的區分,但隨著 JS 引擎越來越獨立,內核也成了渲染引擎的代稱(下文我們將沿用這種叫法)。渲染引擎又包括了 HTML 解釋器、CSS 解釋器、布局、網絡、存儲、圖形、音視頻、圖片解碼器等等零部件。
目前市面上常見的瀏覽器內核可以分為這四種:Trident(IE)、Gecko(火狐)、Blink(Chrome、Opera)、Webkit(Safari)。
大家最耳熟能詳的可能就是 Webkit 內核了。很多同學可能會聽說過 Chrome 的內核就是 Webkit,殊不知 Chrome 內核早已迭代為了 Blink。但是換湯不換藥,Blink 其實也是基于 Webkit 衍生而來的一個分支,因此,Webkit 內核仍然是當下瀏覽器世界真正的霸主。
下面我們就以 Webkit 為例,對現代瀏覽器的渲染過程進行一個深度的剖析。
什么是渲染過程?簡單來說,渲染引擎根據 HTML 文件描述構建相應的數學模型,調用瀏覽器各個零部件,從而將網頁資源代碼轉換為圖像結果,這個過程就是渲染過程(如下圖)。
從這個流程來看,瀏覽器呈現網頁這個過程,宛如一個黑盒。在這個神秘的黑盒中,有許多功能模塊,內核內部的實現正是這些功能模塊相互配合協同工作進行的。其中我們最需要關注的,就是HTML 解釋器、CSS 解釋器、圖層布局計算模塊、視圖繪制模塊與JavaScript 引擎這幾大模塊:
有了對零部件的了解打底,我們就可以一起來走一遍瀏覽器的渲染流程了。在瀏覽器里,每一個頁面的首次渲染都經歷了如下階段(圖中箭頭不代表串行,有一些操作是并行進行的,下文會說明):
在這一步瀏覽器執行了所有的加載解析邏輯,在解析 HTML 的過程中發出了頁面渲染所需的各種外部資源請求。
瀏覽器將識別并加載所有的 CSS 樣式信息與 DOM 樹合并,最終生成頁面 render 樹(:after :before 這樣的偽元素會在這個環節被構建到 DOM 樹中)。
頁面中所有元素的相對位置信息,大小等信息均在這一步得到計算。
在這一步中瀏覽器會根據我們的 DOM 代碼結果,把每一個頁面圖層轉換為像素,并對所有的媒體文件進行解碼。
最后一步瀏覽器會合并合各個圖層,將數據由 CPU 輸出給 GPU 最終繪制在屏幕上。(復雜的視圖層會給這個階段的 GPU 計算帶來一些壓力,在實際應用中為了優化動畫性能,我們有時會手動區分不同的圖層)。
上面的內容沒有理解透徹?別著急,我們一起來捋一捋這個過程中的重點——樹!
為了使渲染過程更明晰一些,我們需要給這些”樹“們一個特寫:
基于這些“樹”,我們再梳理一番:
渲染過程說白了,首先是基于 HTML 構建一個 DOM 樹,這棵 DOM 樹與 CSS 解釋器解析出的 CSSOM 相結合,就有了布局渲染樹。最后瀏覽器以布局渲染樹為藍本,去計算布局并繪制圖像,我們頁面的初次渲染就大功告成了。
之后每當一個新元素加入到這個 DOM 樹當中,瀏覽器便會通過 CSS 引擎查遍 CSS 樣式表,找到符合該元素的樣式規則應用到這個元素上,然后再重新去繪制它。
有心的同學可能已經在思考了,查表是個花時間的活,我怎么讓瀏覽器的查詢工作又快又好地實現呢?OK,講了這么多原理,我們終于引出了我們的第一個可轉化為代碼的優化點——CSS 樣式表規則的優化!
在給出 CSS 選擇器方面的優化建議之前,先告訴大家一個小知識:CSS 引擎查找樣式表,對每條規則都按從右到左的順序去匹配。 看如下規則:
#myList li {}
這樣的寫法其實很常見。大家平時習慣了從左到右閱讀的文字閱讀方式,會本能地以為瀏覽器也是從左到右匹配 CSS 選擇器的,因此會推測這個選擇器并不會費多少力氣:#myList 是一個 id 選擇器,它對應的元素只有一個,查找起來應該很快。定位到了 myList 元素,等于是縮小了范圍后再去查找它后代中的 li 元素,沒毛病。
事實上,CSS 選擇符是從右到左進行匹配的。我們這個看似“沒毛病”的選擇器,實際開銷相當高:瀏覽器必須遍歷頁面上每個 li 元素,并且每次都要去確認這個 li 元素的父元素 id 是不是 myList,你說坑不坑!
說到坑,不知道大家還記不記得這個經典的通配符:
* {}
入門 CSS 的時候,不少同學拿通配符清除默認樣式(我曾經也是通配符用戶的一員)。但這個家伙很恐怖,它會匹配所有元素,所以瀏覽器必須去遍歷每一個元素!大家低頭看看自己頁面里的元素個數,是不是心涼了——這得計算多少次呀!
這樣一看,一個小小的 CSS 選擇器,也有不少的門道!好的 CSS 選擇器書寫習慣,可以為我們帶來非常可觀的性能提升。根據上面的分析,我們至少可以總結出如下性能提升的方案:
錯誤示范:
#myList li{}
理想:
.myList_li {}
不要畫蛇添足,id 和 class 選擇器不應該被多余的標簽選擇器拖后腿。
錯誤示范
.myList#title
理想:
#title
減少嵌套。后代選擇器的開銷是最高的,因此我們應該盡量將選擇器的深度降到最低(最高不要超過三層),盡可能使用類來關聯每一個標簽元素。
搞定了 CSS 選擇器,萬里長征才剛剛開始的第一步。但現在你已經理解了瀏覽器的工作過程,接下來的征程對你來說并不再是什么難題~
說完了過程,我們來說一說特性。
HTML、CSS 和 JS,都具有阻塞渲染的特性。
HTML 阻塞,天經地義——沒有 HTML,何來 DOM?沒有 DOM,渲染和優化,都是空談。
那么 CSS 和 JS 的阻塞又是怎么回事呢?
在剛剛的過程中,我們提到 DOM 和 CSSOM 合力才能構建渲染樹。這一點會給性能造成嚴重影響:默認情況下,CSS 是阻塞的資源。瀏覽器在構建 CSSOM 的過程中,不會渲染任何已處理的內容。即便 DOM 已經解析完畢了,只要 CSSOM 不 OK,那么渲染這個事情就不 OK(這主要是為了避免沒有 CSS 的 HTML 頁面丑陋地“裸奔”在用戶眼前)。
我們知道,只有當我們開始解析 HTML 后、解析到 link 標簽或者 style 標簽時,CSS 才登場,CSSOM 的構建才開始。很多時候,DOM 不得不等待 CSSOM。因此我們可以這樣總結:
CSS 是阻塞渲染的資源。需要將它盡早、盡快地下載到客戶端,以便縮短首次渲染的時間。
事實上,現在很多團隊都已經做到了盡早(將 CSS 放在 head 標簽里)和盡快(啟用 CDN 實現靜態資源加載速度的優化)。這個“把 CSS 往前放”的動作,對很多同學來說已經內化為一種編碼習慣。那么現在我們還應該知道,這個“習慣”不是空穴來風,它是由 CSS 的特性決定的。
不知道大家注意到沒有,前面我們說過程的時候,花了很多筆墨去說 HTML、說 CSS。相比之下,JS 的出鏡率也太低了點。
這當然不是因為 JS 不重要。而是因為,在首次渲染過程中,JS 并不是一個非登場不可的角色——沒有 JS,CSSOM 和 DOM 照樣可以組成渲染樹,頁面依然會呈現——即使它死氣沉沉、毫無交互。
JS 的作用在于修改,它幫助我們修改網頁的方方面面:內容、樣式以及它如何響應用戶交互。這“方方面面”的修改,本質上都是對 DOM 和 CSSDOM 進行修改。因此 JS 的執行會阻止 CSSOM,在我們不作顯式聲明的情況下,它也會阻塞 DOM。
我們通過一個例子來理解一下這個機制:
三個 console 的結果分別為:
注:本例僅使用了內聯 JS 做測試。感興趣的同學可以把這部分 JS 當做外部文件引入看看效果——它們的表現一致。
第一次嘗試獲取 id 為 container 的 DOM 失敗,這說明 JS 執行時阻塞了 DOM,后續的 DOM 無法構建;第二次才成功,這說明腳本塊只能找到在它前面構建好的元素。這兩者結合起來,“阻塞 DOM”得到了驗證。再看第三個 console,嘗試獲取 CSS 樣式,獲取到的是在 JS 代碼執行前的背景色(yellow),而非后續設定的新樣式(blue),說明 CSSOM 也被阻塞了。那么在阻塞的背后,到底發生了什么呢?
我們前面說過,JS 引擎是獨立于渲染引擎存在的。我們的 JS 代碼在文檔的何處插入,就在何處執行。當 HTML 解析器遇到一個 script 標簽時,它會暫停渲染過程,將控制權交給 JS 引擎。JS 引擎對內聯的 JS 代碼會直接執行,對外部 JS 文件還要先獲取到腳本、再進行執行。等 JS 引擎運行完畢,瀏覽器又會把控制權還給渲染引擎,繼續 CSSOM 和 DOM 的構建。 因此與其說是 JS 把 CSS 和 HTML 阻塞了,不如說是 JS 引擎搶走了渲染引擎的控制權。
現在理解了阻塞的表現與原理,我們開始思考一個問題。瀏覽器之所以讓 JS 阻塞其它的活動,是因為它不知道 JS 會做什么改變,擔心如果不阻止后續的操作,會造成混亂。但是我們是寫 JS 的人,我們知道 JS 會做什么改變。假如我們可以確認一個 JS 文件的執行時機并不一定非要是此時此刻,我們就可以通過對它使用 defer 和 async 來避免不必要的阻塞,這里我們就引出了外部 JS 的三種加載方式。
JS的三種加載方式
<script src="index.js"></script>
這種情況下 JS 會阻塞瀏覽器,瀏覽器必須等待 index.js 加載和執行完畢才能去做其它事情。
<script async src="index.js"></script>
async 模式下,JS 不會阻塞瀏覽器做任何其它的事情。它的加載是異步的,當它加載結束,JS 腳本會立即執行。
<script defer src="index.js"></script>
defer 模式下,JS 的加載是異步的,執行是被推遲的。等整個文檔解析完成、DOMContentLoaded 事件即將被觸發時,被標記了 defer 的 JS 文件才會開始依次執行。
從應用的角度來說,一般當我們的腳本與 DOM 元素和其它腳本之間的依賴關系不強時,我們會選用 async;當腳本依賴于 DOM 元素和其它腳本的執行結果時,我們會選用 defer。
通過審時度勢地向 script 標簽添加 async/defer,我們就可以告訴瀏覽器在等待腳本可用期間不阻止其它的工作,這樣可以顯著提升性能。
*請認真填寫需求信息,我們會在24小時內與您取得聯系。