天天看點

前端面試總結八

1.介紹一下标準的CSS的盒子模型?低版本IE的盒子模型有什麼不同的?

有兩種, IE 盒子模型、W3C 盒子模型

盒模型: 内容(content)、填充(padding)、邊界(margin)、 邊框(border);

區 别: IE的content部分把 border 和 padding計算了進去

2.PNG,GIF,JPG的差別及如何選

GIF:

  • 256色
  • 無損,編輯 儲存時候,不會損失。
  • 支援簡單動畫。
  • 支援boolean透明,也就是要麼完全透明,要麼不透明

JPEG:

  • millions of colors
  • 有損壓縮, 意味着每次編輯都會失去品質。
  • 不支援透明。
  • 适合照片,實際上很多相機使用的都是這個格式。

PNG:

無損,其實PNG有好幾種格式的,一般分為兩類:PNG8和truecolor PNGs;

(1)與GIF相比:

  • 它通常會産生較小的檔案大小。
  • 它支援阿爾法(變量)透明度。
  • 無動畫支援

(2)與JPEG相比:

  • 檔案更大
  • 無損
  • 是以可以作為JPEG圖檔中間編輯的中轉格式。

結論:

  • JPEG适合照片
  • GIF适合動畫
  • PNG8适合其他任何種類——圖表,buttons,背景,圖表等等。

3.CSS3動畫(簡單動畫的實作,如旋轉等)

依靠CSS3中提出的三個屬性:transition、transform、animation

  • transition:定義了元素在變化過程中是怎麼樣的,包含transition-property、transition-duration、transition-timing-function、transition-delay。
  • transform:定義元素的變化結果,包含rotate、scale、skew、translate。
  • animation:動畫定義了動作的每一幀(@keyframes)有什麼效果,包括animation-name,animation-duration、animation-timing-function、animation-delay、animation-iteration-count、animation-direction

4.Base64

為什麼Base64編碼可以内聯到HTML中?

我們知道HTTP協定是文本協定,不同于正常的二進制協定那樣直接進行二進制傳輸。Base64編碼是 從二進制到字元的過程,可用于在HTTP環境下傳遞較長的辨別資訊。

什麼是Base64編碼

首先Base64是一種編碼算法,為什麼叫做Base64呢?其實原因也很簡單,是因為該算法共包含64個字元。包括大小寫拉丁字母各26個、數字10個、加号 + 和斜杠 / ,共64個字元。此外還有等号 = 用來作為字尾用途。

但,為什麼Base64編碼算法隻支援64個字元呢?

首先,我們先回顧下ASCII碼。ASCII碼的範圍是0-127,其中0-31和127是控制字元,共33個。其餘95個,即32-126是可列印字元,包括數字、大小寫字母、常用符号等。

早期的一些傳輸協定,例如郵件傳輸協定SMTP,隻能傳輸可列印的ASCII字元。這樣原本的8bit位元組碼(0-255)就會超出使用範圍,進而導緻無法傳輸。

這時,就産生了Base64編碼,它利用 6bit字元來表達原本的8bit字元 。

Base64編碼原理

首先,6bit顯然不夠容納8bit的資料。6和8的最小公倍數是24,是以我們用4個Base64字元剛好能夠表示三個傳統的8bit字元。如下所示,字元串 Man 的編碼圖解如下:

前端面試總結八

Man 的編碼結果為 TWFu ,顯然,Base64編碼會多1/3的長度,這也解釋了文中開頭的疑問,為什麼Base64編碼後的體積會大1/3。

Man 這個字元串的長度剛好是3,我們能用4個Base64來表示。如果待編碼的字元串長度不是三的倍數時應該怎麼處理呢?

這是需要做一個特殊處理,假設待編碼字元串長度為10。這前9個字元可以用12個Base64字元表示。第10個字元的前6bit作為一個Base64字元, 剩下的2bit後面需要先補0,補到6位(此處補4個0) 作為第二個Base64字元,至于第三個和第四個Base64字元,雖然沒有相對應的内容,我們仍需 以 = 填充 。

如下圖所示, A 對應的Base64編碼為 QQ== , BC 對應的Base64編碼為 QkM= 。

前端面試總結八

最後的問題就是解碼啦,解碼的過程比較簡單。 去掉末尾的等号 = 。剩下的Base64字元,每8bit組成一個8bit位元組,最後剩餘不足8位的丢棄即可。

圖檔轉換成base64格式的優缺點

優點

(1)base64格式的圖檔是文本格式,占用記憶體小,轉換後的大小比例大概為1/3,降低了資源伺服器的消耗;

(2)網頁中使用base64格式的圖檔時,不用再請求伺服器調用圖檔資源,減少了伺服器通路次數。

缺點

(1)base64格式的文本内容較多,存儲在資料庫中增大了資料庫伺服器的壓力;

(2)網頁加載圖檔雖然不用通路伺服器了,但因為base64格式的内容太多,是以加載網頁的速度會降低,可能會影響使用者的體驗。

(3)base64無法緩存,要緩存隻能緩存包含base64的檔案,比如js或者css,這比直接緩存圖檔要差很多,而且一般HTML改動比較頻繁,是以等同于得不到緩存效益。

參考:base64原理淺析

5.new操作符具體幹了什麼呢

var Func=function(){
};
var func=new Func ();
           

new共經過了4幾個階段

(1)建立一個空對象

(2)設定原型鍊

(3)讓Func中的this指向obj,并執行Func的函數體。

(4)判斷Func的傳回值類型:

如果是值類型,傳回obj。如果是引用類型,就傳回這個引用類型的對象。

if (typeof(result) == "object"){
  func=result;
}
else{
    func=obj;;
}
           

6.面向過程與面向對象程式設計的差別和優缺點

面向過程程式設計 POP(Process-oriented programming)

面向過程就是分析出解決問題所需要的步驟,然後用函數把這些步驟一步步實作 ,使用的時候再一個一個的依次調用就可以了。

面向對象程式設計OOP (Object Oriented Programming)

面向對象是把事務分解成為一個個對象,然後由對象之間分工與合作。

在面向對象程式開發思想中,每一個對象都是功能中心,具有明确分工。

面向對象程式設計具有靈活、代碼可複用、容易維護和開發的優點,更适合多人合作的大型軟體項目。

面向對象的特性:封裝、繼承、多态

面向過程與面向對象的優缺點

面向過程

  優點:性能比面向對象高,因為類調用時需要執行個體化,開銷比較大,比較消耗資源,比如單片機、嵌入式開發、Linux/Unix等一般采用面向過程開發,性能是最重要的因素。

  缺點:沒有面向對象易維護、易複用、易擴充

面向對象

  優點:易維護、易複用、易擴充,由于面向對象有封裝、繼承、多态性的特性,可以設計出低耦合的系統,使系統更加靈活、更加易于維護

  缺點:性能比面向過程低

7.HTML全局屬性(global attribute)有哪些?

  • accesskey 規定激活元素的快捷鍵;
  • class 規定元素的一個或多個類名(引用樣式表中的類);
  • contenteditable 規定元素内容是否可編輯;
  • contextmenu 規定元素的上下文菜單。上下文菜單在使用者點選元素時顯示。
  • data-* 用于存儲頁面或應用程式的私有定制資料。
  • dir 規定元素中内容的文本方向。
  • draggable 規定元素是否可拖動。
  • dropzone 規定在拖動被拖動資料時是否進行複制、移動或連結。
  • hidden 樣式上會導緻元素不顯示,但是不能用這個屬性實作樣式。
  • id 規定元素的唯一 id。
  • lang 規定元素内容的語言。
  • spellcheck 規定是否對元素進行拼寫和文法檢查。
  • style 規定元素的CSS行内元素。
  • tabindex 規定元素的tab鍵次序。
  • title 規定有關元素的額外資訊。
  • translate 規定是否應該翻譯元素内容。

參考:https://www.nowcoder.com/questionTerminal/5505ebcd04df48dd91e32977cb9a9b11

8.說說超連結target屬性的取值和作用?

  • _blank:在新視窗中打開連結文檔
  • _self:預設。在相同的架構中打開連結文檔
  • _top:在整個視窗中打開連結文檔
  • _parent:在父級架構中集中打開
  • _framename:在指定的架構中打開連結文檔

9.

data-

屬性的作用是什麼?

data-

為H5新增的為前端開發者提供自定義的屬性,這些屬性集可以通過對象的

dataset

屬性擷取,不支援該屬性的浏覽器可以通過

getAttribute

方法擷取 :

需要注意的是:

data-

之後的以連字元分割的多個單詞組成的屬性,擷取的時候使用駝峰風格。 所有主流浏覽器都支援 data-* 屬性。

即:當沒有合适的屬性和元素時,自定義的 data 屬性是能夠存儲頁面或 App 的私有的自定義資料。

10.介紹一下你對浏覽器核心的了解?

主要分成兩部分:渲染引擎(layout engineer或 Rendering Engine) 和 JS 引擎。

渲染引擎:負責取得網頁的内容(HTML、 XML 、圖像等等)、整理訊息(例如加入 CSS 等),以及計算網頁的顯示方式,然後會輸出至顯示器或列印機。浏覽器的核心的不同對于網頁的文法解釋會有不同,是以渲染的效果也不相同。所有網頁浏覽器、電子郵件用戶端以及其它需要編輯、顯示網絡内容的應用程式都需要核心。

JS引擎則:解析和執行 javascript 來實作網頁的動态效果。

最開始渲染引擎和JS引擎并沒有區分的很明确,後來 JS 引擎越來越獨立,核心就傾向于隻指渲染引擎。

11.Label的作用是什麼,是怎麼用的?

label标簽來定義表單控制間的關系 , 當使用者選擇該标簽時,浏覽器會自動将焦點轉到和标簽相關的表單控件上。

<label for='Name'>Number:</label>
<input type=“ text “ name='Name' id='Name'/>
<label>Date:<input type='text' name='B'/></label>
           

注意:label的for屬性值要與後面對應的input标簽id屬性值相同

<label for='Name'>Number:</label>
<input type=“ text “ name='Name' id='Name'/>
           

12.如何實作浏覽器内多個标簽頁之間的通信?

(1)使用localStorage

使用localStorage.setItem(key,value);添加内容

使用storage事件監聽添加、修改、删除的動作

window.addEventListener("storage",function(event){
        $("#name").val(event.key+”=”+event.newValue);
});
           

(2)使用cookie+setInterval

JS代碼-頁面1

$(function(){
        $("#btnOK").click(function(){
            varname=$("#name").val();
            document.cookie="name="+name;
        });
    });
           

JS代碼-頁面2

//擷取Cookie天的内容
    function getKey(key) {
        return JSON.parse("{\""+ document.cookie.replace(/;\s+/gim,"\",\"").replace(/=/gim, "\":\"") +"\"}")[key];
    }
    //每隔1秒擷取Cookie的内容
    setInterval(function(){
        console.log(getKey("name"));
     },1000);
           

13.如何在頁面上實作一個圓形的可點選區域?

(1)純html: map+area;

<img id="blue" class="click-area" src="blue.gif" usemap="#Map" /> 
<map name="Map" id="Map" class="click-area">  <area shape="circle" coords="50,50,50"/>
</map>
           
#blue{
 cursor:pointer;
 width:100px;
 height:100px;
}
           

(2)純CSS: border-radius: 50%;

//第二種 使用CSS border-radius
<div id="red" class="click-area" ></div>
           
#red{  
 cursor:pointer;
 background:red;  
 width:100px;  
 height:100px;  
 border-radius:50%;  
} 
           

(3)純js: 利用點在圓内點在圓外

//第三種 使用js檢測滑鼠位置,需要求一個點在不在圓上簡單算法、擷取滑鼠坐标等等
<div id="yellow" class="click-area" ></div>
           
$("#yellow").on('click',function(e) {    
  var r = 50; 
  var x1 = $(this).offset().left+$(this).width()/2;            
  var y1 = $(this).offset().top+$(this).height()/2;   
  var x2= e.clientX;  
  var y2= e.clientY;    
  var distance = Math.abs(Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2)));    
  if (distance <= 50)  
    alert("Yes!");    
});  
           

14.title與h3的差別、b與strong的差別、i與em的差別?

title屬性沒有明确意義隻表示是個标題, H1 則表示層次明确的标題,對頁面資訊的抓取也有很大的影響;

strong是标明重點内容,有語氣加強的含義,使用閱讀裝置閱讀網絡時:

<strong>

會重讀,而

<B>

是展示強調内容。

i内容展示為斜體, em 表示強調的文本.

15.實作不使用 border 畫出1px高的線,在不同浏覽器的标準模式與怪異模式下都能保持一緻的效果?

16.Null和undefined的差別

null表示沒有對象,即該處不應該有值,undefined表示缺少值,即此處應該有值,但沒有定義.

(1)null是一個表示”無”的對象,轉為數值是為0,undefined是一個表示”無”的原始值,轉為數值時為NaN。當聲明的變量還未被初始化時,能量的預設值為undefined

(2)Null用來表示尚未存在的對象,常用來表示函數企圖傳回一個不存在的對象

(3)Undefined表示”缺少值”,就是此處應該有一個值,但是還沒有定義。典型用法是:

  a、變量被聲明了,但沒有指派時,就等于undefined

  b、調用函數時,應該提供的參數沒有提供,該參數等于undefined

  c、對象沒有指派屬性,該屬性的值為undefined

  d、函數沒有傳回值時,預設傳回undefined

(4)null表示”沒有對象”,即該處不應該有值。典型用法是:

  a、作為函數的參數,表示該函數的參數不是對象

  b、作為對象原型鍊的終點

console.log(null==undefined);    //true  因為兩者都預設轉換成了false
console.log(typeof undefined);    //"undefined"  
console.log(typeof null);       //"object"  
console.log(null===undefined);    //false   "==="表示絕對相等,null和undefined類型是不一樣的,是以輸出“false”
           

17.cookie和session

** 為什麼需要cookie和session **

在Web發展史中,我們知道浏覽器與伺服器間采用的是 http協定,而這種協定是無狀态的,是以這就導緻了伺服器無法知道是誰在浏覽網頁,但很明顯,一些網頁需要知道使用者的狀态,例如登陸,購物車等。

是以為了解決這一問題,先後出現了四種技術,分别是隐藏表單域,URL重寫,cookie,session,而用的最多也是比較重要的就是cookie和session了。

** Cookie **

cookie是浏覽器儲存在使用者電腦上的一小段文本,通俗的來講就是當一個使用者通過 http通路到伺服器時,伺服器會将一些 Key/Value鍵值對傳回給用戶端浏覽器,并給這些資料加上一些限制條件,在條件符合時這個使用者下次通路這個伺服器時,資料通過請求頭又被完整地給帶回伺服器,伺服器根據這些資訊來判斷不同的使用者。

cookie是浏覽器儲存在使用者電腦上的一小段文本,通俗的來講就是當一個使用者通過 http通路到伺服器時,伺服器會将一些 Key/Value鍵值對傳回給用戶端浏覽器,并給這些資料加上一些限制條件,在條件符合時這個使用者下次通路這個伺服器時,資料通過請求頭又被完整地給帶回伺服器,伺服器根據這些資訊來判斷不同的使用者。

Cookie的建立

目前 Cookie有兩個版本,分别對應兩種設定響應頭:“Set-Cookie”和 “Set-Cookie2”。在Servlet中并不支援Set-Cookie2,是以我們來看看Set-Cookie的屬性項:

前端面試總結八

這些屬性項,其他的都說的很清楚了,我們來看看Domain有什麼用:

現在,我們假設這裡有兩個域名:

域名A:

a.b.f.com.cn

域名B:

c.d.f.com.cn

顯然,域名A和域名B都是

f.com.cn

的子域名

  • 如果我們在域名A中的Cookie的domain設定為

    f.com.cn

    ,那麼

    f.com.cn

    及其子域名都可以擷取這個Cookie,即域名A和域名B都可以擷取這個Cookie
  • 如果域名A和域名B同時設定Cookie的doamin為

    f.com.cn

    ,那麼将出現覆寫的現象
  • 如果域名A沒有顯式設定Cookie的domain方法,那麼domain就為

    a.b.f.com.cn

    ,不一樣的是,這時,域名A的子域名将無法擷取這個Cookie

好的,現在了解完了Set-Cookie的屬性項,開始建立Cookie

Web伺服器通過發送一個稱為Set-Cookie的http消息來建立一個Cookie:

Set-Cookie: value[; expires=date][; domain=domain][; path=path][; secure]

這裡我們思考一個問題,當我們在伺服器建立多個Cookie時,這些Cookie最終是在一個Header項中還是以獨立的Header存在的呢?

前端面試總結八

我們可以看到,建構http傳回位元組流時是将Header中所有的項順序寫出,而沒有進行任何修改。是以可以想象在浏覽器在接收http傳回的資料時是分别解析每一個Header項。

接着,在用戶端進行儲存,如何儲存呢?這裡又要對Cookie進行進一步的了解

Cookie的分類

  • 會話級别Cookie:所謂會話級别Cookie,就是在浏覽器關閉之後Cookie就會失效。
  • 持久級别Cookie:儲存在硬碟的Cookie,隻要設定了過期時間就是硬碟級别Cookie。

好的,現在cookie儲存在了用戶端,當我們去請求一個URL時,浏覽器會根據這個URL路徑将符合條件的Cookie放在請求頭中傳給伺服器。

Session

Cookie是有大小限制和數量限制的,并且越來越多的Cookie代表用戶端和伺服器的傳輸量增加,可不可以每次傳的時候不傳所有cookie值,而隻傳一個唯一ID,通過這個ID直接在伺服器查找使用者資訊呢?答案是有的,這就是我們的session。

Session是基于Cookie來工作的,同一個用戶端每次通路伺服器時,隻要當浏覽器在第一次通路伺服器時,伺服器設定一個id并儲存一些資訊(例如登陸就儲存使用者資訊,視具體情況),并把這個id通過Cookie存到用戶端,用戶端每次和伺服器互動時隻傳這個id,就可以實作維持浏覽器和伺服器的狀态,而這個ID通常是NAME為JSESSIONID的一個Cookie。

實際上,有四種方式讓Session正常工作:

  • 通過URL傳遞SessionID

    當浏覽器不支援Cookie功能時,浏覽器會将使用者的SessionCookieName(預設為JSESSIONID)重寫到使用者請求的URL參數中。格式:

    /path/Servlet;name=value;name2=value2?Name3=value3

  • 通過Cookie傳遞SessionID
  • 通過SSL傳遞SessionID

    會根據

    javax.servlet.request.ssl_session

    屬性值設定SessionID。

    注:如果用戶端支援Cookie,又通過URL重寫,Tomcat仍然會解析Cookie中的SessionID并覆寫URL中的SessionID

  • 通過隐藏表單傳遞SessionID

*** session工作原理 ***

前端面試總結八

一、建立session

當用戶端通路到伺服器,伺服器會為這個用戶端通過request.getSession()方法建立一個Session,如果目前SessionID還沒有對應的HttpSession對象,就建立一個新的,并添加到org.apache.catalina.Manager的sessions容器中儲存,這就做到了對狀态的保持。當然,這個SessionID是唯一的

二、session儲存

由圖可知,session對象已經儲存在了Manager類中,StandardManager作為實作類,通過requestedSessionId從StandardManager的sessions集合中取出StandardSession對象。

我們來看看StandardManager時如何對所有StandardSession對象進行生命周期管理

當Servlet容器關閉:

StandardManager将持久化沒過期的StandardSession對象(必須調用Servlet容器中的stop和start指令,不能直接kill)
           

當Servlet容器重新開機時:

StandardManager初始化會重讀這個檔案,解析出所有session對象。
           

三、session的銷毀

這裡有一個誤區,也是我之前的錯誤了解,就是我将session的生命周期了解成一次會話,浏覽器打開就建立,浏覽器關閉就銷毀,這樣了解是錯的!!

session的聲明周期是從建立到逾時過期

也就是說,當session建立後,浏覽器關閉,會話級别的Cookie被銷毀,如果沒有超過設定時間,該SessionID對應的session是沒有被銷毀的,

檢查session失效

檢查每個Session是否失效是在Tomcat的一個背景線程完成的(backgroundProcess()方法中);除了背景程序檢驗session是否失效外,調用request.getSession()也會檢查該session是否過期,當然,調用這種方法如果過期的話又會重新建立一個新的session。

二者的異同

相同點(有關系的地方):

  • Session和Cookie都是為了讓http協定有狀态而存在
  • Session通過Cookie工作,Cookie傳輸的SessionID讓Session知道這個用戶端到底是誰

不同點:

Session将資訊儲存到伺服器,Cookie将資訊儲存在用戶端

工作流程

當浏覽器第一次通路伺服器時,伺服器建立Session并将SessionID通過Cookie帶給浏覽器儲存在用戶端,同時伺服器根據業務邏輯儲存相應的用戶端資訊儲存在session中;用戶端再通路時上傳Cookie,伺服器得到Cookie後擷取裡面的SessionID,來維持狀态。

參考:一文徹底搞懂cookie和session

18.cookie、session、sessionStorage、localStorage 之間的差別及應用場景

cookie

  • 由服務端生成,儲存在用戶端(由于前後端有互動,是以安全性差,且浪費帶寬)
  • 存儲大小有限(最大 4kb )
  • 存儲内容隻接受 String 類型
  • 儲存位置:

    若未設定過期時間,則儲存在 記憶體 中,浏覽器關閉後銷毀

    若設定了過期時間,則儲存在 系統硬碟 中,直到過期時間結束後才消失(即使關閉浏覽器)

  • 資料操作不友善,原生接口不友好,需要自己封裝
  • 應用場景

    判斷使用者是否登陸過網站,以便下次登入時能夠實作自動登入(或者記住密碼)

    儲存登入時間、浏覽次數等資訊

session

  • 是後端關心的内容,依賴于 cookie(sessionID 儲存在cookie中)
  • 儲存在服務端
  • 存儲大小無限制
  • 支援任何類型的存儲内容
  • 儲存位置:伺服器記憶體,若通路較多會影響伺服器性能

webStorage

webStorage 是 html5 提供的本地存儲方案,包括兩種方式:sessionStorage 和 localStorage。相比 cookie 這種傳統的用戶端存儲方式,webStorage 有許多優點:

  • 儲存在用戶端,不與伺服器通信,是以比 cookie 更安全、速度更快
  • 存儲空間有限,但比 cookie 大(5MB)
  • 僅支援 String 類型的存儲内容(和 cookie 一樣)
  • html5 提供了原生接口,資料操作比 cookie 友善

    setItem(key, value) 儲存資料,以鍵值對的方式儲存資訊。

    getItem(key) 擷取資料,将鍵值傳入,即可擷取到對應的value值。

    removeItem(key) 删除單個資料,根據鍵值移除對應的資訊。

    clear() 删除所有的資料

    key(index) 擷取某個索引的key

localStorage

  • 持久化的本地存儲,浏覽器關閉重新打開資料依然存在(除非手動删除資料)。
  • 應用場景:長期登入、判斷使用者是否已登入,适合長期儲存在本地的資料。

sessionStorage

  • 浏覽器視窗關閉後資料被銷毀。
  • 應用場景:敏感賬号一次性登入。

總結

綜上所述,我們可以知道,cookie 和 webStorage( localStorage、sessionStorage )是前端工程師關心的内容,session 是後端關心的内容。

cookie 存儲量小、安全性差、資料操作接口不友好,而 webStorage 存儲量較大、安全性較高、資料接口友好。

若要持久的存儲方式,推薦使用 localStorage

若要一次性會話的存儲,推薦使用 sessionStorage

參考:cookie、session、sessionStorage、localStorage 之間的差別及應用場景

19.Vue的響應式原理(MVVM)深入解析

如何實作一個響應式對象

最近在看 Vue 的源碼,其中最核心基礎的一塊就是 Observer/Watcher/Dep, 簡而言之就是,Vue 是如何攔截資料的讀寫, 如果實作對應的監聽,并且特定的監聽執行特定的回調或者渲染邏輯的。總的可以拆成三大塊來說。這一塊,主要說的是 Vue 是如何将一個 plain object 給處理成 reactive object 的,也就是,Vue 是如何攔截攔截對象的 get/set 的

我們知道,用 Object.defineProperty 攔截資料的 get/set 是 vue 的核心邏輯之一。這裡我們先考慮一個最簡單的情況 一個 plain obj 的資料,經過你的程式之後,使得這個 obj 變成 Reactive Obj (不考慮數組等因素,隻考慮最簡單的基礎資料類型,和對象):

如果這個 obj 的某個 key 被 get, 則列印出 get ${key} - ${val} 的資訊

如果這個 obj 的某個 key 被 set, 如果監測到這個 key 對應的 value 發生了變化,則列印出 set ${key} - ${val} - ${newVal} 的資訊。

對應的簡要代碼如下:

Observer.js

export class Observer {
  constructor(obj) {
    this.obj = obj;
    this.transform(obj);
  }
  // 将 obj 裡的所有層級的 key 都用 defineProperty 重新定義一遍, 使之 reactive 
  transform(obj) {
    const _this = this;
    for (let key in obj) {
      const value = obj[key];
      makeItReactive(obj, key, value);
    }
  }
}
function makeItReactive(obj, key, val) {
  // 如果某個 key 對應的 val 是 object, 則重新疊代該 val, 使之 reactive 
  if (isObject(val)) {
    const childObj = val;
    new Observer(childObj);
  }
  // 如果某個 key 對應的 val 不是 Object, 而是基礎類型,我們則對這個 key 進行 defineProperty 定義 
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: () => {
      console.info(`get ${key}-${val}`)
      return val;
    },
    set: (newVal) => {
      // 如果 newVal 和 val 相等,則不做任何操作(不執行渲染邏輯)
      if (newVal === val) {
        return;
      }
      // 如果 newVal 和 val 不相等,且因為 newVal 為 Object, 是以先用 Observer疊代 newVal, 使之 reactive, 再用 newVal 替換掉 val, 再執行對應操作(渲染邏輯)
      else if (isObject(newVal)) {
        console.info(`set ${key} - ${val} - ${newVal} - newVal is Object`);
        new Observer(newVal);
        val = newVal;
      }
      // 如果 newVal 和 val 不相等,且因為 newVal 為基礎類型, 是以用 newVal 替換掉 val, 再執行對應操作(渲染邏輯)
      else if (!isObject(newVal)) {
        console.info(`set ${key} - ${val} - ${newVal} - newVal is Basic Value`);
        val = newVal;
      }
    }
  })
}

function isObject(data) {
  if (typeof data === 'object' && data != 'null') {
    return true;
  }
  return false;
}
           

index.js

import { Observer } from './source/Observer.js';
// 聲明一個 obj,為 plain Object
const obj = {
  a: {
    aa: 1
  },
  b: 2,
}
// 将 obj 整體 reactive 化
new Observer(obj);
// 無輸出
obj.b = 2;
// set b - 2 - 3 - newVal is Basic Value
obj.b = 3;
// set b - 3 - [object Object] - newVal is Object
obj.b = {
  bb: 4
}
// get b-[object Object]
obj.b;
// get a-[object Object]
obj.a;
// get aa-1
obj.a.aa
// set aa - 1 - 3 - newVal is Basic Value
obj.a.aa = 3
           

這樣,我們就完成了 Vue 的第一個核心邏輯, 成功把一個任意層級的 plain object 轉化成 reactive object

如何實作一個 watcher

前面講的是如何将 plain object 轉換成 reactive object. 接下來講一下,如何實作一個watcher.

實作的僞代碼應如下:

僞代碼

// 傳入 data 參數建立建立一個 vue 對象
const v = new Vue({
    data: {
        a:1,
        b:2,
    }
});
// watch data 裡面某個 a 節點的變動了,如果變動,則執行 cb
v.$watch('a',function(){
    console.info('the value of a has been changed !');
});

//  watch data 裡面某個 b 節點的變動了,如果變動,則執行 cb
v.$watch('b',function(){
    console.info('the value of b has been changed !');
})
           

是以我們自然而然的想到,對某個 key 的 $watch 方法應該是建立了一個 watcher 的執行個體的. 并且,vue 也是這樣實作的. 簡要的 Vue 類的實作如下 (隻貼出相關代碼)

Vue.js

// 引入将上面中實作的 Observer
import { Observer } from './Observer.js';
import { Watcher } from './Watcher.js';

export default class Vue {
  constructor(options) {
    // 在 this 上挂載一個公有變量 $options ,用來暫存所有參數
    this.$options = options
    // 聲明一個私有變量 _data ,用來暫存 data
    let data = this._data = this.$options.data
    // 在 this 上挂載所有 data 裡的 key 值, 這些 key 值對應的 get/set 都被代理到 this._data 上對應的同名 key 值
    Object.keys(data).forEach(key => this._proxy(key));
    // 将 this._data 進行 reactive 化
    new Observer(data, this)
  }
  // 對外暴露 $watch 的公有方法,可以對某個 this._data 裡的 key 值建立一個 watcher 執行個體
  $watch(expOrFn, cb) {
    // 注意,每一個 watcher 的執行個體化都依賴于 Vue 的執行個體化對象, 即 this
    new Watcher(this, expOrFn, cb)
  }
  //  将 this.keyName 的某個 key 值的 get/set 代理到  this._data.keyName 的具體實作
  _proxy(key) {
    var self = this
    Object.defineProperty(self, key, {
      configurable: true,
      enumerable: true,
      get: function proxyGetter() {
        return self._data[key]
      },
      set: function proxySetter(val) {
        self._data[key] = val
      }
    })
  }
}
           

Watch.js

// 引入Dep.js, 是什麼我們待會再說
import { Dep } from './Dep.js';

export class Watcher {
  constructor(vm, expOrFn, cb) {
    this.cb = cb;
    this.vm = vm;
    this.expOrFn = expOrFn;
    // 初始化 watcher 時, vm._data[this.expOrFn] 對應的 val
    this.value = this.get();
  }
  // 用于擷取目前 vm._data 對應的 key = expOrFn 對應的 val 值
  get() {
    Dep.target = this;
    const value = this.vm._data[this.expOrFn];
    Dep.target = null;
    return value;
  }
  // 每次 vm._data 裡對應的 expOrFn, 即 key 的 setter 被觸發,都會調用 watcher 裡對應的 update方法
  update() {
    this.run();
  }
  run() {
    // 這個 value 是 key 被 setter 調用之後的 newVal, 然後比較 this.value 和 newVal, 如果不相等,則替換 this.value 為 newVal, 并執行傳入的cb.
    const value = this.get();
    if (value !== this.value) {
      this.value = value;
      this.cb.call(this.vm);
    }
  }
}
           

對于什麼是 Dep, 和 Watcher 裡的 update() 方法到底是在哪個時候被誰調用的,後面會說

如何收集 watcher 的依賴

前面我們講了 watcher 的大緻實作,以及 Vue 代理 data 到 this 上的原理。現在我們就來梳理一下,Observer/Watcher 之間的關系,來說明它們是如何調用的.

首先, 我們要來了解一下 watcher 執行個體的概念。實際上 Vue 的 v-model, v-bind , {{ mustache }}, computed, watcher 等等本質上是分别對 data 裡的某個 key 節點聲明了一個 watcher 執行個體.

<input v-model="abc">
<span>{{ abc }}</span>
<p :data-key="abc"></p>
...

const v = new Vue({
    data:{
        abc: 111,
    }
    computed:{
        cbd:function(){
            return `${this.abc} after computed`;
        }
    watch:{
        abc:function(val){
            console.info(`${val} after watch`)
        }
     }  
    }
})
           

這裡,Vue 一共聲明了 4 個 watcher 執行個體來監聽abc, 1個 watcher 執行個體來監聽 cbd. 如果 abc 的值被更改,那麼 4 個 abc - watcher 的執行個體會執行自身對應的特定回調(比如重新渲染dom,或者是列印資訊等等)

不過,Vue 是如何知道,某個 key 對應了多少個 watcher, 而 key 對應的 value 發生變化後,又是如何通知到這些 watcher 來執行對應的不同的回調的呢?

實際上更深層次的邏輯是:

在 Observer階段,會為每個 key 都建立一個 dep 執行個體。并且,如果該 key 被某個 watcher 執行個體 get, 把該 watcher 執行個體加入 dep 執行個體的隊列裡。如果該 key 被 set, 則通知該 key 對應的 dep 執行個體, 然後 dep 執行個體會将依次通知隊列裡的 watcher 執行個體, 讓它們去執行自身的回調方法

dep 執行個體是收集該 key 所有 watcher 執行個體的地方.

watcher 執行個體用來監聽某個 key ,如果該 key 産生變化,便會執行 watcher 執行個體自身的回調

前端面試總結八

相關代碼如下:

Dep.js

export class Dep {
  constructor() {
    this.subs = [];
  }
  // 将 watcher 執行個體置入隊列
  addSub(sub) {
    this.subs.push(sub);
  }
  // 通知隊列裡的所有 watcher 執行個體,告知該 key 的 對應的 val 被改變
  notify() {
    this.subs.forEach((sub, index, arr) => sub.update());
  }
}

// Dep 類的的某個靜态屬性,用于指向某個特定的 watcher 執行個體.
Dep.target = null 
           

observer.js

import {Dep} from './dep'
function makeItReactive(obj, key, val) {
 var dep = new Dep()
Object.defineProperty(obj, key, {
  enumerable: true,
  configurable: true,
  get: () => {
    // 收集依賴! 如果該 key 被某個 watcher 執行個體依賴,則将該 watcher 執行個體置入該 key 對應的 dep 執行個體裡
    if(Dep.target){
      dep.addSub(Dep.target)
    }
    return val
  },
  set: (newVal) => {
    if (newVal === val) {
      return;
    }
    else if (isObject(newVal)) {
      new Observer(newVal);
      val = newVal;
    // 通知 dep 執行個體, 該 key 被 set,讓 dep 執行個體向所有收集到的該 key 的 watcher 執行個體發送通知
    dep.notify()
    }
    else if (!isObject(newVal)) {
      val = newVal;
    // 通知 dep 執行個體, 該 key 被 set,讓 dep 執行個體向所有收集到的該 key 的 watcher 發送通知
    dep.notify()
    }
  }
})
     }   
           

watcher.js

import { Dep } from './Dep.js';

export class Watcher {
  constructor(vm, expOrFn, cb) {
    this.cb = cb;
    this.vm = vm;
    this.expOrFn = expOrFn;
    this.value = this.get();
  }
  get() {
    // 在執行個體化某個 watcher 的時候,會将Dep類的靜态屬性 Dep.target 指向這個 watcher 執行個體
    Dep.target = this;
    // 在這一步 this.vm._data[this.expOrFn] 調用了 data 裡某個 key 的 getter, 然後 getter 判斷類的靜态屬性 Dep.target 不為null, 而為 watcher 的執行個體, 進而把這個 watcher 執行個體添加到 這個 key 對應的 dep 執行個體裡。 巧妙!
    const value = this.vm._data[this.expOrFn];
    // 重置類屬性 Dep.target 
    Dep.target = null;
    return value;
  }

  // 如果 data 裡的某個 key 的 setter 被調用,則 key 會通知到 該 key 對應的 dep 執行個體, 該Dep執行個體, 該 dep 執行個體會調用所有 依賴于該 key 的 watcher 執行個體的 update 方法。
  update() {
    this.run();
  }
  run() {
    const value = this.get();
    if (value !== this.value) {
    this.value = value;
    // 執行 cb 回調
    this.cb.call(this.vm);
    }
  }
}
           

總結:

至此, Watcher, Observer , Dep 的關系全都梳理完成。而這些也是 Vue 實作的核心邏輯之一。再來簡單總結一下三者的關系,其實是一個簡單的 觀察-訂閱 的設計模式, 簡單來說就是, 觀察者觀察資料狀态變化, 一旦資料發生變化,則會通知對應的訂閱者,讓訂閱者執行對應的業務邏輯 。我們熟知的事件機制,就是一種典型的觀察-訂閱的模式

Observer, 觀察者,用來觀察資料源變化.

Dep, 觀察者和訂閱者是典型的 一對多 的關系,是以這裡設計了一個依賴中心,來管理某個觀察者和所有這個觀察者對應的訂閱者的關系, 消息排程和依賴管理都靠它。

Watcher, 訂閱者,當某個觀察者觀察到資料發生變化的時候,這個變化經過消息排程中心,最終會傳遞到所有該觀察者對應的訂閱者身上,然後這些訂閱者分别執行自身的業務回調即可

參考:Vue的響應式原理(MVVM)深入解析

20.觀察者模式

什麼是觀察者模式

觀察者模式又叫做釋出—訂閱模式,是我們最常用的設計模式之一。它定義對象間的一種一對多的依賴關系,當一個對象的狀态發生改變時,所有依賴于它的對象都将得到通知和更新。觀察者模式提供了一個訂閱模型,其中對象訂閱事件并在發生時得到通知,這種模式是事件驅動的程式設計基石,它有利益于良好的面向對象的設計。

看了上面的這段描述,可能還是不懂什麼是觀察者模式。我們還可以來看看生活中的觀察者模式:現在房價這麼高,你肯定是想要早點買房,但你看好的樓盤還沒開盤,是以你就将你的電話留給售樓小姐,一旦樓盤推出就讓她打電話給你。主動權在售樓方,而你隻需要提供一個聯系方式就行了這樣你就不用擔心樓盤出來你不知道了,也不需要每天都打電話去問樓盤推出了沒。

觀察者模式的使用場景

DOM事件

實際上,隻要我們曾經在DOM節點上面綁定過事件函數,那我們就使用過觀察者模式,應為JS和DOM之間就是實作了一種觀察者模式。

document.body.addEventListener("click", function() {
    alert("Hello World")
},false )
document.body.click() //模拟使用者點選
           

在上面的代碼中,需要監聽使用者點選 document.body 的動作,但是我們是沒辦法預知使用者将在什麼時候點選的。是以我們訂閱了 document.body 的 click 事件,當 body 節點被點選時,body 節點便會向訂閱者釋出 “Hello World” 消息。

自定義事件

除了DOM事件外,我們還可以實作一些自定義事件,這種依靠自定義時間完成的觀察者模式可以用于任何的JavaScript代碼中。

const event = {
    clientList: [],
    listen: function(key , fn) {
        if (this.clientListen[key]) {
            this.clientList[key] = []
        }
        this.clientList[key].push(fn)
    },
    trigger: function() {
        const key = Array.prototype.shift.call(arguments)
        const fns = this.clientList[key]
        if (!fns || fns.length === 0 ) {
            return false
        }
        for (let i = 0, fn ;fn = fns[i++];) {
            fn.apply(this, arguments)
        }
    },
    remove : function(key , fn) {
        const fns = this.clientList[key]
        if (!fns) {
            return false
        }
        if (!fn) {
            fns && (fns.length = 0)
        } else {
            for (let l = fns.length - 1; l>=0; l--) {
                const _fn = fns[l]
                if ( _fn ===fn) {
                    fns.splice(l, 1)
                }
            }
        }
}

const installEvent = (obj) => {
    for (let i in event) {
        obj[i] = event[i]
    }
}
           

然後就能增加釋出和訂閱功能了:

const events = {}
installEvent(events)
// 訂閱資訊
events.listen('newMessage',fn1 = (say) => {
    console.log('say:' + say)
})
// 釋出資訊
events.trigger('newMessage',"Hello world")
//移除訂閱
events.remove('newMessage',fn1)
           

觀察者模式的不足

觀察者模式的有點非常明顯:一是時間上的解耦,而是對象之間的解耦。既可用于異步程式設計中,也可以用幫助我們完成更松耦合的代碼編寫。但它仍然有所不足:

  • 建立訂閱者本身要消耗一定的時間和記憶體
  • 當訂閱一個消息時,也許此消息并沒有發生,但這個訂閱者會始終存在記憶體中。
  • 觀察者模式弱化了對象之間的聯系,這本是好事情,但如果過度使用,對象與對象之間的聯系也會被隐藏的很深,會導緻項目的難以跟蹤維護和了解。

Observer模式的角色

Subject(被觀察者)

被觀察的對象。當需要被觀察的狀态發生變化時,需要通知隊列中所有觀察者對象。Subject需要維持(添加,删除,通知)一個觀察者對象的隊列清單。

ConcreteSubject

被觀察者的具體實作。包含一些基本的屬性狀态及其他操作。

Observer(觀察者)

接口或抽象類。當Subject的狀态發生變化時,Observer對象将通過一個callback函數得到通知。

ConcreteObserver

觀察者的具體實作:得到通知後将完成一些具體的業務邏輯處理。

觀察者模式( 又叫釋出者-訂閱者模式 )應該是最常用的模式之一. 在很多語言裡都得到大量應用. 包括我們平時接觸的dom事件. 也是js和dom之間實作的一種觀察者模式.

觀察者模式的實作

一、定義觀察者類Pubsub

/* Pubsub */
 function Pubsub(){
     //存放事件和對應的處理方法
    this.handles = {};
 }
           

二、實作事件訂閱on

//傳入事件類型type和事件處理handle
 on: function (type, handle) {
     if(!this.handles[type]){
         this.handles[type] = [];
     }
     this.handles[type].push(handle);
 }
           

三、實作事件釋出emit

emit: function () {
     //通過傳入參數擷取事件類型
    var type = Array.prototype.shift.call(arguments);
     if(!this.handles[type]){
         return false;
     }
 for (var i = 0; i < this.handles[type].length; i++) {
         var handle = this.handles[type][i];
         //執行事件
        handle.apply(this, arguments);
     }
 }
           

需要說明的是Array.prototype.shift.call(arguments)這句代碼,arguments對象是function的内置對象,可以擷取到調用該方法時傳入的實參數組。

shift方法取出數組中的第一個參數,就是type類型。

四、實作事件取消訂閱off

off: function (type, handle) {
     handles = this.handles[type];
     if(handles){
         if(!handle){
             handles.length = 0;//清空數組
        }else{
             for (var i = 0; i < handles.length; i++) {
                 var _handle = handles[i];
                 if(_handle === handle){
                     handles.splice(i,1);
                 }
             }
         }
     }
 }
           

完整代碼:

/* Pubsub */
 function Pubsub(){
     //存放事件和對應的處理方法
    this.handles = {};
 }
 Pubsub.prototype={
     //傳入事件類型type和事件處理handle
     on: function (type, handle) {
         if(!this.handles[type]){
             this.handles[type] = [];
         }
         this.handles[type].push(handle);
     },
     emit: function () {
         //通過傳入參數擷取事件類型
        var type = Array.prototype.shift.call(arguments);
         if(!this.handles[type]){
             return false;
         }
 for (var i = 0; i < this.handles[type].length; i++) {
             var handle = this.handles[type][i];
             //執行事件
            handle.apply(this, arguments);
         }
     },
     off: function (type, handle) {
         handles = this.handles[type];
         if(handles){
             if(!handle){
                 handles.length = 0;//清空數組
            }else{
 for (var i = 0; i < handles.length; i++) {
                     var _handle = handles[i];
                     if(_handle === handle){
                         handles.splice(i,1);
                     }
                 }
             }
         }
     }
 }
           

參考:JavaScript設計模式之觀察者模式

JavaScript原生實作觀察者模式

繼續閱讀