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

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

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

操做DOM時代

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

image.png | center | 486x266

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

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

慢慢的,用戶能夠在Web頁面上進行一些簡單操做了,好比提交表單,文件上傳。可是整個頁面的部分或者總體的更新,仍是靠刷新頁面來實現的。jquery

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

類型 方法
節點查詢 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還擁有良好的生態,海量的插件拿來即用,讓前端開發比之前流暢不少。尤爲是在IE六、IE7時代,沒有jQuery,意味着無窮的兼容性處理。json

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

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

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

漸漸地,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

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

相關文章
相關標籤/搜索