前端MVVM模式及其在Vue和React中的體現

解藥發表於2018-10-29

MVVM相關概念


1) MVVM典型特點是有四個概念:Model、View、ViewModel、繫結器。MVVM可以是單向繫結也可以是雙向繫結甚至是不繫結
2) 繫結器:宣告性的資料和命令,存在於ViewModel之中,讓ViewModel和Model二者進行自動或手動通訊,接下來的“MVVM在React中對應關係”小節有舉例說明。
3) MVVM本質上是M- V-C-VM,它是在MVC的基礎上增加了一層VM,只不過C變弱了,被併入到M概念中,VM用於分離V和M,並且讓使用者避免由於直接操作V層的DOM而帶來的繁瑣和效率低下,MVVM使開發更高效,結構更清晰,增加程式碼的複用性。
4) 在不同的GUI(圖形使用者介面)上進行展示時,Model、Controller、View-Model能夠複用,只需把View層進行替換。
5) 在不同型別的UI(使用者介面)上進行展示時,Model、Controller能夠複用,只需把View-Model、View層進行替換。比如:假設我們開發的是一款針對盲人的應用,那麼輸出裝置或許我們需要考慮使用揚聲器來代替顯示器,輸入裝置使用麥克風,這時我們只需將上述的View-Model替換為Audio-Model作為語音模型,將 V(iew)層替換為Audio層用於播放語音和接收語音輸入。
6) 個人認為:在基於MVVM框架的專案中,不管是雙向資料繫結還是單向資料繫結,你在開發中實際要面對的都是ViewModel和M(odel)層之前的通訊,因為V(iew) 和ViewModel層之間的對映和通訊都是由框架自動完成的,

MVVM四層結構


1) M(odel)層:模型,定義資料結構。
2) C(ontroller)層:實現業務邏輯,資料的增刪改查。在MVVM模式中一般把C層算在M層中,(只有在理想的雙向繫結模式下,Controller 才會完全的消失。這種理想狀態一般不存在)
3) ViewModel層:顧名思義是檢視View的模型、對映和顯示邏輯(如if for等,非業務邏輯),另外繫結器也在此層。ViewModel是基於檢視開發的一套模型,如果你的應用是給盲人用的,那麼也可以開發一套基於Audio的模型AudioModel。
4) V(iew)層:將ViewModel通過特定的GUI展示出來,並在GUI控制元件上繫結檢視互動事件,V(iew)一般由MVVM框架自動生成在瀏覽器中。

MVVM在React中對應關係


1) M(odel):對應元件的方法或生命週期函式中實現的業務邏輯和this.state中儲存的本地資料,如果React整合了redux +react-redux,那麼元件中的業務邏輯和本地資料可以完全被解耦出來單獨存放當做M層,如業務邏輯放在Reducer和Action中。
2) V(iew)-M(odel):對應元件中的JSX,它實質上是Virtual DOM的語法糖。React負責維護 Virtual DOM以及對其進行diff運算,而React-dom 會把Virtual DOM渲染成瀏覽器中的真實DOM
3) View:對應框架在瀏覽器中基於虛擬DOM生成的真實DOM(並不需要我們自己書寫)以及我們書寫的CSS
4)繫結器:對應JSX中的命令以及繫結的資料,如className={ this.props.xxx }、{this.props.xxx}等等

MVVM的雙綁和單綁區別


1) 一般,只有UI表單控制元件才存在雙向資料繫結,非UI表單控制元件只有單向資料繫結。
2) 單向資料繫結是指:M的變化可以自動更新到ViewModel,但ViewModel的變化需要手動更新到M(通過給表單控制元件設定事件監聽)
3) 雙向資料繫結是指念:M的變化可以自動更新到ViewModel,ViewModel的變化也可以自動更新到M
4) 雙向繫結 = 單向繫結 + UI事件監聽。雙向和單向只不過是框架封裝程度上的差異,本質上兩者是可以相互轉換的。
5) 優缺點:在表單互動較多的情況下,單向資料繫結的優點是資料更易於跟蹤管理和維護,缺點是程式碼量較多比較囉嗦,雙向資料繫結的優缺點和單向繫結正好相反。

三大框架的異同


1) 三大框架都是資料驅動型的框架
2) vue及angular是雙向資料繫結;react是單向資料繫結。React貌似使用的也是Object.defineProperty監控資料,只是沒有進一步把表單控制元件的事件封裝進v-model
3) Vuex、Redux都是單項資料繫結的,即M的變化可以自動更新到V,但V的變化必須手動觸發事件更新到M,這種單項資料繫結使資料更易於跟蹤管理和維護。
4) 未完待續……

Vue雙向繫結原理


1) Vue的雙向資料繫結是通過Object.defineProperty的get/set對M層資料進行監控,當資料發生變化時,自動更新VM層繫結的資料,而當使用者更改了VM層表單控制元件的資料時,通過v-model自動更新到M層(v-model是對錶單控制元件的事件的封裝)
精簡示例:

<div id="demo"></div>
<input type="text" id="inp">
<script>
  var obj = {}
  var demo = document.querySelector(`#demo`)
  var inp = document.querySelector(`#inp`)
  Object.defineProperty(obj, `name`, { // 看起來資料name只是作為資料中轉的作用,真正要更新到view層的資料操作在set方法裡。
    get: function() {
      return 0
    },
    set: function (newVal) {
      inp.value = newVal
      demo.innerHTML = newVal
    }
  })
  inp.addEventListener(`input`, function(e) {
    obj.name = e.target.value  // 給obj的name屬性賦值,進而觸發該屬性的set方法,
  })
  obj.name = `fei`// 在給obj設定name屬性的時候,觸發了set這個方法
</script>

2)我們已經知道Vue是雙向資料繫結(通過v-model),Vuex是單向資料繫結,那麼問題來了,在基於Vue+ Vuex的專案中,Vuex中的資料是不允許Vue的v-model對其進行更改的,會報錯,有如下三種解決方案:

  • 依然使用v-model,資料不放進Vuex中,而是放在元件自身的data屬性中
  • 依然使用v-model,不過取值不再是Vuex中的資料,而是元件自身的一個computed(getter/setter)或watch,通過computed或watch裡的回撥來把資料變化提交(commit)到Vuex
    元件模板:

    <template>
      <div>
         <input  type="text" v-model="newName"/>
         <p>{{newName}}</p>
      </div>
    </template>

    元件VUE例項:

    computed: {
        newName: {
          get () {
            return this.$store.state.name
          },
          set (val) {
            this.$store.commit(`changeName`, val) //當newName 值發生改變時,提交一個mutation:changeName,用於改變store中的name/
          }
        }
      }

    mutation:

     changeName (state, val) {
       state.name = val
     }
  • 不使用v-model,通過UI事件監聽觸發一個回撥,然後手動把資料變化提交(commit)到Vuex

3)Vue的雙向資料繫結和Vue的prop的單項資料流是兩個不同的概念,資料繫結的前提是有資料流,但有資料流不一定有資料繫結。prop的單項資料流是指:prop可以把父元件的資料傳遞給子元件並且子元件不能對該資料進行直接修改更不能迴流到父元件(當然,得益於Vue對所有資料使用了Object.defineProperty,所以prop傳遞的資料是繫結的,即父元件中該資料一旦發生變化,子元件中的也跟著變化)

相關文章