天天看點

前端面試總結七

1.JS的事件冒泡和事件捕獲

事件

事件是文檔和浏覽器視窗中發生的特定的互動瞬間。事件是javascript應用跳動的心髒,也是把所有東西黏在一起的膠水,當我們與浏覽器中web頁面進行某些類型的互動時,事件就發生了。

事件可能是使用者在某些内容上的點選,滑鼠經過某個特定元素或按下鍵盤上的某些按鍵,事件還可能是web浏覽器中發生的事情,比如說某個web頁面加載完成,或者是使用者滾動視窗或改變視窗大小。

事件流:

事件流描述的是從頁面中接受事件的順序,但有意思的是,微軟(IE)和網景(Netscape)開發團隊居然提出了兩個截然相反的事件流概念,IE的事件流是事件冒泡流(event bubbling),而Netscape的事件流是事件捕獲流(event capturing)。

事件冒泡

IE提出的事件流叫做事件冒泡,即事件開始時由最具體的元素接收,然後逐級向上傳播到較為不具體的節點,看一下以下示例:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body onclick="bodyClick()">

    <div onclick="divClick()">
        <button onclick="btn()">
            <p onclick="p()">點選冒泡</p>
        </button>
    </div>
    <script>
       
       function p(){
          console.log('p标簽被點選')
       }
        function btn(){
            console.log("button被點選")
        }
         function divClick(event){
             console.log('div被點選');
         }
        function bodyClick(){
            console.log('body被點選')
        }

    </script>

</body>
</html>
           

接下來我們點選一下頁面上的p元素,看看會發生什麼:

前端面試總結七

正如上面我們所說的,它會從一個最具體的的元素接收,然後逐級向上傳播, p=>button=>div=>body…事件冒泡可以形象地比喻為把一顆石頭投入水中,泡泡會一直從水底冒出水面。

事件捕獲

網景公司提出的事件流叫事件捕獲流。

事件捕獲流的思想是不太具體的DOM節點應該更早接收到事件,而最具體的節點應該最後接收到事件,針對上面同樣的例子,點選按鈕,那麼此時click事件會按照這樣傳播:(下面我們就借用addEventListener的第三個參數來模拟事件捕獲流)。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

<div>
    <button>
        <p>點選捕獲</p>
    </button>
</div>
<script>
    var oP=document.querySelector('p');
    var oB=document.querySelector('button');
    var oD=document.querySelector('div');
    var oBody=document.querySelector('body');

    oP.addEventListener('click',function(){
        console.log('p标簽被點選')
    },true);

    oB.addEventListener('click',function(){
        console.log("button被點選")
    },true);

    oD.addEventListener('click',  function(){
        console.log('div被點選')
    },true);

    oBody.addEventListener('click',function(){
        console.log('body被點選')
    },true);

</script>



</body>
</html>
           

同樣我們看一下背景的列印結果:

前端面試總結七

正如我們看到的,和冒泡流萬全相反,從最不具體的元素接收到最具體的元素接收事件 body=>div=>button=>p

DOM事件流:

‘DOM2級事件’規定的事件流包含3個階段,事件捕獲階段、處于目标階段、事件冒泡階段。首先發生的事件捕獲為截獲事件提供機會,然後是實際的目标接收事件,最後一個階段是事件冒泡階段,可以在這個階段對事件做出響應。

   在DOM事件流中,事件的目标在捕獲階段不會接收到事件,這意味着在捕獲階段事件從document到

就停止了,下個階段是處于目标階段,于是事件在

上發生,并在事件進行中被看成冒泡階段的一部分,然後,冒泡階段發生,事件又傳播回document。

下面是我們模拟它的示例:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<button id="btn">DOM事件流</button>
<script>
var btn=document.getElementById("btn");
btn.onclick=function(event){
console.log("div 處于目标階段");
};
document.body.addEventListener("click",function(event){
console.log("event bubble 事件冒泡");
},false);
document.body.addEventListener("click",function(event){
console.log("event catch 事件捕獲");
},true);
</script>


</body>
</html>
           

看看背景給出什麼結果:

前端面試總結七
前端面試總結七

就是這樣一個流程,先捕獲,然後處理,然後再冒泡出去。

2.JavaScript中var、let和const的差別

在ES6(ES2015)出現之前,JavaScript中聲明變量就隻有通過 var 關鍵字,函數聲明是通過 function 關鍵字,而在ES6之後,聲明的方式有 var 、 let 、 const 、 function 、 class ,本文主要讨論 var 、 let 和 const 之間的差別。

var

如果使用關鍵字 var 聲明一個變量,那麼這個變量就屬于目前的函數作用域,如果聲明是發生在任何函數外的頂層聲明,那麼這個變量就屬于全局作用域。舉例說明:

var a = 1; //此處聲明的變量a為全局變量
function foo(){
   var a = 2;//此處聲明的變量a為函數foo的局部變量
   console.log(a);//2
}
foo();
console.log(a);//1
           

如果在聲明變量時,省略 var 的話,該變量就會變成全局變量,如全局作用域中存在該變量,就會更新其值。如:

var a = 1; //此處聲明的變量a為全局變量
function foo(){
   a = 2;//此處的變量a也是全局變量
   console.log(a);//2
}
foo();
console.log(a);//2
           

注意:var 聲明的變量存在提升(hoisting)。

提升是指無論 var 出現在一個作用域的哪個位置,這個聲明都屬于目前的整個作用域,在其中到處都可以通路到。注意隻有變量聲明才會提升,對變量指派并不會提升。如下例所示:

console.log(a);//undefined
var a = 1;
           

該代碼段跟下列代碼段是一樣的邏輯:

var a;
console.log(a);//undefined
a = 1;
           

而如果對未聲明過的變量進行操作,就會報錯

let

let 聲明的變量,具有如下幾個特點:

(1)let 聲明的變量具有塊作用域的特征。

(2)在同一個塊級作用域,不能重複聲明變量。

(3)let 聲明的變量不存在變量提升,換一種說法,就是 let 聲明存在暫時性死區(TDZ)。

如下面幾個例子所示

let a = 1;
console.log(a);//1
console.log(b);//Uncaught ReferenceError: b is not defined
let b = 2;
           
function foo(){
    let a = 1;
    let a = 2;//Uncaught SyntaxError: Identifier 'a' has already been declared
}
           

以下是一個經典的關于 var 和 let 的一個例子:

for (var i = 0; i < 10; i++) {
    setTimeout(function(){
        console.log(i);
    },100)
};
           

該代碼運作後,會在控制台列印出10個10.若修改為:

for (let i = 0; i < 10; i++) {
    setTimeout(function(){
        console.log(i);
    },100)
};
           

則該代碼運作後,就會在控制台列印出0-9.

const

const 聲明方式,除了具有 let 的上述特點外,其還具備一個特點,即 const 定義的變量,一旦定義後,就不能修改,即 const 聲明的為常量。

例如:

const a = 1;
console.log(a);//1
a = 2;
console.log(a);//Uncaught TypeError: Assignment to constant variable.
           

但是,并不是說 const 聲明的變量其内部内容不可變,如:

const obj = {a:1,b:2};
console.log(obj.a);//1
obj.a = 3;
console.log(obj.a);//3
           

是以準确的說,是 const 聲明建立一個值的隻讀引用。但這并不意味着它所持有的值是不可變的,隻是變量辨別符不能重新配置設定。

總結

(1)var 聲明的變量屬于函數作用域,let 和 const 聲明的變量屬于塊級作用域;

(2)var 存在變量提升現象,而 let 和 const 沒有此類現象;

(3)var 變量可以重複聲明,而在同一個塊級作用域,let 變量不能重新聲明,const 變量不能修改。

3.線程和程序

線程是什麼?程序是什麼?二者有什麼差別和聯系?

(1)線程是CPU獨立運作和獨立排程的基本機關;

(2)程序是資源配置設定的基本機關;

兩者的聯系:程序和線程都是作業系統所運作的程式運作的基本單元。

差別:

(1)程序具有獨立的空間位址,一個程序崩潰後,在保護模式下不會對其它程序産生影響。

(2)線程隻是一個程序的不同執行路徑,線程有自己的堆棧和局部變量,但線程之間沒有單獨的位址空間,一個線程死掉就等于整個程序死掉。

什麼是堆棧?有什麼差別?

堆棧都是一種資料項按序排列的資料結構,隻能在一端對資料項進行插入和删除。在單片機應用中,堆棧是個特殊的存儲區,主要功能是暫時存放資料和位址,通常用來保護斷點和現場。要點:堆,隊列優先,先進先出(FIFO—first in first out)。棧,先進後出(FILO—First-In/Last-Out)。

記憶體的分區:常量區,靜态區(全局區),堆,棧,代碼區

(1)管理方式:對于棧來講,是由編譯器自動管理,無需我們手工控制;對于堆來說,釋放工作由程式員控制,容易産生記憶體洩露。

(2)申請大小:如果棧申請的空間超過棧的剩餘空間時,将提示記憶體溢出(8M)。是以,能從棧獲得的空間較小。堆是由于系統是用連結清單來存儲的空閑記憶體位址的,不是連續的堆獲的空間比較靈活,也比較大。

(3)碎片問題:對于堆來講,頻繁的new/delete勢必會造成記憶體空間的不連續,進而造成大量的碎 片,使程式效率降低對于棧來講,則不會存在這個問題,因為棧是先進後出的隊列,他們是如此的一一對應,以至于永遠都不可能有一個記憶體塊從棧中間彈出

(4)配置設定方式:堆都是動态配置設定的,沒有靜态配置設定的堆。棧有2種配置設定方式:靜态配置設定和動态配置設定。靜态配置設定是編譯器完成的,比如局部變量的配置設定。動态配置設定由alloca函數進行配置設定,但是棧的動态配置設定和堆是不同的,他的動态配置設定是由編譯器進行釋放,無需我們手工實作。

(5)配置設定效率:棧是機器系統提供的資料結構,計算機會在底層對棧提供支援:配置設定專門的寄存器存放棧的位址,壓棧出棧都有專門的指令執行,這就決定了棧的效率比較高。堆則是C/C++函數庫提供的,它的機制是很複雜的

多程序的程式要比多線程的程式健壯,但在程序切換時,耗費資源較大,效率要差寫,對于一些要求同時進行并且又要共享某些變量的并發操作,隻能用線程,不能用程序。

什麼是多線程?

多線程:是指從軟體或者硬體上實作多個線程的并發技術。

多線程的好處:

(1)使用多線程可以把程式中占據時間長的任務放到背景去處理,如圖檔、視屏的下載下傳

(2)發揮多核處理器的優勢,并發執行讓系統運作的更快、更流暢,使用者體驗更好

多線程的缺點:

(1)大量的線程降低代碼的可讀性;

(2)更多的線程需要更多的記憶體空間

(3)當多個線程對同一個資源出現争奪時候要注意線程安全的問題。

IOS中實作多線程的方法

常用的:NSThread NSOperationQueue GCD

4.移動端适配方案

目前為止出現的一些關于移動端适配的技術方案:

(1)通過媒體查詢的方式即CSS3的meida queries

(2)以天貓首頁為代表的 flex 彈性布局

(3)以淘寶首頁為代表的 rem+viewport縮放

(4)rem 方式

Media Queries

它主要是通過查詢裝置的寬度來執行不同的 css 代碼,最終達到界面的配置。核心文法是:

@media screen and (max-width: 600px) { /*當螢幕尺寸小于600px時,應用下面的CSS樣式*/
  /*你的css代碼*/
}
           

優點:

  • media query可以做到裝置像素比的判斷,方法簡單,成本低,特别是對移動和PC維護同一套代碼的時候。目前像Bootstrap等架構使用這種方式布局
  • 圖檔便于修改,隻需修改css檔案
  • 調整螢幕寬度的時候不用重新整理頁面即可響應式展示

缺點:

  • 代碼量比較大,維護不友善
  • 為了兼顧大螢幕或高清裝置,會造成其他裝置資源浪費,特别是加載圖檔資源
  • 為了兼顧移動端和PC端各自響應式的展示效果,難免會損失各自特有的互動方式

**Flex彈性布局 **

以天貓的實作方式進行說明:

它的viewport是固定的:

<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no">

前端面試總結七

高度定死,寬度自适應,元素都采用px做機關。

随着螢幕寬度變化,頁面也會跟着變化,效果就和PC頁面的流體布局差不多,在哪個寬度需要調整的時候使用響應式布局調調就行(比如網易新聞),這樣就實作了『适配』。

rem + viewport 縮放 **

** 這也是淘寶使用的方案,根據螢幕寬度設定 rem 值,需要适配的元素都使用 rem 為機關,不需要适配的元素還是使用 px 為機關(1em = 16px)。

實作原理 :

根據rem将頁面放大dpr倍, 然後viewport設定為1/dpr.

如iphone6 plus的dpr為3, 則頁面整體放大3倍, 1px(css機關)在plus下預設為3px(實體像素)

然後viewport設定為1/3, 這樣頁面整體縮回原始大小. 進而實作高清。

前端面試總結七

這樣整個網頁在裝置内顯示時的頁面寬度就會等于裝置邏輯像素大小,也就是device-width。

這個device-width的計算公式為:裝置的實體分辨率/(devicePixelRatio * scale),在scale為1的情況下,device-width = 裝置的實體分辨率/devicePixelRatio 。

**rem實作 **

比如說“魅族”移動端的實作方式,viewport也是固定的:

<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no">

通過以下代碼來控制rem基準值(設計稿以720px寬度量取實際尺寸)

!function (d) {
    var c = d.document;
    var a = c.documentElement;
    var b = d.devicePixelRatio;
    var f;

    function e() {
      var h = a.getBoundingClientRect().width, g;
      if (b === 1) {
        h = 720
      }
      if(h>720) h = 720;//設定基準值的極限值
      g = h / 7.2;
      a.style.fontSize = g + "px"
    }

    if (b > 2) {
      b = 3
    } else {
      if (b > 1) {
        b = 2
      } else {
        b = 1
      }
    }
    a.setAttribute("data-dpr", b);
    d.addEventListener("resize", function () {
      clearTimeout(f);
      f = setTimeout(e, 200)
    }, false);
    e()
  }(window);
           

css通過sass預編譯,設定量取的px值轉化rem的變量$px: (1/100)+rem;

前端面試總結七

5.單頁應用和多頁應用

單頁面應用(SinglePage Web Application,SPA)

隻有一張Web頁面的應用,是一種從Web伺服器加載的富用戶端,單頁面跳轉僅重新整理局部資源 ,公共資源(js、css等)僅需加載一次,常用于PC端官網、購物等網站

如圖:

前端面試總結七

第一次進入頁面的時候會請求一個html檔案,重新整理清除一下。切換到其他元件,此時路徑也相應變化,但是并沒有新的html檔案請求,頁面内容也變化了。

原理是:JS會感覺到url的變化,通過這一點,可以用js動态的将目前頁面的内容清除掉,然後将下一個頁面的内容挂載到目前頁面上,這個時候的路由不是後端來做了,而是前端來做,判斷頁面到底是顯示哪個元件,清除不需要的,顯示需要的元件。這種過程就是單頁應用,每次跳轉的時候不需要再請求html檔案了。

(1)為什麼頁面切換快?

頁面每次切換跳轉時,并不需要做html檔案的請求,這樣就節約了很多http發送時延,我們在切換頁面的時候速度很快。

(2)缺點:首屏時間慢,SEO差

單頁應用的首屏時間慢,首屏時需要請求一次html,同時還要發送一次js請求,兩次請求回來了,首屏才會展示出來。相對于多頁應用,首屏時間慢。

SEO效果差,因為搜尋引擎隻認識html裡的内容,不認識js的内容,而單頁應用的内容都是靠js渲染生成出來的,搜尋引擎不識别這部分内容,也就不會給一個好的排名,會導緻單頁應用做出來的網頁在百度和谷歌上的排名差。

多頁面應用(MultiPage Application,MPA)

多頁面跳轉重新整理所有資源,每個公共資源(js、css等)需選擇性重新加載,常用于 app 或 用戶端等

如圖:

前端面試總結七

(1)為什麼多頁應用的首屏時間快?

首屏時間叫做頁面首個螢幕的内容展現的時間,當我們通路頁面的時候,伺服器傳回一個html,頁面就會展示出來,這個過程隻經曆了一個HTTP請求,是以頁面展示的速度非常快。

(2)為什麼搜尋引擎優化效果好(SEO)?

搜尋引擎在做網頁排名的時候,要根據網頁内容才能給網頁權重,來進行網頁的排名。搜尋引擎是可以識别html内容的,而我們每個頁面所有的内容都放在Html中,是以這種多頁應用,seo排名效果好。

(3)但是它也有缺點,就是切換慢

因為每次跳轉都需要發出一個http請求,如果網絡比較慢,在頁面之間來回跳轉時,就會發現明顯的卡頓。

具體對比分析:

前端面試總結七

6.setTimeout時間設定為0

setTimeout(fn,millisec) 方法用于在指定的毫秒數後調用函數或計算表達式。

很簡單,setTimeout() 隻執行 fn 一次,到底什麼時候執行取決于第二個參數millisec設定的毫秒數,是以很多人習慣上稱之為延遲,無非就是延遲一段時間後再執行裡面的代碼。

setTimeout(function(){
 console.log('我是setTimeout');
}, 1000);
           

正常情況下,我是setTimeout 這句話并不會馬上輸出而是等1000毫秒以後會在浏覽器的控制台輸出。

看一個例子,這個例子的輸出結果是什麼?

setTimeout(function(){
 console.log(1);
}, 0);
console.log(2);
console.log(3);
           

出乎一些人的意料,得到的結果竟然是2、3、1。這似乎不按套路出牌啊,明明是等待了0毫秒也就是不等待直接輸出啊,為啥1卻在最後輸出了呢?

這就需要搞清楚一個很重要的概念:js是單線程的,單線程就意味着,所有任務需要排隊,前一個任務結束,才會執行後一個任務。如果前一個任務耗時很長,後一個任務就不得不一直等着。

其實,當js代碼執行遇到setTimeout(fn,millisec)時,會把fn這個函數放在任務隊列中,當js引擎線程空閑時并達到millisec指定的時間時,才會把fn放到js引擎線程中執行。

setTimeout(fn,0)的含義是,指定某個任務在主線程最早可得的空閑時間執行,也就是說,盡可能早得執行。它在"任務隊列"的尾部添加一個事件,是以要等到同步任務和"任務隊列"現有的事件都處理完,才會得到執行。

參考:setTimeout時間設定為0詳細解析

7.為什麼JavaScript是單線程

JavaScript語言的一大特點就是單線程,也就是說,同一個時間隻能做一件事。那麼,為什麼JavaScript不能有多個線程呢?這樣能提高效率啊。

JavaScript的單線程,與它的用途有關。作為浏覽器腳本語言,JavaScript的主要用途是與使用者互動,以及操作DOM。這決定了它隻能是單線程,否則會帶來很複雜的同步問題。比如,假定JavaScript同時有兩個線程,一個線程在某個DOM節點上添加内容,另一個線程删除了這個節點,這時浏覽器應該以哪個線程為準?

是以,為了避免複雜性,從一誕生,JavaScript就是單線程,這已經成了這門語言的核心特征,将來也不會改變。

為了利用多核CPU的計算能力,HTML5提出Web Worker标準,允許JavaScript腳本建立多個線程,但是子線程完全受主線程控制,且不得操作DOM。是以,這個新标準并沒有改變JavaScript單線程的本質。

參考:JavaScript 運作機制詳解:再談Event Loop

8.Event Loop

任務隊列

單線程就意味着,所有任務需要排隊,前一個任務結束,才會執行後一個任務。如果前一個任務耗時很長,後一個任務就不得不一直等着。

如果排隊是因為計算量大,CPU忙不過來,倒也算了,但是很多時候CPU是閑着的,因為IO裝置(輸入輸出裝置)很慢(比如Ajax操作從網絡讀取資料),不得不等着結果出來,再往下執行。

JavaScript語言的設計者意識到,這時主線程完全可以不管IO裝置,挂起處于等待中的任務,先運作排在後面的任務。等到IO裝置傳回了結果,再回過頭,把挂起的任務繼續執行下去。

于是,所有任務可以分成兩種,一種是同步任務(synchronous),另一種是異步任務(asynchronous)。同步任務指的是,在主線程上排隊執行的任務,隻有前一個任務執行完畢,才能執行後一個任務;異步任務指的是,不進入主線程、而進入"任務隊列"(task queue)的任務,隻有"任務隊列"通知主線程,某個異步任務可以執行了,該任務才會進入主線程執行。

具體來說,異步執行的運作機制如下。(同步執行也是如此,因為它可以被視為沒有異步任務的異步執行。)

(1)所有同步任務都在主線程上執行,形成一個執行棧(execution context stack)。

(2)主線程之外,還存在一個"任務隊列"(task queue)。隻要異步任務有了運作結果,就在"任務隊列"之中放置一個事件。

(3)一旦"執行棧"中的所有同步任務執行完畢,系統就會讀取"任務隊列",看看裡面有哪些事件。那些對應的異步任務,于是結束等待狀态,進入執行棧,開始執行。

(4)主線程不斷重複上面的第三步。

下圖就是主線程和任務隊列的示意圖。

前端面試總結七

隻要主線程空了,就會去讀取"任務隊列",這就是JavaScript的運作機制。這個過程會不斷重複。

事件和回調函數

“任務隊列"是一個事件的隊列(也可以了解成消息的隊列),IO裝置完成一項任務,就在"任務隊列"中添加一個事件,表示相關的異步任務可以進入"執行棧"了。主線程讀取"任務隊列”,就是讀取裡面有哪些事件。

“任務隊列"中的事件,除了IO裝置的事件以外,還包括一些使用者産生的事件(比如滑鼠點選、頁面滾動等等)。隻要指定過回調函數,這些事件發生時就會進入"任務隊列”,等待主線程讀取。

所謂"回調函數"(callback),就是那些會被主線程挂起來的代碼。異步任務必須指定回調函數,當主線程開始執行異步任務,就是執行對應的回調函數。

"任務隊列"是一個先進先出的資料結構,排在前面的事件,優先被主線程讀取。主線程的讀取過程基本上是自動的,隻要執行棧一清空,"任務隊列"上第一位的事件就自動進入主線程。但是,由于存在後文提到的"定時器"功能,主線程首先要檢查一下執行時間,某些事件隻有到了規定的時間,才能傳回主線程。

Event Loop

主線程從"任務隊列"中讀取事件,這個過程是循環不斷的,是以整個的這種運作機制又稱為Event Loop(事件循環)。

前端面試總結七

上圖中,主線程運作的時候,産生堆(heap)和棧(stack),棧中的代碼調用各種外部API,它們在"任務隊列"中加入各種事件(click,load,done)。隻要棧中的代碼執行完畢,主線程就會去讀取"任務隊列",依次執行那些事件所對應的回調函數。

執行棧中的代碼(同步任務),總是在讀取"任務隊列"(異步任務)之前執行。請看下面這個例子。

var req = new XMLHttpRequest();
    req.open('GET', url);    
    req.onload = function (){};    
    req.onerror = function (){};    
    req.send();
           

上面代碼中的req.send方法是Ajax操作向伺服器發送資料,它是一個異步任務,意味着隻有目前腳本的所有代碼執行完,系統才會去讀取"任務隊列"。是以,它與下面的寫法等價。

var req = new XMLHttpRequest();
    req.open('GET', url);
    req.send();
    req.onload = function (){};    
    req.onerror = function (){};  
           

也就是說,指定回調函數的部分(onload和onerror),在send()方法的前面或後面無關緊要,因為它們屬于執行棧的一部分,系統總是執行完它們,才會去讀取"任務隊列"。

參考:JavaScript 運作機制詳解:再談Event Loop

9.Node.js的Event Loop

Node.js也是單線程的Event Loop,但是它的運作機制不同于浏覽器環境。

前端面試總結七

根據上圖,Node.js的運作機制如下。

(1)V8引擎解析JavaScript腳本。

(2)解析後的代碼,調用Node API。

(3)libuv庫負責Node API的執行。它将不同的任務配置設定給不同的線程,形成一個Event Loop(事件循環),以異步的方式将任務的執行結果傳回給V8引擎。

(4)V8引擎再将結果傳回給使用者。

除了setTimeout和setInterval這兩個方法,Node.js還提供了另外兩個與"任務隊列"有關的方法:process.nextTick和setImmediate。它們可以幫助我們加深對"任務隊列"的了解。

process.nextTick方法可以在目前"執行棧"的尾部----下一次Event Loop(主線程讀取"任務隊列")之前----觸發回調函數。也就是說,它指定的任務總是發生在所有異步任務之前。setImmediate方法則是在目前"任務隊列"的尾部添加事件,也就是說,它指定的任務總是在下一次Event Loop時執行,這與setTimeout(fn, 0)很像。請看下面的例子(via StackOverflow)。

process.nextTick(function A() {
  console.log(1);
  process.nextTick(function B(){console.log(2);});
});

setTimeout(function timeout() {
  console.log('TIMEOUT FIRED');
}, 0)
// 1
// 2
// TIMEOUT FIRED
           

上面代碼中,由于process.nextTick方法指定的回調函數,總是在目前"執行棧"的尾部觸發,是以不僅函數A比setTimeout指定的回調函數timeout先執行,而且函數B也比timeout先執行。這說明,如果有多個process.nextTick語句(不管它們是否嵌套),将全部在目前"執行棧"執行。

現在,再看setImmediate。

setImmediate(function A() {
  console.log(1);
  setImmediate(function B(){console.log(2);});
});

setTimeout(function timeout() {
  console.log('TIMEOUT FIRED');
}, 0);
           

上面代碼中,setImmediate與setTimeout(fn,0)各自添加了一個回調函數A和timeout,都是在下一次Event Loop觸發。那麼,哪個回調函數先執行呢?答案是不确定。運作結果可能是1–TIMEOUT FIRED–2,也可能是TIMEOUT FIRED–1--2。

令人困惑的是,Node.js文檔中稱,setImmediate指定的回調函數,總是排在setTimeout前面。實際上,這種情況隻發生在遞歸調用的時候。

setImmediate(function (){
  setImmediate(function A() {
    console.log(1);
    setImmediate(function B(){console.log(2);});
  });

  setTimeout(function timeout() {
    console.log('TIMEOUT FIRED');
  }, 0);
});
// 1
// TIMEOUT FIRED
// 2
           

上面代碼中,setImmediate和setTimeout被封裝在一個setImmediate裡面,它的運作結果總是1–TIMEOUT FIRED–2,這時函數A一定在timeout前面觸發。至于2排在TIMEOUT FIRED的後面(即函數B在timeout後面觸發),是因為setImmediate總是将事件注冊到下一輪Event Loop,是以函數A和timeout是在同一輪Loop執行,而函數B在下一輪Loop執行。

我們由此得到了process.nextTick和setImmediate的一個重要差別:多個process.nextTick語句總是在目前"執行棧"一次執行完,多個setImmediate可能則需要多次loop才能執行完。事實上,這正是Node.js 10.0版添加setImmediate方法的原因,否則像下面這樣的遞歸調用process.nextTick,将會沒完沒了,主線程根本不會去讀取"事件隊列"!

process.nextTick(function foo() {
  process.nextTick(foo);
});
           

事實上,現在要是你寫出遞歸的process.nextTick,Node.js會抛出一個警告,要求你改成setImmediate。

另外,由于process.nextTick指定的回調函數是在本次"事件循環"觸發,而setImmediate指定的是在下次"事件循環"觸發,是以很顯然,前者總是比後者發生得早,而且執行效率也高(因為不用檢查"任務隊列")。

參考:JavaScript 運作機制詳解:再談Event Loop

10.apply,call,bind的差別

在javascript中,call和 apply 都是為了改變某個函數運作時的上下文(context)而存在的,換句話說,就是為了改變函數體内部 this的指向。

JavaScript 的一大特點是,函數存在「定義時上下文」和「運作時上下文」以及「上下文是可以改變的」這樣的概念。

舉個栗子:

function fn() {}
fn.prototype = {
    color: "red",
    say: function() {
        console.log("My color is " + this.color);
    }
}
 
var apple = new fn;
apple.say();    //My color is red
           

但是如果我們有一個對象banana= {color : “yellow”} ,我們不想對它重新定義say方法,那麼我們可以通過 call 或 apply 用 apple 的 say 方法:

banana = {
    color: "yellow"
}
apple.say.call(banana);     //My color is yellow
apple.say.apply(banana);    //My color is yellow
           

是以,可以看出 call 和apply 是為了動态改變this而出現的,當一個 object沒有某個方法(本栗子中banana沒有say方法),但是其他的有(本栗子中apple有say方法),我們可以借助call或apply用其它對象的方法來操作。

apply、call 的差別

對于 apply、call二者而言,作用完全一樣,都是調用一個函數,傳入函數執行上下文及參數。隻是接受參數的方式不太一樣。例如,有一個函數定義如下:

var func = function(arg1, arg2) {
     
};
就可以通過如下方式來調用:
func.call(this, arg1, arg2);
func.apply(this, [arg1, arg2])
           

其中 this 是你想指定的上下文,他可以是任何一個 JavaScript 對象(JavaScript中一切皆對象),call 需要把參數按順序傳遞進去,而 apply 則是把參數放在數組裡。

JavaScript中,某個函數的參數數量是不固定的,是以要說适用條件的話,當你的參數是明确知道數量時用 call。

而不确定的時候用apply,然後把參數push進數組傳遞進去。當參數數量不确定時,函數内部也可以通過 arguments這個數組來周遊所有的參數。

下面列舉一些常用用法:

(1)數組之間追加

var array1 = [12 , "foo" , {name "Joe"} , -2458]; 
var array2 = ["Doe" , 555 , 100]; 
Array.prototype.push.apply(array1, array2); 
//array1 值為  [12 , "foo" , {name "Joe"} , -2458 , "Doe" , 555 , 100] 
           

(2)擷取數組中的最大值和最小值

var  numbers = [5, 458 , 120 , -215 ]; 
var maxInNumbers = Math.max.apply(Math, numbers),   //458
    maxInNumbers = Math.max.call(Math,5, 458 , 120 , -215); //458
           

number本身沒有 max 方法,但是Math 有,我們就可以借助 call或者 apply使用其方法。

(3)驗證是否是數組(前提是toString()方法沒有被重寫過)

function isArray(obj){ 
    return Object.prototype.toString.call(obj) === '[object Array]' ;
}
           

(4)類(僞)數組使用數組方法

Javascript中存在一種名為僞數組的對象結構。比較特别的是 arguments 對象,還有像調用 getElementsByTagName , document.childNodes之類的,它們傳回NodeList對象都屬于僞數組。不能應用 Array下的 push , pop 等方法。

但是我們能通過 Array.prototype.slice.call 轉換為真正的數組的帶有length 屬性的對象,這樣 domNodes就可以應用Array下的所有方法了。

深入了解運用apply、call

下面就借用面試題,來更深入的去了解下 apply和 call。

(1)定義一個log方法,讓它可以代理 console.log方法,常見的解決方法是:

function log(msg) {
  console.log(msg);
}
log(1);    //1
log(1,2);    //1
           

上面方法可以解決最基本的需求,但是當傳入參數的個數是不确定的時候,上面的方法就失效了,這個時候就可以考慮使用 apply或者 call,注意這裡傳入多少個參數是不确定的,是以使用apply是最好的,方法如下:

function log(){
  console.log.apply(console, arguments);
};
log(1);    //1
log(1,2);    //1 2
           

(2)接下來的要求是給每一個 log 消息添加一個"(app)"的前辍,比如:

該怎麼做比較優雅呢?這個時候需要想到arguments參數是個僞數組,通過 Array.prototype.slice.call 轉化為标準數組,再使用數組方法unshift,像這樣:

function log(){
  var args = Array.prototype.slice.call(arguments);
  args.unshift('(app)');
 
  console.log.apply(console, args);
};
           

bind

說完了 apply 和 call,再來說說bind。bind() 方法與apply 和 call 很相似,也是可以改變函數體内 this 的指向。

MDN的解釋是:bind()方法會建立一個新函數,稱為綁定函數,當調用這個綁 定函數時,綁定函數會以建立它時傳入 bind()方法的第一個參數作為 this,傳入 bind()方法的第二個以及以後的參數加上綁定函數運作時 本身的參數按照順序作為原函數的參數來調用原函數。

直接來看看具體如何使用,在常見的單體模式中,通常我們會使用_this , that , self等儲存 this,這樣我們可以在改變了上下文之後繼續引用到它。 像這樣:

var foo = {
    bar : 1,
    eventBind: function(){
        var _this = this;
        $('.someClass').on('click',function(event) {
            /* Act on the event */
            console.log(_this.bar);     //1
        });
    }
}
           

由于Javascript 特有的機制,上下文環境在 eventBind:function(){ }過渡到 $(’.someClass’).on(‘click’,function(event) { })發生了改變,上述使用變量儲存 this 這些方式都是有用的,也沒有什麼問題。當然使用bind()可以更加優雅的解決這個問題:

var foo = {
    bar : 1,
    eventBind: function(){
        $('.someClass').on('click',function(event) {
            /* Act on the event */
            console.log(this.bar);      //1
        }.bind(this));
    }
}
           

在上述代碼裡,bind()建立了一個函數,當這個click事件綁定在被調用的時候,它的 this關鍵詞會被設定成被傳入的值(這裡指調用bind()時傳入的參數)。是以,這裡我們傳入想要的上下文 this(其實就是 foo ),到 bind() 函數中。然後,當回調函數被執行的時候, this 便指向 foo 對象。再來一個簡單的栗子:

var bar = function(){
console.log(this.x);
}
var foo = {
x:3
}
bar(); // undefined
var func = bar.bind(foo);
func(); // 3
           

這裡我們建立了一個新的函數 func,當使用 bind()建立一個綁定函數之後,它被執行的時候,它的 this 會被設定成 foo , 而不是像我們調用 bar() 時的全局作用域。

(3)有個有趣的問題,如果連續 bind() 兩次,亦或者是連續 bind() 三次那麼輸出的值是什麼呢?像這樣:

var bar = function(){
    console.log(this.x);
}
var foo = {
    x:3
}
var sed = {
    x:4
}
var func = bar.bind(foo).bind(sed);
func(); //?
 
var fiv = {
    x:5
}
var func = bar.bind(foo).bind(sed).bind(fiv);
func(); //?
           

答案是,兩次都仍将輸出 3 ,而非期待中的 4 和 5 。原因是,在Javascript中,多次 bind()是無效的。更深層次的原因, bind() 的實作,相當于使用函數在内部包了一個 call / apply,第二次 bind()相當于再包住第一次 bind(),故第二次以後的 bind()是無法生效的。

apply、call、bind比較

var obj = {
    x: 81,
};
 
var foo = {
    getX: function() {
        return this.x;
    }
}
 
console.log(foo.getX.bind(obj)());  //81
console.log(foo.getX.call(obj));    //81
console.log(foo.getX.apply(obj));   //81
           

三個輸出的都是81,但是注意看使用 bind() 方法的,他後面多了對括号。

也就是說,差別是,當你希望改變上下文環境之後并非立即執行,而是回調執行的時候,使用 bind()方法。而 apply/call 則會立即執行函數。

總結一下:

apply 、 call、bind 三者都是用來改變函數的this對象的指向的;

apply 、 call 、bind 三者第一個參數都是this要指向的對象,也就是想指定的上下文;

apply 、 call 、bind 三者都可以利用後續參數傳參;

bind 是傳回對應函數,便于稍後調用;apply 、call 則是立即調用 。

參考:js中的apply,call,bind的差別

11.HTTP/2的多路複用

HTTP/2有三大特性:頭部壓縮、Server Push、多路複用。

keep-Alive

在沒有Keep-Alive前,我們與伺服器請求資料的流程是這樣:

前端面試總結七
  • 浏覽器請求//static.mtime.cn/a.js–>解析域名–>HTTP連接配接–>伺服器處理檔案–>傳回資料–>浏覽器解析、渲染檔案
  • 浏覽器請求//static.mtime.cn/b.js–>解析域名–>HTTP連接配接–>伺服器處理檔案–>傳回資料–>浏覽器解析、渲染檔案
  • 這樣循環下去,直至全部檔案下載下傳完成。

這個流程最大的問題就是:每次請求都會建立一次HTTP連接配接,也就是我們常說的3次握手4次揮手,這個過程在一次請求過程中占用了相當長的時間,而且邏輯上是非必需的,因為不間斷的請求資料,第一次建立連接配接是正常的,以後就占用這個通道,下載下傳其他檔案,這樣效率多高啊!你猜對了,這就是Keep-Alive。

Keep-Alive解決的問題

Keep-Alive解決的核心問題:一定時間内,同一域名多次請求資料,隻建立一次HTTP請求,其他請求可複用每一次建立的連接配接通道,以達到提高請求效率的問題。這裡面所說的一定時間是可以配置的,不管你用的是Apache還是nginx。

HTTP1.1還是存在效率問題

  • 串行的檔案傳輸。當請求a檔案時,b檔案隻能等待,等待a連接配接到伺服器、伺服器處理檔案、伺服器傳回檔案,這三個步驟。我們假設這三步用時都是1秒,那麼a檔案用時為3秒,b檔案傳輸完成用時為6秒,依此類推。(注:此項計算有一個前提條件,就是浏覽器和伺服器是單通道傳輸)
  • 連接配接數過多。我們假設Apache設定了最大并發數為300,因為浏覽器限制,浏覽器發起的最大請求數為6,也就是伺服器能承載的最高并發為50,當第51個人通路時,就需要等待前面某個請求處理完成。

HTTP/2的多路複用

  • 解決第一個:在HTTP1.1的協定中,我們傳輸的request和response都是基本于文本的,這樣就會引發一個問題:所有的資料必須按順序傳輸,比如需要傳輸:hello world,隻能從h到d一個一個的傳輸,不能并行傳輸,因為接收端并不知道這些字元的順序,是以并行傳輸在HTTP1.1是不能實作的。
    前端面試總結七
    HTTP/2引入二進制資料幀和流的概念,其中幀對資料進行順序辨別,如下圖所示,這樣浏覽器收到資料之後,就可以按照序列對資料進行合并,而不會出現合并後資料錯亂的情況。同樣是因為有了序列,伺服器就可以并行的傳輸資料,這就是流所做的事情。
    前端面試總結七
  • 解決第二個問題:HTTP/2對同一域名下所有請求都是基于流,也就是說同一域名不管通路多少檔案,也隻建立一路連接配接。同樣Apache的最大連接配接數為300,因為有了這個新特性,最大的并發就可以提升到300,比原來提升了6倍!

參考:淺析HTTP/2的多路複用

12.SPDY

SPDY(SPDY是Speedy的昵音,意為更快),是Google開發的基于TCP協定的應用層協定。SPDY協定的目标是優化HTTP協定的性能,通過壓縮、多路複用和優先級等技術,縮短網頁的加載時間并提高安全性。SPDY協定核心思想是盡量減少TCP連接配接數,而對于HTTP的語義未做太大修改(比如,HTTP的GET和POST消息格式保持不變),基本上相容HTTP協定。

超文本傳輸協定(HTTP)是一個非常成功的協定,但是HTTP/1.1及之前版本的HTTP協定均是針對20世紀90年代之期網絡與Web應用需求而設計,其一些特點已經對現代應用程式的性能産生了負面影響,比如:

(1)HTTP/1.0一次隻允許在一個TCP連接配接上發起一個請求;HTTP/1.1流水線技術也隻能部分處理請求并發,并仍然存在隊列頭阻塞問題,是以用戶端在需要發起多次請求時,典型情況下,通常采用建立多連接配接來減少延遲。

(2)單向請求,請求隻能由用戶端發起。

(3)請求封包與響應封包首部資訊備援量大。

(4)資料未壓縮,資料傳輸量大。

SPDY正是Google在HTTP即将從1.1向2.0過渡之際推出的協定,長期以來一直被認為是HTTP 2.0可行選擇。

SPDY與HTTP相比,具有如下優點:

(1)SPDY支援多路複用,實作請求優化;

(2)SPDY支援伺服器推送技術;

(3)SPDY壓縮了HTTP封包首部資訊,節省了傳輸資料帶寬;

(4)SPDY強制使用SSL傳輸協定,全部請求由SSL加密,資訊傳輸更安全。

參考:程式員面試必考題(二十五)—SPDY與HTTP/2協定

13.浏覽器的緩存機制

緩存位置

從緩存位置上來說分為四種,并且各自有優先級,當依次查找緩存且都沒有命中的時候,才會去請求網絡。

  • Service Worker
  • Memory Cache
  • Disk Cache
  • Push Cache

Service Worker

Service Worker 是運作在浏覽器背後的獨立線程,一般可以用來實作緩存功能。使用 Service Worker的話,傳輸協定必須為 HTTPS。因為 Service Worker 中涉及到請求攔截,是以必須使用 HTTPS 協定來保障安全。Service Worker 的緩存與浏覽器其他内建的緩存機制不同,它可以讓我們自由控制緩存哪些檔案、如何比對緩存、如何讀取緩存,并且緩存是持續性的。

Service Worker 實作緩存功能一般分為三個步驟:首先需要先注冊 Service Worker,然後監聽到 install 事件以後就可以緩存需要的檔案,那麼在下次使用者通路的時候就可以通過攔截請求的方式查詢是否存在緩存,存在緩存的話就可以直接讀取緩存檔案,否則就去請求資料。

當 Service Worker 沒有命中緩存的時候,我們需要去調用 fetch 函數擷取資料。也就是說,如果我們沒有在 Service Worker 命中緩存的話,會根據緩存查找優先級去查找資料。但是不管我們是從 Memory Cache 中還是從網絡請求中擷取的資料,浏覽器都會顯示我們是從 Service Worker 中擷取的内容。

Memory Cache

Memory Cache 也就是記憶體中的緩存,主要包含的是目前中頁面中已經抓取到的資源,例如頁面上已經下載下傳的樣式、腳本、圖檔等。讀取記憶體中的資料肯定比磁盤快,記憶體緩存雖然讀取高效,可是緩存持續性很短,會随着程序的釋放而釋放。 一旦我們關閉 Tab 頁面,記憶體中的緩存也就被釋放了。

那麼既然記憶體緩存這麼高效,我們是不是能讓資料都存放在記憶體中呢?

這是不可能的。計算機中的記憶體一定比硬碟容量小得多,作業系統需要精打細算記憶體的使用,是以能讓我們使用的記憶體必然不多。

需要注意的事情是,記憶體緩存在緩存資源時并不關心傳回資源的HTTP緩存頭Cache-Control是什麼值,同時資源的比對也并非僅僅是對URL做比對,還可能會對Content-Type,CORS等其他特征做校驗。

Disk Cache

Disk Cache 也就是存儲在硬碟中的緩存,讀取速度慢點,但是什麼都能存儲到磁盤中,比之 Memory Cache 勝在容量和存儲時效性上。

在所有浏覽器緩存中,Disk Cache 覆寫面基本是最大的。它會根據 HTTP Header 中的字段判斷哪些資源需要緩存,哪些資源可以不請求直接使用,哪些資源已經過期需要重新請求。并且即使在跨站點的情況下,相同位址的資源一旦被硬碟緩存下來,就不會再次去請求資料。絕大部分的緩存都來自 Disk Cache。

Push Cache

Push Cache(推送緩存)是 HTTP/2 中的内容,當以上三種緩存都沒有命中時,它才會被使用。它隻在會話(Session)中存在,一旦會話結束就被釋放,并且緩存時間也很短暫,在Chrome浏覽器中隻有5分鐘左右,同時它也并非嚴格執行HTTP頭中的緩存指令。

  • 所有的資源都能被推送,并且能夠被緩存,但是 Edge 和 Safari 浏覽器支援相對比較差
  • 可以推送 no-cache 和 no-store 的資源
  • 一旦連接配接被關閉,Push Cache 就被釋放
  • 多個頁面可以使用同一個HTTP/2的連接配接,也就可以使用同一個Push Cache。這主要還是依賴浏覽器的實作而定,出于對性能的考慮,有的浏覽器會對相同域名但不同的tab标簽使用同一個HTTP連接配接。
  • Push Cache 中的緩存隻能被使用一次
  • 浏覽器可以拒絕接受已經存在的資源推送
  • 你可以給其他域名推送資源

如果以上四種緩存都沒有命中的話,那麼隻能發起請求來擷取資源了。

那麼為了性能上的考慮,大部分的接口都應該選擇好緩存政策,通常浏覽器緩存政策分為兩種:強緩存和協商緩存,并且緩存政策都是通過設定 HTTP Header 來實作的。

緩存過程分析

浏覽器與伺服器通信的方式為應答模式,即是:浏覽器發起HTTP請求 – 伺服器響應該請求,那麼浏覽器怎麼确定一個資源該不該緩存,如何去緩存呢?浏覽器第一次向伺服器發起該請求後拿到請求結果後,将請求結果和緩存辨別存入浏覽器緩存,浏覽器對于緩存的處理是根據第一次請求資源時傳回的響應頭來确定的。具體過程如下圖:

前端面試總結七

由上圖我們可以知道:

  • 浏覽器每次發起請求,都會先在浏覽器緩存中查找該請求的結果以及緩存辨別
  • 浏覽器每次拿到傳回的請求結果都會将該結果和緩存辨別存入浏覽器緩存中

以上兩點結論就是浏覽器緩存機制的關鍵,它確定了每個請求的緩存存入與讀取。根據是否需要向伺服器重新發起HTTP請求将緩存過程分為兩個部分,分别是強緩存和協商緩存。

強緩存

強緩存:不會向伺服器發送請求,直接從緩存中讀取資源,在chrome控制台的Network選項中可以看到該請求傳回200的狀态碼,并且Size顯示from disk cache或from memory cache。強緩存可以通過設定兩種 HTTP Header 實作:Expires 和 Cache-Control。

Expires

緩存過期時間,用來指定資源到期的時間,是伺服器端的具體的時間點。也就是說,Expires=max-age + 請求時間,需要和Last-modified結合使用。Expires是Web伺服器響應消息頭字段,在響應http請求時告訴浏覽器在過期時間前浏覽器可以直接從浏覽器緩存取資料,而無需再次請求。

Expires 是 HTTP/1 的産物,受限于本地時間,如果修改了本地時間,可能會造成緩存失效。Expires: Wed, 22 Oct 2018 08:41:00 GMT表示資源會在 Wed, 22 Oct 2018 08:41:00 GMT 後過期,需要再次請求。

Cache-Control

在HTTP/1.1中,Cache-Control是最重要的規則,主要用于控制網頁緩存,主要取值為:

  • public:所有内容都将被緩存(用戶端和代理伺服器都可緩存)
  • private:所有内容隻有用戶端可以緩存,Cache-Control的預設取值
  • no-cache:用戶端緩存内容,但是是否使用緩存則需要經過協商緩存來驗證決定
  • no-store:所有内容都不會被緩存,即不使用強制緩存,也不使用協商緩存
  • max-age=xxx (xxx is numeric):緩存内容将在xxx秒後失效

需要注意的是,no-cache這個名字有一點誤導。設定了no-cache之後,并不是說浏覽器就不再緩存資料,隻是浏覽器在使用緩存資料時,需要先确認一下資料是否還跟伺服器保持一緻,也就是協商緩存。而no-store才表示不會被緩存,即不使用強制緩存,也不使用協商緩存。

Expires和Cache-Control兩者對比

其實這兩者差别不大,差別就在于 Expires 是http1.0的産物,Cache-Control是http1.1的産物,兩者同時存在的話,Cache-Control優先級高于Expires;在某些不支援HTTP1.1的環境下,Expires就會發揮用處。是以Expires其實是過時的産物,現階段它的存在隻是一種相容性的寫法。

強緩存判斷是否緩存的依據來自于是否超出某個時間或者某個時間段,而不關心伺服器端檔案是否已經更新,這可能會導緻加載檔案不是伺服器端最新的内容,那我們如何獲知伺服器端内容是否已經發生了更新呢?此時我們需要用到協商緩存政策。

協商緩存

協商緩存就是強制緩存失效後,浏覽器攜帶緩存辨別向伺服器發起請求,由伺服器根據緩存辨別決定是否使用緩存的過程,主要有以下兩種情況:

  • 協商緩存生效,傳回304和Not Modified
    前端面試總結七
  • 協商緩存失效,傳回200和請求結果
    前端面試總結七
    協商緩存可以通過設定兩種 HTTP Header 實作:Last-Modified 和 ETag 。

Last-Modified和If-Modified-Since

浏覽器在第一次通路資源時,伺服器傳回資源的同時,在response header中添加 Last-Modified的header,值是這個資源在伺服器上的最後修改時間,浏覽器接收後緩存檔案和header;

浏覽器下一次請求這個資源,浏覽器檢測到有 Last-Modified這個header,于是添加If-Modified-Since這個header,值就是Last-Modified中的值;伺服器再次收到這個資源請求,會根據 If-Modified-Since 中的值與伺服器中這個資源的最後修改時間對比,如果沒有變化,傳回304和空的響應體,直接從緩存讀取,如果If-Modified-Since的時間小于伺服器中這個資源的最後修改時間,說明檔案有更新,于是傳回新的資源檔案和200

前端面試總結七

Last-Modified 存在一些弊端:

  • 如果本地打開緩存檔案,即使沒有對檔案進行修改,但還是會造成 Last-Modified 被修改,服務端不能命中緩存導緻發送相同的資源
  • 因為 Last-Modified 隻能以秒計時,如果在不可感覺的時間内修改完成檔案,那麼服務端會認為資源還是命中了,不會傳回正确的資源

既然根據檔案修改時間來決定是否緩存尚有不足,能否可以直接根據檔案内容是否修改來決定緩存政策?是以在 HTTP / 1.1 出現了 ETag 和If-None-Match

ETag和If-None-Match

Etag是伺服器響應請求時,傳回目前資源檔案的一個唯一辨別(由伺服器生成),隻要資源有變化,Etag就會重新生成。浏覽器在下一次加載資源向伺服器發送請求時,會将上一次傳回的Etag值放到request header裡的If-None-Match裡,伺服器隻需要比較用戶端傳來的If-None-Match跟自己伺服器上該資源的ETag是否一緻,就能很好地判斷資源相對用戶端而言是否被修改過了。如果伺服器發現ETag比對不上,那麼直接以正常GET 200回包形式将新的資源(當然也包括了新的ETag)發給用戶端;如果ETag是一緻的,則直接傳回304知會用戶端直接使用本地緩存即可。

前端面試總結七

兩者之間對比:

  • 首先在精确度上,Etag要優于Last-Modified。

    Last-Modified的時間機關是秒,如果某個檔案在1秒内改變了多次,那麼他們的Last-Modified其實并沒有展現出來修改,但是Etag每次都會改變確定了精度;如果是負載均衡的伺服器,各個伺服器生成的Last-Modified也有可能不一緻。

  • 第二在性能上,Etag要遜于Last-Modified,畢竟Last-Modified隻需要記錄時間,而Etag需要伺服器通過算法來計算出一個hash值。
  • 第三在優先級上,伺服器校驗優先考慮Etag

緩存機制

強制緩存優先于協商緩存進行,若強制緩存(Expires和Cache-Control)生效則直接使用緩存,若不生效則進行協商緩存(Last-Modified / If-Modified-Since和Etag / If-None-Match),協商緩存由伺服器決定是否使用緩存,若協商緩存失效,那麼代表該請求的緩存失效,傳回200,重新傳回資源和緩存辨別,再存入浏覽器緩存中;生效則傳回304,繼續使用緩存。具體流程圖如下:

前端面試總結七

看到這裡,不知道你是否存在這樣一個疑問:如果什麼緩存政策都沒設定,那麼浏覽器會怎麼處理?

對于這種情況,浏覽器會采用一個啟發式的算法,通常會取響應頭中的 Date 減去 Last-Modified 值的 10% 作為緩存時間。

使用者行為對浏覽器緩存的影響

所謂使用者行為對浏覽器緩存的影響,指的就是使用者在浏覽器如何操作時,會觸發怎樣的緩存政策。主要有 3 種:

  • 打開網頁,位址欄輸入位址: 查找 disk cache 中是否有比對。如有則使用;如沒有則發送網絡請求。
  • 普通重新整理 (F5):因為 TAB 并沒有關閉,是以 memory cache 是可用的,會被優先使用(如果比對的話)。其次才是 disk cache。
  • 強制重新整理 (Ctrl + F5):浏覽器不使用緩存,是以發送的請求頭部均帶有 Cache-control: no-cache(為了相容,還帶了 Pragma: no-cache),伺服器直接傳回 200 和最新内容。

參考:深入了解浏覽器的緩存機制

14.Vue Loader

Vue Loader 是一個 webpack 的 loader,它允許你以一種名為單檔案元件 SFC的格式撰寫 Vue 元件:

<template>
  <div class="example">{{ msg }}</div>
</template>
           
<script>
export default {
  data () {
    return {
      msg: 'Hello world!'
    }
  }
}
</script>
           
<style>
.example {
  color: red;
}
</style>
           

Vue Loader 還提供了很多酷炫的特性:

  • 允許為 Vue 元件的每個部分使用其它的 webpack loader,例如在

    <style>

    的部分使用 Sass 和在

    <template>

    的部分使用 Pug;
  • 允許在一個 .vue 檔案中使用自定義塊,并對其運用自定義的 loader 鍊;
  • 使用 webpack loader 将

    <style>

    <template>

    中引用的資源當作子產品依賴來處理;
  • 為每個元件模拟出 scoped CSS;
  • 在開發過程中使用熱重載來保持狀态。

簡而言之,webpack 和 Vue Loader 的結合為你提供了一個現代、靈活且極其強大的前端工作流,來幫助撰寫 Vue.js 應用。

如果你不想手動設定 webpack,我們推薦使用 Vue CLI 直接建立一個項目的腳手架。通過 Vue CLI 建立的項目會針對多數常見的開發需求進行預先配置,做到開箱即用。

處理資源路徑

當 Vue Loader 編譯單檔案元件中的 塊時,它也會将所有遇到的資源 URL 轉換為 webpack 子產品請求。

資源 URL 轉換會遵循如下規則:

  • 如果路徑是絕對路徑 (例如 /images/foo.png),會原樣保留。
  • 如果路徑以 . 開頭,将會被看作相對的子產品依賴,并按照你的本地檔案系統上的目錄結構進行解析。
  • 如果路徑以 ~ 開頭,其後的部分将會被看作子產品依賴。這意味着你可以用該特性來引用一個 Node 依賴中的資源:

    <img src="~some-npm-package/foo.png">

  • 如果路徑以 @ 開頭,也會被看作子產品依賴。如果你的 webpack 配置中給 @ 配置了 alias,這就很有用了。所有 vue-cli 建立的項目都預設配置了将 @ 指向 /src

轉換資源 URL 的好處是:

  • file-loader 可以指定要複制和放置資源檔案的位置,以及如何使用版本哈希命名以獲得更好的緩存。此外,這意味着 你可以就近管理圖檔檔案,可以使用相對路徑而不用擔心部署時 URL 的問題。使用正确的配置,webpack 将會在打包輸出中自動重寫檔案路徑為正确的 URL。
  • url-loader 允許你有條件地将檔案轉換為内聯的 base-64 URL (當檔案小于給定的門檻值),這會減少小檔案的 HTTP 請求數。如果檔案大于該門檻值,會自動的交給 file-loader 處理。

@ 别名在 .vue

<style>

裡無法加載圖檔的問題

Scoped CSS

<style>

标簽有 scoped 屬性時,它的 CSS 隻作用于目前元件中的元素。

<style scoped>
.example {
  color: red;
}
</style>
           
<template>
  <div class="example">hi</div>
</template>
           

轉換結果:

<style>
.example[data-v-f3f3eg9] {
  color: red;
}
</style>
           
<template>
  <div class="example" data-v-f3f3eg9>hi</div>
</template>
           

你可以在一個元件中同時使用有 scoped 和非 scoped 樣式:

<style>
/* 全局樣式 */
</style>
           
<style scoped>
/* 本地樣式 */
</style>
           

深度作用選擇器

如果你希望 scoped 樣式中的一個選擇器能夠作用得“更深”,例如影響子元件,你可以使用

>>>

操作符:

<style scoped>
.a >>> .b { /* ... */ }
</style>
           

上述代碼将會編譯成:

還有一些要留意

Scoped 樣式不能代替 class。考慮到浏覽器渲染各種 CSS 選擇器的方式,當 p { color: red } 是 scoped 時 (即與特性選擇器組合使用時) 會慢很多倍。如果你使用 class 或者 id 取而代之,比如 .example { color: red },性能影響就會消除。

熱重載

當使用腳手架工具 vue-cli 時,熱重載是開箱即用的。

狀态保留規則

  • 當編輯一個元件的

    <template>

    時,這個元件執行個體将就地重新渲染,并保留目前所有的私有狀态。能夠做到這一點是因為模闆被編譯成了新的無副作用的渲染函數。
  • 當編輯一個元件的

    <script>

    時,這個元件執行個體将就地銷毀并重新建立。(應用中其它元件的狀态将會被保留) 是因為

    <script>

    可能包含帶有副作用的生命周期鈎子,是以将重新渲染替換為重新加載是必須的,這樣做可以確定元件行為的一緻性。這也意味着,如果你的元件帶有全局副作用,則整個頁面将會被重新加載。
  • <style>

    會通過 vue-style-loader 自行熱重載,是以它不會影響應用的狀态。

參考:來聊聊 Vue Loader

15.Webpack

// 一個常見的`webpack`配置檔案
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ExtractTextPlugin = require('extract-text-webpack-plugin');

module.exports = {
        entry: __dirname + "/app/main.js", //已多次提及的唯一入口檔案
        output: {
            path: __dirname + "/build",
            filename: "bundle-[hash].js"
        },
        devtool: 'none',
        devServer: {
            contentBase: "./public", //本地伺服器所加載的頁面所在的目錄
            historyApiFallback: true, //不跳轉
            inline: true,
            hot: true
        },
        module: {
            rules: [{
                    test: /(\.jsx|\.js)$/,
                    use: {
                        loader: "babel-loader"
                    },
                    exclude: /node_modules/
                }, {
                    test: /\.css$/,
                    use: ExtractTextPlugin.extract({
                        fallback: "style-loader",
                        use: [{
                            loader: "css-loader",
                            options: {
                                modules: true,
                                localIdentName: '[name]__[local]--[hash:base64:5]'
                            }
                        }, {
                            loader: "postcss-loader"
                        }],
                    })
                }
            }
        ]
    },
    plugins: [
        new webpack.BannerPlugin('版權所有,翻版必究'),
        new HtmlWebpackPlugin({
            template: __dirname + "/app/index.tmpl.html" //new 一個這個插件的執行個體,并傳入相關的參數
        }),
        new webpack.optimize.OccurrenceOrderPlugin(),
        new webpack.optimize.UglifyJsPlugin(),
        new ExtractTextPlugin("style.css")
    ]
};
           

什麼是WebPack,為什麼要使用它?

WebPack可以看做是子產品打包機:它做的事情是,分析你的項目結構,找到JavaScript子產品以及其它的一些浏覽器不能直接運作的拓展語言(Scss,TypeScript等),并将其轉換和打包為合适的格式供浏覽器使用。

開始使用Webpack

(1)安裝

Webpack可以使用npm安裝,建立一個空的練習檔案夾(此處命名為webpack sample project),在終端中轉到該檔案夾後執行下述指令就可以完成安裝。

//全局安裝
npm install -g webpack
//安裝到你的項目目錄
npm install --save-dev webpack
           

(2)正式使用Webpack前的準備

1)在上述練習檔案夾中建立一個package.json檔案,這是一個标準的npm說明檔案,裡面蘊含了豐富的資訊,包括目前項目的依賴子產品,自定義的腳本任務等等。在終端中使用npm init指令可以自動建立這個package.json檔案

npm init
           

輸入這個指令後,終端會問你一系列諸如項目名稱,項目描述,作者等資訊,不過不用擔心,如果你不準備在npm中釋出你的子產品,這些問題的答案都不重要,回車預設即可。

2)package.json檔案已經就緒,我們在本項目中安裝Webpack作為依賴包

// 安裝Webpack
npm install --save-dev webpack
           

3)回到之前的空檔案夾,并在裡面建立兩個檔案夾,app檔案夾和public檔案夾,app檔案夾用來存放原始資料和我們将寫的JavaScript子產品,public檔案夾用來存放之後供浏覽器讀取的檔案(包括使用webpack打包生成的js檔案以及一個index.html檔案)。接下來我們再建立三個檔案:

  • index.html --放在public檔案夾中;
  • Greeter.js-- 放在app檔案夾中;
  • main.js-- 放在app檔案夾中;

此時項目結構如下圖所示

前端面試總結七

我們在index.html檔案中寫入最基礎的html代碼,它在這裡目的在于引入打包後的js檔案(這裡我們先把之後打包後的js檔案命名為bundle.js,之後我們還會詳細講述)。

<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Webpack Sample Project</title>
  </head>
  <body>
    <div id='root'>
    </div>
    <script src="bundle.js"></script>
  </body>
</html>
           

我們在Greeter.js中定義一個傳回包含問候資訊的html元素的函數,并依據CommonJS規範導出這個函數為一個子產品:

// Greeter.js
module.exports = function() {
  var greet = document.createElement('div');
  greet.textContent = "Hi there and greetings!";
  return greet;
};
           

main.js檔案中我們寫入下述代碼,用以把Greeter子產品傳回的節點插入頁面。

//main.js 
const greeter = require('./Greeter.js');
document.querySelector("#root").appendChild(greeter());
           

(3)正式使用Webpack

webpack可以在終端中使用,在基本的使用方法如下:

# {extry file}出填寫入口檔案的路徑,本文中就是上述main.js的路徑,
# {destination for bundled file}處填寫打封包件的存放路徑
# 填寫路徑的時候不用添加{}
webpack {entry file} {destination for bundled file}
           

指定入口檔案後,webpack将自動識别項目所依賴的其它檔案,不過需要注意的是如果你的webpack不是全局安裝的,那麼當你在終端中使用此指令時,需要額外指定其在node_modules中的位址,繼續上面的例子,在終端中輸入如下指令

# webpack非全局安裝的情況
node_modules/.bin/webpack app/main.js public/bundle.js
           

結果如下

前端面試總結七

可以看出webpack同時編譯了main.js 和Greeter,js,現在打開index.html,可以看到如下結果

前端面試總結七

通過配置檔案來使用Webpack

Webpack擁有很多其它的比較進階的功能(比如說本文後面會介紹的loaders和plugins),這些功能其實都可以通過指令行模式實作,但是正如前面提到的,這樣不太友善且容易出錯的,更好的辦法是定義一個配置檔案,這個配置檔案其實也是一個簡單的JavaScript子產品,我們可以把所有的與打包相關的資訊放在裡面。

繼續上面的例子來說明如何寫這個配置檔案,在目前練習檔案夾的根目錄下建立一個名為webpack.config.js的檔案,我們在其中寫入如下所示的簡單配置代碼,目前的配置主要涉及到的内容是入口檔案路徑和打包後檔案的存放路徑。

module.exports = {
  entry:  __dirname + "/app/main.js",//已多次提及的唯一入口檔案
  output: {
    path: __dirname + "/public",//打包後的檔案存放的地方
    filename: "bundle.js"//打包後輸出檔案的檔案名
  }
}
           

注:“__dirname”是node.js中的一個全局變量,它指向目前執行腳本所在的目錄。

有了這個配置之後,再打封包件,隻需在終端裡運作webpack(非全局安裝需使用node_modules/.bin/webpack)指令就可以了,這條指令會自動引用webpack.config.js檔案中的配置選項,示例如下:

前端面試總結七

又學會了一種使用Webpack的方法,這種方法不用管那煩人的指令行參數,有沒有感覺很爽。如果我們可以連webpack(非全局安裝需使用node_modules/.bin/webpack)這條指令都可以不用,那種感覺會不會更爽~,繼續看下文。

更快捷的執行打包任務

在指令行中輸入指令需要代碼類似于node_modules/.bin/webpack這樣的路徑其實是比較煩人的,不過值得慶幸的是npm可以引導任務執行,對npm進行配置後可以在指令行中使用簡單的npm start指令來替代上面略微繁瑣的指令。在package.json中對scripts對象進行相關設定即可,設定方法如下。

{
  "name": "webpack-sample-project",
  "version": "1.0.0",
  "description": "Sample webpack project",
  "scripts": {
    "start": "webpack" // 修改的是這裡,JSON檔案不支援注釋,引用時請清除
  },
  "author": "zhang",
  "license": "ISC",
  "devDependencies": {
    "webpack": "3.10.0"
  }
}
           

注:package.json中的script會安裝一定順序尋找指令對應位置,本地的node_modules/.bin路徑就在這個尋找清單中,是以無論是全局還是局部安裝的Webpack,你都不需要寫前面那指明詳細的路徑了。

npm的start指令是一個特殊的腳本名稱,其特殊性表現在,在指令行中使用npm start就可以執行其對于的指令,如果對應的此腳本名稱不是start,想要在指令行中運作時,需要這樣用npm run {script name}如npm run build,我們在指令行中輸入npm start試試,輸出結果如下:

前端面試總結七

現在隻需要使用npm start就可以打封包件了,有沒有覺得webpack也不過如此嘛,不過不要太小瞧webpack,要充分發揮其強大的功能我們需要修改配置檔案的其它選項,一項項來看。

Webpack的強大功能

Loaders

Loaders是webpack提供的最激動人心的功能之一了。通過使用不同的loader,webpack有能力調用外部的腳本或工具,實作對不同格式的檔案的處理,比如說分析轉換scss為css,或者把下一代的JS檔案(ES6,ES7)轉換為現代浏覽器相容的JS檔案,對React的開發而言,合适的Loaders可以把React的中用到的JSX檔案轉換為JS檔案。

Loaders需要單獨安裝并且需要在webpack.config.js中的modules關鍵字下進行配置,Loaders的配置包括以下幾方面:

  • test:一個用以比對loaders所處理檔案的拓展名的正規表達式(必須)
  • loader:loader的名稱(必須)
  • include/exclude:手動添加必須處理的檔案(檔案夾)或屏蔽不需要處理的檔案(檔案夾)(可選);
  • query:為loaders提供額外的設定選項(可選)

不過在配置loader之前,我們把Greeter.js裡的問候消息放在一個單獨的JSON檔案裡,并通過合适的配置使Greeter.js可以讀取該JSON檔案的值,各檔案修改後的代碼如下:

在app檔案夾中建立帶有問候資訊的JSON檔案(命名為config.json)

{
  "greetText": "Hi there and greetings from JSON!"
}
           

更新後的Greeter.js

var config = require('./config.json');

module.exports = function() {
  var greet = document.createElement('div');
  greet.textContent = config.greetText;
  return greet;
};
           

Babel

Babel其實是一個編譯JavaScript的平台,它可以編譯代碼幫你達到以下目的:

  • 讓你能使用最新的JavaScript代碼(ES6,ES7…),而不用管新标準是否被目前使用的浏覽器完全支援;
  • 讓你能使用基于JavaScript進行了拓展的語言,比如React的JSX;

Babel的安裝與配置

Babel其實是幾個子產品化的包,其核心功能位于稱為babel-core的npm包中,webpack可以把其不同的包整合在一起使用,對于每一個你需要的功能或拓展,你都需要安裝單獨的包(用得最多的是解析Es6的babel-env-preset包和解析JSX的babel-preset-react包)。

我們先來一次性安裝這些依賴包

// npm一次性安裝多個依賴子產品,子產品之間用空格隔開
npm install --save-dev babel-core babel-loader babel-preset-env babel-preset-react
           

在webpack中配置Babel的方法如下:

module.exports = {
    entry: __dirname + "/app/main.js",//已多次提及的唯一入口檔案
    output: {
        path: __dirname + "/public",//打包後的檔案存放的地方
        filename: "bundle.js"//打包後輸出檔案的檔案名
    },
    devtool: 'eval-source-map',
    devServer: {
        contentBase: "./public",//本地伺服器所加載的頁面所在的目錄
        historyApiFallback: true,//不跳轉
        inline: true//實時重新整理
    },
    module: {
        rules: [
            {
                test: /(\.jsx|\.js)$/,
                use: {
                    loader: "babel-loader",
                    options: {
                        presets: [
                            "env", "react"
                        ]
                    }
                },
                exclude: /node_modules/
            }
        ]
    }
};
           

現在你的webpack的配置已經允許你使用ES6以及JSX的文法了。繼續用上面的例子進行測試,不過這次我們會使用React,記得先安裝 React 和 React-DOM

npm install --save react react-dom
           

接下來我們使用ES6的文法,更新Greeter.js并傳回一個React元件

//Greeter,js
import React, {Component} from 'react'
import config from './config.json';

class Greeter extends Component{
  render() {
    return (
      <div>
        {config.greetText}
      </div>
    );
  }
}

export default Greeter
           

修改main.js如下,使用ES6的子產品定義和渲染Greeter子產品

// main.js
import React from 'react';
import {render} from 'react-dom';
import Greeter from './Greeter';

render(<Greeter />, document.getElementById('root'));
           

重新使用npm start打包,如果之前打開的本地伺服器沒有關閉,你應該可以在localhost:8080下看到與之前一樣的内容,這說明react和es6被正常打包了。

前端面試總結七

Babel的配置

Babel其實可以完全在 webpack.config.js 中進行配置,但是考慮到babel具有非常多的配置選項,在單一的webpack.config.js檔案中進行配置往往使得這個檔案顯得太複雜,是以一些開發者支援把babel的配置選項放在一個單獨的名為 “.babelrc” 的配置檔案中。我們現在的babel的配置并不算複雜,不過之後我們會再加一些東西,是以現在我們就提取出相關部分,分兩個配置檔案進行配置(webpack會自動調用.babelrc裡的babel配置選項),如下:

module.exports = {
    entry: __dirname + "/app/main.js",//已多次提及的唯一入口檔案
    output: {
        path: __dirname + "/public",//打包後的檔案存放的地方
        filename: "bundle.js"//打包後輸出檔案的檔案名
    },
    devtool: 'eval-source-map',
    devServer: {
        contentBase: "./public",//本地伺服器所加載的頁面所在的目錄
        historyApiFallback: true,//不跳轉
        inline: true//實時重新整理
    },
    module: {
        rules: [
            {
                test: /(\.jsx|\.js)$/,
                use: {
                    loader: "babel-loader"
                },
                exclude: /node_modules/
            }
        ]
    }
};
           
//.babelrc
{
  "presets": ["react", "env"]
}
           

CSS

webpack提供兩個工具處理樣式表,css-loader 和 style-loader,二者處理的任務不同,css-loader使你能夠使用類似@import 和 url(…)的方法實作 require()的功能,style-loader将所有的計算後的樣式加入頁面中,二者組合在一起使你能夠把樣式表嵌入webpack打包後的JS檔案中。

繼續上面的例子

//安裝
npm install --save-dev style-loader css-loader
           
//使用
module.exports = {

   ...
    module: {
        rules: [
            {
                test: /(\.jsx|\.js)$/,
                use: {
                    loader: "babel-loader"
                },
                exclude: /node_modules/
            },
            {
                test: /\.css$/,
                use: [
                    {
                        loader: "style-loader"
                    }, {
                        loader: "css-loader"
                    }
                ]
            }
        ]
    }
};
           

請注意這裡對同一個檔案引入多個loader的方法。

接下來,在app檔案夾裡建立一個名字為"main.css"的檔案,對一些元素設定樣式

/* main.css */
html {
  box-sizing: border-box;
  -ms-text-size-adjust: 100%;
  -webkit-text-size-adjust: 100%;
}

*, *:before, *:after {
  box-sizing: inherit;
}

body {
  margin: 0;
  font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
}

h1, h2, h3, h4, h5, h6, p, ul {
  margin: 0;
  padding: 0;
}
           

我們這裡例子中用到的webpack隻有單一的入口,其它的子產品需要通過 import, require, url等與入口檔案建立其關聯,為了讓webpack能找到”main.css“檔案,我們把它導入”main.js “中,如下

//main.js
import React from 'react';
import {render} from 'react-dom';
import Greeter from './Greeter';

import './main.css';//使用require導入css檔案

render(<Greeter />, document.getElementById('root'));
           

通常情況下,css會和js打包到同一個檔案中,并不會打包為一個單獨的css檔案,不過通過合适的配置webpack也可以把css打包為單獨的檔案的。

CSS module

在過去的一些年裡,JavaScript通過一些新的語言特性,更好的工具以及更好的實踐方法(比如說子產品化)發展得非常迅速。子產品使得開發者把複雜的代碼轉化為小的,幹淨的,依賴聲明明确的單元,配合優化工具,依賴管理和加載管理可以自動完成。

不過前端的另外一部分,CSS發展就相對慢一些,大多的樣式表卻依舊巨大且充滿了全局類名,維護和修改都非常困難。

被稱為CSS modules的技術意在把JS的子產品化思想帶入CSS中來,通過CSS子產品,所有的類名,動畫名預設都隻作用于目前子產品。Webpack對CSS子產品化提供了非常好的支援,隻需要在CSS loader中進行簡單配置即可,然後就可以直接把CSS的類名傳遞到元件的代碼中,這樣做有效避免了全局污染。具體的代碼如下

module.exports = {

    ...

    module: {
        rules: [
            {
                test: /(\.jsx|\.js)$/,
                use: {
                    loader: "babel-loader"
                },
                exclude: /node_modules/
            },
            {
                test: /\.css$/,
                use: [
                    {
                        loader: "style-loader"
                    }, {
                        loader: "css-loader",
                        options: {
                            modules: true, // 指定啟用css modules
                            localIdentName: '[name]__[local]--[hash:base64:5]' // 指定css的類名格式
                        }
                    }
                ]
            }
        ]
    }
};
           

我們在app檔案夾下建立一個Greeter.css檔案來進行一下測試

/* Greeter.css */
.root {
  background-color: #eee;
  padding: 10px;
  border: 3px solid #ccc;
}
           

導入.root到Greeter.js中

import React, {Component} from 'react';
import config from './config.json';
import styles from './Greeter.css';//導入

class Greeter extends Component{
  render() {
    return (
      <div className={styles.root}> //使用cssModule添加類名的方法
        {config.greetText}
      </div>
    );
  }
}

export default Greeter
           

放心使用把,相同的類名也不會造成不同元件之間的污染。

前端面試總結七

CSS預處理器

Sass 和 Less 之類的預處理器是對原生CSS的拓展,它們允許你使用類似于variables, nesting, mixins, inheritance等不存在于CSS中的特性來寫CSS,CSS預處理器可以這些特殊類型的語句轉化為浏覽器可識别的CSS語句。

你現在可能都已經熟悉了,在webpack裡使用相關loaders進行配置就可以使用了,以下是常用的CSS 處理loaders:

  • Less Loader
  • Sass Loader
  • Stylus Loader

插件(Plugins)

插件(Plugins)是用來拓展Webpack功能的,它們會在整個建構過程中生效,執行相關的任務。

Loaders和Plugins常常被弄混,但是他們其實是完全不同的東西,可以這麼來說,loaders是在打包建構過程中用來處理源檔案的(JSX,Scss,Less…),一次處理一個,插件并不直接操作單個檔案,它直接對整個建構過程其作用。

Webpack有很多内置插件,同時也有很多第三方插件,可以讓我們完成更加豐富的功能。

使用插件的方法

要使用某個插件,我們需要通過npm安裝它,然後要做的就是在webpack配置中的plugins關鍵字部分添加該插件的一個執行個體(plugins是一個數組)繼續上面的例子,我們添加了一個給打包後代碼添加版權聲明的插件。

const webpack = require('webpack');

module.exports = {
...
    module: {
        rules: [
            {
                test: /(\.jsx|\.js)$/,
                use: {
                    loader: "babel-loader"
                },
                exclude: /node_modules/
            },
            {
                test: /\.css$/,
                use: [
                    {
                        loader: "style-loader"
                    }, {
                        loader: "css-loader",
                        options: {
                            modules: true
                        }
                    }, {
                        loader: "postcss-loader"
                    }
                ]
            }
        ]
    },
    plugins: [
        new webpack.BannerPlugin('版權所有,翻版必究')
    ],
};
           

通過這個插件,打包後的JS檔案顯示如下

前端面試總結七

參考:入門Webpack,看這篇就夠了

16.HTML5離線存儲原理

大家都知道Web App是通過浏覽器來通路的,是以離線狀态下是無法使用app的。其中web app中的一些資源并不經常改變,不需要每次都向伺服器發送請求。這時應運而生的離線緩存就顯得尤為突出。通過把需要離線緩存儲的檔案列在一個manifest配置檔案中。醬紫在離線情況下也可以使用app。

使用HTML5,通過建立cache manifest檔案,可輕松建立web應用的離線版本。

HTML5引入了應用程式緩存,這意味着web應用可進行緩存,并可在沒有網絡時進行通路。

應用程式緩存為應用帶來三個優勢:

  • 離線浏覽–使用者可在離線時使用它們。
  • 速度–已經緩存的資源加載得更快。
  • 減少伺服器負載–浏覽器将隻從伺服器下載下傳更改過的資源。

使用方法:

隻要在頭部加一個manifest屬性就ok了

<!DOCTYPE HTML>
<html manifest = "cache.manifest">
...
</html>
           

然後cache.manifest檔案的書寫方式如下:

CACHE MANIFEST
#v0.11

CACHE:

js/app.js
css/style.css

NETWORK:
resourse/logo.png

FALLBACK:
/ /offline.html
           

代碼說明:

離線存儲的manifest一般由三個部分組成:

(1)CACHE:表示需要離線存儲的資源清單,由于包含manifest檔案的頁面将被自動離線存儲,是以不需要把頁面自身也列出來。

(2)NETWORK:表示在它下面列出來的資源隻有在線上的情況下才能通路,他們不會被離線存儲,是以在離線情況下無法使用這些資源。不過,如果在CACHE和NETWORK中有一個相同的資源,那麼這個資源還是會被離線存儲,也就是說CACHE的優先級更高。

(3)FALLBACK:表示如果通路第一個資源失敗,那麼就使用第二個資源來替換他,比如上面這個檔案表示的就是如果通路根目錄下任何一個資源失敗了,那麼就去通路offline.html。

注意事項:

  • 站點離線存儲的容量限制是5M
  • 如果manifest檔案,或者内部列舉的某一個檔案不能正常下載下傳,整個更新過程将視為失敗,浏覽器繼續全部使用老的緩存
  • 引用manifest的html必須與manifest檔案同源,在同一個域下
  • 在manifest中使用的相對路徑,相對參照物為manifest檔案
  • CACHE MANIFEST字元串應在第一行,且必不可少
  • 系統會自動緩存引用清單檔案的 HTML 檔案
  • manifest檔案中CACHE則與NETWORK,FALLBACK的位置順序沒有關系,如果是隐式聲明需要在最前面
  • FALLBACK中的資源必須和manifest檔案同源
  • 當一個資源被緩存後,該浏覽器直接請求這個絕對路徑也會通路緩存中的資源。
  • 站點中的其他頁面即使沒有設定manifest屬性,請求的資源如果在緩存中也從緩存中通路
  • 當manifest檔案發生改變時,資源請求本身也會觸發更新

參考:HTML5離線存儲原理

17.iframe

iframe的優點:

(1)iframe能夠原封不動的把嵌入的網頁展現出來。

(2)如果有多個網頁引用iframe,那麼你隻需要修改iframe的内容,就可以實作調用的每一個頁面内容的更改,友善快捷。

(3)網頁如果為了統一風格,頭部和版本都是一樣的,就可以寫成一個頁面,用iframe來嵌套,可以增加代碼的可重用。

(4)如果遇到加載緩慢的第三方内容如圖示和廣告,這些問題可以由iframe來解決。

iframe的缺點:

(1)會産生很多頁面,不容易管理。

(2)iframe架構結構有時會讓人感到迷惑,如果架構個數多的話,可能會出現上下、左右滾動條,會分散通路者的注意力,使用者體驗度差。

(3)代碼複雜,無法被一些搜尋引擎索引到,這一點很關鍵,現在的搜尋引擎爬蟲還不能很好的處理iframe中的内容,是以使用iframe會不利于搜尋引擎優化。

(4)很多的移動裝置(PDA手機)無法完全顯示架構,裝置相容性差。

(5)iframe架構頁面會增加伺服器的http請求,對于大型網站是不可取的。

18.對 WEB 标準以及 W3C 的了解與認識

web标準簡單來說可以分為結構、表現和行為。web标準一般是将該三部分獨立分開,使其更具有子產品化。但一般産生行為時,就會有結構或者表現的變化,也使這三者的界限并不那麼清晰

W3C對web标準提出了規範化的要求,也就是在實際程式設計中的一些代碼規範:包含如下幾點

(1)對于結構要求:(标簽規範可以提高搜尋引擎對頁面的抓取效率,對SEO很有幫助)

1)标簽字母要小寫

2)标簽要閉合

3)标簽不允許随意嵌套

(2)對于css和js來說

1)盡量使用外鍊css樣式表和js腳本。是結構、表現和行為分為三塊,符合規範。同時提高頁面渲染速度,提高使用者的體驗。

2)樣式盡量少用行間樣式表,使結構與表現分離,标簽的id和class等屬性命名要做到見文知義,标簽越少,加載越快,使用者體驗提高,代碼維護簡單,便于改版

3)不需要變動頁面内容,便可提供列印版本而不需要複制内容,提高網站易用性。

19.嚴格模式與混雜模式如何區分?它們有何意義?

嚴格模式:又稱标準模式,是指浏覽器按照 W3C 标準解析代碼。

混雜模式:又稱怪異模式或相容模式,是指浏覽器用自己的方式解析代碼。

如何區分:浏覽器解析時到底使用嚴格模式還是混雜模式,與網頁中的 DTD 直接相關。

(1)如果文檔包含嚴格的 DOCTYPE ,那麼它一般以嚴格模式呈現。(嚴格 DTD ——嚴格模式)

(2)包含過渡 DTD 和 URI 的 DOCTYPE ,也以嚴格模式呈現,但有過渡 DTD 而沒有 URI (統一資源辨別符,就是聲明最後的位址)會導緻頁面以混雜模式呈現。(有 URI 的過渡 DTD ——嚴格模式;沒有 URI 的過渡 DTD ——混雜模式)

(3)DOCTYPE 不存在或形式不正确會導緻文檔以混雜模式呈現。(DTD不存在或者格式不正确——混雜模式)

(4)HTML5 沒有 DTD ,是以也就沒有嚴格模式與混雜模式的差別,HTML5 有相對寬松的文法,實作時,已經盡可能大的實作了向後相容。( HTML5 沒有嚴格和混雜之分)

意義:嚴格模式與混雜模式存在的意義與其來源密切相關,如果說隻存在嚴格模式,那麼許多舊網站必然受到影響,如果隻存在混雜模式,那麼會回到當時浏覽器大戰時的混亂,每個浏覽器都有自己的解析模式。

嚴格模式與混雜模式的語句解析不同點有哪些?

1)盒模型的高寬包含内邊距padding和邊框border

前端面試總結七

在W3C标準中,如果設定一個元素的寬度和高度,指的是元素内容的寬度和高度,而在IE5.5及以下的浏覽器及其他版本的Quirks模式下,IE的寬度和高度還包含了padding和border。

2)可以設定行内元素的高寬

在Standards模式下,給span等行内元素設定wdith和height都不會生效,而在quirks模式下,則會生效。

3)可設定百分比的高度

在standards模式下,一個元素的高度是由其包含的内容來決定的,如果父元素沒有設定高度,子元素設定一個百分比的高度是無效的。

4)用margin:0 auto設定水準居中在IE下會失效

使用margin:0 auto在standards模式下可以使元素水準居中,但在quirks模式下卻會失效,quirk模式下的解決辦法,用text-align屬性:

body{text-align:center};#content{text-align:left}

5)quirk模式下設定圖檔的padding會失效

6)quirk模式下Table中的字型屬性不能繼承上層的設定

7)quirk模式下white-space:pre會失效

參考:Doctype作用?嚴格模式與混雜模式如何區分?它們有何差異?

20.link與@import的差別

兩者都是外部引入CSS的方式,那麼二者有什麼差別呢?

  • @import是CSS提供的文法規則,隻有導入樣式表的作用;link是HTML提供的标簽,不僅可以加載CSS檔案,還可以定義RSS,rel連接配接屬性等;
  • 加載頁面時,link引入的CSS被同時加載,@import引入的CSS将在頁面加載完畢後加載;
  • link标簽作為HTML元素,不存在相容性問題,而@import是CSS2.1才有的文法,故老版本浏覽器(IE5之前)不能識别;
  • 可以通過JS操作DOM,來插入link标簽改變樣式;由于DOM方法是基于文檔的,無法使用@import方式插入樣式;

建議使用link的方式引入CSS

繼續閱讀