前端面試題:這是我理解的MVVM,請注意查收

MVVM模式是什麼?你是怎麼理解MVVM原理的?理解它不僅是應付面試,對VUE、Backbone.js、angular、Ember、avalon框架的設計模式也會有更進步一步的理解,有可能下一個流行框架就是你的傑做~~本篇文章最後也會實現了一個屬於本身的簡易MVVM庫,裏面實現了一個mvvm庫應有基本功能~css

      如今流行的前端框架也就是 vue、react、angular了,在投遞簡歷時,咱們均可以看到任職要求會有最少熟悉這些框架中的一種,掌握這些框架就好像時多了一個輪子或者說是多了一個車,框架能夠然咱們快速的使用、複用處理一些問題。固然面試中不只會問到這些只是的掌握狀況,也會問些你的框架的理解,由於知其然也要知其因此然,咱們這篇文章來了解MVVM框架模式~若是文章有問題,也請你們指正,不要打我啊~~

1、MVVM的概念

Mvvm定義MVVM是Model-View-ViewModel的簡寫。是一個軟件架構設計模式,由微軟 WPF 和 Silverlight 的架構師 Ken Cooper 和 Ted Peters 開發,是一種簡化用戶界面的事件驅動編程方式。由 John Gossman(一樣也是 WPF 和 Silverlight 的架構師)於2005年在他的博客上發表。即模型-視圖-視圖模型。html

2、MVVM的發展史

在瞭解MVVM以前,咱們先回顧一下前端發展的歷史。下面一趴是來 自廖雪峯官方網站的內容,瞭解歷史就會知道爲啥MVVM會被使用,使用背景,固然也能夠跳過直接看下一趴~

在上個世紀的1989年,歐洲核子研究中心的物理學家Tim Berners-Lee發明了超文本標記語言(HyperText Markup Language),簡稱HTML,並在1993年成爲互聯網草案。今後,互聯網開始迅速商業化,誕生了一大批商業網站。

最先的HTML頁面是徹底靜態的網頁,它們是預先編寫好的存放在Web服務器上的html文件。瀏覽器請求某個URL時,Web服務器把對應的html文件扔給瀏覽器,就能夠顯示html文件的內容了。

若是要針對不一樣的用戶顯示不一樣的頁面,顯然不可能給成千上萬的用戶準備好成千上萬的不一樣的html文件,因此,服務器就須要針對不一樣的用戶,動態生成不一樣的html文件。一個最直接的想法就是利用C、C++這些編程語言,直接向瀏覽器輸出拼接後的字符串。這種技術被稱爲CGI:Common Gateway Interface。

很顯然,像新浪首頁這樣的複雜的HTML是不可能經過拼字符串獲得的。因而,人們又發現,其實拼字符串的時候,大多數字符串都是HTML片斷,是不變的,變化的只有少數和用戶相關的數據,因此,又出現了新的建立動態HTML的方式:ASP、JSP和PHP——分別由微軟、SUN和開源社區開發。

在ASP中,一個asp文件就是一個HTML,可是,須要替換的變量用特殊的<%=var%>
標記出來了,再配合循環、條件判斷,建立動態HTML就比CGI要容易得多。

可是,一旦瀏覽器顯示了一個HTML頁面,要更新頁面內容,惟一的方法就是從新向服務器獲取一份新的HTML內容。若是瀏覽器想要本身修改HTML頁面的內容,就須要等到1995年年末,JavaScript被引入到瀏覽器。

有了JavaScript後,瀏覽器就能夠運行JavaScript,而後,對頁面進行一些修改。JavaScript還能夠經過修改HTML的DOM結構和CSS來實現一些動畫效果,而這些功能無法經過服務器完成,必須在瀏覽器實現。

用JavaScript在瀏覽器中操做HTML,經歷了若干發展階段:

第一階段,直接用JavaScript操做DOM節點,使用瀏覽器提供的原生API:

var dom = document.getElementById('name');
dom.innerHTML = 'Homer';
dom.style.color = 'red';
複製代碼

第二階段,因爲原生API很差用,還要考慮瀏覽器兼容性,jQuery橫空出世,以簡潔的API迅速俘獲了前端開發者的芳心:

$('#name').text('Homer').css('color', 'red');前端

第三階段,MVC模式,須要服務器端配合,JavaScript能夠在前端修改服務器渲染後的數據。

如今,隨着前端頁面愈來愈複雜,用戶對於交互性要求也愈來愈高,想要寫出Gmail這樣的頁面,僅僅用jQuery是遠遠不夠的。MVVM模型應運而生。

MVVM最先由微軟提出來,它借鑑了桌面應用程序的MVC思想,在前端頁面中,把Model用純JavaScript對象表示,View負責顯示,二者作到了最大限度的分離。vue

把Model和View關聯起來的就是ViewModel。ViewModel負責把Model的數據同步到View顯示出來,還負責把View的修改同步回Model。

ViewModel如何編寫?須要用JavaScript編寫一個通用的ViewModel,這樣,就能夠複用整個MVVM模型了。node

3、正式的MVVM理解

  • MVVM模式

MVVM 的出現促進了 GUI 前端開發與後端業務邏輯的分離,極大地提升了前端開發效率。MVVM 的核心是 ViewModel 層,它就像是一箇中轉站(value converter),負責轉換 Model 中的數據對象來讓數據變得更容易管理和使用,該層向上與視圖層進行雙向數據綁定,向下與 Model 層經過接口請求進行數據交互,起呈上啓下做用。以下圖所示:react


  •  MVVM組成部分


# View 層angularjs

View 是視圖層,也就是用戶界面。前端主要由 HTML 和 CSS 來構建,爲了更方便地展示 ViewModel 或者 Model 層的數據,已經產生了各類各樣的先後端模板語言,好比FreeMarker、Marko、Pug、Jinja2等等,各大 MVVM 框架如 avalon,Vue,Angular 等也都有本身用來構建用戶界面的內置模板語言。面試

# Model 層數據庫

Model 是指數據模型,泛指後端進行的各類業務邏輯處理和數據操控,主要圍繞數據庫系統展開。編程

後端業務處理在這就很少贅述了,其實前端人員大多都不須要管,只要後端保證對外接口足夠簡單就好了,我請求api,你把數據返出來,咱倆就這點關係,其餘都扯淡。

# ViewModel 層

ViewModel 是由前端開發人員組織生成和維護的視圖數據層。mvvm模式的核心,它是鏈接view和model的橋樑。在這一層,前端開發者對從後端獲取的 Model 數據進行轉換處理,作二次封裝,以生成符合 View 層使用預期的視圖數據模型。須要注意的是 ViewModel 所封裝出來的數據模型包括視圖的狀態和行爲兩部分,而 Model 層的數據模型是隻包含狀態的,好比頁面的這一塊展現什麼,那一塊展現什麼這些都屬於視圖狀態(展現),而頁面加載進來時發生什麼,點擊這一塊發生什麼,這一塊滾動時發生什麼這些都屬於視圖行爲(交互),視圖狀態和行爲都封裝在了 ViewModel 裏。這樣的封裝使得 ViewModel 能夠完整地去描述 View 層。因爲實現了雙向綁定,ViewModel 的內容會實時展示在 View 層,這是激動人心的,由於前端開發者不再必低效又麻煩地經過操縱 DOM 去更新視圖,MVVM 框架已經把最髒最累的一塊作好了,咱們開發者只須要處理和維護 ViewModel,更新數據視圖就會自動獲得相應更新,真正實現數據驅動開發。看到了吧,View 層展示的不是 Model 層的數據,而是 ViewModel 的數據,由 ViewModel 負責與 Model 層交互,這就徹底解耦了 View 層和 Model 層,這個解耦是相當重要的,它是先後端分離方案實施的重要一環。

  • MVVM設計模式的優缺點:

     優勢:

一、固然是最主要的雙向綁定技術,單向綁定與雙向綁定。

所謂 單向綁定」就是ViewModel變化時,自動更新View

所謂 雙向綁定」就是在單向綁定的基礎上View變化時,自動更新ViewModel

    咱們能夠先觀察下MVVM框架和jQuery操做DOM相比有什麼區別?

原來的html

<p>Hello, <span id="name">LEE</span>!</p><p>You are <span id="age">18</span>.</p>

展現


用jQuery修改name和age節點的內容:

var name = '修改';
var age =100;
 
$('#name').text(name);
$('#age').text(age);
複製代碼

若是咱們使用MVVM框架來實現一樣的功能,咱們首先並不關心DOM的結構,而是關心數據如何存儲。最簡單的數據存儲方式是使用:

var person = {
    name: 'LEEt',
    age: 18
};
複製代碼

咱們把變量person看做Model,把HTML某些DOM節點看做View,並假定它們之間被關聯起來了。

要把顯示的name從LEE改成修改,把顯示的age從18改成100,咱們並不操做DOM,而是直接修改JavaScript對象:

person.name = '修改';

person.age = 100;
複製代碼

這樣能夠看出,咱們的關注點從如何操做DOM變成了如何更新JavaScript對象的狀態,而操做JavaScript對象比DOM簡單多了!

MVVM的設計思想:關注Model的變化,讓MVVM框架去自動更新DOM的狀態,從而把發者從操做DOM的繁瑣步驟中解脫出來!

二、因爲控制器的功能大都移動到View上處理,大大的對控制器進行了瘦身。

三、能夠對View或ViewController的數據處理部分抽象出來一個函數處理model。這樣它們專職頁面佈局和頁面跳轉,它們必然更一步的簡化。

四、提升可維護性

五、可測試。界面素來是比較難於測試的,而如今測試能夠針對ViewModel來寫。

六、低耦合可重用:視圖(View)能夠獨立於Model變化和修改,一個ViewModel能夠綁定不一樣的"View"上,當View變化的時候Model不能夠不變,當Model變化的時候View也能夠不變。你能夠把一些視圖邏輯放在一個ViewModel裏面,讓不少view重用這段視圖邏輯。

缺點:

  1. Bug很難被調試。由於使用雙向綁定的模式,當你看到界面異常了,有多是你View的代碼有Bug,也多是Model的代碼有問題。數據綁定使得一個位置的Bug被快速傳遞到別的位置,要定位原始出問題的地方就變得不那麼容易了。另外,數據綁定的聲明是指令式地寫在View的模版當中的,這些內容是沒辦法去打斷點debug的。
  2. 一個大的模塊中model也會很大,雖然使用方便了也很容易保證了數據的一致性,當時長期持有,不釋放內存就形成了花費更多的內存。
  3. 對於大型的圖形應用程序,視圖狀態較多,ViewModel的構建和維護的成本都會比較高。

  • MVVM的適用範圍

從幾個例子咱們能夠看到,MVVM最大的優點是編寫前端邏輯很是複雜的頁面,尤爲是須要大量DOM操做的邏輯,利用MVVM能夠極大地簡化前端頁面的邏輯。

可是MVVM不是萬能的,它的目的是爲了解決複雜的前端邏輯。對於以展現邏輯爲主的頁面,例如,新聞,博客、文檔等,不能使用MVVM展現數據,由於這些頁面須要被搜索引擎索引,而搜索引擎沒法獲取使用MVVM並經過API加載的數據。

因此,須要 SEO(Search Engine Optimization)的頁面,不能使用MVVM展現數據。不須要SEO的頁面,若是前端邏輯複雜,就適合使用MVVM展現數據,例如,工具類頁面,複雜的表單頁面,用戶登陸後才能操做的頁面等等。固然可能如今有了ssr。

經常使用的MVVM框架有:

Angular:Google出品,名氣大,可是學習難度有些大;適合PC,代碼結構會比較清晰;

Backbone.js:入門很是困難,由於自身API太多;

Ember:一個大而全的框架,想寫個Hello world都很困難。

Avalon:屬於輕量級的,而且對老的瀏覽器支持程度較高,最低支持到IE6,因此適合兼容老劉瀏覽器的項目;

Vue:主打輕量級,僅做爲MV*中的視圖部分使用,優勢輕量級,易學易用,缺點是大項目的時候還要配合其餘框架或者庫來使用,比較麻煩

4、實現MVVM的js庫

目前實現數據雙向綁定主要有一下幾種方式:

  1. 髒值檢測(angular):

以典型的mvvm框架angularjs爲表明,angular經過檢查髒數據來進行UI層的操做更新。關於angular的髒檢測,有幾點須要瞭解些:

  • l髒檢測機制並非使用定時檢測。
  • l髒檢測的時機是在數據發生變化時進行。
  • l angular對經常使用的dom事件,xhr事件等作了封裝, 在裏面觸發進入angular的digest流程。
  • l在digest流程裏面, 會從rootscope開始遍歷, 檢查全部的watcher。 (關於angular的具體設計能夠看其餘文檔,這裏只討論數據綁定),那咱們看下髒檢測該如何去作:主要是經過設置的數據來需找與該數據相關的全部元素,而後再比較數據變化,若是變化則進行指令操做。
  2.前端數據劫持(Hijacking)(vue):基本思路:經過Object.defineProperty() 去劫持數據每一個屬性對應的getter和setter。當有數據讀取和賦值操做時則調用節點的指令,這樣使用最通用的=等號賦值就能夠了。

  3.發佈-訂閱模式(backbone):經過發佈消息,訂閱消息進行數據和視圖的綁定監聽。

比較老的實現方式,使用觀察者編程模式,主要思路是經過在數據對象上定義get和set方法等,調用時手動調用get或set數據,改變數據後觸發UI層的渲染操做;以視圖驅動數據變化的場景主要應用與input、select、textarea等元素,當UI層變化時,經過監聽dom的change,keypress,keyup等事件來觸發事件改變數據層的數據。整個過程均經過函數調用完成。


代碼實現思路:(相似實現 VUE的一個實現MVVM的庫)


模擬的是VUE的MVVM庫使用數據劫持思路實現,MVVM,上圖爲基本思路圖。如上圖所示,咱們能夠看到,總體實現分爲已下步驟

一、實現一個Observer,對數據進行劫持,通知數據的變化(將使用的要點爲:Object.defineProperty()方法)

二、實現一個Compile,對指令進行解析,初始化視圖,而且訂閱數據的變動,綁定好更新函數

三、實現一個Watcher,將其做爲以上二者的一箇中介點,在接收數據變動的同時,讓Dep添加當前Watcher,並及時通知視圖進行update

四、實現一些VUE的其餘功能(Computed、menthods)

五、實現MVVM,整合以上幾點,做爲一個入口函數

如下爲代碼部分:

Html:

<!DOCTYPE html>
<html lang="en">
 
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>
實現MVVM的js庫(模擬vue實現功能)</title>

<script src="./MVVM.js"></script>
</head>
 
<body>
 
<div id="app">
<input type="text" v-model="person.name">
<p>
hello,{{person.name}}
</p>
<p>You are:{{person.age}}</p>
<!-- computed屬性若是數據不變化 視圖不更新 -->
<p>{{getNewName}}</p>
<button type="button" name="button" v-on:click="testToggle">
修更名字</button>

</div>
 
<script>
let vm = new Vue({
el: '#app',
data: {
person: {
name: 'lee',
age: 18
}
},
methods: {
testToggle(){
this.person.name = '修改後的名字:哈哈';
}
},
computed: {
getNewName(){
return this.person.name+' 是要成爲海賊王的人'
}
},
})
</script>
</body>
</html>
複製代碼


js:

// 2019-4-4
// lee 
// 草履蟲的思考
// 簡單模擬vue實現MVVM
/**
 * 實現一個Vue的類
 * 一、實現一個Observer,對數據進行劫持,通知數據的變化(將使用的要點爲:Object.defineProperty()方法)
二、實現一個Compile,對指令進行解析,初始化視圖,而且訂閱數據的變動,綁定好更新函數ComplieUtil解析指令的公共方法
三、實現一個Watcher,將其做爲以上二者的一箇中介點,在接收數據變動的同時,讓Dep添加當前Watcher,並及時通知視圖進行update
四、實現一些VUE的其餘功能(Computed、menthods)
 */
// 觀察者模式(發佈訂閱)
class Dep {
    constructor() {
        this.subs = []; //存放全部watcher
    }
    // 訂閱 添加watcher
    addSub(watcher) {
        this.subs.push(watcher);
    }
    // 發佈
    notify() {
        this.subs.forEach(watcher => watcher.update());
    }
}
// 觀察者 vm.$watch(vm,'person.name',(newVal)=>{ })
class Watcher {
    constructor(vm, expr, cb) {
        this.vm = vm;
        this.expr = expr;
        this.cb = cb;
        // 默認存儲一個老值
        this.oldValue = this.get();
    }
 
    get() {
        Dep.target = this;
        // 取值 把這個觀察者和數據關聯起來
        let val = ComplieUtil.getVal(this.vm,this.expr);
        Dep.target = null;
        return val;
    }
    // 更新操做 數據變化後 會調用觀察者中的update方法
    update() {
        let newVal = ComplieUtil.getVal(this.vm,this.expr);
        if (newVal !== this.oldValue) {
            this.cb(newVal);
        }
    }
}
 
// 實現數據劫持做用
class Observer {
    constructor(data) {
        this.observer(data);
    }
    observer(data) {
        // 若是是對象才觀察
        if (data && typeof data === 'object') {
            for (let key in data) {
                this.defineReactive(data, key, data[key])
            }
        }
    }
 
    defineReactive(obj, key, value) {
        this.observer(value);
        // 給每一個屬性 都加上具備發佈訂閱的功能
        let dep = new Dep();
        Object.defineProperty(obj, key, {
            enumerable: true,   // 可枚舉
            configurable: true, // 可從新定義
            get() {
                // 建立watcher時 會取到對應的內容,而且把watcher放到全局上
                Dep.target && dep.addSub(Dep.target);
                return value;
            },
            set: (newVal) => { // {person:{name:'lee'}
                // 數據沒有變不須要更新
                if (newVal != value) {
                    // 須要遞歸
                    this.observer(newVal);
                    value = newVal;
                    dep.notify();
                }
            }
        })
    }
}
// 編譯器
class Complier {
    constructor(el, vm) {
        // 判斷el屬性是否是一個元素 若是不是元素 那就獲取他 (由於在vue的el中多是el:'#app'
        // 或者document.getElementById('app')
 
        this.el = this.isElementNode(el) ? el : document.querySelector(el);
        this.vm = vm;
        // 把當前節點中的元素 獲取到 放到內存中
        let fragment = this.nodeFragMent(this.el);
 
        // 把節點中的內容進行替換
 
        // 編譯模板 用數據編譯
        this.complie(fragment);
        // 把內容在塞到頁面中
        this.el.appendChild(fragment);
    }
 
    isElementNode(node) { //是否是元素節點
        return node.nodeType === 1;
    }
    //  把節點移動到內存中
    nodeFragMent(node) {
        let frag = document.createDocumentFragment();
        let firstChild;
        while (firstChild = node.firstChild) {
            // appendChild 具備移動性 
            frag.appendChild(firstChild);
        }
        return frag;
    }
    // 是否是指令 
    isDirective(attrName) {
        return attrName.startsWith('v-');
    }
    // 編譯元素
    complieElement(node) {
        let attr = node.attributes;
        [...attr].forEach(item => {
            // item 有key = value ,type="text" v-model="person.name"
            let {
                name,
                value: expr
            } = item;
            if (this.isDirective(name)) {
                // v-mode v-html v-bind...
                let [, directive] = name.split('-');
               let [directiveName,eventName] = directive.split(':');
                console.log(node, expr, this.vm, eventName);
                // ComplieUtil[directive](node, expr, this.vm);
                ComplieUtil[directiveName](node, expr, this.vm, eventName);
            }
        })
    }
    // 編譯文本
    // 判斷當前文本節點中內容是否包括{{}}
    complieText(node) {
        let content = node.textContent;
        var reg = /\{\{(.+?)\}\}/;
        if (reg.test(content)) {
            ComplieUtil['text'](node, content,this.vm); //{{}}
        }
    }
    // 用來編譯內存中的dom節點
    complie(node) {
        let childNode = node.childNodes;
        // childNode 是類數組 轉換爲數組
        [...childNode].forEach(item => {
            // 元素 查找v-開頭
            if (this.isElementNode(item)) {
                this.complieElement(item);
                //若是是元素的話  須要把本身傳進去
                // 在去遍歷子節點
                this.complie(item);
                //    文本 查找{{}}內容
            } else {
                this.complieText(item);
            }
        })
 
    }
}
// 編譯工具
ComplieUtil = {
    // 解析v-model指令
    // node是節點 expr是表達式 vm是實例 person.name vm.$data 解析v-model
    model(node, expr, vm) {
        // 給輸入框賦予value屬性 node.value = xxx
        let fn = this.updater['modelUpdater'];
        let val = this.getVal(vm, expr);
        // 給輸入框加一個觀察者 若是稍後數據更i性能了會觸發此方法,數據會更新
        new Watcher(vm, expr, (newVal) => {
            fn(node, newVal);
        });
        // 輸入事件
        node.addEventListener('input',(e)=>{
            let val = e.target.value; //獲取用戶輸入的內容
            this.setVal(vm, expr, val);
        });
        fn(node, val);
    },
    html() {
 
    },
    // 返回了一個全的字符串
    getContentVal(vm, expr) {
        return expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
            return this.getVal(vm, args[1]);
        });
    },
    text(node, expr, vm) { //expr {{a}} {{b}} {{person.name}}
        let content = expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
        //給表達式{{}}都加上觀察者    
            new Watcher(vm, args[1], () => {
                fn(node, this.getContentVal(vm, expr));
            });
            return this.getVal(vm, args[1]);
        });
        let fn = this.updater['textUpdater'];
        fn(node, content);
    },
    on(node, expr, vm,eventName){ //v-on:click
        console.log(node, expr, vm, eventName);
        node.addEventListener(eventName,(e)=>{
            vm[expr].call(vm,e );
        });
       
    },
    updater: {
        modelUpdater(node, value) {
            node.value = value;
        },
        htmlUpdater() {},
        // 處理文本節點
        textUpdater(node, value) {
            node.textContent = value;
        }
    },
    //根據表達式取到的對應的數據  vm.$data expr是如 'person.name'
    getVal(vm, expr) {
      return  expr.split('.').reduce((data, cur) => {
            return data[cur];
        }, vm.$data);
    },
    setVal(vm, expr,value){
        expr.split('.').reduce((data, cur,index,arr) => {
           if(index == arr.length-1){ //索引是最後一項 
               return data[cur] = value;
           }
            return data[cur];
        }, vm.$data);
    }
}
class Vue {
    constructor(options) {
        this.$el = options.el;
        this.$data = options.data;
        let computed = options.computed;
        let methods = options.methods;
        // 根元素存在在編譯模板
        if (this.$el) {
            // 把數據 所有轉化成用Object.defineProperty來定義
            new Observer(this.$data);
 
 
            // 實現methods中的方法
            for (let key in methods) { 
                Object.defineProperty(this, key, {
                    get() {
                        return methods[key]; //進行了轉化操做
                    }
                });
            }
            // 實現computed中的方法
            for (let key in computed) { //有依賴關係
                Object.defineProperty(this.$data, key, {
                    get() {
                        return computed[key].call(this); //進行了轉化操做
                    }
                });
            }
               // 把數據獲取操做 都代理到vm.$data
            this.proxy(this.$data);
            new Complier(this.$el, this);
        }
 
    }
    // 代理 去掉$data
    proxy(data){
        for(let key in data){
            Object.defineProperty(this,key,{
                get(){
                    return data[key]; //進行了轉化操做
                }
            });
        }
    }
}


複製代碼
相關文章
相關標籤/搜索