对类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 相关文章推荐
一个对于Array的简单扩展
Oct 03 Javascript
javascript RadioButtonList获取选中值
Apr 09 Javascript
jQuery ajax serialize()方法的使用以及常见问题解决
Jan 27 Javascript
详解Javascript动态操作CSS
Dec 08 Javascript
Bootstrap每天必学之表单
Nov 23 Javascript
简单谈谈javascript中this的隐式绑定
Feb 22 Javascript
JavaScript中Promise的使用详解
Feb 26 Javascript
jQuery插件FusionCharts实现的3D柱状图效果实例【附demo源码下载】
Mar 03 Javascript
Vuejs仿网易云音乐实现听歌及搜索功能
Mar 30 Javascript
详解通过源码解析Node.js中cluster模块的主要功能实现
May 16 Javascript
vue实现鼠标移过出现下拉二级菜单功能
Dec 12 Javascript
jquery实现上传文件进度条
Mar 26 jQuery
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
香妃
2021/03/03 冲泡冲煮
3
2006/10/09 PHP
如何使用php脚本给html中引用的js和css路径打上版本号
2015/11/18 PHP
PHP mysqli事务操作常用方法分析
2017/07/22 PHP
php+layui数据表格实现数据分页渲染代码
2019/10/26 PHP
ajax java 实现自动完成功能
2012/12/19 Javascript
把字符串按照特定的字母顺序进行排序的js代码
2014/01/28 Javascript
javascript中的循环语句for语句深入理解
2014/04/04 Javascript
关闭时刷新父窗口两种方法
2014/05/07 Javascript
node.js入门教程
2014/06/01 Javascript
javascript背景时钟实现方法
2015/06/18 Javascript
jQuery+CSS3文字跑马灯特效的简单实现
2016/06/25 Javascript
Vuex之理解state的用法实例
2017/04/19 Javascript
在Vue.js中使用Mixins的方法
2017/09/12 Javascript
vue2.0 路由不显示router-view的解决方法
2018/03/06 Javascript
vue中v-text / v-html使用实例代码详解
2019/04/02 Javascript
基于VUE的v-charts的曲线显示功能
2019/10/01 Javascript
webpack的 rquire.context用法实现工程自动化的方法
2020/02/07 Javascript
[05:39]2014DOTA2西雅图国际邀请赛 淘汰赛7月14日TOPPLAY
2014/07/14 DOTA
python统计文本字符串里单词出现频率的方法
2015/05/26 Python
Python的Django应用程序解决AJAX跨域访问问题的方法
2016/05/31 Python
使用Python将数组的元素导出到变量中(unpacking)
2016/10/27 Python
Python cv2 图像自适应灰度直方图均衡化处理方法
2018/12/07 Python
selenium2.0中常用的python函数汇总
2019/08/05 Python
Pytorch 保存模型生成图片方式
2020/01/10 Python
python数据预处理方式 :数据降维
2020/02/24 Python
pycharm无法安装第三方库的问题及解决方法以scrapy为例(图解)
2020/05/09 Python
如何使用Pytorch搭建模型
2020/10/26 Python
CSS3制作文字半透明倒影效果的两种实现方式
2014/08/08 HTML / CSS
html5使用html2canvas实现浏览器截图的示例
2017/08/31 HTML / CSS
J2EE面试题大全
2016/08/06 面试题
怎样写好自我鉴定
2013/12/04 职场文书
客房主管岗位职责
2013/12/09 职场文书
小学英语教学反思案例
2014/02/04 职场文书
事业单位考核材料
2014/05/21 职场文书
Python中的协程(Coroutine)操作模块(greenlet、gevent)
2022/05/30 Python