对类Vue的MVVM前端库的实现

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

关于双向绑定

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

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

代码思路图

Fo6m0DmgK22kbz4gb-3US3pq6eVk.png

两个版本:

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

简单版本

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

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

class MVVM {constructor(options) {const {el,data,methods} = optionsthis.methods = methodsthis.target = nullthis.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 = newValuedispatcher.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) { // 元素节点返回1const 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) { // 文本节点返回3let 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 = nodethis.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} = optionsthis.methods = methods // 提取methods,便于后面将this给methodsthis.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 = newValuedispatcher.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) { // 元素节点返回1const 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) { // 文本节点返回3let 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就已经实现了

现在代码模型是这样

Fml57sZd2r9BOBMOEdoNiGx_-T0q.png

调度员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 = newValuedispatcher.notify(newValue)},

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

class Watcher {constructor(node, type) {this.node = nodethis.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的实现过程了

Fo6m0DmgK22kbz4gb-3US3pq6eVk.png

最简单版本的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里面执行了DispatcheraddSub事件,
  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)
},

转载于:https://www.cnblogs.com/wuvkcyan/p/9602562.html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/249567.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

django的母板系统

一.母板渲染语法 1.变量 {{ 变量 }} 2.逻辑 {% 逻辑语 %} 二.变量 在母板中有变量时,母板引擎会去反向解析找到这个传来的变量,然后替换掉. .(点),在母板中是深度查询据点符,它的查询顺序: 字典 > 属性或方法 > 数字索引 三.过滤器 1.语法 {{ value|filter_name:参数}} 2…

【CSS】flex的常用布局

1、垂直居中&#xff0c;写在父级上div{display: flex;justify-content: center;align-items: center; } 2、flex-左右两端&#xff0c;垂直居中该布局在移动端较为常见<style> .wrap{display: flex;justify-content: space-between;align-items: center;width: 200px;he…

java.util.Properties

ava.util.Properties是对properties这类配置文件的映射。支持key-value类型和xml类型两种 首先&#xff0c;新建一个文件&#xff0c;如图&#xff1a; 然后再Java代码段输入如下代码&#xff1a; import java.io.FileInputStream; import java.io.InputStream; import java…

Spring-boot 打成jar包后使用外部配置文件

官网说明 第一种是在jar包的同一目录下建一个config文件夹&#xff0c;然后把配置文件放到这个文件夹下&#xff1b; 第二种是直接把配置文件放到jar包的同级目录&#xff1b; 第三种在classpath下建一个config文件夹&#xff0c;然后把配置文件放进去&#xff1b; 第四种是在c…

UI自动化之元素定位(xpath、css)

很早之前就已经写过自动化了&#xff0c;不过点着功能久了就会容易忘记元素定位&#xff0c;尤其是xpath和css定位&#xff0c;所以就花点时间做下总结收集。 xpath有两种定位&#xff1a; 一.绝对路径&#xff08;不推荐使用&#xff0c;除非已经使用了所有方式仍然无法定位&a…

属性编辑器PropertyEditor

在Spring配置文件里&#xff0c;我们往往通过字面值为Bean各种类型的属性提供设置值&#xff1a;不管是double类型还是int类型&#xff0c;在配置文件中都对应字符串类型的字面值。BeanWrapper填充Bean属性时如何将这个字面值转换为对应的double或int等内部类型呢&#xff1f;我…

Linux下查看当前文件大小的命令

1、ls -lht 列出每个文件的大小和当前目录所有文件大小总和 2、du -sh * 列出当前文件夹下的所有子文件的大小 看你需要啥样的&#xff0c;自己来吧 转载于:https://www.cnblogs.com/xbxxf/p/9619818.html

Spring IOC-BeanFactory的继承体系结构

本文主要介绍BeanFactory以及它的各种继承层级的接口、抽象类及实现类&#xff0c;因为内容很多&#xff0c;所以这里不介绍ApplicationContext继承体系下的类&#xff08;虽然ApplicationContext本质上也是BeanFactory&#xff0c;但是毕竟这这是我们平时接触最多的两种类别&a…

【PHP】xampp配置多个监听端口和不同的网站目录(转)

转自&#xff1a;https://blog.csdn.net/cc1314_/article/details/75646344 windows下使用xampp配置多个监听端口和不同的网站目录 一&#xff1a;配置Apache文件httpd.conf打开Apache的配置文件httpd.conf&#xff0c;可以通过点击xampp的Apache的config下的Apache(httpd.conf…

Java消息中间件

1.概述 中间件 非底层操作系统软件&#xff0c;非业务应用软件&#xff0c;不是直接给最终用户使用的&#xff0c;不能直接给客户带来价值的软件统称为中间件。 消息中间件 管制关注于数据的发送和接收&#xff0c;利用高效可靠的异步消息传递机制集成分布式系统。 优点 ① 解…

form 源码刨析

def clean_name(self) value self.cleaned_data.get(name) if "金-瓶-梅" not in value: raise ValidationError("不符合要求") return value 重写clean方法 转载于:https://www.cnblogs.com/wuheng-123/p/9623289.html

Activemq源码、编译、导入idea、源码调试总结

1、在本地下载源码 在GitHub官网搜activemq&#xff0c;找到排名第一的&#xff0c;并打开&#xff0c;如图所示&#xff0c;拷贝url地址。 activemq托管地址&#xff1a;https://github.com/apache/activemq.git 切换到git bash下&#xff0c;输入命令&#xff1a; mkdir a…

activiti 视图

1. application.properties增加如下配置 spring.activiti.database-schema-updatefalsespring.activiti.db-history-usedfalsespring.activiti.db-identity-usedfalse 2. 视图sql -- 修改表名称 ALTER TABLE act_id_user RENAME act_id_user_bak1; ALTER TABLE act_id_group RE…

ActiveMQ源码解析 建立连接

作为一个消息中间件&#xff0c;有客户端和服务端两部分代码&#xff0c;这次的源码解析系列主要从客户端的代码入手&#xff0c;分成建立连接、消息发送、消息消费三个部分。趁着我昨天弄明白了源码编译的兴奋劲头还没过去&#xff0c;今天研究一下建立连接的部分。 如果读起…

原生Js_实现广告弹窗

广告样式当页面加载后5s刷新在右下角 <!DOCTYPE html> <html><head><meta charset"utf-8" /><title>Gary图片轮播</title><style type"text/css">#ad{width:300px;height: 300px;background-color:antiquewhite…

springcloud注册中心eureka

1、前提 springcloud的注册中心是以springboot为基础搭建起来的。 开发工具&#xff1a;IDEA 项目管理工具&#xff1a;maven 2、搭建步骤 创建一个web项目&#xff08;建议使用IDEA工具构建项目&#xff09;修改pom文件 <dependency><groupId>org.springframework…

Nancy in .Net Core学习笔记 - 视图引擎

前文中我们介绍了Nancy中的路由&#xff0c;这一篇我们来介绍一下Nancy中的视图引擎。 Nancy中如何返回一个视图(View) 在ASP.NET Mvc中&#xff0c;我们使用ViewResult类来返回一个视图。Nancy中也提供了类似的功能, 在NancyModule类中&#xff0c;Nancy提供了一个ViewRendere…

设计模式之组合模式(Composite 模式)

引入composite模式 在计算机文件系统中&#xff0c;有文件夹的概念&#xff0c;文件夹里面既可以放入文件也可以放入文件夹&#xff0c;但是文件中却不能放入任何东西。文件夹和文件构成了一种递归结构和容器结构。 虽然文件夹和文件是不同的对象&#xff0c;但是他们都可以被放…

HierarchicalBeanFactory接口

HierarchicalBeanFactory 提供父容器的访问功能.至于父容器的设置,需要找ConfigurableBeanFactory的setParentBeanFactory(接口把设置跟获取给拆开了!). HierarchicalBeanFactory源码具体&#xff1a; 1、第一个方法返回本Bean工厂的父工厂。这个方法实现了工厂的分层。 2、第…

C++: C++函数声明的时候后面加const

C: C函数声明的时候后面加const 转自&#xff1a;http://blog.csdn.net/zhangss415/article/details/7998123 非静态成员函数后面加const&#xff08;加到非成员函数或静态成员后面会产生编译错误&#xff09;&#xff0c;表示成员函数隐含传入的this指针为const指针&#xff0…