整合營銷服務(wù)商

          電腦端+手機(jī)端+微信端=數(shù)據(jù)同步管理

          免費咨詢熱線:

          JavaScript基礎(chǔ)-前端不懂它,會再多框架也不

          JavaScript基礎(chǔ)-前端不懂它,會再多框架也不過只是會用而已

          要混淆JavaScipt與瀏覽器

          語言和環(huán)境是兩個不同的概念。提及JavaScript,大多數(shù)人可能會想到瀏覽器,脫離瀏覽器JavaScipt是不可能運行的,這與其他系統(tǒng)級的語言有著很大的不同。例如C語言可以開發(fā)系統(tǒng)和制造環(huán)境,而JavaScript只能寄生在某個具體的環(huán)境中才能夠工作。

          JavaScipt運行環(huán)境一般都有宿主環(huán)境和執(zhí)行期環(huán)境。如下圖所示:

          宿主環(huán)境是由外殼程序生成的,比如瀏覽器就是一個外殼環(huán)境(但是瀏覽器并不是唯一,很多服務(wù)器、桌面應(yīng)用系統(tǒng)都能也能夠提供JavaScript引擎運行的環(huán)境)。執(zhí)行期環(huán)境則有嵌入到外殼程序中的JavaScript引擎(比如V8引擎,稍后會詳細(xì)介紹)生成,在這個執(zhí)行期環(huán)境,首先需要創(chuàng)建一個代碼解析的初始環(huán)境,初始化的內(nèi)容包含:

          1. 一套與宿主環(huán)境相關(guān)聯(lián)系的規(guī)則
          2. JavaScript引擎內(nèi)核(基本語法規(guī)則、邏輯、命令和算法)
          3. 一組內(nèi)置對象和API
          4. 其他約定

          雖然,不同的JavaScript引擎定義初始化環(huán)境是不同的,這就形成了所謂的瀏覽器兼容性問題,因為不同的瀏覽器使用不同JavaScipt引擎。不過最近的這條消息想必大家都知道——瀏覽器市場,微軟居然放棄了自家的EDGE(IE的繼任者),轉(zhuǎn)而投靠競爭對手Google主導(dǎo)的Chromium核心(國產(chǎn)瀏覽器百度、搜狗、騰訊、獵豹、UC、傲游、360用的都是Chromium(Chromium用的是鼎鼎大名的V8引擎,想必大家都十分清楚吧),可以認(rèn)為全是Chromium的馬甲),真是大快人心,我們終于在同一環(huán)境下愉快的編寫代碼了,想想真是開心!

          重溫編譯原理

          一提起JavaScript語言,大部分的人都將其歸類為“動態(tài)”或“解釋執(zhí)行”語言,其實他是一門“編譯性”語言。與傳統(tǒng)的編譯語言不同,它不是提前編譯的,編譯結(jié)果也不能在分布式系統(tǒng)中進(jìn)行移植。在介紹JavaScript編譯器原理之前,小編和大家一起重溫下基本的編譯器原理,因為這是最基礎(chǔ)的,了解清楚了我們更能了解JavaScript編譯器。

          編譯程序一般步驟分為:詞法分析、語法分析、語義檢查、代碼優(yōu)化和生成字節(jié)碼。具體的編譯流程如下圖:

          分詞/詞法分析(Tokenizing/Lexing)

          所謂的分詞,就好比我們將一句話,按照詞語的最小單位進(jìn)行分割。計算機(jī)在編譯一段代碼前,也會將一串串代碼拆解成有意義的代碼塊,這些代碼塊被稱為詞法單元(token)。例如,考慮程序var a=2。這段程序通常會被分解成為下面這些詞法單元:var、a、=、2、;空格是否作為當(dāng)為詞法單位,取決于空格在這門語言中是否具有意義。

          解析/語法分析(Parsing)

          這個過程是將詞法單元流轉(zhuǎn)換成一個由元素逐級嵌套所組成的代表了程序語法結(jié)構(gòu)的樹。這個樹稱為“抽象語法樹”(Abstract Syntax Tree,AST)。

          詞法分析和語法分析不是完全獨立的,而是交錯進(jìn)行的,也就是說,詞法分析器不會在讀取所有的詞法記號后再使用語法分析器來處理。在通常情況下,每取得一個詞法記號,就將其送入語法分析器進(jìn)行分析。

          語法分析的過程就是把詞法分析所產(chǎn)生的記號生成語法樹,通俗地說,就是把從程序中收集的信息存儲到數(shù)據(jù)結(jié)構(gòu)中。注意,在編譯中用到的數(shù)據(jù)結(jié)構(gòu)有兩種:符號表和語法樹。

          符號表:就是在程序中用來存儲所有符號的一個表,包括所有的字符串變量、直接量字符串,以及函數(shù)和類。

          語法樹:就是程序結(jié)構(gòu)的一個樹形表示,用來生成中間代碼。下面是一個簡單的條件結(jié)構(gòu)和輸出信息代碼段,被語法分析器轉(zhuǎn)換為語法樹之后,如以下代碼:

          if (typeof a=="undefined") {
           a=0;
          } else {
           a=a;
          }
          alert(a);
          

          如果JavaScript解釋器在構(gòu)造語法樹的時候發(fā)現(xiàn)無法構(gòu)造,就會報語法錯誤,并結(jié)束整個代碼塊的解析。對于傳統(tǒng)強類型語言來說,在通過語法分析構(gòu)造出語法樹后,翻譯出來的句子可能還會有模糊不清的地方,需要進(jìn)一步的語義檢查。語義檢查的主要部分是類型檢查。例如,函數(shù)的實參和形參類型是否匹配。但是,對于弱類型語言來說,就沒有這一步。

          經(jīng)過編譯階段的準(zhǔn)備, JavaScript代碼在內(nèi)存中已經(jīng)被構(gòu)建為語法樹,然后 JavaScript引擎就會根據(jù)這個語法樹結(jié)構(gòu)邊解釋邊執(zhí)行。

          代碼生成

          將AST轉(zhuǎn)換成可執(zhí)行代碼的過程被稱為代碼生成。這個過程與語言、目標(biāo)平臺相關(guān)。

          了解完編譯原理后,其實JavaScript引擎要復(fù)雜的許多,因為大部分情況,JavaScript的編譯過程不是發(fā)生在構(gòu)建之前,而是發(fā)生在代碼執(zhí)行前的幾微妙,甚至?xí)r間更短。為了保證性能最佳,JavaScipt使用了各種辦法,稍后小編將會詳細(xì)介紹。

          神秘的編譯器——V8引擎

          由于JavaScipt大多數(shù)都是運行在瀏覽器上,不同瀏覽器的使用的引擎也各不相同,以下是目前主流瀏覽器引擎:

          由于谷歌的V8編譯器的出現(xiàn),由于性能良好吸引了相當(dāng)?shù)淖⒛浚接捎赩8的出現(xiàn),我們目前的前端才能大放光彩,百花齊放,V8引擎用C++進(jìn)行編寫, 作為一個 JavaScript 引擎,最初是服役于 Google Chrome 瀏覽器的。它隨著 Chrome 的第一版發(fā)布而發(fā)布以及開源。現(xiàn)在它除了 Chrome 瀏覽器,已經(jīng)有很多其他的使用者了。諸如 NodeJS、MongoDB、CouchDB 等。最近最讓人振奮前端新聞莫過于微軟居然放棄了自家的EDGE(IE的繼任者),轉(zhuǎn)而投靠競爭對手Google主導(dǎo)的Chromium核心(國產(chǎn)瀏覽器百度、搜狗、騰訊、獵豹、UC、傲游、360用的都是Chromium(Chromium用的是鼎鼎大名的V8引擎,想必大家都十分清楚吧),看來V8引擎在不久的將來就會一統(tǒng)江湖,下面小編將重點介紹V8引擎。

          當(dāng)V8編譯JavaScript 代碼時,解析器(parser)將生成一個抽象語法樹(上一小節(jié)已介紹過)。語法樹是JavaScript代碼的句法結(jié)構(gòu)的樹形表示形式。解釋器 Ignition 根據(jù)語法樹生成字節(jié)碼。TurboFan 是V8的優(yōu)化編譯器,TurboFan將字節(jié)碼(Bytecode)生成優(yōu)化的機(jī)器代碼(Machine Code)。

          V8曾經(jīng)有兩個編譯器

          在5.9版本之前,該引擎曾經(jīng)使用了兩個編譯器:

          full-codegen - 一個簡單而快速的編譯器,可以生成簡單且相對較慢的機(jī)器代碼。

          Crankshaft - 一種更復(fù)雜的(即時)優(yōu)化編譯器,可生成高度優(yōu)化的代碼。

          V8引擎還在內(nèi)部使用多個線程:

          • 主線程:獲取代碼,編譯代碼然后執(zhí)行它
          • 優(yōu)化線程:與主線程并行,用于優(yōu)化代碼的生成
          • Profiler線程:它將告訴運行時我們花費大量時間的方法,以便Crankshaft可以優(yōu)化它們
          • 其他一些線程來處理垃圾收集器掃描

          字節(jié)碼

          字節(jié)碼是機(jī)器代碼的抽象。如果字節(jié)碼采用和物理 CPU 相同的計算模型進(jìn)行設(shè)計,則將字節(jié)碼編譯為機(jī)器代碼更容易。這就是為什么解釋器(interpreter)常常是寄存器或堆棧。Ignition 是具有累加器的寄存器。

          您可以將V8的字節(jié)碼看作是小型的構(gòu)建塊(bytecodes as small building blocks),這些構(gòu)建塊組合在一起構(gòu)成任何 JavaScript 功能。V8 有數(shù)以百計的字節(jié)碼。比如 Add 或 TypeOf 這樣的操作符,或者像 LdaNamedProperty 這樣的屬性加載符,還有很多類似的字節(jié)碼。V8還有一些非常特殊的字節(jié)碼,如 CreateObjectLiteral 或 SuspendGenerator。頭文件bytecodes.h(https://github.com/v8/v8/blob/master/src/interpreter/bytecodes.h) 定義了 V8 字節(jié)碼的完整列表。

          在早期的V8引擎里,在多數(shù)瀏覽器都是基于字節(jié)碼的,V8引擎偏偏跳過這一步,直接將jS編譯成機(jī)器碼,之所以這么做,就是節(jié)省了時間提高效率,但是后來發(fā)現(xiàn),太占用內(nèi)存了。最終又退回字節(jié)碼了,之所以這么做的動機(jī)是什么呢?

          • 減輕機(jī)器碼占用的內(nèi)存空間,即犧牲時間換空間(主要動機(jī))
          • 提高代碼的啟動速度 對 v8 的代碼進(jìn)行重構(gòu),
          • 降低 v8 的代碼復(fù)雜度

          每個字節(jié)碼指定其輸入和輸出作為寄存器操作數(shù)。Ignition 使用寄存器 r0,r1,r2,... 和累加器寄存器(accumulator register)。幾乎所有的字節(jié)碼都使用累加器寄存器。它像一個常規(guī)寄存器,除了字節(jié)碼沒有指定。例如,Add r1 將寄存器 r1 中的值和累加器中的值進(jìn)行加法運算。這使得字節(jié)碼更短,節(jié)省內(nèi)存。

          許多字節(jié)碼以 Lda 或 Sta 開頭。Lda 和 Stastands 中的 a 為累加器(accumulator)。例如,LdaSmi [42] 將小整數(shù)(Smi)42 加載到累加器寄存器中。Star r0 將當(dāng)前在累加器中的值存儲在寄存器 r0 中。

          以現(xiàn)在掌握的基礎(chǔ)知識,花點時間來看一個具有實際功能的字節(jié)碼。

          function incrementX(obj) {
           return 1 + obj.x;
          }
          incrementX({x: 42}); // V8 的編譯器是惰性的,如果一個函數(shù)沒有運行,V8 將不會解釋它
          

          如果要查看 V8 的 JavaScript 字節(jié)碼,可以使用在命令行參數(shù)中添加 --print-bytecode運行 D8 或Node.js(8.3 或更高版本)來打印。對于 Chrome,請從命令行啟動 Chrome,使用 --js-flags="--print-bytecode",請參考 Run Chromium with flags。

          $ node --print-bytecode incrementX.js
          ... [generating bytecode for function: incrementX] Parameter count 2 Frame size 8
           12 E> 0x2ddf8802cf6e @ StackCheck
           19 S> 0x2ddf8802cf6f @ LdaSmi [1]
           0x2ddf8802cf71 @ Star r0
           34 E> 0x2ddf8802cf73 @ LdaNamedProperty a0, [0], [4]
           28 E> 0x2ddf8802cf77 @ Add r0, [6]
           36 S> 0x2ddf8802cf7a @ Return
          Constant pool (size=1) 0x2ddf8802cf21: [FixedArray] in OldSpace
          - map=0x2ddfb2d02309 <Map(HOLEY_ELEMENTS)>
          - length: 1 0: 0x2ddf8db91611 <String[1]: x>
          Handler Table (size=16)
          

          我們忽略大部分輸出,專注于實際的字節(jié)碼。接下來我們來一起分析相關(guān)的關(guān)鍵字節(jié)碼:

          LdaSmi [1]

          LdaSmi [1] 將常量 1 加載到累加器中。

          Star r0

          接下來,Star r0 將當(dāng)前在累加器中的值 1 存儲在寄存器 r0 中。

          LdaNamedProperty a0, [0], [4]

          LdaNamedProperty 將 a0 的命名屬性加載到累加器中。ai 指向 incrementX() 的第 i 個參數(shù)。在這個例子中,我們在 a0 上查找一個命名屬性,這是 incrementX() 的第一個參數(shù)。該屬性名由常量 0 確定。LdaNamedProperty 使用 0 在單獨的表中查找名稱:

          - length: 1
           0: 0x2ddf8db91611 <String[1]: x>
          

          可以看到,0 映射到了 x。因此這行字節(jié)碼的意思是加載 obj.x。

          那么值為 4 的操作數(shù)是干什么的呢?它是函數(shù) incrementX() 的反饋向量的索引。反饋向量包含用于性能優(yōu)化的 runtime 信息。

          現(xiàn)在寄存器看起來是這樣的:

          Add r0, [6]

          最后一條指令將 r0 加到累加器,結(jié)果是 43。6 是反饋向量的另一個索引。

          Return 返回累加器中的值。返回語句是函數(shù) incrementX() 的結(jié)束。此時 incrementX() 的調(diào)用者可以在累加器中獲得值 43,并可以進(jìn)一步處理此值。

          V8引擎為啥這么快?

          由于JavaScript弱語言的特性(一個變量可以賦值不同的數(shù)據(jù)類型),同時很彈性,允許我們在任何時候在對象上新增或是刪除屬性和方法等, JavaScript語言非常動態(tài),我們可以想象會大大增加編譯引擎的難度,盡管十分困難,但卻難不倒V8引擎,v8引擎運用了好幾項技術(shù)達(dá)到加速的目的:

          內(nèi)聯(lián)(Inlining):

          內(nèi)聯(lián)特性是一切優(yōu)化的基礎(chǔ),對于良好的性能至關(guān)重要,所謂的內(nèi)聯(lián)就是如果某一個函數(shù)內(nèi)部調(diào)用其它的函數(shù),編譯器直接會將函數(shù)中的執(zhí)行內(nèi)容,替換函數(shù)方法。如下圖所示:

          如何理解呢?看如下代碼:

          function add(a, b) {
           return a + b;
          }
          function calculateTwoPlusFive() {
           var sum;
           for (var i=0; i <=1000000000; i++) {
           sum=add(2 + 5);
           }
          }
          var start=new Date();
          calculateTwoPlusFive();
          var end=new Date();
          var timeTaken=end.valueOf() - start.valueOf();
          console.log("Took " + timeTaken + "ms");
          

          由于內(nèi)聯(lián)屬性特性,在編譯前,代碼將會被優(yōu)化成:

          function add(a, b) {
           return a + b;
          }
          function calculateTwoPlusFive() {
           var sum;
           for (var i=0; i <=1000000000; i++) {
           sum=2 + 5;
           }
          }
          var start=new Date();
          calculateTwoPlusFive();
          var end=new Date();
          var timeTaken=end.valueOf() - start.valueOf();
          console.log("Took " + timeTaken + "ms");
          

          如果沒有內(nèi)聯(lián)屬性的特性,你能想象運行的有多慢嗎?把第一段JS代碼嵌入HTML文件里,我們用不同的瀏覽器打開(硬件環(huán)境:i7,16G內(nèi)存,mac系統(tǒng)),用safari打開如下圖所示,17秒:

          如果用Chrome打開,還不到1秒,快了16秒!

          隱藏類(Hidden class):

          例如C++/Java這種靜態(tài)類型語言的每一個變量,都有一個唯一確定的類型。因為有類型信息,一個對象包含哪些成員和這些成員在對象中的偏移量等信息,編譯階段就可確定,執(zhí)行時CPU只需要用對象首地址 —— 在C++中是this指針,加上成員在對象內(nèi)部的偏移量即可訪問內(nèi)部成員。這些訪問指令在編譯階段就生成了。

          但對于JavaScript這種動態(tài)語言,變量在運行時可以隨時由不同類型的對象賦值,并且對象本身可以隨時添加刪除成員。訪問對象屬性需要的信息完全由運行時決定。為了實現(xiàn)按照索引的方式訪問成員,V8“悄悄地”給運行中的對象分了類,在這個過程中產(chǎn)生了一種V8內(nèi)部的數(shù)據(jù)結(jié)構(gòu),即隱藏類。隱藏類本身是一個對象。

          考慮以下代碼:

          function Point(x, y) {
           this.x=x;
           this.y=y;
          }
          var p1=new Point(1, 2);
          

          如果new Point(1, 2)被調(diào)用,v8引擎就會創(chuàng)建一個引隱藏的類C0,如下圖所示:

          由于Point沒有定于任何屬性,因此“C0”為空

          一旦“this.x=x”被執(zhí)行,v8引擎就會創(chuàng)建一個名為“C1”的第二個隱藏類。基于“c0”,“c1”描述了可以找到屬性X的內(nèi)存中的位置(相當(dāng)指針)。在這種情況下,隱藏類則會從C0切換到C1,如下圖所示:

          每次向?qū)ο筇砑有碌膶傩詴r,舊的隱藏類會通過路徑轉(zhuǎn)換切換到新的隱藏類。由于轉(zhuǎn)換的重要性,因為引擎允許以相同的方式創(chuàng)建對象來共享隱藏類。如果兩個對象共享一個隱藏類的話,并且向兩個對象添加相同的屬性,轉(zhuǎn)換過程中將確保這兩個對象使用相同的隱藏類和附帶所有的代碼優(yōu)化。

          當(dāng)執(zhí)行this.y=y,將會創(chuàng)建一個C2的隱藏類,則隱藏類更改為C2。

          隱藏類的轉(zhuǎn)換的性能,取決于屬性添加的順序,如果添加順序的不同,效果則不同,如以下代碼:

          function Point(x, y) {
           this.x=x;
           this.y=y;
          }
          var p1=new Point(1, 2);
          p1.a=5;
          p1.b=6;
          var p2=new Point(3, 4);
          p2.b=7;
          p2.a=8;
          

          你可能以為P1、p2使用相同的隱藏類和轉(zhuǎn)換,其實不然。對于P1對象而言,隱藏類先a再b,對于p2而言,隱藏類則先b后a,最終會產(chǎn)生不同的隱藏類,增加編譯的運算開銷,這種情況下,應(yīng)該以相同的順序動態(tài)的修改對象屬性,以便可以復(fù)用隱藏類。

          內(nèi)聯(lián)緩存(Inline caching)

          正常訪問對象屬性的過程是:首先獲取隱藏類的地址,然后根據(jù)屬性名查找偏移值,然后計算該屬性的地址。雖然相比以往在整個執(zhí)行環(huán)境中查找減小了很大的工作量,但依然比較耗時。能不能將之前查詢的結(jié)果緩存起來,供再次訪問呢?當(dāng)然是可行的,這就是內(nèi)嵌緩存。

          內(nèi)嵌緩存的大致思路就是將初次查找的隱藏類和偏移值保存起來,當(dāng)下次查找的時候,先比較當(dāng)前對象是否是之前的隱藏類,如果是的話,直接使用之前的緩存結(jié)果,減少再次查找表的時間。當(dāng)然,如果一個對象有多個屬性,那么緩存失誤的概率就會提高,因為某個屬性的類型變化之后,對象的隱藏類也會變化,就與之前的緩存不一致,需要重新使用以前的方式查找哈希表。

          內(nèi)存管理

          內(nèi)存的管理組要由分配和回收兩個部分構(gòu)成。V8的內(nèi)存劃分如下:

          • Zone:管理小塊內(nèi)存。其先自己申請一塊內(nèi)存,然后管理和分配一些小內(nèi)存,當(dāng)一塊小內(nèi)存被分配之后,不能被Zone回收,只能一次性回收Zone分配的所有小內(nèi)存。當(dāng)一個過程需要很多內(nèi)存,Zone將需要分配大量的內(nèi)存,卻又不能及時回收,會導(dǎo)致內(nèi)存不足情況。
          • 堆:管理JavaScript使用的數(shù)據(jù)、生成的代碼、哈希表等。為方便實現(xiàn)垃圾回收,堆被分為三個部分:
          1. 年輕分代:為新創(chuàng)建的對象分配內(nèi)存空間,經(jīng)常需要進(jìn)行垃圾回收。為方便年輕分代中的內(nèi)容回收,可再將年輕分代分為兩半,一半用來分配,另一半在回收時負(fù)責(zé)將之前還需要保留的對象復(fù)制過來。
          2. 年老分代:根據(jù)需要將年老的對象、指針、代碼等數(shù)據(jù)保存起來,較少地進(jìn)行垃圾回收。
          3. 大對象:為那些需要使用較多內(nèi)存對象分配內(nèi)存,當(dāng)然同樣可能包含數(shù)據(jù)和代碼等分配的內(nèi)存,一個頁面只分配一個對象。

          垃圾回收

          V8 使用了分代和大數(shù)據(jù)的內(nèi)存分配,在回收內(nèi)存時使用精簡整理的算法標(biāo)記未引用的對象,然后消除沒有標(biāo)記的對象,最后整理和壓縮那些還未保存的對象,即可完成垃圾回收。為了控制 GC 成本并使執(zhí)行更加穩(wěn)定, V8 使用增量標(biāo)記, 而不是遍歷整個堆, 它試圖標(biāo)記每個可能的對象, 它只遍歷一部分堆, 然后恢復(fù)正常的代碼執(zhí)行. 下一次 GC 將繼續(xù)從之前的遍歷停止的位置開始. 這允許在正常執(zhí)行期間非常短的暫停. 如前所述, 掃描階段由單獨的線程處理.

          優(yōu)化回退

          V8 為了進(jìn)一步提升JavaScript代碼的執(zhí)行效率,編譯器生直接生成更高效的機(jī)器碼。程序在運行時,V8會采集JavaScript代碼運行數(shù)據(jù)。當(dāng)V8發(fā)現(xiàn)某函數(shù)執(zhí)行頻繁(內(nèi)聯(lián)函數(shù)機(jī)制),就將其標(biāo)記為熱點函數(shù)。針對熱點函數(shù),V8的策略較為樂觀,傾向于認(rèn)為此函數(shù)比較穩(wěn)定,類型已經(jīng)確定,于是編譯器,生成更高效的機(jī)器碼。后面的運行中,萬一遇到類型變化,V8采取將JavaScript函數(shù)回退到優(yōu)化前的編譯成機(jī)器字節(jié)碼。如以下代碼:

          function add(a, b) {
           return a + b
          }
          for (var i=0; i < 10000; ++i) {
           add(i, i);
          }
          add('a', 'b');//千萬別這么做!
          

          再來看下面的一個例子:

          // 片段 1
          var person={
           add: function (a, b) {
           return a + b;
           }
           };
          obj.name='li';
          // 片段 2
          var person={
           add: function (a, b) {
           return a + b;
           },
           name: 'li'
           };
          

          以上代碼實現(xiàn)的功能相同,都是定義了一個對象,這個對象具有一個屬性name和一個方法add()。但使用片段2的方式效率更高。片段1給對象obj添加了一個屬性name,這會造成隱藏類的派生。給對象動態(tài)地添加和刪除屬性都會派生新的隱藏類。假如對象的add函數(shù)已經(jīng)被優(yōu)化,生成了更高效的代碼,則因為添加或刪除屬性,這個改變后的對象無法使用優(yōu)化后的代碼。

          從例子中我們可以看出:

          函數(shù)內(nèi)部的參數(shù)類型越確定,V8越能夠生成優(yōu)化后的代碼。

          家好,我是皮皮。

          前言

          對于前端來說,HTML 都是最基礎(chǔ)的內(nèi)容。

          今天,我們來了解一下 HTML 和網(wǎng)頁有什么關(guān)系,以及與 DOM 有什么不同。通過本講內(nèi)容,你將掌握瀏覽器是怎么處理 HTML 內(nèi)容的,以及在這個過程中我們可以進(jìn)行怎樣的處理來提升網(wǎng)頁的性能,從而提升用戶的體驗。


          一、瀏覽器頁面加載過程

          不知你是否有過這樣的體驗:當(dāng)打開某個瀏覽器的時候,發(fā)現(xiàn)一直在轉(zhuǎn)圈,或者等了好長時間才打開頁面……

          此時的你,會選擇關(guān)掉頁面還是耐心等待呢?

          這一現(xiàn)象,除了網(wǎng)絡(luò)不穩(wěn)定、網(wǎng)速過慢等原因,大多數(shù)都是由于頁面設(shè)計不合理導(dǎo)致加載時間過長導(dǎo)致的。

          我們都知道,頁面是用 HTML/CSS/JavaScript 來編寫的。

          • HTML 的職責(zé)在于告知瀏覽器如何組織頁面,以及搭建頁面的基本結(jié)構(gòu);
          • CSS 用來裝飾 HTML,讓我們的頁面更好看;
          • JavaScript 則可以豐富頁面功能,使靜態(tài)頁面動起來。

          HTML由一系列的元素組成,通常稱為HTML元素。HTML 元素通常被用來定義一個網(wǎng)頁結(jié)構(gòu),基本上所有網(wǎng)頁都是這樣的 HTML 結(jié)構(gòu):

          <html>
              <head></head>
              <body></body>
          </html>

          其中:

          • html元素是頁面的根元素,它描述完整的網(wǎng)頁;
          • head元素包含了我們想包含在 HTML 頁面中,但不希望顯示在網(wǎng)頁里的內(nèi)容;
          • body元素包含了我們訪問頁面時所有顯示在頁面上的內(nèi)容,是用戶最終能看到的內(nèi)容;


          HTML 中的元素特別多,其中還包括可用于 Web Components 的自定義元素。

          前面我們提到頁面 HTML 結(jié)構(gòu)不合理可能會導(dǎo)致頁面響應(yīng)慢,這個過程很多時候體現(xiàn)在<script><style>元素的設(shè)計上,它們會影響頁面加載過程中對 Javascript 和 CSS 代碼的處理。

          因此,如果想要提升頁面的加載速度,就需要了解瀏覽器頁面的加載過程是怎樣的,從根本上來解決問題。

          瀏覽器在加載頁面的時候會用到 GUI 渲染線程和 JavaScript 引擎線程(更詳細(xì)的瀏覽器加載和渲染機(jī)制將在第 7 講中介紹)。其中,GUI 渲染線程負(fù)責(zé)渲染瀏覽器界面 HTML 元素,JavaScript 引擎線程主要負(fù)責(zé)處理 JavaScript 腳本程序。

          由于 JavaScript 在執(zhí)行過程中還可能會改動界面結(jié)構(gòu)和樣式,因此它們之間被設(shè)計為互斥的關(guān)系。也就是說,當(dāng) JavaScript 引擎執(zhí)行時,GUI 線程會被掛起。

          以網(wǎng)易云課堂官網(wǎng)為例,我們來看看網(wǎng)頁加載流程。

          (1)當(dāng)我們打開官網(wǎng)的時候,瀏覽器會從服務(wù)器中獲取到 HTML 內(nèi)容。

          (2)瀏覽器獲取到 HTML 內(nèi)容后,就開始從上到下解析 HTML 的元素。

          (3)<head>元素內(nèi)容會先被解析,此時瀏覽器還沒開始渲染頁面。

          我們看到<head>元素里有用于描述頁面元數(shù)據(jù)的<meta>元素,還有一些<link>元素涉及外部資源(如圖片、CSS 樣式等),此時瀏覽器會去獲取這些外部資源。除此之外,我們還能看到<head>元素中還包含著不少的<script>元素,這些<script>元素通過src屬性指向外部資源。

          (4)當(dāng)瀏覽器解析到這里時(步驟 3),會暫停解析并下載 JavaScript 腳本。

          (5)當(dāng) JavaScript 腳本下載完成后,瀏覽器的控制權(quán)轉(zhuǎn)交給 JavaScript 引擎。當(dāng)腳本執(zhí)行完成后,控制權(quán)會交回給渲染引擎,渲染引擎繼續(xù)往下解析 HTML 頁面。

          (6)此時<body>元素內(nèi)容開始被解析,瀏覽器開始渲染頁面。

          在這個過程中,我們看到<head>中放置的<script>元素會阻塞頁面的渲染過程:把 JavaScript 放在<head>里,意味著必須把所有 JavaScript 代碼都下載、解析和解釋完成后,才能開始渲染頁面。

          到這里,我們就明白了:如果外部腳本加載時間很長(比如一直無法完成下載),就會造成網(wǎng)頁長時間失去響應(yīng),瀏覽器就會呈現(xiàn)“假死”狀態(tài),用戶體驗會變得很糟糕。

          因此,對于對性能要求較高、需要快速將內(nèi)容呈現(xiàn)給用戶的網(wǎng)頁,常常會將 JavaScript 腳本放在<body>的最后面。這樣可以避免資源阻塞,頁面得以迅速展示。我們還可以使用defer/async/preload等屬性來標(biāo)記<script>標(biāo)簽,來控制 JavaScript 的加載順序。

          百度首頁

          三、DOM 解析

          對于百度這樣的搜索引擎來說,必須要在最短的時間內(nèi)提供到可用的服務(wù)給用戶,其中就包括搜索框的顯示及可交互,除此之外的內(nèi)容優(yōu)先級會相對較低。

          瀏覽器在渲染頁面的過程需要解析 HTML、CSS 以得到 DOM 樹和 CSS 規(guī)則樹,它們結(jié)合后才生成最終的渲染樹并渲染。因此,我們還常常將 CSS 放在<head>里,可用來避免瀏覽器渲染的重復(fù)計算。


          二、HTML 與 DOM 有什么不同

          我們知道<p>是 HTML 元素,但又常常將<p>這樣一個元素稱為 DOM 節(jié)點,那么 HTML 和 DOM 到底有什么不一樣呢?

          根據(jù) MDN 官方描述:文檔對象模型(DOM)是 HTML 和 XML 文檔的編程接口。

          也就是說,DOM 是用來操作和描述 HTML 文檔的接口。如果說瀏覽器用 HTML 來描述網(wǎng)頁的結(jié)構(gòu)并渲染,那么使用 DOM 則可以獲取網(wǎng)頁的結(jié)構(gòu)并進(jìn)行操作。一般來說,我們使用 JavaScript 來操作 DOM 接口,從而實現(xiàn)頁面的動態(tài)變化,以及用戶的交互操作。

          在開發(fā)過程中,常常用對象的方式來描述某一類事物,用特定的結(jié)構(gòu)集合來描述某些事物的集合。DOM 也一樣,它將 HTML 文檔解析成一個由 DOM 節(jié)點以及包含屬性和方法的相關(guān)對象組成的結(jié)構(gòu)集合。


          三、DOM 解析

          我們常見的 HTML 元素,在瀏覽器中會被解析成節(jié)點。比如下面這樣的 HTML 內(nèi)容:

          <html>
              <head>
                  <title>標(biāo)題</title>
              </head>
              <body>
                  <a href='xx.com'>我的超鏈接</a>
                  <h1>頁面第一標(biāo)題</h1>
              </body>
          </html>

          打開控制臺 Elements 面板,可以看到這樣的 HTML 結(jié)構(gòu),如下圖所示:

          在瀏覽器中,上面的 HTML 會被解析成這樣的 DOM 樹,如下圖所示:


          我們都知道,對于樹狀結(jié)構(gòu)來說,常常使用parent/child/sibling等方式來描述各個節(jié)點之間的關(guān)系,對于 DOM 樹也不例外。

          舉個例子,我們常常會對頁面功能進(jìn)行抽象,并封裝成組件。但不管怎么進(jìn)行整理,頁面最終依然是基于 DOM 的樹狀結(jié)構(gòu),因此組件也是呈樹狀結(jié)構(gòu),組件間的關(guān)系也同樣可以使用parent/child/sibling這樣的方式來描述。同時,現(xiàn)在大多數(shù)應(yīng)用程序同樣以root為根節(jié)點展開,我們進(jìn)行狀態(tài)管理、數(shù)據(jù)管理也常常會呈現(xiàn)出樹狀結(jié)構(gòu)。


          四、事件委托

          我們知道,瀏覽器中各個元素從頁面中接收事件的順序包括事件捕獲階段、目標(biāo)階段、事件冒泡階段。其中,基于事件冒泡機(jī)制,我們可以實現(xiàn)將子元素的事件委托給父級元素來進(jìn)行處理,這便是事件委托。

          如果我們在每個元素上都進(jìn)行監(jiān)聽的話,則需要綁定三個事件;(假設(shè)頁面上有a,b,c三個兄弟節(jié)點)

          function clickEventFunction(e) {
            console.log(e.target===this); // logs `true`
            // 這里可以用 this 獲取當(dāng)前元素
          }
          // 元素a,b,c綁定
          element2.addEventListener("click", clickEventFunction, false);
          element5.addEventListener("click", clickEventFunction, false);
          element8.addEventListener("click", clickEventFunction, false);

          使用事件委托,可以通過將事件添加到它們的父節(jié)點,而將事件委托給父節(jié)點來觸發(fā)處理函數(shù):

          function clickEventFunction(event) {
            console.log(e.target===this); // logs `false`
            // 獲取被點擊的元素
            const eventTarget=event.target;
            // 檢查源元素`event.target`是否符合預(yù)期
            // 此處控制廣告面板的展示內(nèi)容
          }
          // 元素1綁定
          element1.addEventListener("click", clickEventFunction, false);

          這樣能解決什么問題呢?

          • 綁定子元素會綁定很多次的事件,而綁定父元素只需要一次綁定。
          • 將事件委托給父節(jié)點,這樣我們對子元素的增加和刪除、移動等,都不需要重新進(jìn)行事件綁定。

          常見的使用方式主要是上述這種列表結(jié)構(gòu),每個選項都可以進(jìn)行編輯、刪除、添加標(biāo)簽等功能,而把事件委托給父元素,不管我們新增、刪除、更新選項,都不需要手動去綁定和移除事件。

          如果在列表數(shù)量內(nèi)容較大的時候,對成千上萬節(jié)點進(jìn)行事件監(jiān)聽,也是不小的性能消耗。使用事件委托的方式,我們可以大量減少瀏覽器對元素的監(jiān)聽,也是在前端性能優(yōu)化中比較簡單和基礎(chǔ)的一個做法。

          注意:

          1. 如果我們直接在document.body上進(jìn)行事件委托,可能會帶來額外的問題;
          2. 由于瀏覽器在進(jìn)行頁面渲染的時候會有合成的步驟,合成的過程會先將頁面分成不同的合成層,而用戶與瀏覽器進(jìn)行交互的時候需要接收事件。此時,瀏覽器會將頁面上具有事件處理程序的區(qū)域進(jìn)行標(biāo)記,被標(biāo)記的區(qū)域會與主線程進(jìn)行通信。
          3. 如果我們document.body上被綁定了事件,這時候整個頁面都會被標(biāo)記;
          4. 即使我們的頁面不關(guān)心某些部分的用戶交互,合成器線程也必須與主線程進(jìn)行通信,并在每次事件發(fā)生時進(jìn)行等待。這種情況,我們可以使用passive: true選項來解決


          五、總結(jié)

          我們了解了 HTML 的作用,以及它是如何影響瀏覽器中頁面的加載過程的,同時還介紹了使用 DOM 接口來控制 HTML 的展示和功能邏輯。我們了解了DOM解析事件委托等相關(guān)概念。

          其說我愛Javascript,不如說我恨它。它是c語言和self語言lIQ的產(chǎn)物。18世紀(jì)英國文學(xué)家約翰遜博士說得好,它的優(yōu)秀之處并非原創(chuàng),它的原創(chuàng)之處并不優(yōu)秀。這句話出自Javascript的創(chuàng)造者布蘭登艾奇。

          作為Javascript的發(fā)明人,為什么不為其感到驕傲而說出這樣的話?因為他對Java一點興趣也沒有,只是為了應(yīng)付公司安排的任務(wù),他只用10天時間就把Javascript設(shè)計出來了。雖然設(shè)計初期存在諸多不夠嚴(yán)謹(jǐn)?shù)牡胤剑@并不影響它在之后成為世界上使用最為廣泛的語言之一。

          故事的序幕在1992年緩緩拉開,當(dāng)時一家名為numbers的公司研發(fā)出了一種名為c簡簡的嵌入式腳本語言。它的初衷是創(chuàng)造一個功能強大到足以取代宏操作的腳本語言,并且與c語言保持高度的相似性,從而降低開發(fā)人員的學(xué)習(xí)門檻。

          這款語言最初與一款名為CMV的共享工具一同推出,然而因為mm這個詞在某種語境下聽起來顯得過于消極,同時字母c也被認(rèn)為令人畏懼。numbers公司最終決定將CMM更名為scriptes。

          隨著時間的推移,numbers公司看準(zhǔn)了當(dāng)時勢頭正盛的Nanscape瀏覽器,這款瀏覽器在90年代的市場份額一度高達(dá)九成。于是他們?yōu)镹etscape瀏覽器開發(fā)了一個可以嵌入網(wǎng)頁的CNV版本,這也標(biāo)志著scriptes成為了歷史上首個客戶端腳本語言。

          在那個時代,上網(wǎng)沖浪剛剛興起并日益普及,當(dāng)時大多數(shù)人還依賴著速度約為28CBTS的調(diào)制解調(diào)器接入網(wǎng)絡(luò)。隨著網(wǎng)頁內(nèi)容逐漸豐富和復(fù)雜化,瀏覽速度開始顯著下降。更糟糕的是,由于缺乏瀏覽器腳本語言,即便是簡單的表單有效性驗證也需要客戶端與服務(wù)器之間頻繁交互。這常常導(dǎo)致用戶在提交表單后,經(jīng)過漫長的30秒等待,卻只收到一個自斷無效的提示,這無疑讓人倍感沮喪。

          受到scriptist的啟發(fā),行業(yè)領(lǐng)軍者Netscape公司開始深入思考,尋求一種客戶端腳本語言來解決這一難題。Nanscape公司內(nèi)的Brandon ad接受了這個挑戰(zhàn),他的任務(wù)是為即將在1995年發(fā)布的Nanscape Navigator2.0版本開發(fā)一個名為liver的腳本語言。不久后這個語言更名為Newscript,其初衷是為非專業(yè)開發(fā)人員提供一個便捷的工具,使得沒有編程背景的網(wǎng)站設(shè)計者也能輕松使用,因此一個簡單易學(xué)的弱類型動態(tài)解釋語言應(yīng)運而生。

          Brandon后來回憶到,他從未想到當(dāng)年吳昕設(shè)計的一個語言竟然會發(fā)展成為如今最流行的腳本語言,因此它也被譽為Javascript之父。

          那為啥叫Javascript?在Netscape籌備開發(fā)瀏覽器腳本語言之際,一個關(guān)鍵事件悄然發(fā)生。1995年sun公司推出了重命名的0C語言,即Java并大力推廣javablit的概念,這是一種能在瀏覽器中運行的客戶端組件,與我們今天所熟知的Javascript的應(yīng)用形態(tài)頗為相似。

          Netscape看到了Java的潛力,決定與Sam公司合作讓Java程序以EVID的形式直接在瀏覽器中運行,他們甚至一度考慮將Java直接遷入網(wǎng)頁作為腳本語言。然而由于這會使html網(wǎng)頁變得復(fù)雜運行緩慢且操作繁瑣,最終這個計劃被放棄。

          然而當(dāng)時Mascape的管理層對Java的熱情不減,這也間接影響了即將誕生的腳本語言的命運。在這個關(guān)鍵時刻,34歲的Brandoni接下了這項重任。Netsk高層對他的要求是未來的腳本語言必須與JOVO保持一定的相似性,但要比Java更簡單,以便更多人能夠輕松上手。

          然而Brandon對Java并無太大興趣,如果不是公司的決策,他或許不會選擇Java作為Javascript的設(shè)計原型。為了完成任務(wù),他在短短10天內(nèi)設(shè)計出了new script,他的設(shè)計理念融合了c語言的基本語法朝瓦的數(shù)據(jù)類型與內(nèi)存管理,同時提升了函數(shù)的地位,并借鑒了self語言的基于原型的繼承機(jī)制,這也使得Javascript成為了一個獨特的結(jié)合體簡化的函數(shù)式編程與簡化的面向?qū)ο缶幊痰慕蝗凇?/p>

          然而Brandon本人對這個作品并不滿意,他曾表示與其說我愛Javascript,不如說我恨它。它是c語言和self語言結(jié)合的產(chǎn)物。隨后Netscape與sun公司合作完成了new script的實現(xiàn)。在Netscape Navigator2.0發(fā)布之前,為了獲取sun的支持,并借助Java這一當(dāng)時的熱門詞匯,Newscript更名為Javascript。這個名稱僅僅是Netscape公司的一個市場決策,然而他們未曾預(yù)料到這個決策會帶來如此大的負(fù)面影響。

          多年來人們常常混淆Java和Javascript這兩種毫不相干的語言,實際上它們僅僅是名字相似且有著一些公司層面的歷史聯(lián)系而已。Brandon IG對此深感遺憾,他在10年后的演講Javascriptat ten years中京告到不要讓市場營銷決定你的語言名稱。


          主站蜘蛛池模板: 国产一区二区三区高清视频 | 亚洲综合无码精品一区二区三区| 亚洲一本一道一区二区三区| 日本片免费观看一区二区| 亚洲AV成人一区二区三区观看 | 亚洲AV无一区二区三区久久| 中文字幕日韩一区| 久久久久国产一区二区| 国产日本一区二区三区| 国产精品xxxx国产喷水亚洲国产精品无码久久一区 | 日本一区二区三区在线视频观看免费 | 好吊视频一区二区三区| 国内精品视频一区二区三区八戒| 曰韩精品无码一区二区三区| 影院成人区精品一区二区婷婷丽春院影视 | 一区二区在线视频免费观看| 精品免费久久久久国产一区| 人妻久久久一区二区三区 | 国产精品福利一区二区久久| 久久久久人妻精品一区二区三区| 精品少妇人妻AV一区二区| 亚洲AV美女一区二区三区| 国产一区美女视频| 国产一区在线mmai| 一区高清大胆人体| 成人一区二区免费视频| 日本一区二区三区免费高清 | 久久无码一区二区三区少妇| 成人一区二区三区视频在线观看| 2014AV天堂无码一区| 人成精品视频三区二区一区| 精品午夜福利无人区乱码一区| 亚洲综合av永久无码精品一区二区| 少妇一夜三次一区二区| 亚洲一区动漫卡通在线播放| 超清无码一区二区三区| 少妇激情一区二区三区视频| 91精品一区二区三区在线观看| 亚洲爆乳无码一区二区三区| 大屁股熟女一区二区三区| 中文字幕精品亚洲无线码一区应用|