本文首发在 前端开发博客
MVVM
是当前时代前端平常业务开发中的必备模式(相关框架如react
,vue
,angular
等), 使用 MVVM
能够将开发者的精力更专一于业务上的逻辑,而不须要关心如何操做 dom
。虽然如今都 9012 年了,mvvm
相关原理的介绍已经烂大街了,但出于学习基础知识的目的(使用 proxy
实现的 vue
3.0 还在开发中), 在参考了以前 vue.js
的总体思路以后,本身动手实现了一个简易的经过 proxy
实现的 mvvm
。javascript
本项目代码已经开源在 github,项目正在持续完善中,欢迎交流学习,喜欢请点个 star 吧!
<html> <body> <div id="app"> <div>{{title}}</div> </div> </body> </html>
import MVVM from '@fe_korey/mvvm'; new MVVM({ view: document.getElementById('app'), model: { title: 'hello mvvm!' }, mounted() { console.log('主程编译完成,欢迎使用MVVM!'); } });
Complier
模块实现解析、收集指令,并初始化视图Observer
模块实现了数据的监听,包括添加订阅者和通知订阅者Parser
模块实现解析指令,提供该指令的更新视图的更新方法Watcher
模块实现创建指令与数据的关联Dep
模块实现一个订阅中心,负责收集,触发数据模型各值的订阅列表流程为:Complier
收集编译好指令后,根据指令不一样选择不一样的Parser
,根据Parser
在Watcher
中订阅数据的变化并更新初始视图。Observer
监听数据变化而后通知给 Watcher
,Watcher
再将变化结果通知给对应Parser
里的 update
刷新函数进行视图的刷新。html
将整个数据模型 data
传入Observer
模块进行数据监听前端
this.$data = new Observer(option.model).getData();
循环遍历整个 dom
,对每一个 dom
元素的全部指令进行扫描提取vue
function collectDir(element) { const children = element.childNodes; const childrenLen = children.length; for (let i = 0; i < childrenLen; i++) { const node = children[i]; const nodeType = node.nodeType; if (nodeType !== 1 && nodeType !== 3) { continue; } if (hasDirective(node)) { this.$queue.push(node); } if (node.hasChildNodes() && !hasLateCompileChilds(node)) { collectDir(element); } } }
对每一个指令进行编译,选择对应的解析器Parser
java
const parser = this.selectParsers({ node, dirName, dirValue, cs: this });
将获得的解析器Parser
传入Watcher
,并初始化该 dom
节点的视图node
const watcher = new Watcher(parser); parser.update({ newVal: watcher.value });
全部指令解析完毕后,触发 MVVM
编译完成回调$mounted()
react
this.$mounted();
使用文档碎片document.createDocumentFragment()
来代替真实 dom
节点片断,待全部指令编译完成后,再将文档碎片追加回真实 dom
节点git
let child; const fragment = document.createDocumentFragment(); while ((child = this.$element.firstChild)) { fragment.appendChild(child); } //解析完后 this.$element.appendChild(fragment); delete $fragment;
在Complier
模块编译后的指令,选择不一样听解析器解析,目前包括ClassParser
,DisplayParser
,ForParser
,IfParser
,StyleParser
,TextParser
,ModelParser
,OnParser
,OtherParser
等解析模块。es6
switch (name) { case 'text': parser = new TextParser({ node, dirValue, cs }); break; case 'style': parser = new StyleParser({ node, dirValue, cs }); break; case 'class': parser = new ClassParser({ node, dirValue, cs }); break; case 'for': parser = new ForParser({ node, dirValue, cs }); break; case 'on': parser = new OnParser({ node, dirName, dirValue, cs }); break; case 'display': parser = new DisplayParser({ node, dirName, dirValue, cs }); break; case 'if': parser = new IfParser({ node, dirValue, cs }); break; case 'model': parser = new ModelParser({ node, dirValue, cs }); break; default: parser = new OtherParser({ node, dirName, dirValue, cs }); }
不一样的解析器提供不一样的视图刷新函数update()
,经过update
更新dom
视图github
//text.js function update(newVal) { this.el.textContent = _toString(newVal); }
OnParser
解析事件绑定,与数据模型中的 methods
字段对应
//详见 https://github.com/zhaoky/mvvm/blob/master/src/core/parser/on.ts el.addEventListener(handlerType, e => { handlerFn(scope, e); });
ForParser
解析数组
//详见 https://github.com/zhaoky/mvvm/blob/master/src/core/parser/for.ts
ModelParser
解析双向绑定,目前支持input[text/password] & textarea
,input[radio]
,input[checkbox]
,select
四种状况的双向绑定,双绑原理:
数据变化更新表单:跟其余指令更新视图同样,经过update
方法触发更新表单的value
function update({ newVal }) { this.model.el.value = _toString(newVal); }
表单变化更新数据:监听表单变化事件如input
,change
,在回调里set
数据模型
this.model.el.addEventListener('input', e => { model.watcher.set(e.target.value); });
MVVM
模型中的核心,通常经过 Object.defineProperty
的 get
,set
方法进行数据的监听,在 get
里添加订阅者,set
里通知订阅者更新视图。在本项目采用 Proxy
来实现数据监听,好处有三:
Proxy
只会监听自身的每个属性,若是属性是对象,则该对象不会被监听,因此须要递归监听Proxy
替代原数据对象var proxy = new Proxy(data, { get: function(target, key, receiver) { //若是知足条件则添加订阅者 dep.addDep(curWatcher); return Reflect.get(target, key, receiver); }, set: function(target, key, value, receiver) { //若是知足条件则通知订阅者 dep.notfiy(); return Reflect.set(target, key, value, receiver); } });
在 Complier
模块里对每个解析后的 Parser
进行指令与数据模型直接的绑定,并触发 Observer
的 get
监听,添加订阅者(Watcher
)
this._getter(this.parser.dirValue)(this.scope || this.parser.cs.$data);
Observer
的 set
监听 -> Dep
的 notfiy
方法(通知订阅者的全部订阅列表) -> 执行订阅列表全部 Watcher
的 update
方法 -> 执行对应 Parser
的 update
-> 完成更新视图Watcher
里的 set
方法用于设置双向绑定值,注意访问层级MVVM
的订阅中心,在这里收集数据模型的每一个属性的订阅列表class Dep { constructor() { this.dependList = []; } addDep() { this.dependList.push(dep); } notfiy() { this.dependList.forEach(item => { item.update(); }); } }
目前该 mvvm
项目只实现了数据绑定
和视图更新
的功能,经过这个简易轮子的实现,对 dom
操做,proxy
,发布订阅模式
等若干基础知识都进行了再次理解,查漏补缺。同时欢迎你们一块儿探讨交流,后面会继续完善!