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


Posted in Javascript onJuly 15, 2020

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

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

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

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

一、思路整理

实现的流程图:

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

我们要实现一个类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的资料请关注三水点靠木其它相关文章!

Javascript 相关文章推荐
发布BlueShow v1.0 图片浏览器(类似lightbox)blueshow.js 打包下载
Jul 21 Javascript
javascript数组组合成字符串的脚本
Jan 06 Javascript
如何解决谷歌浏览器下jquery无法获取图片的尺寸
Sep 10 Javascript
如何解决ligerUI布局时Center中的Tab高度大小
Nov 24 Javascript
JavaScript 浏览器兼容性总结及常用浏览器兼容性分析
Mar 30 Javascript
AngularJS入门教程之AngularJS 模板
Aug 18 Javascript
Canvas 绘制粒子动画背景
Feb 15 Javascript
jQuery中layer分页器的使用
Mar 13 Javascript
VUE实现一个分页组件的示例
Sep 13 Javascript
weebox弹出窗口不居中显示的解决方法
Nov 27 Javascript
详解babel升级到7.X采坑总结
May 12 Javascript
微信小程序自定义波浪组件使用方法详解
Sep 21 Javascript
详解JavaScript 作用域
Jul 14 #Javascript
如何在Vue中使localStorage具有响应式(思想实验)
Jul 14 #Javascript
Jquery使用each函数实现遍历及数组处理
Jul 14 #jQuery
javaScript实现一个队列的方法
Jul 14 #Javascript
0基础学习前端开发的一些建议
Jul 14 #Javascript
jQuery 实现DOM元素拖拽交换位置的实例代码
Jul 14 #jQuery
Vue切换div显示隐藏,多选,单选代码解析
Jul 14 #Javascript
You might like
一个程序下载的管理程序(三)
2006/10/09 PHP
php数组合并的二种方法
2014/03/21 PHP
php出现内存位置访问无效错误问题解决方法
2014/08/16 PHP
php中使用base HTTP验证的方法
2015/04/20 PHP
php中关于长度计算容易混淆的问题分析
2016/05/27 PHP
Yii2基于Ajax自动获取表单数据的方法
2016/08/10 PHP
SCP远程VPS快速搬家和WDCP升级php5.3安装memcached和eaccelerator教程
2017/07/27 PHP
php实现有序数组旋转后寻找最小值方法
2018/09/27 PHP
php进行md5加密简单实例方法
2019/09/19 PHP
JS 巧妙获取剪贴板数据 Excel数据的粘贴
2009/07/09 Javascript
javascrip客户端验证文件大小及文件类型并重置上传
2011/01/12 Javascript
jQuery过滤选择器用法示例
2016/09/12 Javascript
浅谈在js传递参数中含加号(+)的处理方式
2016/10/11 Javascript
微信小程序学习(4)-系统配置app.json详解
2017/01/12 Javascript
浅谈jquery拼接字符串效率比较高的方法
2017/02/22 Javascript
JavaScript实现经纬度转换成地址功能
2017/03/28 Javascript
JavaScript编写棋盘覆盖代码详解
2017/08/28 Javascript
jQuery动态添加.active 实现导航效果代码思路详解
2017/08/29 jQuery
nodejs项目windows下开机自启动的方法
2017/11/22 NodeJs
Vue resource三种请求格式和万能测试地址
2018/09/26 Javascript
微信小程序自定义菜单切换栏tabbar组件代码实例
2019/12/30 Javascript
vue-preview动态获取图片宽高并增加旋转功能的实现
2020/07/29 Javascript
swiper自定义分页器的样式
2020/09/14 Javascript
vue3.0实现插件封装
2020/12/14 Vue.js
python 图片验证码代码
2008/12/07 Python
Python使用multiprocessing实现一个最简单的分布式作业调度系统
2016/03/14 Python
python实现多进程代码示例
2018/10/31 Python
Python中新式类与经典类的区别详析
2019/07/10 Python
对Tensorflow中tensorboard日志的生成与显示详解
2020/02/04 Python
python实现滑雪者小游戏
2020/02/22 Python
Python使用lambda抛出异常实现方法解析
2020/08/20 Python
Python 解析xml文件的示例
2020/09/29 Python
给老婆的搞笑检讨书
2014/01/12 职场文书
同学会主持词
2014/03/18 职场文书
2016年教师学习教师法心得体会
2016/01/20 职场文书
MySQL数据库实验实现简单数据库应用系统设计
2022/06/21 MySQL