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 亚洲国产中文在线,手机看片日韩国产一区二区,国产成人综合95精品视频免费

          整合營銷服務商

          電腦端+手機端+微信端=數據同步管理

          免費咨詢熱線:

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

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

          要混淆JavaScipt與瀏覽器

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

          JavaScipt運行環境一般都有宿主環境和執行期環境。如下圖所示:

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

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

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

          重溫編譯原理

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

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

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

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

          解析/語法分析(Parsing)

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

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

          語法分析的過程就是把詞法分析所產生的記號生成語法樹,通俗地說,就是把從程序中收集的信息存儲到數據結構中。注意,在編譯中用到的數據結構有兩種:符號表和語法樹。

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

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

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

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

          經過編譯階段的準備, JavaScript代碼在內存中已經被構建為語法樹,然后 JavaScript引擎就會根據這個語法樹結構邊解釋邊執行。

          代碼生成

          將AST轉換成可執行代碼的過程被稱為代碼生成。這個過程與語言、目標平臺相關。

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

          神秘的編譯器——V8引擎

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

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

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

          V8曾經有兩個編譯器

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

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

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

          V8引擎還在內部使用多個線程:

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

          字節碼

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

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

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

          • 減輕機器碼占用的內存空間,即犧牲時間換空間(主要動機)
          • 提高代碼的啟動速度 對 v8 的代碼進行重構,
          • 降低 v8 的代碼復雜度

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

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

          以現在掌握的基礎知識,花點時間來看一個具有實際功能的字節碼。

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

          如果要查看 V8 的 JavaScript 字節碼,可以使用在命令行參數中添加 --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)
          

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

          LdaSmi [1]

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

          Star r0

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

          LdaNamedProperty a0, [0], [4]

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

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

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

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

          現在寄存器看起來是這樣的:

          Add r0, [6]

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

          Return 返回累加器中的值。返回語句是函數 incrementX() 的結束。此時 incrementX() 的調用者可以在累加器中獲得值 43,并可以進一步處理此值。

          V8引擎為啥這么快?

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

          內聯(Inlining):

          內聯特性是一切優化的基礎,對于良好的性能至關重要,所謂的內聯就是如果某一個函數內部調用其它的函數,編譯器直接會將函數中的執行內容,替換函數方法。如下圖所示:

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

          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");
          

          由于內聯屬性特性,在編譯前,代碼將會被優化成:

          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");
          

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

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

          隱藏類(Hidden class):

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

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

          考慮以下代碼:

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

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

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

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

          每次向對象添加新的屬性時,舊的隱藏類會通過路徑轉換切換到新的隱藏類。由于轉換的重要性,因為引擎允許以相同的方式創建對象來共享隱藏類。如果兩個對象共享一個隱藏類的話,并且向兩個對象添加相同的屬性,轉換過程中將確保這兩個對象使用相同的隱藏類和附帶所有的代碼優化。

          當執行this.y=y,將會創建一個C2的隱藏類,則隱藏類更改為C2。

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

          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使用相同的隱藏類和轉換,其實不然。對于P1對象而言,隱藏類先a再b,對于p2而言,隱藏類則先b后a,最終會產生不同的隱藏類,增加編譯的運算開銷,這種情況下,應該以相同的順序動態的修改對象屬性,以便可以復用隱藏類。

          內聯緩存(Inline caching)

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

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

          內存管理

          內存的管理組要由分配和回收兩個部分構成。V8的內存劃分如下:

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

          垃圾回收

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

          優化回退

          V8 為了進一步提升JavaScript代碼的執行效率,編譯器生直接生成更高效的機器碼。程序在運行時,V8會采集JavaScript代碼運行數據。當V8發現某函數執行頻繁(內聯函數機制),就將其標記為熱點函數。針對熱點函數,V8的策略較為樂觀,傾向于認為此函數比較穩定,類型已經確定,于是編譯器,生成更高效的機器碼。后面的運行中,萬一遇到類型變化,V8采取將JavaScript函數回退到優化前的編譯成機器字節碼。如以下代碼:

          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'
           };
          

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

          從例子中我們可以看出:

          函數內部的參數類型越確定,V8越能夠生成優化后的代碼。

          家好,我是皮皮。

          前言

          對于前端來說,HTML 都是最基礎的內容。

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


          一、瀏覽器頁面加載過程

          不知你是否有過這樣的體驗:當打開某個瀏覽器的時候,發現一直在轉圈,或者等了好長時間才打開頁面……

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

          這一現象,除了網絡不穩定、網速過慢等原因,大多數都是由于頁面設計不合理導致加載時間過長導致的。

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

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

          HTML由一系列的元素組成,通常稱為HTML元素。HTML 元素通常被用來定義一個網頁結構,基本上所有網頁都是這樣的 HTML 結構:

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

          其中:

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


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

          前面我們提到頁面 HTML 結構不合理可能會導致頁面響應慢,這個過程很多時候體現在<script><style>元素的設計上,它們會影響頁面加載過程中對 Javascript 和 CSS 代碼的處理。

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

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

          由于 JavaScript 在執行過程中還可能會改動界面結構和樣式,因此它們之間被設計為互斥的關系。也就是說,當 JavaScript 引擎執行時,GUI 線程會被掛起。

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

          (1)當我們打開官網的時候,瀏覽器會從服務器中獲取到 HTML 內容。

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

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

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

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

          (5)當 JavaScript 腳本下載完成后,瀏覽器的控制權轉交給 JavaScript 引擎。當腳本執行完成后,控制權會交回給渲染引擎,渲染引擎繼續往下解析 HTML 頁面。

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

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

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

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

          百度首頁

          三、DOM 解析

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

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


          二、HTML 與 DOM 有什么不同

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

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

          也就是說,DOM 是用來操作和描述 HTML 文檔的接口。如果說瀏覽器用 HTML 來描述網頁的結構并渲染,那么使用 DOM 則可以獲取網頁的結構并進行操作。一般來說,我們使用 JavaScript 來操作 DOM 接口,從而實現頁面的動態變化,以及用戶的交互操作。

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


          三、DOM 解析

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

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

          打開控制臺 Elements 面板,可以看到這樣的 HTML 結構,如下圖所示:

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


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

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


          四、事件委托

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

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

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

          使用事件委托,可以通過將事件添加到它們的父節點,而將事件委托給父節點來觸發處理函數:

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

          這樣能解決什么問題呢?

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

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

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

          注意:

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


          五、總結

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

          其說我愛Javascript,不如說我恨它。它是c語言和self語言lIQ的產物。18世紀英國文學家約翰遜博士說得好,它的優秀之處并非原創,它的原創之處并不優秀。這句話出自Javascript的創造者布蘭登艾奇。

          作為Javascript的發明人,為什么不為其感到驕傲而說出這樣的話?因為他對Java一點興趣也沒有,只是為了應付公司安排的任務,他只用10天時間就把Javascript設計出來了。雖然設計初期存在諸多不夠嚴謹的地方,但這并不影響它在之后成為世界上使用最為廣泛的語言之一。

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

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

          隨著時間的推移,numbers公司看準了當時勢頭正盛的Nanscape瀏覽器,這款瀏覽器在90年代的市場份額一度高達九成。于是他們為Netscape瀏覽器開發了一個可以嵌入網頁的CNV版本,這也標志著scriptes成為了歷史上首個客戶端腳本語言。

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

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

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

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

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

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

          然而Brandon對Java并無太大興趣,如果不是公司的決策,他或許不會選擇Java作為Javascript的設計原型。為了完成任務,他在短短10天內設計出了new script,他的設計理念融合了c語言的基本語法朝瓦的數據類型與內存管理,同時提升了函數的地位,并借鑒了self語言的基于原型的繼承機制,這也使得Javascript成為了一個獨特的結合體簡化的函數式編程與簡化的面向對象編程的交融。

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

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


          主站蜘蛛池模板: 在线视频一区二区三区四区| 久久久久人妻精品一区| 国产精品被窝福利一区| 国产精品特级毛片一区二区三区| AV鲁丝一区鲁丝二区鲁丝三区| 国产高清在线精品一区小说| 成人精品视频一区二区三区尤物| 午夜影视日本亚洲欧洲精品一区 | 国产一区二区三区福利| 在线播放国产一区二区三区 | 色噜噜狠狠一区二区三区| 国产婷婷一区二区三区| 国产精品av一区二区三区不卡蜜| 日韩爆乳一区二区无码| 久久精品一区二区免费看| 国产麻豆精品一区二区三区v视界| 动漫精品专区一区二区三区不卡| 国产成人一区二区在线不卡| 午夜视频在线观看一区| 中文字幕乱码人妻一区二区三区 | 国产精品分类视频分类一区 | 国产A∨国片精品一区二区| 国产丝袜一区二区三区在线观看| 少妇人妻精品一区二区| 精品国产一区二区三区久| 性色AV一区二区三区无码| 国产精品一区二区电影| 国产成人一区二区动漫精品 | 亚欧在线精品免费观看一区| 91在线视频一区| 亚无码乱人伦一区二区| 日韩aⅴ人妻无码一区二区| 一区二区三区观看| 制服丝袜一区二区三区| 国产精品视频一区二区噜噜| 无码少妇一区二区| 亚洲欧美日韩一区二区三区| 色综合久久一区二区三区| 白丝爆浆18禁一区二区三区 | 亚洲欧美日韩一区二区三区在线 | 精品国产福利第一区二区三区|