探索從 MVC 到 MVVM + Flux 架構模式的轉變

牧云云發表於2018-02-11

探索從 MVC 到 MVVM + Flux 架構模式的轉變

在業務中一般 MVVM 框架一般都會配合上資料狀態庫(redux, mobx 等)一起使用,本文會通過一個小 demo 來講述為什麼會引人資料狀態庫。

從 MVC 到 MVVM 模式說起

傳統 MVC 架構(如 JSP)在當今移動端流量寸土寸金的年代一個比較頭疼的問題就是會進行大量的全域性重複渲染。但是 MVC 架構是好東西,其對資料、檢視、邏輯有了清晰的分工,於是前端 MVC 框架(比如 backbone.js) 出來了,對於很多業務規模不大的場景,前端 MVC 框架已經夠用了,它也能做到前後端分離開發單頁面應用,那麼它的缺陷在哪呢?

拿 backbone.js 說,它的 Model 對外暴露了 set 方法,也就是說可以在不止一個 View 裡修改同個 Model 的資料,然後一個 Model 的資料同時對應多個 View 的呈現,如下圖所示。當業務邏輯過多時,多個 Model 和多個 View 就會耦合到一塊,可以想到排查 bug 的時候會比較痛苦。

探索從 MVC 到 MVVM + Flux 架構模式的轉變

針對傳統 MVC 架構效能低(多次全域性渲染)以及前端 MVC 框架耦合度高(Model 和 View) 的痛處,MVVM 框架完美地解決了以上兩點。可以參閱之前寫的 MVVM 框架解析之雙向繫結

only MVVM

假設有這麼一個場景,在輸入框中查詢條件,點選查詢,然後在列表中返回相應內容。如下圖所示:

探索從 MVC 到 MVVM + Flux 架構模式的轉變

假設用 react 實現,思路大體是先呼叫查詢介面,呼叫成功後將獲取到的資料通過 setState 存進 list 中,列表顯示部分程式碼如下:

const Decorate = (ListComponent) => class extends Component {
  constructor() {
    super()
    this.state = { list: [] }
  }

  componentDidMount() {
    fetch('./list.json')
      .then((res) => res.json())
      .then(result => this.setState({ list: result.data }))
  }

  render() {
    return (
      <ListComponent data={this.state.list} />
    )
  }
}
複製程式碼

接著往封裝的 Decorate 元件裡,傳入無狀態函式構建的 List 元件用來展示列表資料,程式碼如下:

function List(props) {
  return (
    <div>
      {props.data.map(r =>
        <p key={r.id}>{r.content}</p>
      )}
    </div>
  )
}
複製程式碼

可以看到 List 元件相當於是 View 層,而封裝的 Decorate 元件相當於是 Model 層。但是這麼做還是把業務邏輯寫進了元件當中。而我們期望的是能得到一個純粹的 Model 層和 View 層。接著一起看看 Flux 架構模式是如何解決這個問題的。

引人 Flux 架構模式

探索從 MVC 到 MVVM + Flux 架構模式的轉變

Flux 架構模式的 4 個重要組成部分以及它們的關係如上圖所示,下文會根據 dispatch,store, action, view 的順序逐步揭開 Flux 架構模式的面紗。

Flux 的原始碼中可以看出 Dispacher.js 是其的核心檔案,其核心是基於事件的釋出/訂閱模式完成的,核心原始碼如下:

class Dispatcher {
  ...
  // 註冊回撥函式,
  register(callback) {
    var id = _prefix + this._lastID++;
    this._callbacks[id] = callback;
  }

  // 當呼叫 dispatch 的時候會呼叫 register 中註冊的回撥函式
  dispatch(payload) {
    this._startDispatching(payload);
    for (var id in this._callbacks) {
      this._invokeCallback(id);
    }
  }
}
複製程式碼

回顧下之前的目的:讓 Store 層變得純粹。於是定義了一個變數 comments 用來專門存放列表資料,在瞭解 Dispatcher 的核心原理之後,當呼叫 dispatch(obj) 方法時,就可以把引數傳遞到事先註冊的 register 函式中,程式碼如下:

// commentStore.js
let comments = []
const CommentStore = {
  getComment() {
    return comments
  }
}

dispathcer.register((action) => { // 呼叫 Dispatcher 例項上的 register 函式
  switch (action.type) {
    case 'GET_LIST_SUCCESS': {
      comments = action.comment
    }
  }
})
複製程式碼

以及 action 中的函式如下:

// commentAction.js
const commentAction = {
  getList() {
    fetch('./list.json')
      .then((res) => res.json())
      .then(result =>
        dispathcer.dispatch({ // 呼叫 Dispatcher 例項上的 dispatch 函式
          type: 'GET_LIST_SUCCESS',
          comment: result.data
        }))
  }
}
複製程式碼

但是似乎少了點什麼,當 GET_LIST_SUCCESS 成功後,發現還缺少通知到頁面再次呼叫 CommentStore.getComment() 的能力,所以再次引用事件釋出/訂閱模式,這次使用了 Node.js 提供的 events 模組,對 commentStore.js 檔案進行修改,修改後程式碼如下:

let comments = []
const CommentStore = Object.assign({}, EventEmitter.prototype, {
  getComment() {
    return comments
  },

  emitChange() {
    this.emit('change')
  },

  addListener(callback) { // 提供給頁面元件使用
    this.on('change', callback)
  }
})

appDispathcer.register((action) => {
  switch (action.type) {
    case 'GET_LIST_SUCCESS': {
      comments = action.comment
      CommentStore.emitChange() // 有了這行程式碼,也就有了通知頁面再次進行呼叫 CommentStore.getComment 的能力
    }
  }
})
複製程式碼

剩下最後一步了,就是整合 store 和 action 進頁面中,程式碼如下:

class ComponentList extends Component {
  constructor() {
    super()
    this.state = {
      comment: commentStore.getComment()
    }
  }

  componentDidMount() {
    commentStore.addListener(() => this.setState({ // 註冊函式,上面已經提過,供 store 使用
      comment: commentStore.getComment()
    }))
  }

  render() {
    return (
      <div>
        {this.state.comment.map(r =>
          <p key={r.id}>{r.content}</p>
        )}
      </div>
    )
  }
}
複製程式碼

小結

單純以 mvvm 構建應用會發現業務邏輯以及資料都耦合在元件之中,引入了 Flux 架構模式後資料和業務邏輯得到較好的分離。但是使用 Flux 有什麼缺點呢?在下篇 《聊聊 Redux 架構模式》中會進行分析,下回見。

本文實踐案例已上傳至 stateManage

系列部落格,歡迎 Star

相關文章