详解Vue中的MVVM原理和如何实现方法

虽然春天的劲风,着实让我不太喜欢,但不得不说,春天是个好季节,俗话说 "一年之际在于春 ",春天是一年的开始,也是让人充满希望和幻想的季节。刚刚历经了寒冬的人们,开始在春天慢慢苏醒,他们慢慢的重拾自己的梦想,渐渐骨起斗志,一步步朝着自己的梦想出发,一点点靠近理想的终端。

下面由我阿巴阿巴的详细走一遍Vue中MVVM原理的实现,这篇文章大家可以学习到:

1.Vue数据双向绑定核心代码模块以及实现原理

2.订阅者-发布者模式是如何做到让数据驱动视图、视图驱动数据再驱动视图

3.如何对元素节点上的指令进行解析并且关联订阅者实现视图更新

一、思路整理

实现的流程图:

我们要实现一个类MVVM简单版本的Vue框架,就需要实现一下几点:

1、实现一个数据监听Observer,对数据对象的所有属性进行监听,数据发生变化可以获取到最新值通知订阅者。

2、实现一个解析器Compile解析页面节点指令,初始化视图。

3、实现一个观察者Watcher,订阅数据变化同时绑定相关更新函数。并且将自己放入观察者集合Dep中。Dep是Observer和Watcher的桥梁,数据改变通知到Dep,然后Dep通知相应的Watcher去更新视图。

二、实现

以下采用ES6的写法,比较简洁,所以大概在300多行代码实现了一个简单的MVVM框架。

1、实现html页面

按Vue的写法在页面定义好一些数据跟指令,引入了两个JS文件。先实例化一个MVue的对象,传入我们的el,data,methods这些参数。待会再看Mvue.js文件是什么?

html

<body>
 <div id="app">
  <h2>{{person.name}} --- {{person.age}}</h2>
  <h3>{{person.fav}}</h3>
  <h3>{{person.a.b}}</h3>
  <ul>
   <li>1</li>
   <li>2</li>
   <li>3</li>
  </ul>
  <h3>{{msg}}</h3>
  <div v-text="msg"></div>
  <div v-text="person.fav"></div>
  <div v-html="htmlStr"></div>
  <input type="text" v-model="msg">
  <button v-on:click="click111">按钮on</button>
  <button @click="click111">按钮@</button>
 </div>
 <script src="./MVue.js"></script>
 <script src="./Observer.js"></script>
 <script>
  let vm = new MVue({
   el: '#app',
   data: {
    person: {
     name: '星哥',
     age: 18,
     fav: '姑娘',
     a: {
      b: '787878'
     }
    },
    msg: '学习MVVM实现原理',
    htmlStr: '<h4>大家学的怎么样</h4>',
   },
   methods: {
    click111() {
     console.log(this)
     this.person.name = '学习MVVM'
     // this.$data.person.name = '学习MVVM'
    }
   }
  })
 </script>

</body>

2、实现解析器和观察者

MVue.js

// 先创建一个MVue类,它是一个入口
Class MVue {
  construction(options) {
    this.$el = options.el
    this.$data = options.data
    this.$options = options
  }
  if(this.$el) {
    // 1.实现一个数据的观察者   --先看解析器,再看Obeserver
    new Observer(this.$data)
    // 2.实现一个指令解析器
    new Compile(this.$el,this)
  }
}
​
// 定义一个Compile类解析元素节点和指令
class Compile {
  constructor(el,vm) {
    // 判断el是否是元素节点对象,不是就通过DOM获取
    this.el = this.isElementNode(el) ? el : document.querySelector(el)
    this.vm = vm
    // 1.获取文档碎片对象,放入内存中可以减少页面的回流和重绘
    const fragment = this.node2Fragment(this.el)

    // 2.编辑模板
    this.compile(fragment)

    // 3.追加子元素到根元素(还原页面)
    this.el.appendChild(fragment)
  }

  // 将元素插入到文档碎片中
  node2Fragment(el) {
    const f = document.createDocumnetFragment();
    let firstChild
    while(firstChild = el.firstChild) {
      // appendChild
      // 将已经存在的节点再次插入,那么原来位置的节点自动删除,并在新的位置重新插入。
      f.appendChild(firstChild)
    }
    // 此处执行完,页面已经没有元素节点了
    return f
  }

  // 解析模板
  compile(frafment) {
    // 1.获取子节点
    conts childNodes = fragment.childNodes;
    [...childNodes].forEach(child => {
      if(this.isElementNode(child)) {
        // 是元素节点
        // 编译元素节点
        this.compileElement(child)
      } else {
        // 文本节点
        // 编译文本节点
        this.compileText(child)
      }

      // 嵌套子节点进行遍历解析
      if(child.childNodes && child.childNodes.length) {
        this.compule(child)
      }
    })
  }

  // 判断是元素节点还是属性节点
  isElementNode(node) {
    // nodeType属性返回 以数字值返回指定节点的节点类型。1-元素节点 2-属性节点
    return node.nodeType === 1
  }

  // 编译元素节点
  compileElement(node) {
    // 获得元素属性集合
    const attributes = node.attributes
    [...attributes].forEach(attr => {
      const {name, value} = attr
      if(this.isDirective(name)) { // 判断属性是不是以v-开头的指令
        // 解析指令(v-mode v-text v-on:click 等...)
        const [, dirctive] = name.split('-')
        const [dirName, eventName] = dirctive.split(':')
        // 初始化视图 将数据渲染到视图上
        compileUtil[dirName](node, value, this.vm, eventName)

        // 删除有指令的标签上的属性
        node.removeAttribute('v-' + dirctive)
      } else if (this.isEventName(name)) { //判断属性是不是以@开头的指令
        // 解析指令
        let [, eventName] = name.split('@')
        compileUtil['on'](node,val,this.vm, eventName)

        // 删除有指令的标签上的属性
        node.removeAttribute('@' + eventName)
      } else if(this.isBindName(name)) { //判断属性是不是以:开头的指令
        // 解析指令
        let [, attrName] = name.split(':')
        compileUtil['bind'](node,val,this.vm, attrName)

        // 删除有指令的标签上的属性
        node.removeAttribute(':' + attrName)
      }
    })
  }

  // 编译文本节点
  compileText(node) {
    const content = node.textContent
    if(/\{\{(.+?)\}\}/.test(content)) {
      compileUtil['text'](node, content, this.vm)
    }
  }

  // 判断属性是不是指令
  isDirective(attrName) {
    return attrName.startsWith('v-')
  }
  // 判断属性是不是以@开头的事件指令
  isEventName(attrName) {
    return attrName.startsWith('@')
  }
  // 判断属性是不是以:开头的事件指令
  isBindName(attrName) {
    return attrName.startsWith(':')
  }
}
​
​
// 定义一个对象,针对不同指令执行不同操作
const compileUtil = {
  // 解析参数(包含嵌套参数解析),获取其对应的值
  getVal(expre, vm) {
    return expre.split('.').reduce((data, currentVal) => {
      return data[currentVal]
    }, vm.$data)
  },
  // 获取当前节点内参数对应的值
  getgetContentVal(expre,vm) {
    return expre.replace(/\{\{(.+?)\}\}/g, (...arges) => {
      return this.getVal(arges[1], vm)
    })
  },
  // 设置新值
  setVal(expre, vm, inputVal) {
    return expre.split('.').reduce((data, currentVal) => {
      return data[currentVal] = inputVal
    }, vm.$data)
  },

  // 指令解析:v-test
  test(node, expre, vm) {
    let value;
    if(expre.indexOf('{{') !== -1) {
      // 正则匹配{{}}里的内容
      value = expre.replace(/\{\{(.+?)\}\}/g, (...arges) => {

        // new watcher这里相关的先可以不看,等后面讲解写到观察者再回头看。这里是绑定观察者实现   的效果是通过改变数据会触发视图,即数据=》视图。
        // 没有new watcher 不影响视图初始化(页面参数的替换渲染)。
        // 订阅数据变化,绑定更新函数。
        new watcher(vm, arges[1], () => {
          // 确保 {{person.name}}----{{person.fav}} 不会因为一个参数变化都被成新值
          this.updater.textUpdater(node, this.getgetContentVal(expre,vm))
        })

        return this.getVal(arges[1],vm)
      })
    } else {
      // 同上,先不看
      // 数据=》视图
      new watcher(vm, expre, (newVal) => {
      // 找不到{}说明是test指令,所以当前节点只有一个参数变化,直接用回调函数传入的新值
    this.updater.textUpdater(node, newVal)
     })

      value = this.getVal(expre,vm)
    }

    // 将数据替换,更新到视图上
    this.updater.textUpdater(node,value)
  },
  //指令解析: v-html
  html(node, expre, vm) {
    const value = this.getVal(expre, vm)

    // 同上,先不看
    // 绑定观察者 数据=》视图
    new watcher(vm, expre (newVal) => {
      this.updater.htmlUpdater(node, newVal)
    })

    // 将数据替换,更新到视图上
    this.updater.htmlUpdater(node, newVal)
  },
  // 指令解析:v-mode
  model(node,expre, vm) {
    const value = this.getVal(expre, vm)

    // 同上,先不看
    // 绑定观察者 数据=》视图
    new watcher(vm, expre, (newVal) => {
      this.updater.modelUpdater(node, newVal)
    })

    // input框 视图=》数据=》视图
    node.addEventListener('input', (e) => {
      //设置新值 - 将input值赋值到v-model绑定的参数上
      this.setVal(expre, vm, e.traget.value)
    })
    // 将数据替换,更新到视图上
    this.updater.modelUpdater(node, value)
  },
  // 指令解析: v-on
  on(node, expre, vm, eventName) {
    // 或者指令绑定的事件函数
    let fn = vm.$option.methods && vm.$options.methods[expre]
    // 监听函数并调用
    node.addEventListener(eventName,fn.bind(vm),false)
  },
  // 指令解析: v-bind
  bind(node, expre, vm, attrName) {
    const value = this.getVal(expre,vm)
    this.updater.bindUpdate(node, attrName, value)
  }

// updater对象,管理不同指令对应的更新方法
updater: {
    // v-text指令对应更新方法
    textUpdater(node, value) {
      node.textContent = value
    },
    // v-html指令对应更新方法
    htmlUpdater(node, value) {
      node.innerHTML = value
    },
    // v-model指令对应更新方法
    modelUpdater(node,value) {
      node.value = value
    },
    // v-bind指令对应更新方法
    bindUpdate(node, attrName, value) {
      node[attrName] = value
    }
  },
}

3、实现数据劫持监听

我们有了数据监听,还需要一个观察者可以触发更新视图。因为需要数据改变才能触发更新,所有还需要一个桥梁Dep收集所有观察者(观察者集合),连接Observer和Watcher。数据改变通知Dep,Dep通知相应的观察者进行视图更新。

Observer.js

// 定义一个观察者
class watcher {
  constructor(vm, expre, cb) {
    this.vm = vm
    this.expre = expre
    this.cb =cb
    // 把旧值保存起来
    this.oldVal = this.getOldVal()
  }
  // 获取旧值
  getOldVal() {
    // 将watcher放到targe值中
    Dep.target = this
    // 获取旧值
    const oldVal = compileUtil.getVal(this.expre, this.vm)
    // 将target值清空
    Dep.target = null
    return oldVal
  }
  // 更新函数
  update() {
    const newVal = compileUtil.getVal(this.expre, this.vm)
    if(newVal !== this.oldVal) {
      this.cb(newVal)
    }
  }
}
​
​
// 定义一个观察者集合
class Dep {
  constructor() {
    this.subs = []
  }
  // 收集观察者
  addSub(watcher) {
    this.subs.push(watcher)
  }
  //通知观察者去更新
  notify() {
    this.subs.forEach(w => w.update())
  }
}
​
​
​
// 定义一个Observer类通过gettr,setter实现数据的监听绑定
class Observer {
  constructor(data) {
    this.observer(data)
  }

  // 定义函数解析data,实现数据劫持
  observer (data) {
    if(data && typeof data === 'object') {
      // 是对象遍历对象写入getter,setter方法
      Reflect.ownKeys(data).forEach(key => {
        this.defineReactive(data, key, data[key]);
      })
    }
  }

  // 数据劫持方法
  defineReactive(obj,key, value) {
    // 递归遍历
    this.observer(data)
    // 实例化一个dep对象
    const dep = new Dep()
    // 通过ES5的API实现数据劫持
    Object.defineProperty(obj, key, {
      enumerable: true,
      configurable: false,
      get() {
        // 当读当前值的时候,会触发。
        // 订阅数据变化时,往Dep中添加观察者
        Dep.target && dep.addSub(Dep.target)
        return value
      },
      set: (newValue) => {
        // 对新数据进行劫持监听
        this.observer(newValue)
        if(newValue !== value) {
          value = newValue
        }
        // 告诉dep通知变化
        dep.notify()
      }
    })
  }

}

三、总结

其实复杂的地方有三点:

1、指令解析的各种操作有点复杂饶人,其中包含DOM的基本操作和一些ES中的API使用。但是你静下心去读去想,肯定是能理顺的。

2、数据劫持中Dep的理解,一是收集观察者的集合,二是连接Observer和watcher的桥梁。

3、观察者是什么时候进行绑定的?又是如何工作实现了数据驱动视图,视图驱动数据驱动视图的。

在gitHub上有上述源码地址,欢迎clone打桩尝试,还请不要吝啬一个小星星哟!

以上就是详解Vue中的MVVM原理和实现方法的详细内容,更多关于Vue中的MVVM的资料请关注其它相关文章!

您可能有感兴趣的文章
Vue路由参数的传递与获取方式详细介绍

vue学习记录之动态组件浅析

如何解决ElementUI组件中el-upload上传图片不显示问题

解读element el-upload上传的附件名称不显示 file-list赋值

一篇关于el-table-column的formatter的如何使用及说明