前端基礎整理-MVVM相關

前言

MVC、MVP、MVVM區別

MVC、MVP及MVVM都是一種架構模式,爲了解決圖形和數據的相互轉化的問題。

mvc

Model、View、Controller
mvc有以下兩種形式,無論哪一種都是單向通訊的,
image.png
實際項目通常會採用靈活的方式,如backbone.js,至關於兩種形式的結合
image.png
MVC實現了功能的分層,可是當你有變化的時候你須要同時維護三個對象和三個交互,這顯然讓事情複雜化了。

mvp

Model、 View 、Presenter
MVP切斷的View和Model的聯繫,讓View只和Presenter(原Controller)交互,減小在需求變化中須要維護的對象的數量。
MVP定義了Presenter和View之間的接口,讓一些能夠根據已有的接口協議去各自分別獨立開發,以此去解決界面需求變化頻繁的問題
image.png
用更低的成本解決問題
用更容易理解的方式解決問題

mvvm

Model 、View 、ViewModel
ViewModel大體上就是MVP的Presenter和MVC的Controller了,而View和ViewModel間沒有了MVP的界面接口,而是直接交互,用數據「綁定」的形式讓數據更新的事件不須要開發人員手動去編寫特殊用例,而是自動地雙向同步。
image.png
比起MVP,MVVM不只簡化了業務與界面的依賴關係,還優化了數據頻繁更新的解決方案,甚至能夠說提供了一種有效的解決模式。

用一個例子來闡述他們差別

栗子?:如今用戶下拉刷新一個頁面,頁面上出現10條新的新聞,新聞總數從10條變成20條。那麼MVC、MVP、MVVM的處理依次是:html

  1. View獲取下拉事件,通知Controller
  2. Controller向後臺Model發起請求,請求內容爲下拉刷新
  3. Model得到10條新聞數據,傳遞給Controller
  4. Controller拿到10條新聞數據,可能作一些數據處理,而後拿處理好的數據渲染View
MVC: 拿到UI節點,渲染10條新聞
MVP: 經過View提供的接口渲染10條新聞
MVVM: 無需操做,只要VM的數據變化,經過數據雙向綁定,View直接變化

小結

mv?都是在處理view和model相互轉化的問題;
mvc解決view和model職責劃分的問題;
mvp解決view和model隔離的問題;
mvvm解決了model與view自動映射的問題。

MVVM設計模式

image.png

極簡雙向綁定實現

JavaScript中的Object.defineProperty()和defineProperties()node

<input type="text" id="inpText">
<div class="content"></div>
<script>
  /* 實現一個最簡單的雙向綁定 */
  const content = document.querySelector('.content')
  const data = {
    name: ''
  }
  inpText.addEventListener('input', function() {
    data.name = this.value
  })
  let value // 臨時變量
  Object.defineProperty(data, 'name', {
    get() {
      return value
    },
    set(newValue) {
      if (value === newValue) return
      value = newValue
      content.innerHTML = value
      console.log('data.name=>' + data.name)
    }
  })
</script>

代碼演示git

mvvm僞代碼實現

  • Oberser.js
import Dep from './dep.js'
export default class Observer {
  constructor(data) {
    this.data = data
    this.init()
  }
  init() {
    const data = this.data
    Object.keys(data).forEach(key => {
      this.defineReactive(data, key, data[key])
    })
  }
  defineReactive(data, key, value) {
    const dep = new Dep()
    /** 子屬性爲對象 **/
    Observer.create(value)
    Object.defineProperty(data, key, {
      enumerable: true,
      configurable: true,
      get() {
        /**添加watcher**/
        if (Dep.target) {
          dep.addSub(Dep.target)
        }
        return value
      },
      set(newVal) {
        if(newVal === value) return
        value = newVal
        /** 賦值後爲對象 **/
        Observer.create(value)
        /**通知watcher**/
        dep.notify()
        // console.log('set', key)
      }
    })
  }
  static create(value) {
    if (!value || typeof value !== 'object') return
    new Observer(value)
  }
}
  • Dep.js
export default class Dep {
  constructor(props) {
    this.subs = []
  }
  addSub(sub) {
    this.subs.push(sub)
  }
  notify() {
    this.subs.forEach(sub => {
      sub.update()
    })
  }
}
Dep.target = null
  • Watcher.js
import Dep from './dep.js'
export default class Watcher {
  constructor(vm, exp, cb) {
    this.vm = vm
    this.exp = exp
    this.cb = cb
    this.value = this.get()
  }
  get() {
    Dep.target = this
    const value = this.vm[this.exp] // new Watcher時強制添加
    Dep.target = null
    return value
  }
  update() {
    this.run()
  }
  run() {
    let value = this.vm[this.exp]
    let oldVal = this.value
    if (oldVal === value) return
    this.value = value
    this.cb && this.cb.call(this.vm , value, oldVal)
  }
}
  • Compile.js
import Watcher from './watcher.js'
export default class Compile {
  constructor(el, vm) {
    this.vm = vm
    this.el = el
    this.fragment = null
    this.init()
  }
  init() {
    this.el = this.isElementNode(this.el) ? this.el : document.querySelector(this.el)
    this.fragment = this.node2fragment(this.el)
    this.compile(this.fragment)
    this.el.appendChild(this.fragment)
  }
  // 生成dom片斷
  node2fragment(el) {
    let fragment = document.createDocumentFragment()
    let child = el.firstChild
    while(child) {
      fragment.appendChild(child)
      child = el.firstChild
    }
    return fragment
  }
  // 編譯
  compile(fragment) {
    let childNode = fragment.childNodes
    const reg = /\{\{(.*)\}\}/
    Array.from(childNode).forEach(child => {
      const text = child.textContent
      if (this.isElementNode(child)) {
        // 元素節點
        this.compileElement(child)
      } else if (this.isTextNode(child) && reg.test(text)) {
        // 文本節點
        this.compileText(child, reg.exec(text)[1])
      }
      if (child.childNodes && child.childNodes.length) {
        this.compile(child)
      }
    })
  }
  // 編譯元素節點
  compileElement(node) {
    const attrs = node.attributes
    Array.from(attrs).forEach(attr => {
      let dir = attr.name
      // 是否爲指令
      if (this.isDirective(dir)) {
        const exp = attr.value
        dir = dir.substring(2)
        // 事件指令
        if (this.isDirEvent(dir)) {
          this.compileEvent(node, dir, exp)
        }
        // v-model指令
        if (this.isDirModel(dir)) {
          this.compileModel(node, dir, exp)
        }
        // ...
        node.removeAttribute(attr.name)
      }
    })
  }
  // 編譯文本節點
  compileText(node, exp) {
    const initText = this.vm[exp]
    // 初始化文本節點
    this.updaterText(node, initText)
    // 監聽數據變化,更新文本節點
    new Watcher(this.vm, exp, value => {
      this.updaterText(node, value)
    })
  }
  // 編譯事件
  compileEvent(node, dir, exp) {
    const eventType = dir.split(':')[1]
    const fn = this.vm.$options.methods && this.vm.$options.methods[exp]
    if (!eventType || !fn) return
    node.addEventListener(eventType, fn.bind(this.vm), false)
  }
  // 編譯v-model
  compileModel(node, dir, exp) {
    let value = this.vm[exp]
    this.updateModel(node, value)
    node.addEventListener('input', e=> {
      let newVal = e.target.value
      if(value === newVal) return
      value = newVal
      this.vm[exp] = value
    })
  }
  // 更新文本節點
  updaterText(node, value) {
    node.textContent = typeof value === 'undefined' ? '' : value
  }
  // 更新v-model節點
  updateModel(node, value) {
    node.value = typeof value === 'undefined' ? '' : value
  }
  // 判斷指令 事件 元素 文本
  isDirective(dir) {
    return dir.indexOf('v-') === 0
  }
  isDirModel(dir) {
    return dir.indexOf('model') === 0
  }
  isDirEvent(dir) {
    return dir.indexOf('on:') === 0
  }
  isElementNode(node) {
    return  node.nodeType === 1
  }
  isTextNode(node) {
    return node.nodeType === 3
  }
}
  • main.js
import Observer from './observer.js'
import Compile from './compile.js'
const LIFE_CYCLE = [
  'beforeCreate',
  'create',
  'beforeMount',
  'mounted',
  'beforeUpdate',
  'updated',
  'beforeDestroy',
  'destroy'
]
export default class MVVM{
  constructor(props) {
    this.$options = props
    this._data = this.$options.data
    this._el = this.$options.el
    // 數據劫持掛載在最外層
    this._proxyData()
    Observer.create(this._data)
    new Compile(this._el, this)
    this.setHook('mounted')
  }
  _proxyData() {
    const data = this._data
    const self = this
    Object.keys(data).forEach(key => {
      Object.defineProperty(this, key, {
        enumerable: true,
        configurable: true,
        get() {
          return data[key]
        },
        set(newVal) {
          if (data[key] === newVal) return
          data[key] = newVal
          self.setHook('updated')
        }
      })
    })
  }
  setHook(name) {
    if (!LIFE_CYCLE.find(val => val === name)) return
    this.$options[name] && this.$options[name].call(this)
  }
}
  • index.html
<!doctype html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>mvvm框架僞代碼</title>
</head>
<body>
  <div id="app">
    <p>{{title}}</p>
    <input type="text" v-model="name">
    <p>{{name}}</p>
    <hr>
    <p>{{count}}<button v-on:click="add" style="transform: translateX(20px)">+數量</button></p>
  </div>
  <script type="module">
    import MVVM from './my-mvvm/main.js'
    window.$vm = new MVVM({
      el: '#app',
      data: {
        title: 'hello mvvm',
        name: 'jtr',
        count: 0
      },
      methods: {
        add() {
          this.count++
        }
      },
      mounted() {
        this.timer = setInterval(() => {
          this.title += '*'
        }, 500)
      },
      updated() {
        this.title.length > 20 && clearInterval(this.timer)
      }
    })
  </script>
</body>
</html>

代碼演示segmentfault

參考資料

相關文章
相關標籤/搜索