【教學向】150行代碼教你實現一個低配版的MVVM庫(2)- 代碼篇

書接上一篇: 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) —原理篇

相關文章
相關標籤/搜索