前端技術演進(五):現代前端交互框架

這個來自之前做的培訓,刪減了一些業務相關的,參考了很多資料(參考資料列表),謝謝前輩們,麼麼噠 😘

隨着前端技術的發展,前端框架也在不斷的改變。

操作DOM時代

DOM(Document Object Model,文檔對象模型)將 HTML 文檔表達爲樹結構,並定義了訪問和操作 HTML 文檔的標準方法。

image.png | center | 486x266

前端開發基本上都會涉及到HTML頁面,也就避免不了和DOM打交道。

最早期的Web前端,就是一個靜態的黃頁,網頁上的內容不能更新。

慢慢的,用戶可以在Web頁面上進行一些簡單操作了,比如提交表單,文件上傳。但是整個頁面的部分或者整體的更新,還是靠刷新頁面來實現的。

隨着AJAX技術的出現,前端頁面上的用戶操作越來越多,越來越複雜,所以就進入了對DOM元素的直接操作時代。要對DOM元素操作,就要使用DOM API,常見的DOM API有:

類型 方法
節點查詢 getElementById、getElementsByName、getElementsByClassName、getElementsByTagName、querySelector、querySelectorAll
節點創建 createElement、createDocumentFragment、createTextNode、cloneNode
節點修改 appendChild、replaceChild、removeChild、insertBefore、innerHTML
節點關係 parentNode、previousSibling、childNodes
節點屬性 innerHTML、attributes、getAttribute、setAttribure、getComputedStyle
內容加載 XMLHttpRequest、ActiveX

使用DOM API可以完成前端頁面中的任何操作,但是隨着網站應用的複雜化,使用原生的API非常低效。所以 jQuery 這個用來操作DOM的交互框架就誕生了。

jQuery 爲什麼能成爲在這個時代最流行的框架呢?主要是他幫前端開發人員解決了太多問題:

  • 封裝了DOM API,提供了統一和方便的調用方式。
  • 簡化了元素的選擇,可以很快的選取到想要的元素。
  • 提供了AJAX接口,對XMLHttpRequest和ActiveX統一封裝。
  • 統一了事件處理。
  • 提供異步處理機制。
  • 兼容大部分主流瀏覽器。

除了解決了上面這些問題,jQuery還擁有良好的生態,海量的插件拿來即用,讓前端開發比以前流暢很多。尤其是在IE6、IE7時代,沒有jQuery,意味着無窮的兼容性處理。

// DOM API:
document.querySelectorAll('#container li');

// jQuery
$('#container').find('li');

隨着HTML5技術的發展,jQuery提供的很多方法已經在原生的標準中實現了,慢慢的,jQuery的必要性在逐漸降低。http://youmightnotneedjquery.com/

漸漸地,SPA(Single Page Application,單頁面應用)開始被廣泛認可,整個應用的內容都在一個頁面中並完全通過異步交互來加載不同的內容,這時候使用 jQuery 直接操作DOM的方式就不容易管理了,頁面上事件的綁定會變得混亂,在這種情況下,迫切需要一個可以自動管理頁面上DOM和數據之間交互操作的框架。

MV* 模式

MVC,MVP和MVVM都是常見的軟件架構設計模式(Architectural Pattern),它通過分離關注點來改進代碼的組織方式。

單純從概念上,很難區分和感受出來這三種模式在前端框架中有什麼不同。我們通過一個例子來體會一下:有一個可以對數值進行加減操作的組件:上面顯示數值,兩個按鈕可以對數值進行加減操作,操作後的數值會更新顯示。

image.png | center | 512x360

Model層用於封裝和應用程序的業務邏輯相關的數據以及對數據的處理方法。這裏我們把需要用到的數值變量封裝在Model中,並定義了add、sub、getVal三種操作數值方法。

var myapp = {}; // 創建這個應用對象

myapp.Model = function() {
    var val = 0; // 需要操作的數據

    /* 操作數據的方法 */
    this.add = function(v) {
        if (val < 100) val += v;
    };

    this.sub = function(v) {
        if (val > 0) val -= v;
    };

    this.getVal = function() {
        return val;
    };
};

View作爲視圖層,主要負責數據的展示。

myapp.View = function() {

    /* 視圖元素 */
    var $num = $('#num'),
        $incBtn = $('#increase'),
        $decBtn = $('#decrease');

    /* 渲染數據 */
    this.render = function(model) {
        $num.text(model.getVal() + 'rmb');
    };
};

這裏,通過Model&View完成了數據從模型層到視圖層的邏輯。但對於一個應用程序,這遠遠是不夠的,我們還需要響應用戶的操作、同步更新View和Model。

前端 MVC 模式

image.png | center | 500x320

MVC(Model View Controller)是一種很經典的設計模式。用戶對View的操作交給了Controller處理,在Controller中響應View的事件調用Model的接口對數據進行操作,一旦Model發生變化便通知相關視圖進行更新。

Model層用來存儲業務的數據,一旦數據發生變化,模型將通知有關的視圖。

// Model
myapp.Model = function() {
    var val = 0;

    this.add = function(v) {
        if (val < 100) val += v;
    };

    this.sub = function(v) {
        if (val > 0) val -= v;
    };

    this.getVal = function() {
        return val;
    };

    /* 觀察者模式 */
    var self = this, 
        views = [];

    this.register = function(view) {
        views.push(view);
    };

    this.notify = function() {
        for(var i = 0; i < views.length; i++) {
            views[i].render(self);
        }
    };
};

Model和View之間使用了觀察者模式,View事先在此Model上註冊,進而觀察Model,以便更新在Model上發生改變的數據。

View和Controller之間使用了策略模式,這裏View引入了Controller的實例來實現特定的響應策略,比如這個栗子中按鈕的 click 事件:

// View
myapp.View = function(controller) {
    var $num = $('#num'),
        $incBtn = $('#increase'),
        $decBtn = $('#decrease');

    this.render = function(model) {
        $num.text(model.getVal() + 'rmb');
    };

    /*  綁定事件  */
    $incBtn.click(controller.increase);
    $decBtn.click(controller.decrease);
};

控制器是模型和視圖之間的紐帶,MVC將響應機制封裝在Controller對象中,當用戶和應用產生交互時,控制器中的事件觸發器就開始工作了。

// Controller
myapp.Controller = function() {
    var model = null,
        view = null;

    this.init = function() {
        /* 初始化Model和View */
        model = new myapp.Model();
        view = new myapp.View(this);

        /* View向Model註冊,當Model更新就會去通知View啦 */
        model.register(view);
        model.notify();
    };

    /* 讓Model更新數值並通知View更新視圖 */
    this.increase = function() {
        model.add(1);
        model.notify();
    };

    this.decrease = function() {
        model.sub(1);
        model.notify();
    };
};

這裏我們實例化View並向對應的Model實例註冊,當Model發生變化時就去通知View做更新。

可以明顯感覺到,MVC模式的業務邏輯主要集中在Controller,而前端的View其實已經具備了獨立處理用戶事件的能力,當每個事件都流經Controller時,這層會變得十分臃腫。而且MVC中View和Controller一般是一一對應的,捆綁起來表示一個組件,視圖與控制器間的過於緊密的連接讓Controller的複用性成了問題,如果想多個View共用一個Controller該怎麼辦呢?

前端 MVP 模式

MVP(Model-View-Presenter)是MVC模式的改良。和MVC的相同之處在於:Controller/Presenter負責業務邏輯,Model管理數據,View負責顯示。

image.png | center | 500x320

在MVC裏,View是可以直接訪問Model的。而MVP中的View並不能直接使用Model,而是通過爲Presenter提供接口,讓Presenter去更新Model,再通過觀察者模式更新View。

與MVC相比,MVP模式通過解耦View和Model,完全分離視圖和模型使職責劃分更加清晰;由於View不依賴Model,可以將View抽離出來做成組件,它只需要提供一系列接口提供給上層操作。

// Model
myapp.Model = function() {
    var val = 0;

    this.add = function(v) {
        if (val < 100) val += v;
    };

    this.sub = function(v) {
        if (val > 0) val -= v;
    };

    this.getVal = function() {
        return val;
    };
};

Model層依然是主要與業務相關的數據和對應處理數據的方法,很簡單。

// View
myapp.View = function() {
    var $num = $('#num'),
        $incBtn = $('#increase'),
        $decBtn = $('#decrease');

    this.render = function(model) {
        $num.text(model.getVal() + 'rmb');
    };

    this.init = function() {
        var presenter = new myapp.Presenter(this);

        $incBtn.click(presenter.increase);
        $decBtn.click(presenter.decrease);
    };
};

MVP定義了Presenter和View之間的接口,用戶對View的操作都轉移到了Presenter。比如這裏的View暴露setter接口(render方法)讓Presenter調用,待Presenter通知Model更新後,Presenter調用View提供的接口更新視圖。

// Presenter
myapp.Presenter = function(view) {
    var _model = new myapp.Model();
    var _view = view;

    _view.render(_model);

    this.increase = function() {
        _model.add(1);
        _view.render(_model);
    };

    this.decrease = function() {
        _model.sub(1);
        _view.render(_model);
    };
};

Presenter作爲View和Model之間的“中間人”,除了基本的業務邏輯外,還有大量代碼需要對從View到Model和從Model到View的數據進行“手動同步”,這樣Presenter顯得很重,維護起來會比較困難。如果Presenter對視圖渲染的需求增多,它不得不過多關注特定的視圖,一旦視圖需求發生改變,Presenter也需要改動。

前端 MVVM 模式

MVVM(Model-View-ViewModel)最早由微軟提出。ViewModel指 "Model of View"——視圖的模型。

image.png | center | 500x320

MVVM把View和Model的同步邏輯自動化了。以前Presenter負責的View和Model同步不再手動地進行操作,而是交給框架所提供的數據綁定功能進行負責,只需要告訴它View顯示的數據對應的是Model哪一部分即可。

我們使用Vue來完成這個栗子。

在MVVM中,我們可以把Model稱爲數據層,因爲它僅僅關注數據本身,不關心任何行爲(格式化數據由View的負責),這裏可以把它理解爲一個類似json的數據對象。

// Model
var data = {
    val: 0
};

和MVC/MVP不同的是,MVVM中的View通過使用模板語法來聲明式的將數據渲染進DOM,當ViewModel對Model進行更新的時候,會通過數據綁定更新到View。

<!-- View -->
<div id="myapp">
    <div>
        <span>{{ val }}rmb</span>
    </div>
    <div>
        <button v-on:click="sub(1)">-</button>
        <button v-on:click="add(1)">+</button>
    </div>
</div>

ViewModel大致上就是MVC的Controller和MVP的Presenter了,也是整個模式的重點,業務邏輯也主要集中在這裏,其中的一大核心就是數據綁定。與MVP不同的是,沒有了View爲Presente提供的接口,之前由Presenter負責的View和Model之間的數據同步交給了ViewModel中的數據綁定進行處理,當Model發生變化,ViewModel就會自動更新;ViewModel變化,Model也會更新。

new Vue({
    el: '#myapp',
    data: data,
    methods: {
        add(v) {
            if(this.val < 100) {
                this.val += v;
            }
        },
        sub(v) {
            if(this.val > 0) {
                this.val -= v;
            }
        }
    }
});

整體來看,比MVC/MVP精簡了很多,不僅僅簡化了業務與界面的依賴,還解決了數據頻繁更新(之前用jQuery操作DOM很繁瑣)的問題。因爲在MVVM中,View不知道Model的存在,ViewModel和Model也察覺不到View,這種低耦合模式可以使開發過程更加容易,提高應用的可重用性。

數據綁定

image.png | center | 500x320

在Vue中,使用了雙向綁定技術(Two-Way-Data-Binding),就是View的變化能實時讓Model發生變化,而Model的變化也能實時更新到View。其實雙向數據綁定,可以簡單地理解爲一個模版引擎,但是會根據數據變更實時渲染。

有人還不要臉的申請了專利:

image.png | center | 747x757

數據變更檢測

不同的MVVM框架中,實現雙向數據綁定的技術有所不同。目前一些主流的實現數據綁定的方式大致有以下幾種:

手動觸發綁定

手動觸發指令綁定是比較直接的實現方式,主要思路是通過在數據對象上定義get()方法和set()方法,調用時手動觸發get ()或set()函數來獲取、修改數據,改變數據後會主動觸發get()和set()函數中View層的重新渲染功能。

髒檢測機制

Angularjs是典型的使用髒檢測機制的框架,通過檢查髒數據來進行View層操作更新。

髒檢測的基本原理是在ViewModel對象的某個屬性值發生變化時找到與這個屬性值相關的所有元素,然後再比較數據變化,如果變化則進行Directive 指令調用,對這個元素進行重新掃描渲染。

前端數據對象劫持

數據劫持是目前使用比較廣泛的方式。其基本思路是使用 Object.defineProperty 和 Object.defineProperies 對ViewModel數據對象進行屬性get ()和set()的監聽,當有數據讀取和賦值操作時則掃描元素節點,運行指定對應節點的Directive指令,這樣ViewModel使用通用的等號賦值就可以了。

Vue就是典型的採用數據劫持和發佈訂閱模式的框架。

image.png | center | 827x256

  • Observer 數據監聽器:負責對數據對象的所有屬性進行監聽(數據劫持),監聽到數據發生變化後通知訂閱者。
  • Compiler 指令解析器:掃描模板,並對指令進行解析,然後綁定指定事件。
  • Watcher 訂閱者:關聯Observer和Compile,能夠訂閱並收到屬性變動的通知,執行指令綁定的相應操作,更新視圖。

ES6 Proxy

之前我們說過 Proxy 實現數據劫持的方法

總結來看,前端框架從直接DOM操作到MVC設計模式,然後到MVP,再到MVVM框架,前端設計模式的改進原則一直向着高效、易實現、易維護、易擴展的基本方向發展。雖然目前前端各類框架也已經成熟並開始向高版本迭代,但是還沒有結束,我們現在的編程對象依然沒有脫離DOM編程的基本套路,一次次框架的改進大大提高了開發效率,但是DOM元素運行的效率仍然沒有變。對於這個問題的解決,有的框架提出了Virtual DOM的概念。

Virtual DOM

MVVM的前端交互模式大大提高了編程效率,自動雙向數據綁定讓我們可以將頁面邏輯實現的核心轉移到數據層的修改操作上,而不再是在頁面中直接操作DOM。儘管MVVM改變了前端開發的邏輯方式,但是最終數據層反應到頁面上View層的渲染和改變仍是通過對應的指令進行DOM操作來完成的,而且通常一次ViewModel的變化可能會觸發頁面上多個指令操作DOM的變化,帶來大量的頁面結構層DOM操作或渲染。

比如一段僞代碼:

<ul>
    <li repeat="list">{{ list.value }}</li>
</ul>

let viewModel = new VM({
    data:{
        list:[{value: 1},{value: 2},{value: 3}]
    }
})

使用MVVM框架生成一個數字列表,此時如果需要顯示的內容變成了 [{value: 1}, {value: 2}, {value: 3}, {value: 4}],在MVVM框架中一般會重新渲染整個列表,包括列表中無須改變的部分也會重新渲染一次。 但實際上如果直接操作改變DOM的話,只需要在<ul>子元素最後插入一個新的<li>元素就可以了。但在一般的MVVM框架中,我們通常不會這樣做。毫無疑問,這種情況下MVVM的View層更新模式就消耗了更多沒必要的性能。

那麼該如何對ViewModel進行改進,讓瀏覽器知道實際上只是增加了一個元素呢?通過對比

[{value: 1},{value: 2},{value: 3}][{value: 1}, {value: 2}, {value: 3}, {value: 4}]

其實只是增加了一個 {value: 4},那麼該怎樣將這個增加的數據反映到View層上呢?可以將新的Model data 和舊的Model data 進行對比,然後記錄ViewModel的改變方式和位置,就知道了這次View 層應該怎樣去更新,這樣比直接重新渲染整個列表高效得多。

這裏其實可以理解爲,ViewModel 裏的數據就是描述頁面View 內容的另一種數據結構標識,不過需要結合特定的MVVM描述語法編譯來生成完整的DOM結構。

可以用JavaScript對象的屬性層級結構來描述上面HTML DOM對象樹的結構,當數據改變時,新生成一份改變後的Elements,並與原來的Elemnets結構進行對比,對比完成後,再決定改變哪些DOM元素。

image.png | left | 827x581

剛纔例子裏的 ulElement 對象可以理解爲VirtualDOM。通常認爲,Virtual DOM是一個能夠直接描述一段HTMLDOM結構的JavaScript對象,瀏覽器可以根據它的結構按照一定規則創建出確定唯一的HTML DOM結構。整體來看,Virtual DOM的交互模式減少了MVVM或其他框架中對DOM的掃描或操作次數,並且在數據發生改變後只在合適的地方根據JavaScript對象來進行
最小化的頁面DOM操作,避免大量重新渲染。

diff算法

Virtual-DOM的執行過程:

用JS對象模擬DOM樹 -> 比較兩棵虛擬DOM樹的差異 -> 把差異應用到真正的DOM樹上

在Virtual DOM中,最主要的一環就是通過對比找出兩個Virtual DOM的差異性,得到一個差異樹對象。

對於Virtual DOM的對比算法實際上是對於多叉樹結構的遍歷算法。但是找到任意兩個樹之間最小的修改步驟,一般會循環遞歸對節點進行依次對比,算法複雜度達到 O(n^3),這個複雜度非常高,比如要展示1000多個節點,最悲觀要依次執行上十億次的比較。所以不同的框架採用的對比算法其實是一個略簡化的算法。

拿React來說,由於web應用中很少出現將一個組件移動到不同的層級,絕大多數情況下都是橫向移動。因此React嘗試逐層的對比兩棵樹,一旦出現不一致,下層就不再比較了,在損失較小的情況下顯著降低了比較算法的複雜度。

image.png | center | 377x199

前端框架的演進非常快,所以只有知道演進的原因,才能去理解各個框架的優劣,從而根據應用的實際情況來選擇最合適的框架。對於其他技術也是如此。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章