├── .gitignore ├── README.md ├── demo ├── dataBinder │ ├── hikack.js │ └── index.html ├── hijack │ ├── hijack.js │ └── index.html ├── mediator │ ├── index.html │ └── mediator.js ├── view │ ├── binder.js │ ├── browser.js │ ├── index.html │ └── view.js └── viewModel │ ├── binder.js │ ├── browser.js │ ├── hijack.js │ ├── index.html │ ├── mediator.js │ └── view.js ├── images ├── JSP Model1.png ├── JSP Model2.png ├── data-binding.png ├── gui.png ├── history.png ├── model2_mvc.png ├── mvc.png ├── mvc_model.png ├── mvc_model2.png ├── mvp.png ├── mvvm-demo.gif ├── mvvm.png ├── mvvm_design.png ├── observer.png ├── pub_sub.png ├── view.png ├── vue.png └── ziyi2-mvvm.png ├── mvvm ├── binder.js ├── browser.js ├── hijack.js ├── index.html ├── mediator.js ├── mixin.js ├── mvvm.js └── view.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 基于Vue的简易MVVM实现 2 | 3 | 本文可以帮助你了解什么? 4 | 5 | - 了解MV*架构设计模式的演变历史 6 | - 了解观察者设计模式 7 | - 了解Vue的运行机制 8 | - 了解基于Vue的简易MVVM实现过程 9 | 10 | > 需要注意阅读有风险,因为本文又臭又长...... 11 | 12 | ## MV*设计模式的演变历史 13 | 14 | 我们先来花点时间想想,如果你是一个前端三贱客(Vue、React或者Angular)的开发者,你是有多么频繁的听到“MVVM”这个词,但你真正明白它的含义吗? 15 | 16 | 17 | ### Web前端的演变历史 18 | 19 | 从单纯的HTML静态页面到MVVM模式的成熟应用,自我能感受的Web前端模式粗略的发展如下所示(可能顺序不是很严谨): 20 | 21 | - HTML 22 | - CGI(Common Gateway Interface)、SSI(Server Side Includes) 23 | - JavaScript 24 | - ASP(Active Serve Pages)、JSP(Java Serve Pages) 25 | - JQuery 26 | - Node.js、EJS、JADE 27 | - MVC 28 | - MVP 29 | - MVVM - 包括服务端渲染 30 | 31 | 32 | > CGI和SSI分别是早期服务端的HTML渲染器和模板引擎,学生时期在嵌入式STM32芯片上利用lwIP(小型开源的TCP/IP协议栈)和FreeRTOS(轻量级操作系统)搭建了一个嵌入式Web服务器,如果您感兴趣,可以在中国知网查看我写的小论文[基于嵌入式Web服务器的停车场管理系统](http://www.cnki.com.cn/Article/CJFDTotal-ZJGD201604007.htm),那也是我从嵌入式转向Web前端的转折点,啊哈哈哈,有点扯远了... 33 | 34 | 35 | ### MV*设计模式的起源 36 | 37 | 38 | 起初**计算机科学家(现在的我们是小菜鸡)**在设计GUI(图形用户界面)应用程序的时候,代码是杂乱无章的,通常难以管理和维护。GUI的设计结构一般包括**视图**(View)、**模型**(Model)、**逻辑**(Application Logic、Business Logic以及Sync Logic),例如: 39 | 40 | - 用户在**视图**(View)上的键盘、鼠标等行为执行**应用逻辑**(Application Logic),**应用逻辑**会触发**业务逻辑**(Business Logic),从而变更**模型**(Model) 41 | - **模型**(Model)变更后需要**同步逻辑**(Sync Logic)将变化反馈到**视图**(View)上供用户感知 42 | 43 | 可以发现在GUI中**视图**和**模型**是天然可以进行分层的,杂乱无章的部分主要是**逻辑**。于是我们的程序员们不断的绞尽脑汁在想办法优化GUI设计的**逻辑**,然后就出现了MVC、MVP以及MVVM等设计模式。 44 | 45 | ### MV*设计模式在B/S架构中的思考 46 | 47 | 在B/S架构的应用开发中,MV*设计模式概述并封装了应用程序及其环境中需要关注的地方,尽管JavaScript已经变成一门同构语言,但是在浏览器和服务器之间这些关注点可能不一样: 48 | 49 | - 视图能否跨案例或场景使用? 50 | - 业务逻辑应该放在哪里处理?(在**Model**中还是**Controller**中) 51 | - 应用的状态应该如何持久化和访问? 52 | 53 | 54 | ### MVC(Model-View-Controller) 55 | 56 | 早在上个世纪70年代,美国的施乐公司(Xerox)的工程师研发了Smalltalk编程语言,并且开始用它编写GUI。而在Smalltalk-80版本的时候,一位叫Trygve Reenskaug的工程师设计了MVC的架构模式,极大地降低了GUI的管理难度。 57 | 58 | 59 | ![MVC](https://raw.githubusercontent.com/ziyi2/mvvm/master/images/mvc.png) 60 | 61 | 62 | 如图所示,MVC把GUI分成**View**(视图)、**Model**(模型)、**Controller**(控制 63 | 器)(可热插拔,主要进行**Model**和**View**之间的协作,包括路由、输入预处理等业务逻辑)三个模块: 64 | 65 | - **View**:检测用户的键盘、鼠标等行为,传递调用**Controller**执行应用逻辑。**View**更新需要重新获取**Model**的数据。 66 | - **Controller**:**View**和**Model**之间协作的应用逻辑或业务逻辑处理。 67 | - **Model**:**Model**变更后,通过观察者模式通知**View**更新视图。 68 | 69 | > **Model**的更新通过观察者模式,可以实现多视图共享同一个**Model**。 70 | 71 | 72 | 传统的MVC设计对于Web前端开发而言是一种十分有利的模式,因为**View**是持续性的,并且**View**可以对应不同的**Model**。[Backbone.js](https://backbonejs.org/)就是一种稍微变种的MVC模式实现(和经典MVC较大的区别在于**View**可以直接操作**Model**,因此这个模式不能同构)。这里总结一下MVC设计模式可能带来的好处以及不够完美的地方: 73 | 74 | 优点: 75 | - 职责分离:模块化程度高、**Controller**可替换、可复用性、可扩展性强。 76 | - 多视图更新:使用观察者模式可以做到单**Model**通知多视图实现数据更新。 77 | 78 | 缺点: 79 | - 测试困难:**View**需要UI环境,因此依赖**View**的**Controller**测试相对比较困难(现在Web前端的很多测试框架都已经解决了该问题)。 80 | - 依赖强烈:**View**强依赖**Model**(特定业务场景),因此**View**无法组件化设计。 81 | 82 | 83 | ####服务端MVC 84 | 85 | 经典MVC只用于解决GUI问题,但是随着B/S架构的不断发展,Web服务端也衍生出了MVC设计模式。 86 | 87 | 88 | ##### JSP Model1和JSP Model2的演变过程 89 | 90 | JSP Model1是早期的Java动态Web应用技术,它的结构如下所示: 91 | 92 | ![JSP Model1](https://raw.githubusercontent.com/ziyi2/mvvm/master/images/JSP%20Model1.png) 93 | 94 | 在Model1中,**JSP**同时包含了**Controller**和**View**,而**JavaBean**包含了**Controller**和**Model**,模块的职责相对混乱。在JSP Model1的基础上,Govind Seshadri借鉴了MVC设计模式提出了JSP Model2模式(具体可查看文章[Understanding JavaServer Pages Model 2 architecture](https://www.javaworld.com/article/2076557/understanding-javaserver-pages-model-2-architecture.html)),它的结构如下所示: 95 | 96 | 97 | ![JSP Model2](https://raw.githubusercontent.com/ziyi2/mvvm/master/images/JSP%20Model2.png) 98 | 99 | 100 | 在JSP Model2中,**Controller**、**View**和**Model**分工明确,**Model**的数据变更,通常通过**JavaBean**修改**View**然后进行前端实时渲染,这样从Web前端发起请求到数据回显路线非常明确。不过这里专门询问了相应的后端开发人员,也可能通过**JavaBean**到**Controller**(**Controller**主要识别当前数据对应的JSP)再到**JSP**,因此在服务端MVC中,也可能产生这样的流程**View** -> **Controller** -> **Model** -> **Controller** -> **View**。 101 | 102 | 103 | > 在JSP Model2模式中,没有做到前后端分离,前端的开发大大受到了限制。 104 | 105 | ##### Model2的衍生 106 | 107 | ![Model2](https://raw.githubusercontent.com/ziyi2/mvvm/master/images/model2_mvc.png) 108 | 109 | 对于Web前端开发而言,最直观的感受就是在Node服务中衍生Model2模式(例如结合Express以及EJS模板引擎等)。 110 | 111 | 112 | 113 | ##### 服务端MVC和经典MVC的区别 114 | 115 | 在服务端的MVC模式设计中采用了HTTP协议通信(HTTP是单工无状态协议),因此**View**在不同的请求中都不保持状态(状态的保持需要额外通过Cookie存储),并且经典MVC中**Model**通过观察者模式告知**View**的环节被破坏(例如难以实现服务端推送)。当然在经典MVC中,**Controller**需要监听**View**并对输入做出反应,逻辑会变得很繁重,而在Model2中, **Controller**只关注路由处理等,而**Model**则更多的处理业务逻辑。 116 | 117 | 118 | 119 | ### MVP(Model-View-Presenter) 120 | 121 | 在上个世纪90年代,IBM旗下的子公司Taligent在用C/C++开发一个叫CommonPoint的图形界面应用系统的时候提出了MVP的概念。 122 | 123 | 124 | ![MVP](https://raw.githubusercontent.com/ziyi2/mvvm/master/images/mvp.png) 125 | 126 | 127 | 如上图所示,MVP是MVC的模式的一种改良,打破了**View**对于**Model**的依赖,其余的依赖关系和MVC保持不变。 128 | 129 | 130 | - **Passive View**:**View**不再处理同步逻辑,对**Presenter**提供接口调用。由于不再依赖**Model**,可以让**View**从特定的业务场景中抽离,完全可以做到组件化。 131 | - **Presenter**(**Supervising Controller**):和经典MVC的**Controller**相比,任务更加繁重,不仅要处理应用业务逻辑,还要处理同步逻辑(高层次复杂的UI操作)。 132 | - **Model**:**Model**变更后,通过观察者模式通知**Presenter**,如果有视图更新,**Presenter**又可能调用**View**的接口更新视图。 133 | 134 | 135 | MVP模式可能产生的优缺点如下: 136 | 137 | - **Presenter**便于测试、**View**可组件化设计 138 | - **Presenter**厚、维护困难 139 | 140 | 141 | ### MVVM(Model-View-ViewModel) 142 | 143 | 144 | 145 | ![MVVM](https://raw.githubusercontent.com/ziyi2/mvvm/master/images/mvvm.png) 146 | 147 | 148 | 如上图所示:MVVM模式是在MVP模式的基础上进行了改良,将**Presenter**改良成**ViewModel**(抽象视图): 149 | 150 | - **ViewModel**:内部集成了**Binder**(Data-binding Engine,数据绑定引擎),在MVP中派发器**View**或**Model**的更新都需要通过**Presenter**手动设置,而**Binder**则会实现**View**和**Model**的双向绑定,从而实现**View**或**Model**的自动更新。 151 | - **View**:可组件化,例如目前各种流行的UI组件框架,**View**的变化会通过**Binder**自动更新相应的**Model**。 152 | - **Model**:**Model**的变化会被**Binder**监听(仍然是通过观察者模式),一旦监听到变化,**Binder**就会自动实现视图的更新。 153 | 154 | 可以发现,MVVM在MVP的基础上带来了大量的好处,例如: 155 | 156 | - 提升了可维护性,解决了MVP大量的手动同步的问题,提供双向绑定机制。 157 | - 简化了测试,同步逻辑是交由**Binder**处理,**View**跟着**Model**同时变更,所以只需要保证**Model**的正确性,**View**就正确。 158 | 159 | 当然也带来了一些额外的问题: 160 | 161 | - 产生性能问题,对于简单的应用会造成额外的性能消耗。 162 | - 对于复杂的应用,视图状态较多,视图状态的维护成本增加,**ViewModel**构建和维护成本高。 163 | 164 | 165 | 166 | 167 | 对前端开发而言MVVM是非常好的一种设计模式。在浏览器中,路由层可以将控制权交由适当的**ViewModel**,后者又可以更新并响应持续的View,并且通过一些小修改MVVM模式可以很好的运行在服务器端,其中的原因就在于**Model**与**View**已经完全没有了依赖关系(通过View与Model的去耦合,可以允许短暂**View**与持续**View**的并存),这允许**View**经由给定的**ViewModel**进行渲染。 168 | 169 | 170 | > 目前流行的框架Vue、React以及Angular都是MVVM设计模式的一种实现,并且都可以实现服务端渲染。需要注意目前的Web前端开发和传统Model2需要模板引擎渲染的方式不同,通过Node启动服务进行页面渲染,并且通过代理的方式转发请求后端数据,完全可以从后端的苦海中脱离,这样一来也可以大大的解放Web前端的生产力。 171 | 172 | ### 观察者模式和发布/订阅模式 173 | 174 | 175 | #### 观察者模式 176 | 177 | 观察者模式是使用一个subject目标对象维持一系列依赖于它的observer观察者对象,将有关状态的任何变更自动通知给这一系列观察者对象。当subject目标对象需要告诉观察者发生了什么事情时,它会向观察者对象们广播一个通知。 178 | 179 | 180 | ![Observer](https://raw.githubusercontent.com/ziyi2/mvvm/master/images/observer.png) 181 | 182 | 如上图所示:一个或多个观察者对目标对象的状态感兴趣时,可以将自己依附在目标对象上以便注册感兴趣的目标对象的状态变化,目标对象的状态发生改变就会发送一个通知消息,调用每个观察者的更新方法。如果观察者对目标对象的状态不感兴趣,也可以将自己从中分离。 183 | 184 | 185 | #### 发布/订阅模式 186 | 187 | 发布/订阅模式使用一个事件通道,这个通道介于订阅者和发布者之间,该设计模式允许代码定义应用程序的特定事件,这些事件可以传递自定义参数,自定义参数包含订阅者需要的信息,采用事件通道可以避免发布者和订阅者之间产生依赖关系。 188 | 189 | ![Pub/Sub](https://raw.githubusercontent.com/ziyi2/mvvm/master/images/pub_sub.png) 190 | 191 | > 学生时期很长一段时间内用过Redis的发布/订阅机制,具体可查看[zigbee-door/zigbee-tcp](https://github.com/zigbee-door/zigbee-tcp),但是惭愧的是没有好好阅读过这一块的源码。 192 | 193 | #### 两者的区别 194 | 195 | 观察者模式:允许观察者实例对象(订阅者)执行适当的事件处理程序来注册和接收目标实例对象(发布者)发出的通知(即在观察者实例对象上注册`update`方法),使订阅者和发布者之间产生了依赖关系,且没有事件通道。不存在封装约束的单一对象,目标对象和观察者对象必须合作才能维持约束。 观察者对象向订阅它们的对象发布其感兴趣的事件。通信只能是单向的。 196 | 197 | 发布/订阅模式:单一目标通常有很多观察者,有时一个目标的观察者是另一个观察者的目标。通信可以实现双向。该模式存在不稳定性,发布者无法感知订阅者的状态。 198 | 199 | 200 | ## Vue的运行机制简述 201 | 202 | ![Vue](https://raw.githubusercontent.com/ziyi2/mvvm/master/images/vue.png) 203 | 204 | 205 | 这里简单的描述一下Vue的运行机制(需要注意分析的是 Runtime + Compiler 的 Vue.js)。 206 | 207 | 208 | ### 初始化流程 209 | 210 | - 创建Vue实例对象 211 | - `init`过程会初始化生命周期,初始化事件中心,初始化渲染、执行`beforeCreate`周期函数、初始化 `data`、`props`、`computed`、`watcher`、执行`created`周期函数等。 212 | - 初始化后,调用`$mount`方法对Vue实例进行挂载(挂载的核心过程包括**模板编译**、**渲染**以及**更新**三个过程)。 213 | - 如果没有在Vue实例上定义`render`方法而是定义了`template`,那么需要经历编译阶段。需要先将`template` 字符串编译成 `render function`,`template` 字符串编译步骤如下 : 214 | - `parse`正则解析`template`字符串形成AST(抽象语法树,是源代码的抽象语法结构的树状表现形式) 215 | - `optimize`标记静态节点跳过diff算法(diff算法是逐层进行比对,只有同层级的节点进行比对,因此时间的复杂度只有O(n)。如果对于时间复杂度不是很清晰的,可以查看我写的文章[ziyi2/algorithms-javascript/渐进记号](https://github.com/ziyi2/algorithms-javascript/blob/master/doc/function-growth/asymptotic-symbol.md)) 216 | - `generate`将AST转化成`render function`字符串 217 | - 编译成`render function` 后,调用`$mount`的`mountComponent`方法,先执行`beforeMount`钩子函数,然后核心是实例化一个渲染`Watcher`,在它的回调函数(初始化的时候执行,以及组件实例中监测到数据发生变化时执行)中调用`updateComponent`方法(此方法调用`render`方法生成虚拟Node,最终调用`update`方法更新DOM)。 218 | - 调用`render`方法将`render function`渲染成虚拟的Node(真正的 DOM 元素是非常庞大的,因为浏览器的标准就把 DOM 设计的非常复杂。如果频繁的去做 DOM 更新,会产生一定的性能问题,而 Virtual DOM 就是用一个原生的 JavaScript 对象去描述一个 DOM 节点,所以它比创建一个 DOM 的代价要小很多,而且修改属性也很轻松,还可以做到跨平台兼容),`render`方法的第一个参数是`createElement`(或者说是`h`函数),这个在官方文档也有说明。 219 | - 生成虚拟DOM树后,需要将虚拟DOM树转化成真实的DOM节点,此时需要调用`update`方法,`update`方法又会调用`pacth`方法把虚拟DOM转换成真正的DOM节点。需要注意在图中忽略了新建真实DOM的情况(如果没有旧的虚拟Node,那么可以直接通过`createElm`创建真实DOM节点),这里重点分析在已有虚拟Node的情况下,会通过`sameVnode`判断当前需要更新的Node节点是否和旧的Node节点相同(例如我们设置的`key`属性发生了变化,那么节点显然不同),如果节点不同那么将旧节点采用新节点替换即可,如果相同且存在子节点,需要调用`patchVNode `方法执行diff算法更新DOM,从而提升DOM操作的性能。 220 | 221 | 222 | > 需要注意在初始化阶段,没有详细描述数据的响应式过程,这个在响应式流程里做说明。 223 | 224 | ### 响应式流程 225 | 226 | 227 | - 在`init`的时候会利用`Object.defineProperty`方法(不兼容IE8)监听Vue实例的响应式数据的变化从而实现数据劫持能力(利用了JavaScript对象的访问器属性`get`和`set`,在未来的Vue3中会使用ES6的`Proxy`来优化响应式原理)。在初始化流程中的编译阶段,当`render function`被渲染的时候,会读取Vue实例中和视图相关的响应式数据,此时会触发`getter`函数进行**依赖收集**(将观察者`Watcher`对象存放到当前闭包的订阅者`Dep`的`subs`中),此时的数据劫持功能和观察者模式就实现了一个MVVM模式中的**Binder**,之后就是正常的渲染和更新流程。 228 | - 当数据发生变化或者视图导致的数据发生了变化时,会触发数据劫持的`setter`函数,`setter`会通知初始化**依赖收集**中的`Dep`中的和视图相应的`Watcher`,告知需要重新渲染视图,`Wather`就会再次通过`update`方法来更新视图。 229 | 230 | 231 | > 可以发现只要视图中添加监听事件,自动变更对应的数据变化时,就可以实现数据和视图的双向绑定了。 232 | 233 | 234 | ## 基于Vue机制的简易MVVM实现 235 | 236 | 了解了MV*设计模式、观察者模式以及Vue运行机制之后,可能对于整个MVVM模式有了一个感性的认知,因此可以来手动实现一下,这里实现过程包括如下几个步骤: 237 | 238 | - MVVM的实现演示 239 | - MVVM的流程设计 240 | - [中介者模式](https://github.com/ziyi2/mvvm/tree/master/demo/mediator)的实现 241 | - [数据劫持](https://github.com/ziyi2/mvvm/tree/master/demo/hijack)的实现 242 | - [数据双向绑定](https://github.com/ziyi2/mvvm/tree/master/demo/dataBinder)的实现 243 | - [简易视图指令的编译过程](https://github.com/ziyi2/mvvm/tree/master/demo/view)的实现 244 | - [ViewModel](https://github.com/ziyi2/mvvm/tree/master/demo/viewModel)的实现 245 | - [MVVM](https://github.com/ziyi2/mvvm/tree/master/mvvm)的实现 246 | 247 | 248 | ### MVVM的实现演示 249 | 250 | 251 | MVVM示例的使用如下所示,包括`browser.js`(View视图的更新)、`mediator.js`(中介者)、`binder.js`(MVVM的数据绑定引擎)、`view.js`(视图)、`hijack.js`(数据劫持)以及`mvvm.js`(MVVM实例)。本示例相关的代码可查看github的[ziyi2/mvvm](https://github.com/ziyi2/mvvm): 252 | 253 | ``` javascript 254 |
255 | 256 |
{{ input.message }}
257 |
258 |
{{ text }}
259 |
260 |
261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 287 | ``` 288 | 289 | 290 | 291 | ![MVVM Demo](https://raw.githubusercontent.com/ziyi2/mvvm/master/images/mvvm-demo.gif) 292 | 293 | 294 | 295 | 296 | 297 | ### MVVM的流程设计 298 | 299 | 300 | ![Mvvm](https://raw.githubusercontent.com/ziyi2/mvvm/master/images/ziyi2-mvvm.png) 301 | 302 | 这里简单的描述一下MVVM实现的运行机制。 303 | 304 | #### 初始化流程 305 | 306 | - 创建MVVM实例对象,初始化实例对象的`options`参数 307 | - `proxyData`将MVVM实例对象的`data`数据代理到MVVM实例对象上 308 | - `Hijack`类实现数据劫持功能(对MVVM实例跟视图对应的响应式数据进行监听,这里和Vue运行机制不同,干掉了`getter`依赖搜集功能) 309 | - 解析视图指令,对MVVM实例与视图关联的DOM元素转化成文档碎片并进行绑定指令解析(`b-value`、`b-on-input`、`b-html`等,其实是Vue编译的超级简化版), 310 | - 添加数据订阅和用户监听事件,将视图指令对应的数据挂载到**Binder**数据绑定引擎上(数据变化时通过Pub/Sub模式通知**Binder**绑定器更新视图) 311 | - 使用Pub/Sub模式代替Vue中的Observer模式 312 | - **Binder**采用了命令模式解析视图指令,调用`update`方法对View解析绑定指令后的文档碎片进行更新视图处理 313 | - `Browser`采用了外观模式对浏览器进行了简单的兼容性处理 314 | 315 | 316 | #### 响应式流程 317 | 318 | 319 | - 监听用户输入事件,对用户的输入事件进行监听 320 | - 调用MVVM实例对象的数据设置方法更新数据 321 | - 数据劫持触发`setter`方法 322 | - 通过发布机制发布数据变化 323 | - 订阅器接收数据变更通知,更新数据对应的视图 324 | 325 | 326 | ### 中介者模式的实现 327 | 328 | 最简单的中介者模式只需要实现发布、订阅和取消订阅的功能。发布和订阅之间通过事件通道(channels)进行信息传递,可以避免观察者模式中产生依赖的情况。中介者模式的代码如下: 329 | 330 | ``` javascript 331 | class Mediator { 332 | constructor() { 333 | this.channels = {} 334 | this.uid = 0 335 | } 336 | 337 | /** 338 | * @Desc: 订阅频道 339 | * @Parm: {String} channel 频道 340 | * {Function} cb 回调函数 341 | */ 342 | sub(channel, cb) { 343 | let { channels } = this 344 | if(!channels[channel]) channels[channel] = [] 345 | this.uid ++ 346 | channels[channel].push({ 347 | context: this, 348 | uid: this.uid, 349 | cb 350 | }) 351 | console.info('[mediator][sub] -> this.channels: ', this.channels) 352 | return this.uid 353 | } 354 | 355 | /** 356 | * @Desc: 发布频道 357 | * @Parm: {String} channel 频道 358 | * {Any} data 数据 359 | */ 360 | pub(channel, data) { 361 | console.info('[mediator][pub] -> chanel: ', channel) 362 | let ch = this.channels[channel] 363 | if(!ch) return false 364 | let len = ch.length 365 | // 后订阅先触发 366 | while(len --) { 367 | ch[len].cb.call(ch[len].context, data) 368 | } 369 | return this 370 | } 371 | 372 | /** 373 | * @Desc: 取消订阅 374 | * @Parm: {String} uid 订阅标识 375 | */ 376 | cancel(uid) { 377 | let { channels } = this 378 | for(let channel of Object.keys(channels)) { 379 | let ch = channels[channel] 380 | if(ch.length === 1 && ch[0].uid === uid) { 381 | delete channels[channel] 382 | console.info('[mediator][cancel][delete] -> chanel: ', channel) 383 | console.info('[mediator][cancel] -> chanels: ', channels) 384 | return 385 | } 386 | for(let i=0,len=ch.length; i chanel: ', channel) 390 | console.info('[mediator][cancel] -> chanels: ', channels) 391 | return 392 | } 393 | } 394 | } 395 | } 396 | } 397 | ``` 398 | 399 | 在每一个MVVM实例中,都需要实例化一个中介者实例对象,中介者实例对象的使用方法如下: 400 | 401 | ``` javascript 402 | let mediator = new Mediator() 403 | // 订阅channel1 404 | let channel1First = mediator.sub('channel1', (data) => { 405 | console.info('[mediator][channel1First][callback] -> data', data) 406 | }) 407 | // 再次订阅channel1 408 | let channel1Second = mediator.sub('channel1', (data) => { 409 | console.info('[mediator][channel1Second][callback] -> data', data) 410 | }) 411 | // 订阅channel2 412 | let channel2 = mediator.sub('channel2', (data) => { 413 | console.info('[mediator][channel2][callback] -> data', data) 414 | }) 415 | // 发布(广播)channel1,此时订阅channel1的两个回调函数会连续执行 416 | mediator.pub('channel1', { name: 'ziyi1' }) 417 | // 发布(广播)channel2,此时订阅channel2的回调函数执行 418 | mediator.pub('channel2', { name: 'ziyi2' }) 419 | // 取消channel1标识为channel1Second的订阅 420 | mediator.cancel(channel1Second) 421 | // 此时只会执行channel1中标识为channel1First的回调函数 422 | mediator.pub('channel1', { name: 'ziyi1' }) 423 | ``` 424 | 425 | 426 | ### 数据劫持的实现 427 | 428 | #### 对象的属性 429 | 430 | 对象的属性可分为数据属性(特性包括`[[Value]]`、`[[Writable]]`、`[[Enumerable]]`、`[[Configurable]]`)和存储器/访问器属性(特性包括`[[ Get ]]`、`[[ Set ]]`、`[[Enumerable]]`、`[[Configurable]]`),对象的属性只能是数据属性或访问器属性的其中一种,这些属性的含义: 431 | 432 | - `[[Configurable]]`: 表示能否通过 `delete` 删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为访问器属性。 433 | - `[[Enumerable]]`: 对象属性的可枚举性。 434 | - `[[Value]]`: 属性的值,读取属性值的时候,从这个位置读;写入属性值的时候,把新值保存在这个位置。这个特性的默认值为 `undefined`。 435 | - `[[Writable]]`: 表示能否修改属性的值。 436 | - `[[ Get ]]`: 在读取属性时调用的函数。默认值为 `undefined`。 437 | - `[[ Set ]]`: 在写入属性时调用的函数。默认值为 `undefined`。 438 | 439 | > 数据劫持就是使用了`[[ Get ]]`和`[[ Set ]]`的特性,在访问对象的属性和写入对象的属性时能够自动触发属性特性的调用函数,从而做到监听数据变化的目的。 440 | 441 | 对象的属性可以通过ES5的设置特性方法`Object.defineProperty(data, key, descriptor)`改变属性的特性,其中`descriptor`传入的就是以上所描述的特性集合。 442 | 443 | #### 数据劫持 444 | 445 | ``` javascript 446 | let hijack = (data) => { 447 | if(typeof data !== 'object') return 448 | for(let key of Object.keys(data)) { 449 | let val = data[key] 450 | Object.defineProperty(data, key, { 451 | enumerable: true, 452 | configurable: false, 453 | get() { 454 | console.info('[hijack][get] -> val: ', val) 455 | // 和执行 return data[key] 有什么区别 ? 456 | return val 457 | }, 458 | set(newVal) { 459 | if(newVal === val) return 460 | console.info('[hijack][set] -> newVal: ', newVal) 461 | val = newVal 462 | // 如果新值是object, 则对其属性劫持 463 | hijack(newVal) 464 | } 465 | }) 466 | } 467 | } 468 | 469 | let person = { name: 'ziyi2', age: 1 } 470 | hijack(person) 471 | // [hijack][get] -> val: ziyi2 472 | person.name 473 | // [hijack][get] -> val: 1 474 | person.age 475 | // [hijack][set] -> newVal: ziyi 476 | person.name = 'ziyi' 477 | 478 | // 属性类型变化劫持 479 | // [hijack][get] -> val: { familyName:"ziyi2", givenName:"xiankang" } 480 | person.name = { familyName: 'zhu', givenName: 'xiankang' } 481 | // [hijack][get] -> val: ziyi2 482 | person.name.familyName = 'ziyi2' 483 | 484 | // 数据属性 485 | let job = { type: 'javascript' } 486 | console.info(Object.getOwnPropertyDescriptor(job, "type")) 487 | // 访问器属性 488 | console.info(Object.getOwnPropertyDescriptor(person, "name")) 489 | ``` 490 | 491 | 492 | 注意Vue3.0将不产用`Object.defineProperty`方式进行数据监听,原因在于 493 | - 无法监听数组的变化(目前的数组监听都基于对原生数组的一些方法进行`hack`,所以如果要使数组响应化,需要注意使用Vue官方推荐的一些数组方法) 494 | - 无法深层次监听对象属性 495 | 496 | 在Vue3.0中将产用`Proxy`解决以上痛点问题,当然会产生浏览器兼容性问题(例如万恶的IE,具体可查看[Can I use proxy](https://caniuse.com/#search=proxy))。 497 | 498 | > 需要注意是的在`hijack`中只进行了一层属性的遍历,如果要做到对象深层次属性的监听,需要继续对`data[key]`进行`hijack`操作,从而可以达到属性的深层次遍历监听,具体可查看[mvvm/mvvm/hijack.js](https://github.com/ziyi2/mvvm/blob/master/mvvm/hijack.js), 499 | 500 | 501 | ### 数据双向绑定的实现 502 | 503 | 504 | ![data-binding](https://raw.githubusercontent.com/ziyi2/mvvm/master/images/data-binding.png) 505 | 506 | 如上图所示,数据双向绑定主要包括数据的变化引起视图的变化(**Model** -> 监听数据变化 -> **View**)、视图的变化又改变数据(**View** -> 用户输入监听事件 -> **Model**),从而实现数据和视图之间的强联系。 507 | 508 | 在实现了数据监听的基础上,加上用户输入事件以及视图更新,就可以简单实现数据的双向绑定(其实就是一个最简单的**Binder**,只是这里的代码耦合严重): 509 | 510 | 511 | ``` htmlbars 512 | 513 |
514 | ``` 515 | 516 | ``` javascript 517 | // 监听数据变化 518 | function hijack(data) { 519 | if(typeof data !== 'object') return 520 | for(let key of Object.keys(data)) { 521 | let val = data[key] 522 | Object.defineProperty(data, key, { 523 | enumerable: true, 524 | configurable: false, 525 | get() { 526 | console.log('[hijack][get] -> val: ', val) 527 | // 和执行 return data[key] 有什么区别 ? 528 | return val 529 | }, 530 | set(newVal) { 531 | if(newVal === val) return 532 | console.log('[hijack][set] -> newVal: ', newVal) 533 | val = newVal 534 | 535 | // 更新所有和data.input数据相关联的视图 536 | input.value = newVal 537 | div.innerHTML = newVal 538 | 539 | // 如果新值是object, 则对其属性劫持 540 | hijack(newVal) 541 | } 542 | }) 543 | } 544 | } 545 | 546 | let input = document.getElementById('input') 547 | let div = document.getElementById('div') 548 | 549 | // model 550 | let data = { input: '' } 551 | 552 | // 数据劫持 553 | hijack(data) 554 | 555 | // model -> view 556 | data.input = '11111112221' 557 | 558 | // view -> model 559 | input.oninput = function(e) { 560 | // model -> view 561 | data.input = e.target.value 562 | } 563 | ``` 564 | 565 | 566 | > [数据双向绑定的demo源码](https://github.com/ziyi2/mvvm/tree/master/demo/dataBinder)。 567 | 568 | ### 简易视图指令的编译过程实现 569 | 570 | 在MVVM的实现演示中,可以发现使用了`b-value`、`b-text`、`b-on-input`、`b-html`等绑定属性(这些属性在该MVVM示例中自行定义的,并不是html标签原生的属性,类似于vue的`v-html`、`v-model`、`v-text`指令等),这些指令只是方便用户进行Model和View的同步绑定操作而创建的,需要MVVM实例对象去识别这些指令并重新渲染出最终需要的DOM元素,例如 571 | 572 | ``` javascript 573 |
574 | 575 |
576 | ``` 577 | 578 | 最终需要转化成真实的DOM 579 | 580 | ``` javascript 581 |
582 | 583 |
584 | ``` 585 | 586 | 那么实现以上指令解析的步骤主要如下: 587 | 588 | - 获取对应的`#app`元素 589 | - 转换成文档碎片(从DOM中移出`#app`下的所有子元素) 590 | - 识别出文档碎片中的绑定指令并重新修改该指令对应的DOM元素 591 | - 处理完文档碎片后重新渲染`#app`元素 592 | 593 | HTML代码如下: 594 | 595 | ``` htmlbars 596 |
597 | 598 | 599 | 600 |
601 | 602 | 603 | 604 | 605 | ``` 606 | 607 | 首先来看示例的使用 608 | 609 | ``` javascript 610 | // 模型 611 | let model = { 612 | message: 'Hello World', 613 | 614 | getData(key) { 615 | let val = this 616 | let keys = key.split('.') 617 | for(let i=0, len=keys.length; i 730 | // this.model.getData('a.b') = 111 731 | // 从而可以将input元素更新为 732 | browser.val(node, this.model.getData(key)) 733 | } 734 | } 735 | })(window, browser) 736 | ``` 737 | 738 | 在`browser.js`中使用外观模式对浏览器原生的事件以及DOM操作进行了再封装,从而可以做到浏览器的兼容处理等,这里只对`b-value`需要的DOM操作进行了封装处理,方便阅读 739 | 740 | ```javascript 741 | let browser = { 742 | /** 743 | * @Desc: Node节点的value处理 744 | * @Parm: {Object} node Node节点 745 | * {String} val 节点的值 746 | */ 747 | val(node, val) { 748 | // 将b-value转化成value,需要注意的是解析完后在view.js中会将b-value属性移除 749 | node.value = val || '' 750 | console.info(`[browser][val] -> node: `, node) 751 | console.info(`[browser][val] -> val: `, val) 752 | } 753 | } 754 | ``` 755 | 756 | > 至此MVVM示例中简化的**Model** -> **ViewModel** (未实现数据监听功能)-> **View**路走通,可以查看[视图绑定指令的解析的demo](https://github.com/ziyi2/mvvm/tree/master/demo/view)。 757 | 758 | 759 | ### ViewModel的实现 760 | 761 | **ViewModel**(内部绑定器**Binder**)的作用不仅仅是实现了**Model**到**View**的自动同步(Sync Logic)逻辑(以上视图绑定指令的解析的实现只是实现了一个视图的绑定指令初始化,一旦**Model**变化,视图要更新的功能并没有实现),还实现了**View**到**Model**的自动同步逻辑,从而最终实现了数据的双向绑定。 762 | 763 | ![MVVM](https://raw.githubusercontent.com/ziyi2/mvvm/master/images/mvvm.png) 764 | 765 | 766 | 因此只要在视图绑定指令的解析的基础上增加**Model**的数据监听功能(数据变化更新视图)和**View**视图的`input`事件监听功能(监听视图从而更新相应的**Model**数据,注意**Model**的变化又会因为数据监听从而更新和**Model**相关的视图)就可以实现**View**和**Model**的双向绑定。同时需要注意的是,数据变化更新视图的过程需要使用发布/订阅模式,如果对流程不清晰,可以继续回看MVVM的结构设计。 767 | 768 | 769 | 在**简易视图指令的编译过程实现**的基础上进行修改,首先是HTML代码 770 | 771 | ``` htmlbars 772 |
773 | 774 | 775 | 776 |
777 | 778 | 779 | 780 | 781 | 782 | 783 | 784 | 785 | ``` 786 | 787 | >`mediator.js`不再叙述,具体回看**中介者模式的实现**,`view.js`和`browser.js`也不再叙述,具体回看**简易视图指令的编译过程实现**。 788 | 789 | 790 | 示例的使用: 791 | 792 | ``` javascript 793 | // 模型 794 | let model = { 795 | message: 'Hello World', 796 | setData(key, newVal) { 797 | let val = this 798 | let keys = key.split('.') 799 | for(let i=0, len=keys.length; i val: ', val) 807 | }, 808 | getData(key) { 809 | let val = this 810 | let keys = key.split('.') 811 | for(let i=0, len=keys.length; i view (会触发数据劫持的set函数,从而发布model变化,在binder中订阅model数据变化后会更新视图) 825 | model.message = 'Hello Ziyi233333222' 826 | ``` 827 | 828 | 首先看下数据劫持,在** 数据劫持的实现**的基础上,增加了中介者对象的发布数据变化功能(在抽象视图的**Binder**中会订阅这个数据变化) 829 | 830 | ``` javascript 831 | var hijack = (function() { 832 | 833 | class Hijack { 834 | /** 835 | * @Desc: 数据劫持构造函数 836 | * @Parm: {Object} model 数据 837 | * {Object} mediator 发布订阅对象 838 | */ 839 | constructor(model, mediator) { 840 | this.model = model 841 | this.mediator = mediator 842 | } 843 | 844 | /** 845 | * @Desc: model数据劫持 846 | * @Parm: 847 | * 848 | */ 849 | hijackData() { 850 | let { model, mediator } = this 851 | for(let key of Object.keys(model)) { 852 | let val = model[key] 853 | Object.defineProperty(model, key, { 854 | enumerable: true, 855 | configurable: false, 856 | get() { 857 | return val 858 | }, 859 | set(newVal) { 860 | if(newVal === val) return 861 | val = newVal 862 | // 发布数据劫持的数据变化信息 863 | console.log('[mediator][pub] -> key: ', key) 864 | // 重点注意这里的通道,在最后的MVVM示例中和这里的实现不一样 865 | mediator.pub(key) 866 | } 867 | }) 868 | } 869 | } 870 | } 871 | 872 | return (model, mediator) => { 873 | if(!model || typeof model !== 'object') return 874 | new Hijack(model, mediator).hijackData() 875 | } 876 | })() 877 | ``` 878 | 879 | 接着重点来看`binder.js`中的实现 880 | 881 | ``` javascript 882 | (function(window, browser){ 883 | window.binder = { 884 | /** 885 | * @Desc: 判断是否是绑定属性 886 | * @Parm: {String} attr Node节点的属性 887 | */ 888 | is(attr) { 889 | return attr.includes('b-') 890 | }, 891 | 892 | /** 893 | * @Desc: 解析绑定指令 894 | * @Parm: {Object} attr html属性对象 895 | * {Object} node Node节点 896 | * {Object} model 数据 897 | * {Object} mediator 中介者 898 | */ 899 | parse(node, attr, model, mediator) { 900 | if(!this.is(attr.name)) return 901 | this.model = model 902 | this.mediator = mediator 903 | let bindValue = attr.value, 904 | bindType = attr.name.substring(2) 905 | // 绑定视图指令处理 906 | this[bindType](node, bindValue.trim()) 907 | }, 908 | 909 | /** 910 | * @Desc: 值绑定处理(b-value) 911 | * @Parm: {Object} node Node节点 912 | * {String} key model的属性 913 | */ 914 | value(node, key) { 915 | this.update(node, key) 916 | // View -> ViewModel -> Model 917 | // 监听用户的输入事件 918 | browser.event.add(node, 'input', (e) => { 919 | // 更新model 920 | let newVal = browser.event.target(e).value 921 | // 设置对应的model数据(因为进行了hijack(model)) 922 | // 因为进行了hijack(model),对model进行了变化监听,因此会触发hijack中的set,从而触发set中的mediator.pub 923 | this.model.setData(key, newVal) 924 | }) 925 | 926 | // 一旦model变化,数据劫持会mediator.pub变化的数据 927 | // 订阅数据变化更新视图(闭包) 928 | this.mediator.sub(key, () => { 929 | console.log('[mediator][sub] -> key: ', key) 930 | console.log('[mediator][sub] -> node: ', node) 931 | this.update(node, key) 932 | }) 933 | }, 934 | 935 | /** 936 | * @Desc: 值绑定更新(b-value) 937 | * @Parm: {Object} node Node节点 938 | * {String} key model的属性 939 | */ 940 | update(node, key) { 941 | browser.val(node, this.model.getData(key)) 942 | } 943 | } 944 | })(window, browser) 945 | ``` 946 | 947 | > 最终实现了具有**viewModel**的MVVM简单实例,具体查看[ViewModel的实现的demo](https://github.com/ziyi2/mvvm/tree/master/demo/viewModel)。 948 | 949 | 950 | ### MVVM的实现 951 | 952 | 在**ViewModel的实现**的基础上: 953 | 954 | - 新增了`b-text`、`b-html`、`b-on-*`(事件监听)指令的解析 955 | - 代码封装更优雅,新增了MVVM类用于约束管理之前示例中零散的实例对象(建造者模式) 956 | - `hijack.js`实现了对**Model**数据的深层次监听 957 | - `hijack.js`中的发布和订阅的`channel`采用HTML属性中绑定的指令对应的值进行处理(例如`b-value="a.b.c.d"`,那么`channel`就是`'a.b.c.d'`,这里是将Vue的观察者模式改成中介者模式后的一种尝试,只是一种实现方式,当然采用观察者模式关联性更强,而采用中介者模式会更解耦)。 958 | - `browser.js`中新增了事件监听的兼容处理、`b-html`和`b-text`等指令的DOM操作api等 959 | 960 | 961 | 由于篇幅太长了,这里就不过多做说明了,感兴趣的童鞋可以直接查看[ziyi2/mvvm](https://github.com/ziyi2/mvvm/tree/master/mvvm),需要注意该示例中还存在一定的缺陷,例如**Model**的属性是一个对象,且该对象被重写时,发布和订阅维护的`channels`中未将旧的属性监听的`channel`移除处理。 962 | 963 | 964 | ## 设计模式 965 | 966 | 在以上MVVM示例的实现中,我也是抱着学习的心态用到了以下[设计模式](https://ziyi2.github.io/2018/07/15/js%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F.html#more),如果对这些设计模式不了解,则可以前往查看示例代码。 967 | 968 | 969 | >- [外观模式](https://github.com/ziyi2/js/blob/master/JS%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F.md#facade%E5%A4%96%E8%A7%82%E6%A8%A1%E5%BC%8F) 970 | >- [构造器模式](https://github.com/ziyi2/js/blob/master/JS%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F.md#constructor%E6%9E%84%E9%80%A0%E5%99%A8%E6%A8%A1%E5%BC%8F) 971 | >- [模块模式](https://github.com/ziyi2/js/blob/master/JS%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F.md#module%E6%A8%A1%E5%9D%97%E6%A8%A1%E5%BC%8F) 972 | >- [中介者模式(发布/订阅模式)](https://github.com/ziyi2/js/blob/master/JS%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F.md#mediator%E4%B8%AD%E4%BB%8B%E8%80%85%E6%A8%A1%E5%BC%8F) 973 | >- [原型模式](https://github.com/ziyi2/js/blob/master/JS%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F.md#prototype%E5%8E%9F%E5%9E%8B%E6%A8%A1%E5%BC%8F) 974 | >- [命令模式](https://github.com/ziyi2/js/blob/master/JS%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F.md#command%E5%91%BD%E4%BB%A4%E6%A8%A1%E5%BC%8F) 975 | >- [建造者模式](https://github.com/ziyi2/js/blob/master/JS%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F.md#%E5%BB%BA%E9%80%A0%E8%80%85%E6%A8%A1%E5%BC%8F) 976 | 977 | 978 | 979 | ## 参考资源 980 | 981 | - [GUI Architectures](https://martinfowler.com/eaaDev/uiArchs.html) - 多种架构在UI设计中追溯思想的知识史 982 | - [Understanding JavaServer Pages Model 2 architecture](https://www.javaworld.com/article/2076557/understanding-javaserver-pages-model-2-architecture.html) - 讲述JSP Model1参考MVC进阶JSP Model2的故事 983 | - [Scaling Isomorphic Javascript Code](https://blog.nodejitsu.com/scaling-isomorphic-javascript-code/) - 单页应用的首屏速度与SEO优化问题启示录 984 | - [界面之下:还原真实的MV*模式 ](https://github.com/livoras/blog/issues/11) - 了解MV*模式 985 | - [DMQ/mvvm](https://github.com/DMQ/mvvm) - 剖析vue实现原理,自己动手实现mvvm 986 | 987 | -------------------------------------------------------------------------------- /demo/dataBinder/hikack.js: -------------------------------------------------------------------------------- 1 | function hijack(data) { 2 | if(typeof data !== 'object') return 3 | for(let key of Object.keys(data)) { 4 | let val = data[key] 5 | Object.defineProperty(data, key, { 6 | enumerable: true, 7 | configurable: false, 8 | get() { 9 | console.log('[hijack][get] -> val: ', val) 10 | // 和执行 return data[key] 有什么区别 ? 11 | return val 12 | }, 13 | set(newVal) { 14 | if(newVal === val) return 15 | console.log('[hijack][set] -> newVal: ', newVal) 16 | val = newVal 17 | 18 | // 更新所有和data.input数据相关联的视图 19 | input.value = newVal 20 | div.innerHTML = newVal 21 | 22 | // 如果新值是object, 则对其属性劫持 23 | hijack(newVal) 24 | } 25 | }) 26 | } 27 | } -------------------------------------------------------------------------------- /demo/dataBinder/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 |

数据双向绑定演示

11 | 12 |
13 | 14 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /demo/hijack/hijack.js: -------------------------------------------------------------------------------- 1 | 2 | let hijack = (data) => { 3 | if(typeof data !== 'object') return 4 | for(let key of Object.keys(data)) { 5 | let val = data[key] 6 | Object.defineProperty(data, key, { 7 | enumerable: true, 8 | configurable: false, 9 | get() { 10 | console.log('[hijack][get] -> val: ', val) 11 | // 和执行 return data[key] 有什么区别 ? 12 | return val 13 | }, 14 | set(newVal) { 15 | if(newVal === val) return 16 | console.log('[hijack][set] -> newVal: ', newVal) 17 | val = newVal 18 | // 如果新值是object, 则对其属性劫持 19 | hijack(newVal) 20 | } 21 | }) 22 | } 23 | } 24 | 25 | let person = { name: 'ziyi2', age: 1 } 26 | hijack(person) 27 | // [hijack][get] -> val: ziyi2 28 | person.name 29 | // [hijack][get] -> val: 1 30 | person.age 31 | // [hijack][set] -> newVal: ziyi 32 | person.name = 'ziyi' 33 | 34 | // 属性类型变化劫持 35 | // [hijack][get] -> val: { familyName:"ziyi2", givenName:"xiankang" } 36 | person.name = { familyName: 'zhu', givenName: 'xiankang' } 37 | // [hijack][get] -> val: ziyi2 38 | person.name.familyName = 'ziyi2' 39 | 40 | // 数据属性 41 | let job = { type: 'javascript' } 42 | console.log(Object.getOwnPropertyDescriptor(job, "type")) 43 | // 访问器属性 44 | console.log(Object.getOwnPropertyDescriptor(person, "name")) 45 | 46 | -------------------------------------------------------------------------------- /demo/hijack/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 |

数据劫持演示

11 | 12 | 13 | -------------------------------------------------------------------------------- /demo/mediator/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 |

发布/订阅模式演示

11 | 12 | 13 | -------------------------------------------------------------------------------- /demo/mediator/mediator.js: -------------------------------------------------------------------------------- 1 | // 中介者 2 | class Mediator { 3 | constructor() { 4 | this.channels = {} 5 | this.uid = 0 6 | } 7 | 8 | /** 9 | * @Author: zhuxiankang 10 | * @Date: 2018-07-11 10:34:33 11 | * @Desc: 订阅频道 12 | * @Parm: {String} channel 频道 13 | * {Function} cb 回调函数 14 | */ 15 | sub(channel, cb) { 16 | let { channels } = this 17 | if(!channels[channel]) channels[channel] = [] 18 | this.uid ++ 19 | channels[channel].push({ 20 | context: this, 21 | uid: this.uid, 22 | cb 23 | }) 24 | console.log('[mediator][sub] -> this.channels: ', this.channels) 25 | return this.uid 26 | } 27 | 28 | /** 29 | * @Author: zhuxiankang 30 | * @Date: 2018-07-11 10:35:15 31 | * @Desc: 发布频道 32 | * @Parm: {String} channel 频道 33 | * {Any} data 数据 34 | */ 35 | pub(channel, data) { 36 | console.log('[mediator][pub] -> chanel: ', channel) 37 | let ch = this.channels[channel] 38 | if(!ch) return false 39 | let len = ch.length 40 | // 后订阅先触发 41 | while(len --) { 42 | ch[len].cb.call(ch[len].context, data) 43 | } 44 | return this 45 | } 46 | 47 | /** 48 | * @Author: zhuxiankang 49 | * @Date: 2018-07-11 11:56:06 50 | * @Desc: 取消订阅 51 | * @Parm: {String} uid 订阅标识 52 | */ 53 | cancel(uid) { 54 | let { channels } = this 55 | for(let channel of Object.keys(channels)) { 56 | let ch = channels[channel] 57 | if(ch.length === 1 && ch[0].uid === uid) { 58 | delete channels[channel] 59 | console.log('[mediator][cancel][delete] -> chanel: ', channel) 60 | console.log('[mediator][cancel] -> chanels: ', channels) 61 | return 62 | } 63 | for(let i=0,len=ch.length; i chanel: ', channel) 67 | console.log('[mediator][cancel] -> chanels: ', channels) 68 | return 69 | } 70 | } 71 | } 72 | } 73 | } 74 | 75 | 76 | let mediator = new Mediator() 77 | 78 | let channel1First = mediator.sub('channel1', (data) => { 79 | console.log('[mediator][channel1First][callback] -> data', data) 80 | }) 81 | <<<<<<< HEAD 82 | ======= 83 | 84 | >>>>>>> 886af83f9a50c6528600bbb2606841ddc8e1039f 85 | let channel1Second = mediator.sub('channel1', (data) => { 86 | console.log('[mediator][channel1Second][callback] -> data', data) 87 | }) 88 | let channel2 = mediator.sub('channel2', (data) => { 89 | console.log('[mediator][channel2][callback] -> data', data) 90 | }) 91 | 92 | mediator.pub('channel1', { name: 'ziyi1' }) 93 | mediator.pub('channel2', { name: 'ziyi2' }) 94 | 95 | mediator.cancel(channel1Second) 96 | mediator.pub('channel1', { name: 'ziyi1' }) -------------------------------------------------------------------------------- /demo/view/binder.js: -------------------------------------------------------------------------------- 1 | (function(window, browser){ 2 | window.binder = { 3 | /** 4 | * @Author: zhuxiankang 5 | * @Date: 2018-07-12 19:01:21 6 | * @Desc: 判断是否是绑定属性 7 | * @Parm: {String} attr Node节点的属性 8 | */ 9 | is(attr) { 10 | return attr.includes('b-') 11 | }, 12 | 13 | /** 14 | * @Author: zhuxiankang 15 | * @Date: 2018-07-12 20:14:04 16 | * @Desc: 解析绑定指令 17 | * @Parm: {Object} attr html属性对象 18 | * {Object} node Node节点 19 | * {Object} model 数据 20 | */ 21 | parse(node, attr, model) { 22 | if(!this.is(attr.name)) return 23 | 24 | this.model = model 25 | 26 | let bindValue = attr.value, 27 | bindType = attr.name.substring(2) 28 | // 绑定视图指令处理 29 | this[bindType](node, bindValue.trim()) 30 | }, 31 | 32 | /** 33 | * @Author: zhuxiankang 34 | * @Date: 2018-07-12 20:14:04 35 | * @Desc: 值绑定处理(b-value) 36 | * @Parm: {Object} node Node节点 37 | * {String} key model的属性 38 | */ 39 | value(node, key) { 40 | this.update(node, key) 41 | }, 42 | 43 | 44 | /** 45 | * @Author: zhuxiankang 46 | * @Date: 2018-07-12 20:17:12 47 | * @Desc: 值绑定更新(b-value) 48 | * @Parm: {Object} node Node节点 49 | * {String} key model的属性 50 | */ 51 | update(node, key) { 52 | browser.val(node, this.model.getData(key)) 53 | } 54 | } 55 | })(window, browser) 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /demo/view/browser.js: -------------------------------------------------------------------------------- 1 | let browser = { 2 | event: { 3 | /** 4 | * @Author: zhuxiankang 5 | * @Date: 2018-07-13 08:37:10 6 | * @Desc: 添加事件 7 | * @Parm: {Object} node 添加事件的节点 8 | * {String} type 事件类型 9 | * {Function} fn 句柄函数 10 | */ 11 | add(node, type, fn) { 12 | // DOM2 13 | if(node.addEventListener) { 14 | node.addEventListener(type, fn, false) 15 | // IE 16 | } else if(dom.attachEvent) { 17 | node.attachEvent(`on${type}`, fn) 18 | // DOM0 19 | } else { 20 | node[`on${type}`] = fn 21 | } 22 | }, 23 | 24 | /** 25 | * @Author: zhuxiankang 26 | * @Date: 2018-07-13 08:43:36 27 | * @Desc: 获取事件的目标对象 28 | * @Parm: {Object} event 事件对象 29 | */ 30 | target(event) { 31 | return event.target || event.srcElement 32 | } 33 | }, 34 | 35 | /** 36 | * @Author: zhuxiankang 37 | * @Date: 2018-07-16 09:13:54 38 | * @Desc: Node节点的value处理 39 | * @Parm: {Object} node Node节点 40 | * {String} val 节点的值 41 | */ 42 | val(node, val) { 43 | node.value = val || '' 44 | console.log(`[browser][val] -> node: `, node) 45 | console.log(`[browser][val] -> val: `, val) 46 | } 47 | } -------------------------------------------------------------------------------- /demo/view/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 |

文档编译演示

11 |
12 | 13 | 14 | 15 |
16 | 17 | 20 | 21 | 22 | 23 | 24 | 44 | 45 | -------------------------------------------------------------------------------- /demo/view/view.js: -------------------------------------------------------------------------------- 1 | class View { 2 | constructor(el, model) { 3 | this.model = model 4 | // 获取需要处理的node节点 5 | this.el = el.nodeType === Node.ELEMENT_NODE ? el : document.querySelector(el) 6 | if(!this.el) return 7 | // 将已有的el元素的所有子元素转成文档碎片 8 | this.fragment = this.node2Fragment(this.el) 9 | // 解析和处理绑定指令并修改文档碎片 10 | this.parseFragment(this.fragment) 11 | // 将文档碎片重新添加到dom树 12 | this.el.appendChild(this.fragment) 13 | } 14 | 15 | /** 16 | * @Author: zhuxiankang 17 | * @Date: 2018-07-12 09:15:18 18 | * @Desc: 将node节点转为文档碎片 19 | * @Parm: {Object} node Node节点 20 | */ 21 | node2Fragment(node) { 22 | let fragment = document.createDocumentFragment(), 23 | child; 24 | while(child = node.firstChild) { 25 | // 给文档碎片添加节点时,该节点会自动从dom中删除 26 | fragment.appendChild(child) 27 | } 28 | return fragment 29 | } 30 | 31 | /** 32 | * @Author: zhuxiankang 33 | * @Date: 2018-07-12 17:09:25 34 | * @Desc: 解析文档碎片 35 | * @Parm: {Object} fragment 文档碎片 36 | */ 37 | parseFragment(fragment) { 38 | // 类数组转化成数组进行遍历 39 | for(let node of [].slice.call(fragment.childNodes)) { 40 | if(node.nodeType !== Node.ELEMENT_NODE) continue 41 | // 绑定视图指令解析 42 | for(let attr of [].slice.call(node.attributes)) { 43 | binder.parse(node, attr, this.model) 44 | // 移除绑定属性 45 | node.removeAttribute(attr.name) 46 | } 47 | // 遍历node节点树 48 | if(node.childNodes && node.childNodes.length) this.parseFragment(node) 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /demo/viewModel/binder.js: -------------------------------------------------------------------------------- 1 | (function(window, browser){ 2 | window.binder = { 3 | /** 4 | * @Author: zhuxiankang 5 | * @Date: 2018-07-12 19:01:21 6 | * @Desc: 判断是否是绑定属性 7 | * @Parm: {String} attr Node节点的属性 8 | */ 9 | is(attr) { 10 | return attr.includes('b-') 11 | }, 12 | 13 | /** 14 | * @Author: zhuxiankang 15 | * @Date: 2018-07-12 20:14:04 16 | * @Desc: 解析绑定指令 17 | * @Parm: {Object} attr html属性对象 18 | * {Object} node Node节点 19 | * {Object} model 数据 20 | * {Object} mediator 中介者 21 | */ 22 | parse(node, attr, model, mediator) { 23 | if(!this.is(attr.name)) return 24 | 25 | this.model = model 26 | 27 | this.mediator = mediator 28 | 29 | let bindValue = attr.value, 30 | bindType = attr.name.substring(2) 31 | 32 | // 绑定视图指令处理 33 | this[bindType](node, bindValue.trim()) 34 | }, 35 | 36 | /** 37 | * @Author: zhuxiankang 38 | * @Date: 2018-07-12 20:14:04 39 | * @Desc: 值绑定处理(b-value) 40 | * @Parm: {Object} node Node节点 41 | * {String} key model的属性 42 | */ 43 | value(node, key) { 44 | this.update(node, key) 45 | 46 | // 数据双向绑定 47 | browser.event.add(node, 'input', (e) => { 48 | // 更新model和model对应的视图 49 | let newVal = browser.event.target(e).value 50 | // 设置对应的数据 51 | this.model.setData(key, newVal) 52 | }) 53 | 54 | // 订阅数据变化更新视图(闭包) 55 | this.mediator.sub(key, () => { 56 | console.log('[mediator][sub] -> key: ', key) 57 | console.log('[mediator][sub] -> node: ', node) 58 | this.update(node, key) 59 | }) 60 | }, 61 | 62 | 63 | /** 64 | * @Author: zhuxiankang 65 | * @Date: 2018-07-12 20:17:12 66 | * @Desc: 值绑定更新(b-value) 67 | * @Parm: {Object} node Node节点 68 | * {String} key model的属性 69 | */ 70 | update(node, key) { 71 | browser.val(node, this.model.getData(key)) 72 | } 73 | } 74 | })(window, browser) 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /demo/viewModel/browser.js: -------------------------------------------------------------------------------- 1 | let browser = { 2 | event: { 3 | /** 4 | * @Author: zhuxiankang 5 | * @Date: 2018-07-13 08:37:10 6 | * @Desc: 添加事件 7 | * @Parm: {Object} node 添加事件的节点 8 | * {String} type 事件类型 9 | * {Function} fn 句柄函数 10 | */ 11 | add(node, type, fn) { 12 | // DOM2 13 | if(node.addEventListener) { 14 | node.addEventListener(type, fn, false) 15 | // IE 16 | } else if(dom.attachEvent) { 17 | node.attachEvent(`on${type}`, fn) 18 | // DOM0 19 | } else { 20 | node[`on${type}`] = fn 21 | } 22 | }, 23 | 24 | /** 25 | * @Author: zhuxiankang 26 | * @Date: 2018-07-13 08:43:36 27 | * @Desc: 获取事件的目标对象 28 | * @Parm: {Object} event 事件对象 29 | */ 30 | target(event) { 31 | return event.target || event.srcElement 32 | } 33 | }, 34 | 35 | /** 36 | * @Author: zhuxiankang 37 | * @Date: 2018-07-16 09:13:54 38 | * @Desc: Node节点的value处理 39 | * @Parm: {Object} node Node节点 40 | * {String} val 节点的值 41 | */ 42 | val(node, val) { 43 | node.value = val || '' 44 | console.log(`[browser][val] -> val: `, val) 45 | } 46 | } -------------------------------------------------------------------------------- /demo/viewModel/hijack.js: -------------------------------------------------------------------------------- 1 | 2 | var hijack = (function() { 3 | 4 | class Hijack { 5 | /** 6 | * @Author: zhuxiankang 7 | * @Date: 2018-07-17 09:09:12 8 | * @Desc: 数据劫持构造函数 9 | * @Parm: {Object} model 数据 10 | * {Object} mediator 发布订阅对象 11 | */ 12 | constructor(model, mediator) { 13 | this.model = model 14 | this.mediator = mediator 15 | } 16 | 17 | /** 18 | * @Author: zhuxiankang 19 | * @Date: 2018-07-17 20:31:40 20 | * @Desc: model数据劫持 21 | * @Parm: 22 | * 23 | */ 24 | hijackData() { 25 | 26 | let { model, mediator } = this 27 | 28 | for(let key of Object.keys(model)) { 29 | 30 | let val = model[key] 31 | 32 | Object.defineProperty(model, key, { 33 | enumerable: true, 34 | configurable: false, 35 | get() { 36 | // console.log('[hijack][get] -> dataKey: ', dataKey) 37 | // console.log('[hijack][get] -> val: ', val) 38 | return val 39 | }, 40 | set(newVal) { 41 | if(newVal === val) return 42 | console.log('[hijack][set] -> key: ', key) 43 | console.log('[hijack][set] -> val: ', val) 44 | console.log('[hijack][set] -> newVal: ', newVal) 45 | val = newVal 46 | // 发布数据劫持的数据变化信息 47 | console.log('[mediator][pub] -> key: ', key) 48 | mediator.pub(key) 49 | } 50 | }) 51 | } 52 | } 53 | } 54 | 55 | return (model, mediator) => { 56 | if(!model || typeof model !== 'object') return 57 | new Hijack(model, mediator).hijackData() 58 | } 59 | })() 60 | 61 | 62 | -------------------------------------------------------------------------------- /demo/viewModel/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 |

ViewModel演示

11 |
12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 63 | 64 | -------------------------------------------------------------------------------- /demo/viewModel/mediator.js: -------------------------------------------------------------------------------- 1 | // 中介者 2 | class Mediator { 3 | constructor() { 4 | this.channels = {} 5 | this.uid = 0 6 | } 7 | 8 | /** 9 | * @Author: zhuxiankang 10 | * @Date: 2018-07-11 10:34:33 11 | * @Desc: 订阅频道 12 | * @Parm: {String} channel 频道 13 | * {Function} cb 回调函数 14 | */ 15 | sub(channel, cb) { 16 | let { channels } = this 17 | if(!channels[channel]) channels[channel] = [] 18 | this.uid ++ 19 | channels[channel].push({ 20 | context: this, 21 | uid: this.uid, 22 | cb 23 | }) 24 | // console.log('[mediator][sub] -> dataKey: ', channel) 25 | // console.log('[mediator][sub] -> this.channels: ', this.channels) 26 | return this.uid 27 | } 28 | 29 | /** 30 | * @Author: zhuxiankang 31 | * @Date: 2018-07-11 10:35:15 32 | * @Desc: 发布频道 33 | * @Parm: {String} channel 频道 34 | * {Any} data 数据 35 | */ 36 | pub(channel, data) { 37 | let ch = this.channels[channel] 38 | if(!ch) return false 39 | let len = ch.length 40 | while(len --) { 41 | ch[len].cb.call(ch[len].context, data) 42 | } 43 | // console.log('[mediator][pub] -> dataKey: ', channel) 44 | return this 45 | } 46 | 47 | /** 48 | * @Author: zhuxiankang 49 | * @Date: 2018-07-11 11:56:06 50 | * @Desc: 取消订阅 51 | * @Parm: {String} uid 订阅标识 52 | */ 53 | cancel(uid) { 54 | let { channels } = this 55 | for(let channel of Object.keys(channels)) { 56 | let ch = channels[channel] 57 | if(ch.length === 1 && ch[0].uid === uid) { 58 | delete channels[channel] 59 | return 60 | } 61 | for(let i=0,len=ch.length; i { 52 | let newVal = browser.event.target(e).value 53 | // console.log(`[binder][value][input(event)] -> key: `, key) 54 | // console.log(`[binder][value][input(event)] -> newVal: `, newVal) 55 | vm.setData(key, newVal) 56 | }) 57 | }, 58 | 59 | 60 | /** 61 | * @Author: zhuxiankang 62 | * @Date: 2018-07-15 14:23:30 63 | * @Desc: 文本值绑定处理(b-text或{{}}模板) 64 | * @Parm: {Object} node Node节点 65 | * {Object} vm MVVM实例对象 66 | * {String} key mvvm实例的data对象的属性名称 67 | */ 68 | text(node, vm, key) { 69 | this.bind(node, vm, key, 'text') 70 | }, 71 | 72 | 73 | /** 74 | * @Author: zhuxiankang 75 | * @Date: 2018-07-16 08:56:35 76 | * @Desc: html文本处理(b-html) 77 | * @Parm: {Object} node Node节点 78 | * {Object} vm MVVM实例对象 79 | * {String} key mvvm实例的data对象的属性名称 80 | */ 81 | html(node, vm, key) { 82 | this.bind(node, vm, key, 'html') 83 | }, 84 | 85 | /** 86 | * @Author: zhuxiankang 87 | * @Date: 2018-07-12 20:17:12 88 | * @Desc: 绑定处理(b-) 89 | * @Parm: {Object} node Node节点 90 | * {Object} vm MVVM实例对象 91 | * {String} key mvvm实例的data对象的属性名称 92 | * {String} type 绑定类型 93 | */ 94 | bind(node, vm, key, type) { 95 | let update = this.update[type] 96 | update && update(node, vm.getData(key)) 97 | 98 | // 订阅数据劫持的数据变化信息,详见hijack(hijackKey) 99 | let keys = key.split('.') 100 | let dataKey 101 | 102 | for(let k of keys) { 103 | dataKey = dataKey ? `${dataKey}.${k}` : k 104 | 105 | vm.mediator.sub(dataKey, function() { 106 | // console.log(`[binder][update][${type}] -> node: `, node) 107 | // console.log(`[binder][update][${type}] -> dataKey: `, dataKey) 108 | console.log('[mediator][sub] -> dataKey: ', dataKey) 109 | update && update(node, vm.getData(key)) 110 | }) 111 | } 112 | }, 113 | 114 | 115 | update: { 116 | /** 117 | * @Author: zhuxiankang 118 | * @Date: 2018-07-15 11:01:38 119 | * @Desc: 值绑定更新(b-value) 120 | * @Parm: {Object} node Node节点 121 | * {String} val 绑定值 122 | */ 123 | value(node, val) { 124 | browser.val(node, val) 125 | }, 126 | 127 | 128 | /** 129 | * @Author: zhuxiankang 130 | * @Date: 2018-07-15 11:01:38 131 | * @Desc: 文本值绑定更新(b-text或{{}}模板) 132 | * @Parm: {Object} node Node节点 133 | * {String} val 绑定值 134 | */ 135 | text(node, val) { 136 | browser.text(node, val) 137 | }, 138 | 139 | /** 140 | * @Author: zhuxiankang 141 | * @Date: 2018-07-16 08:59:32 142 | * @Desc: html文本更新(b-html) 143 | * @Parm: {Object} node Node节点 144 | * {String} val 绑定值 145 | */ 146 | html(node, val) { 147 | browser.html(node, val) 148 | } 149 | } 150 | } 151 | })(window, browser) 152 | 153 | 154 | 155 | -------------------------------------------------------------------------------- /mvvm/browser.js: -------------------------------------------------------------------------------- 1 | let browser = { 2 | event: { 3 | /** 4 | * @Author: zhuxiankang 5 | * @Date: 2018-07-13 08:37:10 6 | * @Desc: 添加事件 7 | * @Parm: {Object} node 添加事件的节点 8 | * {String} type 事件类型 9 | * {Function} fn 句柄函数 10 | */ 11 | add(node, type, fn) { 12 | // DOM2 13 | if(node.addEventListener) { 14 | node.addEventListener(type, fn, false) 15 | // IE 16 | } else if(dom.attachEvent) { 17 | node.attachEvent(`on${type}`, fn) 18 | // DOM0 19 | } else { 20 | node[`on${type}`] = fn 21 | } 22 | }, 23 | 24 | /** 25 | * @Author: zhuxiankang 26 | * @Date: 2018-07-13 08:38:07 27 | * @Desc: 移除事件 28 | * @Parm: {Object} node 添加事件的节点 29 | * {String} type 事件类型 30 | * {Function} fn 句柄函数 31 | */ 32 | remove(node, type, fn) { 33 | if(node.removeEventListener) { 34 | node.removeEventListener(type, fn, false) 35 | } else if(dom.detachEvent) { 36 | node.detachEvent(`on${type}`, fn) 37 | } else { 38 | node[`on${type}`] = null 39 | } 40 | }, 41 | 42 | 43 | /** 44 | * @Author: zhuxiankang 45 | * @Date: 2018-07-13 08:42:41 46 | * @Desc: 获取事件对象 47 | * @Parm: {Object} event 事件对象 48 | */ 49 | self(event) { 50 | return event || window.event 51 | }, 52 | 53 | /** 54 | * @Author: zhuxiankang 55 | * @Date: 2018-07-13 08:43:36 56 | * @Desc: 获取事件的目标对象 57 | * @Parm: {Object} event 事件对象 58 | */ 59 | target(event) { 60 | return event.target || event.srcElement 61 | }, 62 | 63 | /** 64 | * @Author: zhuxiankang 65 | * @Date: 2018-07-13 08:44:29 66 | * @Desc: 取消事件的默认行为 67 | * @Parm: {Object} event 事件对象 68 | */ 69 | preventDefault(event) { 70 | event.preventDefault 71 | ? event.preventDefault() 72 | : event.returnValue = false 73 | }, 74 | 75 | /** 76 | * @Author: zhuxiankang 77 | * @Date: 2018-07-13 08:46:56 78 | * @Desc: 取消事件的捕获或冒泡 79 | * @Parm: {Object} event 事件对象 80 | */ 81 | stopPropagation(event) { 82 | event.stopPropagation 83 | ? event.stopPropagation() 84 | : event.cancelBubble = true 85 | } 86 | }, 87 | 88 | /** 89 | * @Author: zhuxiankang 90 | * @Date: 2018-07-13 08:48:28 91 | * @Desc: 获取元素 92 | * @Parm: {String} selector 选择器 93 | */ 94 | query(selector) { 95 | return document.querySelector(selector) 96 | }, 97 | 98 | /** 99 | * @Author: zhuxiankang 100 | * @Date: 2018-07-16 09:13:54 101 | * @Desc: Node节点的value处理 102 | * @Parm: {Object} node Node节点 103 | * {String} val 节点的值 104 | */ 105 | val(node, val) { 106 | node.value = val || '' 107 | console.log(`[browser][val] -> node: `, node) 108 | console.log(`[browser][val] -> val: `, val) 109 | }, 110 | 111 | /** 112 | * @Author: zhuxiankang 113 | * @Date: 2018-07-16 09:17:47 114 | * @Desc: Node节点的文本处理 115 | * @Parm: {Object} node Node节点 116 | * {String} val 节点的文本 117 | */ 118 | text(node, val) { 119 | node.textContent = val || '' 120 | console.log(`[browser][text] -> node: `, node) 121 | console.log(`[browser][text] -> val: `, val) 122 | }, 123 | 124 | /** 125 | * @Author: zhuxiankang 126 | * @Date: 2018-07-16 09:12:32 127 | * @Desc: innerHTML处理 128 | * @Parm: {Object} node Node节点 129 | * {String} val 填充的html片段 130 | */ 131 | html(node, val) { 132 | node.innerHTML = val || '' 133 | console.log(`[browser][html] -> node: `, node) 134 | console.log(`[browser][html] -> val: `, val) 135 | } 136 | } -------------------------------------------------------------------------------- /mvvm/hijack.js: -------------------------------------------------------------------------------- 1 | 2 | var hijack = (function() { 3 | 4 | class Hijack { 5 | /** 6 | * @Author: zhuxiankang 7 | * @Date: 2018-07-17 09:09:12 8 | * @Desc: 数据劫持构造函数 9 | * @Parm: {Object} mvvm实例对象的data属性 10 | * {Object} vm mvvm实例对象 11 | * {String} dataKey data对象的属性名标识 12 | */ 13 | constructor(data, vm, dataKey) { 14 | this.vm = vm 15 | this.dataKey = dataKey 16 | this.data = data 17 | this.hijackData() 18 | } 19 | 20 | /** 21 | * @Author: zhuxiankang 22 | * @Date: 2018-07-17 20:31:40 23 | * @Desc: data对象的数据劫持 24 | * @Parm: 25 | * 26 | */ 27 | hijackData() { 28 | let { data, dataKey } = this 29 | for(let key of Object.keys(data)) { 30 | this.dataKey = dataKey ? `${dataKey}.${key}` : key 31 | this.hijackKey(key, data[key]) 32 | } 33 | } 34 | 35 | /** 36 | * @Author: zhuxiankang 37 | * @Date: 2018-07-17 20:31:40 38 | * @Desc: data对象的属性的数据劫持 39 | * @Parm: {Any} key data对象的属性名 40 | * {Any} val data对象的属性值 41 | */ 42 | hijackKey(key, val) { 43 | let { vm, data, dataKey } = this 44 | let me = this 45 | 46 | this.hijack(val) 47 | 48 | Object.defineProperty(data, key, { 49 | enumerable: true, 50 | configurable: false, 51 | get() { 52 | // console.log('[hijack][get] -> dataKey: ', dataKey) 53 | // console.log('[hijack][get] -> val: ', val) 54 | return val 55 | }, 56 | set(newVal) { 57 | if(newVal === val) return 58 | // console.log('[hijack][set] -> dataKey: ', dataKey) 59 | // console.log('[hijack][set] -> val: ', val) 60 | // console.log('[hijack][set] -> newVal: ', newVal) 61 | val = newVal 62 | // 发布数据劫持的数据变化信息,详见binder(bind) 63 | console.log('[mediator][pub] -> dataKey: ', dataKey) 64 | vm.mediator.pub(dataKey) 65 | // 如果新值是object, 则对其属性劫持 66 | me.hijack(newVal) 67 | } 68 | }) 69 | } 70 | 71 | /** 72 | * @Author: zhuxiankang 73 | * @Date: 2018-07-18 20:00:34 74 | * @Desc: 子属性如果是对象,则继续进行数据劫持 75 | * @Parm: 76 | */ 77 | hijack(val) { 78 | if(!this.data || typeof this.data !== 'object') return 79 | hijack(val, this.vm, this.dataKey) 80 | } 81 | } 82 | 83 | return (data, vm, dataKey) => { 84 | if(!data || typeof data !== 'object') return 85 | return new Hijack(data, vm, dataKey) 86 | } 87 | })() 88 | 89 | 90 | -------------------------------------------------------------------------------- /mvvm/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 |

mvvm演示

11 |
12 | 13 |
{{ input.message }}
14 |
15 |
{{ text }}
16 |
17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 44 | 45 | -------------------------------------------------------------------------------- /mvvm/mediator.js: -------------------------------------------------------------------------------- 1 | // 中介者 2 | class Mediator { 3 | constructor() { 4 | this.channels = {} 5 | this.uid = 0 6 | } 7 | 8 | /** 9 | * @Author: zhuxiankang 10 | * @Date: 2018-07-11 10:34:33 11 | * @Desc: 订阅频道 12 | * @Parm: {String} channel 频道 13 | * {Function} cb 回调函数 14 | */ 15 | sub(channel, cb) { 16 | let { channels } = this 17 | if(!channels[channel]) channels[channel] = [] 18 | this.uid ++ 19 | channels[channel].push({ 20 | context: this, 21 | uid: this.uid, 22 | cb 23 | }) 24 | // console.log('[mediator][sub] -> dataKey: ', channel) 25 | // console.log('[mediator][sub] -> this.channels: ', this.channels) 26 | return this.uid 27 | } 28 | 29 | /** 30 | * @Author: zhuxiankang 31 | * @Date: 2018-07-11 10:35:15 32 | * @Desc: 发布频道 33 | * @Parm: {String} channel 频道 34 | * {Any} data 数据 35 | */ 36 | pub(channel, data) { 37 | let ch = this.channels[channel] 38 | if(!ch) return false 39 | let len = ch.length 40 | while(len --) { 41 | ch[len].cb.call(ch[len].context, data) 42 | } 43 | // console.log('[mediator][pub] -> dataKey: ', channel) 44 | return this 45 | } 46 | 47 | /** 48 | * @Author: zhuxiankang 49 | * @Date: 2018-07-11 11:56:06 50 | * @Desc: 取消订阅 51 | * @Parm: {String} uid 订阅标识 52 | */ 53 | cancel(uid) { 54 | let { channels } = this 55 | for(let channel of Object.keys(channels)) { 56 | let ch = channels[channel] 57 | if(ch.length === 1 && ch[0].uid === uid) { 58 | delete channels[channel] 59 | return 60 | } 61 | for(let i=0,len=ch.length; i val: ', val) 52 | return val 53 | } 54 | 55 | /** 56 | * @Author: zhuxiankang 57 | * @Date: 2018-07-16 21:54:15 58 | * @Desc: 获取data对象的某个属性值 59 | * @Parm: {String} key data对象的属性 60 | * {String} newVal 绑定值 61 | */ 62 | setData(key, newVal) { 63 | let val = this 64 | let keys = key.split('.') 65 | for(let i=0, len=keys.length; i val: ', val) 73 | } 74 | } -------------------------------------------------------------------------------- /mvvm/view.js: -------------------------------------------------------------------------------- 1 | class View { 2 | /** 3 | * @Author: zhuxiankang 4 | * @Date: 2018-07-12 09:01:39 5 | * @Desc: view实例的构造函数 6 | * @Parm: {String/Node} el 选择器或node节点 7 | * {Object} vm mvvm实例对象 8 | */ 9 | constructor(el, vm) { 10 | this.vm = vm 11 | this.el = this.isElementNode(el) ? el : document.querySelector(el) 12 | if(this.el) { 13 | // 将已有的el元素的所有子元素转成文档碎片 14 | this.fragment = this.node2Fragment(this.el) 15 | // 解析文档碎片 16 | this.parseFragment(this.fragment) 17 | // 将文档碎片添加到el元素中 18 | this.el.appendChild(this.fragment) 19 | } 20 | } 21 | 22 | /** 23 | * @Author: zhuxiankang 24 | * @Date: 2018-07-12 17:09:25 25 | * @Desc: 解析文档碎片 26 | * @Parm: {Object} fragment 文档碎片 27 | */ 28 | parseFragment(fragment) { 29 | let nodes = [].slice.call(fragment.childNodes) 30 | for(let node of nodes) { 31 | switch(node.nodeType) { 32 | case Node.ELEMENT_NODE: 33 | this.parseNodeBind(node) 34 | break 35 | case Node.TEXT_NODE: 36 | let text = node.textContent 37 | if(text && text.trim().length && /\{\{(.*)\}\}/.test(text)) { 38 | this.parseNodeText(node,RegExp.$1) 39 | } 40 | break 41 | default: 42 | break 43 | } 44 | 45 | if(node.childNodes && node.childNodes.length) { 46 | this.parseFragment(node) 47 | } 48 | } 49 | } 50 | 51 | /** 52 | * @Author: zhuxiankang 53 | * @Date: 2018-07-12 18:46:43 54 | * @Desc: 解析Node节点的绑定属性 55 | * @Parm: {Object} node Node节点 56 | */ 57 | parseNodeBind(node) { 58 | let nodeAttrs = [].slice.call(node.attributes) 59 | for(let attr of nodeAttrs) { 60 | if(!binder.is(attr.name)) continue 61 | let bindValue = attr.value, 62 | bindType = attr.name.substring(2) 63 | binder.isEvent(bindType) 64 | ? binder.event(node, this.vm, bindValue, bindType) 65 | : binder[bindType] && binder[bindType](node, this.vm, bindValue.trim()) 66 | // 移出绑定属性 67 | node.removeAttribute(attr.name) 68 | } 69 | } 70 | 71 | /** 72 | * @Author: zhuxiankang 73 | * @Date: 2018-07-15 14:11:03 74 | * @Desc: 解析Node节点的模板{{}} 75 | * @Parm: {Object} node Node节点 76 | * {String} val 模板中的绑定值 77 | */ 78 | parseNodeText(node, val) { 79 | binder.text(node, this.vm, val.trim()) 80 | } 81 | 82 | /** 83 | * @Author: zhuxiankang 84 | * @Date: 2018-07-12 09:08:28 85 | * @Desc: 判断节点类型是否是Element节点 86 | * @Parm: {Object} node Node节点 87 | */ 88 | isElementNode(node) { 89 | return node.nodeType === Node.ELEMENT_NODE 90 | } 91 | 92 | /** 93 | * @Author: zhuxiankang 94 | * @Date: 2018-07-12 09:15:18 95 | * @Desc: 将node节点转为文档碎片 96 | * @Parm: {Object} node Node节点 97 | */ 98 | node2Fragment(node) { 99 | let fragment = document.createDocumentFragment(), 100 | child; 101 | while(child = node.firstChild) { 102 | // 注意,给文档碎片添加一个节点,该节点会自动从node中删除 103 | fragment.appendChild(child) 104 | } 105 | return fragment 106 | } 107 | } 108 | 109 | 110 | 111 | 112 | let arr = new Array(1000000).fill(1) 113 | 114 | // for测试 ++测试 115 | console.time('for ++') 116 | for(let i=0, len=arr.length; i item) 136 | console.timeEnd('map') 137 | 138 | console.time('map') 139 | arr.map(item => item) 140 | console.timeEnd('map') 141 | 142 | console.time('map') 143 | arr.map(item => item) 144 | console.timeEnd('map') 145 | 146 | 147 | console.time('forEach') 148 | arr.forEach(item => item) 149 | console.timeEnd('forEach') 150 | 151 | console.time('forEach') 152 | arr.forEach(item => item) 153 | console.timeEnd('forEach') 154 | 155 | console.time('forEach') 156 | arr.forEach(item => item) 157 | console.timeEnd('forEach') 158 | 159 | 160 | console.time('for...of...') 161 | for(let index of arr) { 162 | 163 | } 164 | console.timeEnd('for...of...') 165 | 166 | 167 | 168 | 169 | 170 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mvvm", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "src/index.html", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/ziyi2/MVVM.git" 12 | }, 13 | "author": "", 14 | "license": "ISC", 15 | "bugs": { 16 | "url": "https://github.com/ziyi2/MVVM/issues" 17 | }, 18 | "homepage": "https://github.com/ziyi2/MVVM#readme", 19 | "devDependencies": { 20 | "cz-conventional-changelog": "^2.1.0" 21 | }, 22 | "config": { 23 | "commitizen": { 24 | "path": "./node_modules/cz-conventional-changelog" 25 | } 26 | } 27 | } 28 | --------------------------------------------------------------------------------