书接上一篇: 150行代码教你实现一个低配版的MVVM库(1)- 原理篇html
为了便于分模块,和阅读,我使用了Typescript来进行coding,总行数是正好150行,最先写DEMO的时候用了ES2015,代码行数应该在100行出头,若是你不会搭ts+webpack的编译UMD环境,你也能够把本文中的ts语法人肉转成es6或者es2015,我相信这对你(一个有志于学写mvvm库的青年)来讲没有什么难度。vue
做为做者呢,虽然最后我会放出源码的地址,你能够去github上扫一眼代码,但我仍是但愿大家能够跟我一块儿,打开个文本编辑器,一个模块一个模块把代码人肉敲出来,这样的感受是不同的,就比如是你可能以前就阅读过angular,vue的源码,但你如今还不是在读个人文章么?node
仍是再上一遍设计图
设计的类很少,一共就5个webpack
//SegmentFault.ts export let SegmentFault = class SegmentFault { private viewModelPool = {}; //用来维护viewModel的别名alias与viewModel之间的关系 private viewViewModelMap = {};//用来维护viewModel和被绑定的view之间的关系 public registerViewModel(alias:string, vm:object) {};//在sf正式运做以前咱们先要注册一个下viewModel并给他起一个别名 public init() {}; //sf库开始运做的入口函数 public refresh(alias:string){}; // 暴露一个强制刷新整个viewModel的方法,由于毕竟有你监控不到的角落 }
SegmentFault是对用户暴露的惟一的对象,就像Angular他会暴露一个angular对象给用户使用同样。
最终,用户会这样来操做SF以达到双向绑定的目的
不妨再看看使用效果git
<script src="dist/sf.js"></script> <!-- 这里引入咱们的sf.js库--> <script> var sf = new SegmentFault(); //生成一个sf的实例 sf.registerViewModel("vm", new ViewModel()); //注册一个viewModel,起一个叫vm的别名 sf.init(); //调用init方法,开始初始化,sf正式开始一些列工做 //如下是viewModel的定义 function ViewModel() { this.message = "hello, SegmentFault"; this.buttonClickHandler = function() { this.message = "clicked: " + this.message; } } </script>
有没有以为SF的API干净利落,清新爽洁!es6
根据设计图的Step 1,先给已注册的viewModel加上监视,这里咱们须要一个Watcher类github
export class Watcher { private sf; //构造函数里传入一个sf的对象,便于callback调用时的做用域肯定。。。这是后话 constructor(sf) { this.sf = sf; } public observe(viewModel, callback) {} //暗中观察 }
再来看一下Step 2, 另外一个主要的类Scanner,Scanner是干什么的呢?做用就一个遍历整个DOM Tree把出现sf-xxxx这个attribute的Elements所有挑出来,而后找sf-xxxx = expression,等号右边这个表达式里若是出现了viewModel的alias,那就说么这个element是跟viewModel搭界了,是绑定在一块儿了,scanner负责把这对"恋人"关系用一个数据结构维护一下,等所有扫描完了一块儿返回给SegmentFault去听候发落。web
//Scanner.ts export class Scanner { private prefix = "sf-"; //库的前缀 private viewModelPool; constructor(viewModelPool) { this.viewModelPool = viewModelPool; //Scanner确定是为SegmentFault服务的,因此初始化的时候SegmentFault会把以前注册过的viewModel信息传给Scanner,便于它去扫描。 } public scanBindDOM():object {} //找出attribute里带sf-,且等号右边表达式里含有viewModel的alias的Element,并返回一个view与viewModel的map }
接下去,SegmentFault会得到Scanner.scanBindDOM()所返回的view_viewModel Map,来看看这个Map的具体数据结构express
//template { "vm_alias":[ { "viewModel":viewModel, "element":element, "expression":expression, "attributeName":attributeName } ] } //若是实际中的DOM Tree是这样的, <body> <p sf-text="userVM.username"></p> <input type="text" sf-value="userVM.username"> </body> //那么,Scanner扫描到的结果应该是 { "userVM":[ { "viewModel": userViewModel, "element": <p/>, "expression": "vm.username", "attributeName": "sf-text" }, { "viewModel": userViewModel, "element": <input>, "expression": "vm.username", "attributeName": "sf-value" } ] }
个人实现中特意定一个了一个BoundItem类来描述 {"viewModel":viewModel,"element":element,"expression":expression,"attributeName":attributeName}segmentfault
//BoundItem.ts export class BoundItem { public viewModel: object; public element: Element; public expression: string; public attributeName: string; constructor(viewModel: object, element: Element, expression: string, attributeName: string) { this.viewModel = viewModel; this.element = element; this.expression = expression; this.attributeName = attributeName; } }
拿到view_viewModel map后,SegmentFault会调用Renderer去挨个渲染每个BoundItem。
export class Renderer{ public render(boundItem:BoundItem) {}; }
好至此,几个主要的类都一一登场了,接下去咱们完善下SegmentFault类,让ta和其它几个类联动起来
import {Scanner} from "./Scanner"; import {Watcher} from "./Watcher"; import {Renderer} from "./Renderer"; export let SegmentFault = class SegmentFault { private viewModelPool = {}; private viewViewModelMap = {}; private renderer = new Renderer(); public init() { let scanner = new Scanner(this.viewModelPool); let watcher = new Watcher(this); //step 1, 暗中观察各个viewModel for (let key in this.viewModelPool) { watcher.observe(this.viewModelPool[key],this.viewModelChangedHandler); } /step 2 3, 扫描DOM Tree并返回Map this.viewViewModelMap = scanner.scanBindDOM(); //step 4, 渲染DOM Object.keys(this.viewViewModelMap).forEach(alias=>{ this.refresh(alias); }); }; public registerViewModel(alias:string, viewModel:object) { viewModel["_alias"] = alias; window[alias] = this.viewModelPool[alias] = viewModel; }; public refresh(alias:string){ let boundItems = this.viewViewModelMap[alias]; boundItems.forEach(boundItem => { this.renderer.render(boundItem); }); } private viewModelChangedHandler(viewModel,prop) { this.refresh(viewModel._alias); } }
好,写到这里,骨架所有构建完成,你有没有兴趣本身花点时间去填充血肉呢?
我但愿你能作到
这里贴出其它几个类的具体实现,仅供参考,你必定能够写得比我更好。
也放出github地址,上面有完整工程
https://github.com/momoko8443...
以及在线演示地址
https://momoko8443.github.io/...
//Watcher.ts export class Watcher { private sf; constructor(sf) { this.sf = sf; } public observe(viewModel, callback) { let host = this.sf; for (var key in viewModel) { var defaultValue = viewModel[key]; (function (k, dv) { if (k !== "_alias") { Object.defineProperty(viewModel, k, { get: function () { return dv; }, set: function (value) { dv = value; console.log("do something after set a new value"); callback.call(host, viewModel, k); } }); } })(key, defaultValue); } } }
//Scanner.ts import { BoundItem } from "./BoundItem"; export class Scanner { private prefix = "sf-"; private viewModelPool; constructor(viewModelPool) { this.viewModelPool = viewModelPool; } public scanBindDOM() :object{ let boundMap = {}; let boundElements = this.getAllBoundElements(this.prefix); boundElements.forEach(element => { for (let i = 0; i < element.attributes.length; i++) { let attr = element.attributes[i]; if (attr.nodeName.search(this.prefix) > -1) { let attributeName = attr.nodeName; let expression = element.getAttribute(attributeName); for (let alias in this.viewModelPool) { if (expression.search(alias + ".") != -1) { let boundItem = new BoundItem(this.viewModelPool[alias], element, expression,attributeName); if (!boundMap[alias]) { boundMap[alias] = [boundItem]; } else { boundMap[alias].push(boundItem); } } } } } }); return boundMap; } private fuzzyFind(element:HTMLElement,text:string):HTMLElement { if (element && element.attributes) { for (let i = 0; i < element.attributes.length; i++) { let attr = element.attributes[i]; if (attr.nodeName.search(text) > -1) { return element; } } } return null; } private getAllBoundElements(prefix): Array<HTMLElement> { let elements = []; let allChildren = document.querySelectorAll("*"); for (let i = 0; i < allChildren.length; i++) { let child: HTMLElement = allChildren[i] as HTMLElement; let matchElement = this.fuzzyFind(child, prefix); if (matchElement) { elements.push(matchElement); } } return elements; } }
//BoundItem.ts export class BoundItem { public viewModel: object; public element: Element; public expression: string; public attributeName: string; private interactiveDomConfig = { "INPUT":{ "text":"input", "password":"input", "email":"input", "url":"input", "tel":"input", "radio":"change", "checkbox":"change", "color":"change", "date":"change", "datetime":"change", "datetime-local":"change", "month":"change", "number":"change", "range":"change", "search":"change", "time":"change", "week":"change", "button":"N/A", "submit":"N/A" }, "SELECT":"change", "TEXTAREA":"change" } constructor(viewModel: object, element: Element, expression: string, attributeName: string) { this.viewModel = viewModel; this.element = element; this.expression = expression; this.attributeName = attributeName; this.addListener(this.element,this.expression); } private addListener(element,expression){ let tagName = element.tagName; let eventName = this.interactiveDomConfig[tagName]; if(!eventName){ return; } if(typeof eventName === "object"){ let type = element.getAttribute("type"); eventName = eventName[type]; } element.addEventListener(eventName, (e)=> { let newValue = (element as HTMLInputElement).value; let cmd = expression + "= \"" + newValue + "\""; try{ eval(cmd); }catch(e){ console.error(e); } }); } }
//Renderer.ts import {BoundItem} from "./BoundItem"; export class Renderer{ public render(boundItem:BoundItem) { var value = this.getValue(boundItem.viewModel, boundItem.expression); var attribute = boundItem.attributeName.split('-')[1]; if (attribute.toLowerCase() === "innertext") { attribute = "innerText"; } boundItem.element[attribute] = value; }; private getValue(viewModel, expression) { return (function () { var alias = viewModel._alias; var tempScope = {}; tempScope[alias] = viewModel; try { var pattern = new RegExp("\\b" + alias + "\\b", "gm"); expression = expression.replace(pattern, "tempScope." + alias); var result = eval(expression); tempScope = null; return result; } catch (e) { throw e; } })(); } }
【教学向】150行代码教你实现一个低配版的MVVM库(1)- 原理篇
【教学向】150行代码教你实现一个低配版的MVVM库(2)- 代码篇
【教学向】再加150行代码教你实现一个低配版的web component库(1) —设计篇
【教学向】再加150行代码教你实现一个低配版的web component库(2) —原理篇