对类Vue的MVVM前端库的实现代码


Posted in Javascript onSeptember 07, 2018

MVVM

(ModelView ViewModel)是一种基于MVC的设计,开发人员在HTML上写一些Bindings,利用一些指令绑定,就能在Model和ViewModel保持不变的情况下,很方便的将UI设计与业务逻辑分离,从而大大的减少繁琐的DOM操作。

关于实现MVVM,网上实在是太多了,本文为个人总结,结合源码以及一些别人的实现

关于双向绑定

•vue 数据劫持 + 订阅 - 发布
•ng 脏值检查
•backbone.js 订阅-发布(这个没有使用过,并不是主流的用法)

双向绑定,从最基本的实现来说,就是在defineProperty绑定的基础上在绑定input事件,达到v-model的功能

代码思路图

对类Vue的MVVM前端库的实现代码

两个版本:

•简单版本: 非常简单,但是因为是es6,并且代码极度简化,所以不谈功能,思路还是很清晰的
•标准版本: 参照了Vue的部分源码,代码的功能高度向上抽取,阅读稍微有点困难,实现了基本的功能,包括计算属性,watch,核心功能都实现没问题,但是不支持数组

简单版本

简单版本的地址: 简单版本

​ 这个MVVM也许代码逻辑上面实现的并不完美,并不是正统的MVVM, 但是代码很精简,相对于源码,要好理解很多,并且实现了v-model以及v-on methods的功能,代码非常少,就100多行

class MVVM {
 constructor(options) {
 const {
  el,
  data,
  methods
 } = options
 this.methods = methods
 this.target = null
 this.observer(this, data)
 this.instruction(document.getElementById(el)) // 获取挂载点
 }
 // 数据监听器 拦截所有data数据 传给defineProperty用于数据劫持
 observer(root, data) {
 for (const key in data) {
  this.definition(root, key, data[key])
 }
 }
 // 将拦截的数据绑定到this上面
 definition(root, key, value) {
 // if (typeof value === 'object') { // 假如value是对象则接着递归
 // return this.observer(value, value)
 // }
 let dispatcher = new Dispatcher() // 调度员
 Object.defineProperty(root, key, {
  set(newValue) {
  value = newValue
  dispatcher.notify(newValue)
  },
  get() {
  dispatcher.add(this.target)
  return value
  }
 })
 }
 //指令解析器
 instruction(dom) {
 const nodes = dom.childNodes; // 返回节点的子节点集合
 // console.log(nodes); //查看节点属性
 for (const node of nodes) { // 与for in相反 for of 获取迭代的value值
  if (node.nodeType === 1) { // 元素节点返回1
  const attrs = node.attributes //获取属性
  for (const attr of attrs) {
   if (attr.name === 'v-model') {
   let value = attr.value //获取v-model的值
   node.addEventListener('input', e => { // 键盘事件触发
    this[value] = e.target.value
   })
   this.target = new Watcher(node, 'input') // 储存到订阅者
   this[value] // get一下,将 this.target 给调度员
   }
   if (attr.name == "@click") {
   let value = attr.value // 获取点击事件名
   node.addEventListener('click',
    this.methods[value].bind(this)
   )
   }
  }
  }
  if (node.nodeType === 3) { // 文本节点返回3
  let reg = /\{\{(.*)\}\}/; //匹配 {{ }}
  let match = node.nodeValue.match(reg)
  if (match) { // 匹配都就获取{{}}里面的变量
   const value = match[1].trim()
   this.target = new Watcher(node, 'text')
   this[value] = this[value] // get set更新一下数据
  }
  }
 }
 }
}
//调度员 > 调度订阅发布
class Dispatcher {
 constructor() {
 this.watchers = []
 }
 add(watcher) {
 this.watchers.push(watcher) // 将指令解析器解析的数据节点的订阅者存储进来,便于订阅
 }
 notify(newValue) {
 this.watchers.map(watcher => watcher.update(newValue))
 // 有数据发生,也就是触发set事件,notify事件就会将新的data交给订阅者,订阅者负责更新
 }
}
//订阅发布者 MVVM核心
class Watcher {
 constructor(node, type) {
 this.node = node
 this.type = type
 }
 update(value) {
 if (this.type === 'input') {
  this.node.value = value // 更新的数据通过订阅者发布到dom
 }
 if (this.type === 'text') {
  this.node.nodeValue = value
 }
 }
}
<!DOCTYPE html>
<html lang="en">
<head>
 <meta charset="UTF-8">
 <meta name="viewport" content="width=device-width, initial-scale=1.0">
 <meta http-equiv="X-UA-Compatible" content="ie=edge">
 <title>MVVM</title>
</head>
<body>
 <div id="app">
 <input type="text" v-model="text">{{ text }}
 <br>
 <button @click="update">重置</button>
 </div>
 <script src="./index.js"></script>
 <script>
 let mvvm = new MVVM({
  el: 'app',
  data: {
  text: 'hello MVVM'
  },
  methods: {
  update() {
   this.text = ''
  }
  }
 })
 </script>
</body>
</html>

这个版本的MVVM因为代码比较少,并且是ES6的原因,思路非常清晰

我们来看看从new MVVM开始,他都做了什么

解读简单版本

new MVVM

首先,通过解构获取所有的new MVVM传进来的对象

class MVVM {
 constructor(options) {
 const {
  el,
  data,
  methods
 } = options
 this.methods = methods // 提取methods,便于后面将this给methods
 this.target = null // 后面有用
 this.observer(this, data)
 this.instruction(document.getElementById(el)) // 获取挂载点
 }

属性劫持

开始执行this.observer observer是一个数据监听器,将data的数据全部拦截下来

observer(root, data) {
 for (const key in data) {
  this.definition(root, key, data[key])
 }
 }

在this.definition里面把data数据都劫持到this上面

definition(root, key, value) {
 if (typeof value === 'object') { // 假如value是对象则接着递归
  return this.observer(value, value)
 }
 let dispatcher = new Dispatcher() // 调度员

 Object.defineProperty(root, key, {
  set(newValue) {
  value = newValue
  dispatcher.notify(newValue)
  },
  get() {
  dispatcher.add(this.target)
  return value
  }
 })
 }

此时data的数据变化我们已经可以监听到了,但是我们监听到后还要与页面进行实时相应,所以这里我们使用调度员,在页面初始化的时候get(),这样this.target,也就是后面的指令解析器解析出来的v-model这样的指令储存到调度员里面,主要请看后面的解析器的代码

指令解析器

指令解析器通过执行 this.instruction(document.getElementById(el)) 获取挂载点

instruction(dom) {
 const nodes = dom.childNodes; // 返回节点的子节点集合
 // console.log(nodes); //查看节点属性
 for (const node of nodes) { // 与for in相反 for of 获取迭代的value值
  if (node.nodeType === 1) { // 元素节点返回1
  const attrs = node.attributes //获取属性
  for (const attr of attrs) {
   if (attr.name === 'v-model') {
   let value = attr.value //获取v-model的值
   node.addEventListener('input', e => { // 键盘事件触发
    this[value] = e.target.value
   })
   this.target = new Watcher(node, 'input') // 储存到订阅者
   this[value] // get一下,将 this.target 给调度员
   }
   if (attr.name == "@click") {
   let value = attr.value // 获取点击事件名
   node.addEventListener('click',
    this.methods[value].bind(this)
   )
   }
  }
  }
  if (node.nodeType === 3) { // 文本节点返回3
  let reg = /\{\{(.*)\}\}/; //匹配 {{ }}
  let match = node.nodeValue.match(reg)
  if (match) { // 匹配都就获取{{}}里面的变量
   const value = match[1].trim()
   this.target = new Watcher(node, 'text')
   this[value] = this[value] // get set更新一下数据
  }
  }
 }
 }

这里代码首先解析出来我们自定义的属性然后,我们将@click的事件直接指向methods,methds就已经实现了

现在代码模型是这样

对类Vue的MVVM前端库的实现代码

调度员Dispatcher与订阅者Watcher

我们需要将Dispatcher和Watcher联系起来

于是我们之前创建的变量this.target开始发挥他的作用了

正执行解析器里面使用this.target将node节点,以及触发关键词存储到当前的watcher 订阅,然后我们获取一下数据

this.target = new Watcher(node, 'input') // 储存到订阅者
this[value] // get一下,将 this.target 给调度员

在执行this[value]的时候,触发了get事件

get() {
 dispatcher.add(this.target)
 return value
}

这get事件里面,我们将watcher订阅者告知到调度员,调度员将订阅事件存储起来

//调度员 > 调度订阅发布
class Dispatcher {
 constructor() {
 this.watchers = []
 }
 add(watcher) {
 this.watchers.push(watcher) // 将指令解析器解析的数据节点的订阅者存储进来,便于订阅
 }
 notify(newValue) {
 this.watchers.map(watcher => watcher.update(newValue))
 // 有数据发生,也就是触发set事件,notify事件就会将新的data交给订阅者,订阅者负责更新
 }
}

与input不太一样的是文本节点不仅需要获取,还需要set一下,因为要让订阅者更新node节点

this.target = new Watcher(node, 'text')
this[value] = this[value] // get set更新一下数据

所以在订阅者就添加了该事件,然后执行set

set(newValue) {
  value = newValue
  dispatcher.notify(newValue)
  },

notfiy执行,订阅发布者执行update更新node节点信息

class Watcher {
 constructor(node, type) {
 this.node = node
 this.type = type
 }
 update(value) {
 if (this.type === 'input') {
  this.node.value = value // 更新的数据通过订阅者发布到dom
 }
 if (this.type === 'text') {
  this.node.nodeValue = value
 }
 }
}

页面初始化完毕

更新数据

node.addEventListener('input', e => { // 键盘事件触发
 this[value] = e.target.value
})

this[value]也就是data数据发生变化,触发set事件,既然触发notfiy事件,notfiy遍历所有节点,在遍历的节点里面根据页面初始化的时候订阅的触发类型.进行页面的刷新

现在可以完成的看看new MVVM的实现过程了

最简单版本的MVVM完成

标准版本

标准版本额外实现了component,watch,因为模块化代码很碎的关系,看起来还是有难度的

从理念上来说,实现的思想基本是一样的,可以参照上面的图示,都是开始的时候都是拦截属性,解析指令

代码有将近300行,所以就贴一个地址标准版本MVVM

执行顺序

1.new MVVM
2.获取$options = 所以参数
3.获取data,便于后面劫持
4.因为是es5,后面forEach内部指向window,这不是我们想要的,所以存储当前this 为me
5._proxyData劫持所有data数据
6.初始化计算属性
7.通过Object.key()获取计算属性的属性名
8.初始化计算属性将计算属性挂载到vm上
9.开始observer监听数据
10.判断data是否存在
11.存在就new Observer(创建监听器)
12.数据全部进行进行defineProperty存取监听处理,让后面的数据变动都触发这个的get/set
13.开始获取挂载点
14.使用querySelector对象解析el
15.创建一个虚拟节点,并存储当前的dom
16.解析虚拟dom
17.使用childNodes解析对象
18.因为是es5,所以使用[].slice.call将对象转数组
19.获取到后进行 {{ }}匹配 指令的匹配 以及递归子节点
20.指令的匹配: 匹配到指令因为不知道多少个指令名称,所以这里还是使用[].slice.call循环遍历
21.解析到有 v-的指令使用substring(2)截取后面的属性名称
22.再判断是不是指令v-on 这里就是匹配on关键字,匹配到了就是事件指令,匹配不到就是普通指令
23.普通指令解析{{ data }} _getVMValget会触发MVVM的_proxyData事件 在_proxyData事件里面触发data的get事件
24.这时候到了observer的defineReactive的get里面获取到了数据,因为没有Dispatcher.target,所以不进行会触发调度员
25.至此_getVMVal获取到了数据
26.modelUpdater进行Dom上面的数据更新
27.数据开始进行订阅,在订阅里面留一个回调函数用于更新dom
28.在watcher(订阅者)获取this,订阅的属性,回调
29.在this.getter这个属性上面返回一个匿名函数,用于获取data的值
30.触发get事件,将当前watcher的this存储到Dispatcher.garget上面
31.给this.getters,callvm的的this,执行匿名函数,获取劫持下来的data,又触发了MVVM的_proxyData的get事件,继而有触发了observer的defineReactive的get事件,不过这一次Dispatcher.target有值,执行了depend事件
32.在depend里面执行了自己的addDep事件,并且将Observer自己的this传进去
33.addDep里面执行了Dispatcher的addSub事件,
34.在addUsb事件里面将订阅存储到Dispatcher里面的this.watchers里面的
35.订阅完成,后面将这些自定义的指令进行移除
36.重复操作,解析所有指令,v-on:click = "data"直接执行methods[data].bind(vm)

更新数据:

1.触发input事件
2.触发_setVMVal事件
3.触发MVVM的set事件
4.触发observer的set事件
5.触发dep.notify()
6.触发watcher的run方法
7.触发new Watcher的回调 this.cb
8.触发compile里面的updaterFn 事件
9.更新视图

component的实现

计算属性的触发 查看这个例子

computed: {
  getHelloWord: function () {
   return this.someStr + this.child.someStr;
  }
  },

其实计算属性就是defineproperty的一个延伸

1.首先compile里面解析获取到{{ getHelloword }}'
2.执行updater[textUpdater]
3.执行_getVMVal获取计算属性的返回值
4.获取vm[component]就会执行下面的get事件

Object.defineProperty(me, key, {
   get: typeof computed[key] === 'function' ? computed[key] : computed[key].get,
   set: function () {}
  })

是function执行computed[getHelloword],也就是return 的 函数

this.someStr + this.child.someStr;

1.依次获取data,触发mvvm的get 以及observer的get,

初始化完成,到这里还没有绑定数据,仅仅是初始化完成了

1.开始订阅该事件 new Watcher()
2.component不是函数所以不是function 执行this.parseGetter(expOrFn);
3.返回一个覆盖expOrrn的匿名函数
4.开始初始化 执行get()
5.存储当前this,开始获取vm[getHelloword]
6.触发component[getHelloword]
7.开始执行MVVM的get this.someStr
8.到MVVM的get 到 observer的get 因为 Dispatcher.target存着 getHelloWord 的 this.depend ()所以执行
9.Dispatcher的depend(),执行watcher的addDep(),执行 Dispatcher的addSub() 将当前的watcher存储到监听器
10.开始get第二个数据 this.child.someStr,同理也将getHelloWord的this存入了当前的Dispatcher
11.开始get第三个数据 this.child,同理也将getHelloWord的this存入了当前的Dispatcher

这个执行顺序有点迷,第二第三方反来了

this.parseGetter(expOrFn);就执行完毕了

目前来看为什么component会实时属性数据?

因为component的依赖属性一旦发生变化都会更新 getHelloword 的 watcher ,随之执行回调更新dom

watch的实现

watch的实现相对来说要简单很多

1.我们只要将watch监听的数据告诉订阅者就可以了
2.这样,wacth更新了
3.触发set,set触发notify
4.notify更新watcher
5.watcher执行run
6.run方法去执行watch的回调
7.即完成了watch的监听

watch: function (key, cb) {
 new Watcher(this, key, cb)
},

Javascript 相关文章推荐
符合标准的js表单提交的代码
Sep 13 Javascript
JS中字符问题(二进制/十进制/十六进制及ASCII码之间的转换)
Nov 03 Javascript
javascript 面向对象全新理练之继承与多态
Dec 03 Javascript
一个分享按钮的插件使用介绍(可扩展,内附开发制作流程)
Sep 19 Javascript
通过复制Table生成word和excel的javascript代码
Jan 20 Javascript
JS判断浏览器是否支持某一个CSS3属性的方法
Oct 17 Javascript
js实现window.open不被拦截的解决方法汇总
Oct 30 Javascript
基于JavaScript实现鼠标向下滑动加载div的代码
Aug 31 Javascript
jQuery基本选择器之标签名选择器
Sep 03 Javascript
Bootstrap模态窗口源码解析
Feb 08 Javascript
Angular 4依赖注入学习教程之简介(一)
Jun 04 Javascript
JS中使用new Option()实现时间联动效果
Dec 10 Javascript
cnpm加速Angular项目创建的方法
Sep 07 #Javascript
vue.js 实现点击按钮动态添加li的方法
Sep 07 #Javascript
vue 点击按钮增加一行的方法
Sep 07 #Javascript
详解使用jest对vue项目进行单元测试
Sep 07 #Javascript
Vue 实现列表动态添加和删除的两种方法小结
Sep 07 #Javascript
koa-router源码学习小结
Sep 07 #Javascript
Vue.js实现表格渲染的方法
Sep 07 #Javascript
You might like
简单PHP上传图片、删除图片实现代码
2010/05/12 PHP
一个简单且很好用的php分页类
2013/10/26 PHP
使用js获取QueryString的方法小结
2010/02/28 Javascript
杨氏矩阵查找的JS代码
2013/03/21 Javascript
jQuery中ajax的使用与缓存问题的解决方法
2013/12/19 Javascript
jquery幻灯片插件bxslider样式改进实例
2014/10/15 Javascript
jQuery插件扩展实例【添加回调函数】
2016/11/26 Javascript
拖动时防止选中
2017/02/03 Javascript
angular中不同的组件间传值与通信的方法
2017/11/04 Javascript
利用JQUERY实现多个AJAX请求等待的实例
2017/12/14 jQuery
使用Vue的slot插槽分发父组件内容实现高度复用、更加灵活的组件(推荐)
2018/05/01 Javascript
vue2 设置router-view默认路径的实例
2018/09/20 Javascript
js实现简单模态框实例
2018/11/16 Javascript
百度小程序自定义通用toast组件
2019/07/17 Javascript
[16:43]Heroes19_剃刀(完美)
2014/10/31 DOTA
python的id()函数介绍
2013/02/10 Python
Python脚本实现下载合并SAE日志
2015/02/10 Python
Python获取当前路径实现代码
2017/05/08 Python
Python利用itchat对微信中好友数据实现简单分析的方法
2017/11/21 Python
Django 中自定义 Admin 样式与功能的实现方法
2019/07/04 Python
Django中ajax发送post请求 报403错误CSRF验证失败解决方案
2019/08/13 Python
python实现while循环打印星星的四种形状
2019/11/23 Python
python matplotlib包图像配色方案分享
2020/03/14 Python
Pygame的程序开始示例代码
2020/05/07 Python
CSS3 对过渡(transition)进行调速以及延时
2020/10/21 HTML / CSS
html5 冒号分隔符对齐的实现
2019/07/31 HTML / CSS
大都会艺术博物馆商店:The Met Store
2018/06/22 全球购物
装饰资料员岗位职责
2013/12/30 职场文书
办公室前台岗位职责
2014/01/04 职场文书
公司活动策划方案
2014/01/13 职场文书
乔迁宴答谢词
2014/01/21 职场文书
公司门卫的岗位职责
2014/02/19 职场文书
正风肃纪剖析材料
2014/09/30 职场文书
2014年办公室主任工作总结
2014/11/12 职场文书
我的中国梦主题教育活动总结
2015/05/07 职场文书
预备党员入党感想
2015/08/10 职场文书