2

前言

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的处理依次是:

  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()

<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>

代码演示

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>

代码演示

参考资料


JTR354
21 声望1 粉丝

读书点亮生活