搞懂:MVVM模式和Vue中的MVVM模式
MVVM
- MVVM :
model - view - viewmodel
的縮寫,說都能直接說出來model
:模型,view
:視圖,view-Model
:視圖模型- V:視圖,即瀏覽器最前端渲染的頁面
- M:模型,數據模型,就是後端頁面渲染依賴的數據
- VM:稍後再說,因爲暫時還不知道怎麼工作,什麼場景,直接解釋有點沒用
- 那就先說說前端場景:
- 如果數據改變,想要前端頁面做出相應的改變,有幾種方法:
- 1.使用原生js
var dom = document.getElementById('xxx') dom.value = xxx; // 直接修改值 dom.innerHtml = xxx; //改變開始 和 結束標籤中的html
- 2.使用jquery
$('#name').text('Homer').css('color', 'red');
- 1.使用原生js
- 上面可以看出來,jquery確實在dom操作方面簡化了很多,鏈式調用和更加人性化的api在沒有mvvm模型出世之前,使用率極高
- 但是,也可以看出來,數據和頁面視圖之間存在斷層,數據影響視圖,甚至是視圖中的節點改變數據,這都是極其頻繁的頁面操作,雖然一再簡化這個面向過程的邏輯操作,但是還是避免不了手動修改的弊端。
- 有沒有一種更好的方式,可以實現這種視圖(
view
)和模型(model
)之間的關係
- 如果數據改變,想要前端頁面做出相應的改變,有幾種方法:
- VM:
-
再看看現在VUE框架中怎麼做到這種視圖和模型的聯動
//html <input v-model = 'val' placeholder = 'edit here'> //script export defaults{ data:function(){ return { val:'' } } }
很簡單,很常用的v-model指令,那麼在input值修改的時候,data中的val變量值也會改變,直接在js中改變val的值的時候,input中的value也會改變??我們做了什麼,我們怎麼將數據和視圖聯繫起來的?自動會關聯這兩個東西
-
可能,這就是VM吧~
- vm:viewModel視圖模型,就是將數據model和視圖view關聯了起來,負責將
model
數據同步到view
顯示,也同時把view
修改的數據同步到model
,我們無需關心中間的邏輯,開發者更多的是直接操作數據,至於更新視圖或者會寫model,都是我們寫好的視圖模型(viewModel
)幫我們處理
- vm:viewModel視圖模型,就是將數據model和視圖view關聯了起來,負責將
-
概念:視圖模型層,是一個抽象化的邏輯模型,連接視圖(
view
)和模型(model
),負責:數據到視圖的顯示,視圖到數據的回寫
-
VUE中的MVVM(雙向綁定)
vue框架中雙向綁定是最常用的一個實用功能。實現的方式也網上很多文章,vue2.x是Object.DefineProperty,vue3.x是Es6語法的proxy
代理語法
-
具體是怎麼做到的
ps:暫時先看vue2.x
- Object.setProperty(),設置和修改Javascript中對象屬性值,定義對象屬性的get和set方法,可以在對象獲取值和修改值時觸發回調函數,實現數據劫持,並且拿到新的改變後的值
- 需要根據初始化對象值和修改之後拿到改變後的值,對已綁定模板節點進行數據更新。
-
第一步:監聽對象所有屬性值變化(
Observer
)var data = {test: '1'}; observe(data); data.test = '2'; // changed 1 --> 2 function observe(data) { if (!data || typeof data !== 'object') { return; } // 取出所有屬性遍歷 Object.keys(data).forEach(function(key) { defineReactive(data, key, data[key]); }); }; function defineReactive(data, key, val) { observe(val); // 監聽子屬性 Object.defineProperty(data, key, { enumerable: true, // 可枚舉 configurable: false, // 防止重複定義或者衝突 get: function() { return val; }, set: function(newVal) { console.log('changed ', val, ' --> ', newVal); val = newVal; } }); }
- 第二步:怎麼做到對有綁定關係的節點進行更新和初始化值呢?如果一個數據對象綁定了多個dom節點,怎麼統一通知所有dom節點呢,這就需要用到發佈者-訂閱者模式
-
這裏是Observer作爲一個察覺數據變化的發佈者,發現數據變化時,觸發所有訂閱者(
Watcher
)的更新update
事件,首先要擁有一個能存儲所有訂閱者隊列,並且能通知所有訂閱者的中間件(消息訂閱器Dep
)function Dep () { // 訂閱者數組 this.subs = []; } Dep.prototype = { addSub: function(sub) { this.subs.push(sub); }, notify: function() { //通知所有訂閱者 this.subs.forEach(function(sub) { sub.update(); }); } };
-
並且在觀察者
Observer
中修改當Object對象屬性發生變化時,觸發Dep
中的notify事件,所有訂閱者可以接收到這個改變function defineReactive(data, key, val) { observe(val); var dep = new Dep(); Object.defineProperty(data, key, { enumerable: true, configurable: false, get: function() { return val; }, set: function(newVal) { //修改的在這裏 if(newVal === val){ return } // 如果新值不等於舊值發生變化,觸發所有訂閱中間件的notice方法,所有訂閱者發生變化 val = newVal console.log('changed ', val, ' --> ', newVal); dep.notify(); } }); }
-
但是有沒有發現還有一個問題,Dep訂閱中間件中的訂閱者數組一直是空的,什麼時候把訂閱者添加進來我們的訂閱中間件中間,哪些訂閱者需要添加到我們的中間件數組中
- 1.我們希望的是訂閱者Watcher在實例化的時候自動添加到Dep中
- 2.有且僅有在第一次實例化的時候添加進去,不允許重複添加
- 3.由於Dep在發佈者數據變化時會觸發所有訂閱則的update事件,所以Watcher實例(訂閱者)能夠觸發update事件,並進行相關操作
- 怎麼能讓Watcher在實例化的時候自動添加到Dep訂閱者數組中
function Watcher(vm, exp, cb) { this.cb = cb; // 構造函數中執行,只有可能在實例化的時候執行一遍 this.vm = vm; this.exp = exp; this.value = this.get(); // 將自己添加到訂閱器的操作---HACK開始 // 在構造函數中調用了一個get方法 } Watcher.prototype = { update: function() { this.run(); }, run: function() { var value = this.vm.data[this.exp]; var oldVal = this.value; if (value !== oldVal) { this.value = value; this.cb.call(this.vm, value, oldVal); } }, get: function() { //get方法中首先緩存了自己本身到target屬性 Dep.target = this; // 獲取了一下Observer中的值,相當於調用了一下get方法 var value = this.vm.data[this.exp] // get 完成之後清除了自己的target屬性??? Dep.target = null; return value; } //很明顯,get方法只在實例化的時候調用了,滿足了只有在Watcher實例化第一次的時候調用 //update方法接收了發佈者的notice 發佈消息,並且執行回調函數,這裏的回調函數還是通過外部定義(簡化版) //但是,好像在get方法中有一個很神奇的操作,緩存自己,然後調用Observer的getter,然後清除自己 //這裏其實是一步巧妙地操作把自己添加到Dep訂閱者數組中,當然Observer 的getter方法也要變化如下 }; //Observer.js function defineReactive(data, key, val) { observe(val); var dep = new Dep(); Object.defineProperty(data, key, { enumerable: true, configurable: true, get: function() { if (Dep.target) {. dep.addSub(Dep.target); // 關鍵的在這裏,當第一次實例化時,調用Watcher的get方法,get方法內部會獲取Object的屬性,會觸發這個get方法,在這裏將Watcher 添加到Dep的訂閱者數組中 } return val; }, set: function(newVal) { if (val === newVal) { return; } val = newVal; dep.notify(); } }); } Dep.target = null;
-
看似好像發佈者訂閱者模式實現了,數據劫持也實現了,在數據改變的時候,觸發Object.setProperty中定義的set函數,set函數觸發Dep訂閱者中間件的notice方法,觸發所有訂閱者的update方法,並且訂閱者在實例化的時候就加入到了Dep訂閱者的數組內部,讓我們來看看怎麼用
- html部分,
<body> <!-- 這裏其實還是會直接顯示{{name}} --> <h1 id="name">{{name}}</h1> </body>
- 封裝一個方法(類)將Observer,Watcher,關聯起來
function SelfVue (data, el, exp) { //初始化data屬性 this.data = data; //將其設置爲觀察者 observe(data); //手動設置初始值 el.innerHTML = this.data[exp]; //初始化watcher,添加到訂閱者數組中,並且回調函數是重新渲染頁面,觸發update方法時通過回調函數重寫html節點 new Watcher(this, exp, function (value) { el.innerHTML = value; }); return this; }
- 使用:
var ele = document.querySelector('#name'); var selfVue = new SelfVue({ name: 'hello world' }, ele, 'name'); //設定延時函數,直接修改數據值,看能否綁定到頁面視圖節點 window.setTimeout(function () { console.log('name值改變了'); selfVue.data.name = 'canfoo'; }, 2000);
- html部分,
-
到上面爲止:基本實現了數據(
model
)到視圖(view
)層的單向數據綁定,只有v-model是使用到了雙向綁定,很多vue的數據綁定的理解,和難點也就在上面的單向綁定 -
那麼:model->view單向綁定似乎已經成功了,那麼view -> model呢?
- 這個在於如果視圖層的value改變了,如何修改已經綁定的model層的對象屬性呢?
- 這個指令在vue中是:v-model,指令部分會在之後的學習中繼續講解
- 但是,視圖view節點在value屬性改變時,一般會觸發change或者input事件,而且也一般是一些可輸入視圖節點,直接將事件寫在change事件或者input事件裏面,並且修改Object裏面的值
var dom = document.getElementById('xx') dom.addEventListener('input',function(e){ selfVue.data.xxx = e.target.value })
- 具體input事件和v-model指令這種用法怎麼聯繫起來,之後會慢慢學習
-
- 第二步:怎麼做到對有綁定關係的節點進行更新和初始化值呢?如果一個數據對象綁定了多個dom節點,怎麼統一通知所有dom節點呢,這就需要用到發佈者-訂閱者模式
總結:
- MVVM其實是現在很多前端框架的實現基礎,除了vue 的數據劫持和觀察訂閱模式,其他框架的例如髒數據檢測,或者直接使用觀察者訂閱者模式,都是一些很巧妙的實現方式,使程序員能夠更多的關注數據層面或者邏輯層面的代碼,而不需要手動去做更新兩者之間關係的繁瑣操作
- vue的數據劫持和發佈者訂閱者模式理解起來一開始看起來理解有點費勁,大概瞭解如何做的,學習其方法,當然手寫完全流程的寫出來,我也很難
- 學習的路上,大家一起加油,多問一個爲什麼
非常感謝:下面的文章給了我很多的幫助,感謝各位前行者的辛苦付出,可以點擊查閱更多信息