摘要:也放出地址,上面有完整工程以及在線演示地址相關(guān)閱讀教學(xué)向行代碼教你實現(xiàn)一個低配版的庫原理篇教學(xué)向行代碼教你實現(xiàn)一個低配版的庫代碼篇教學(xué)向再加行代碼教你實現(xiàn)一個低配版的庫設(shè)計篇教學(xué)向再加行代碼教你實現(xiàn)一個低配版的庫原理篇
書接上一篇: 150行代碼教你實現(xiàn)一個低配版的MVVM庫(1)- 原理篇
寫在前面為了便于分模塊,和閱讀,我使用了Typescript來進(jìn)行coding,總行數(shù)是正好150行,最早寫DEMO的時候用了ES2015,代碼行數(shù)應(yīng)該在100行出頭,如果你不會搭ts+webpack的編譯UMD環(huán)境,你也可以把本文中的ts語法人肉轉(zhuǎn)成es6或者es2015,我相信這對你(一個有志于學(xué)寫mvvm庫的青年)來說沒有什么難度。
作為作者呢,雖然最后我會放出源碼的地址,你可以去github上掃一眼代碼,但我還是希望你們可以跟我一起,打開個文本編輯器,一個模塊一個模塊把代碼人肉敲出來,這樣的感覺是不一樣的,就好比是你可能之前就閱讀過angular,vue的源碼,但你現(xiàn)在還不是在讀我的文章么?
第一步 先把骨架搭好, 血肉晚點再填充還是再上一遍設(shè)計圖
設(shè)計的類不多,一共就5個
//SegmentFault.ts export let SegmentFault = class SegmentFault { private viewModelPool = {}; //用來維護(hù)viewModel的別名alias與viewModel之間的關(guān)系 private viewViewModelMap = {};//用來維護(hù)viewModel和被綁定的view之間的關(guān)系 public registerViewModel(alias:string, vm:object) {};//在sf正式運作之前我們先要注冊一個下viewModel并給他起一個別名 public init() {}; //sf庫開始運作的入口函數(shù) public refresh(alias:string){}; // 暴露一個強制刷新整個viewModel的方法,因為畢竟有你監(jiān)控不到的角落 }
SegmentFault是對用戶暴露的唯一的對象,就像Angular他會暴露一個angular對象給用戶使用一樣。
最終,用戶會這樣來操作SF以達(dá)到雙向綁定的目的
不妨再看看使用效果
有沒有覺得SF的API干凈利落,清新爽潔!
根據(jù)設(shè)計圖的Step 1,先給已注冊的viewModel加上監(jiān)視,這里我們需要一個Watcher類
export class Watcher { private sf; //構(gòu)造函數(shù)里傳入一個sf的對象,便于callback調(diào)用時的作用域確定。。。這是后話 constructor(sf) { this.sf = sf; } public observe(viewModel, callback) {} //暗中觀察 }
再來看一下Step 2, 另一個主要的類Scanner,Scanner是干什么的呢?作用就一個遍歷整個DOM Tree把出現(xiàn)sf-xxxx這個attribute的Elements全部挑出來,然后找sf-xxxx = expression,等號右邊這個表達(dá)式里如果出現(xiàn)了viewModel的alias,那就說么這個element是跟viewModel搭界了,是綁定在一起了,scanner負(fù)責(zé)把這對"戀人"關(guān)系用一個數(shù)據(jù)結(jié)構(gòu)維護(hù)一下,等全部掃描完了一起返回給SegmentFault去聽候發(fā)落。
//Scanner.ts export class Scanner { private prefix = "sf-"; //庫的前綴 private viewModelPool; constructor(viewModelPool) { this.viewModelPool = viewModelPool; //Scanner肯定是為SegmentFault服務(wù)的,所以初始化的時候SegmentFault會把之前注冊過的viewModel信息傳給Scanner,便于它去掃描。 } public scanBindDOM():object {} //找出attribute里帶sf-,且等號右邊表達(dá)式里含有viewModel的alias的Element,并返回一個view與viewModel的map }
接下去,SegmentFault會獲得Scanner.scanBindDOM()所返回的view_viewModel Map,來看看這個Map的具體數(shù)據(jù)結(jié)構(gòu)
//template { "vm_alias":[ { "viewModel":viewModel, "element":element, "expression":expression, "attributeName":attributeName } ] } //如果實際中的DOM Tree是這樣的, //那么,Scanner掃描到的結(jié)果應(yīng)該是 { "userVM":[ { "viewModel": userViewModel, "element": , "expression": "vm.username", "attributeName": "sf-text" }, { "viewModel": userViewModel, "element": , "expression": "vm.username", "attributeName": "sf-value" } ] }
我的實現(xiàn)中特地定一個了一個BoundItem類來描述 {"viewModel":viewModel,"element":element,"expression":expression,"attributeName":attributeName}
//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會調(diào)用Renderer去挨個渲染每一個BoundItem。
export class Renderer{ public render(boundItem:BoundItem) {}; }
好至此,幾個主要的類都一一登場了,接下去我們完善下SegmentFault類,讓ta和其它幾個類聯(lián)動起來
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); } }
好,寫到這里,骨架全部構(gòu)建完成,你有沒有興趣自己花點時間去填充血肉呢?
我希望你能做到
這里貼出其它幾個類的具體實現(xiàn),僅供參考,你一定可以寫得比我更好。
也放出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{ 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("" + alias + "", "gm"); expression = expression.replace(pattern, "tempScope." + alias); var result = eval(expression); tempScope = null; return result; } catch (e) { throw e; } })(); } }相關(guān)閱讀
【教學(xué)向】150行代碼教你實現(xiàn)一個低配版的MVVM庫(1)- 原理篇
【教學(xué)向】150行代碼教你實現(xiàn)一個低配版的MVVM庫(2)- 代碼篇
【教學(xué)向】再加150行代碼教你實現(xiàn)一個低配版的web component庫(1) —設(shè)計篇
【教學(xué)向】再加150行代碼教你實現(xiàn)一個低配版的web component庫(2) —原理篇
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://specialneedsforspecialkids.com/yun/87385.html
摘要:為的內(nèi)置一個方法,用法和原生的事件機制一毛一樣。 前言 上兩篇Mvvm教程的熱度超出我的預(yù)期,很多碼友留言表揚同時希望我繼續(xù)出下一篇教程,當(dāng)時我也半開玩笑說只要點贊超10就兌現(xiàn)承諾,沒想到還真破了10,所以就有了今天的文章。 準(zhǔn)備工作 熟讀 【教學(xué)向】150行代碼教你實現(xiàn)一個低配版的MVVM庫(1)- 原理篇【教學(xué)向】150行代碼教你實現(xiàn)一個低配版的MVVM庫(2)- 代碼篇 本篇是在...
摘要:模塊則負(fù)責(zé)維護(hù),以及各個模塊間的調(diào)度思考題了解了的實現(xiàn)機制,你能否自己動手也試著用百來行代碼實現(xiàn)一個庫呢好了本教程第一部分設(shè)計篇就寫到這里,具體請移步下一篇教學(xué)向行代碼教你實現(xiàn)一個低配版的庫代碼篇我會用給出一版實現(xiàn)。 適讀人群 本文適合對MVVM有一定了解(如有主流框架ng,vue等使用經(jīng)驗配合本文服用則效果更佳),雖然會用這類框架,但是對框架底層核心實現(xiàn)又不太清楚,或者能說出個所以然...
摘要:前端日報精選瀏覽器兼容性問題解決方案配置指南全新的模塊化框架,知乎專欄現(xiàn)學(xué)現(xiàn)賣中文教學(xué)向再加行代碼教你實現(xiàn)一個低配版的庫原理篇我把最美的青春都獻(xiàn)給了代碼技術(shù)周刊開啟瀏覽器全屏模式如何進(jìn)行的操作掘金內(nèi)存分配與垃圾回收寫一 2017-08-29 前端日報 精選 瀏覽器兼容性問題解決方案AlloyTeam ESLint 配置指南全新的redux模塊化框架,redux-arena - 知乎專欄...
摘要:前言月份開始出沒社區(qū),現(xiàn)在差不多月了,按照工作的說法,就是差不多過了三個月的試用期,準(zhǔn)備轉(zhuǎn)正了一般來說,差不多到了轉(zhuǎn)正的時候,會進(jìn)行總結(jié)或者分享會議那么今天我就把看過的一些學(xué)習(xí)資源主要是博客,博文推薦分享給大家。 1.前言 6月份開始出沒社區(qū),現(xiàn)在差不多9月了,按照工作的說法,就是差不多過了三個月的試用期,準(zhǔn)備轉(zhuǎn)正了!一般來說,差不多到了轉(zhuǎn)正的時候,會進(jìn)行總結(jié)或者分享會議!那么今天我就...
閱讀 1072·2021-11-25 09:43
閱讀 696·2021-11-22 14:45
閱讀 3816·2021-09-30 09:48
閱讀 1061·2021-08-31 09:41
閱讀 1970·2019-08-30 13:52
閱讀 1976·2019-08-30 11:24
閱讀 1341·2019-08-30 11:07
閱讀 950·2019-08-29 12:15