手动实现一个简单MVVM

864 阅读3分钟

MVVM的理解

MVVM : 即 M (model-数据模型), V(view视图), VM(view-model同步M与V的对象),本质上,数据模型与视图之间是不能直接更新的,都需要通过VM来通知双方;比如,视图更新,由VM通知数据模型, 数据模型变化时,VM检测到,再通知视图层更新。这个过程是全自动的,使用者无需关心这其中的操作。

MVVM的原理

MVVM原理有多种,这里使用的是Object.defindeProperty(), 通过其对数据源进行数据绑定,并将设置了数据绑定的数据 通过'dom绑定' 的方式呈现在视图层。视图层的数据发生变化时,使用事件绑定来监听到变化,并通知dom。

Snipaste_2021-10-24_16-32-31.png

实现步骤:

· 初始化数据

· 为数据进行数据绑定

· 获取dom(input)元素并且添加事件绑定

· 将数据显示到dom上

Snipaste_2021-10-24_16-32-31.png

MVVM的实现

dom准备

<body> 标签内:

这里有几个输入数据的input,后面的dom事件监听也会绑定到上面

这里有几个p标签,用来回显数据。

p内又嵌套了一个同样的标签,是为了模拟多种情况下获取dom元素的过程(递归)

    <div id="app">
      <div>
        <input type="text" v-model="name" placeholder="姓名" />
        <input type="text" v-model="age" placeholder="年龄" />
        <input type="text" v-model="email" placeholder="邮箱" />
        <input type="text" v-model="tel" placeholder="电话号码" />
      </div>

      <div>
        <div>
          <p>姓名:<span>{{ name }}</span></p>
          <p>年龄:<span>{{ age }}</span></p>
          <p>邮箱:<span>{{ email }}</span></p>
          <p>
            <p>电话:<span>{{ tel }}</span></p>
          </p>
        </div>
      </div>
      <button id="btn">改变名字</button>
    </div>

这里实例化自定义的MVVM类,并且传入一些初始化数据,当然,此时dom上并没有效果。

    <script src="mvvm2.js"></script>
    <script>
      const app = new MVVM('#app', {
        name: 'GUJI',
        age: '11',
        email: '',
        tel: ''
      })
    </script>

开始 JS

我在其中关键地方都加了注释,并且代码思路都是由上而下的,阅读起来非常轻松。

class MVVM {
  constructor(el, data) {
    this._data = data
    this.el = document.querySelector(el)
    // 存储了绑定数据的dom
    this.domPool = {}
    this.init()
  }

  init() {
    this.initData()
    this.initDom()
  }

  initDom() {
    this.initInput(this.el)
    this.bindDom(this.el)
  }

  // 初始化数据
  initData() {
    this.data = {}
    const _this = this
    for (const key in this._data) {
      Object.defineProperty(this.data, key, {
        get() {
          return _this._data[key]
        },
        set(newValue) {
          // 修改dom中的值
          _this.domPool[key].innerHTML = newValue
          _this._data[key] = newValue
        },
      })
    }
  }

  // 得到input中的数据
  initInput(el) {
    const _allInputs = el.querySelectorAll('input')
    // 循环这些input
    _allInputs.forEach((input) => {
      const _vModel = input.getAttribute('v-model')
      if (_vModel) {
        //绑定数据
        input.addEventListener(
          'keyup',
          this.handleInput.bind(this, _vModel, input),
          false
        )
      }
    })
  }

  // 设置数据
  handleInput(key, input) {
    const _value = input.value
    this.data[key] = _value
  }

  // 将数据绑定到dom上
  bindDom(el) {
    const childNodes = el.childNodes
    childNodes.forEach((item) => {
      // 总是得到空的,原因:没有解析到最后一层,所以要在循环后面进行递归调用
      const _value = item.nodeValue
      if (item.nodeType === 3 && _value.trim().length > 0) {
        // 匹配到v-model的键
        let isVaile = /\{\{(.+?)\}\}/.test(_value)
        if (isVaile) {
          // 去除大括号 以及多余空格
          // 到这里就得到了这个dom的v-model中的值了
          const key = _value.match(/\{\{(.+?)\}\}/)[1].trim()
          // 因为item是文本节点,所以插入值的地方应该是它的父节点
          // 先将dom订阅到domPool中
          this.domPool[key] = item.parentNode
          // 绑定数据
          item.parentNode.innerText = this.data[key] || ''
        }
      }

      // 如果这个不是最后一层(有子节点)的话,就继续解析
      item.childNodes && this.bindDom(item)
    })
  }
  // 当需要在js中手动添加数据时,可以调用这个方法
  setData(key, data) {
    this.data[key] = data
  }
}

使用Object.defindeProperty()缺点:

使用Object.defindeProperty()的缺点是,当你添加或者删除该属性时,Object.defindeProperty()无法检测到,这也是vue2.0的缺点,当然它里面也提供了相应的解决方案。

可是这不能更方便开发者使用,所以vue3.0后就开始使用proxy进行数据代理。使用proxy来实现mvvm功能更加简单,有兴趣可以试一试