资讯专栏INFORMATION COLUMN

【教学向】150行代码教你实现一个低配版的MVVM库(2)- 代码篇

loonggg / 3625人阅读

摘要:也放出地址,上面有完整工程以及在线演示地址相关阅读教学向行代码教你实现一个低配版的库原理篇教学向行代码教你实现一个低配版的库代码篇教学向再加行代码教你实现一个低配版的库设计篇教学向再加行代码教你实现一个低配版的库原理篇

书接上一篇: 150行代码教你实现一个低配版的MVVM库(1)- 原理篇

写在前面

为了便于分模块,和阅读,我使用了Typescript来进行coding,总行数是正好150行,最早写DEMO的时候用了ES2015,代码行数应该在100行出头,如果你不会搭ts+webpack的编译UMD环境,你也可以把本文中的ts语法人肉转成es6或者es2015,我相信这对你(一个有志于学写mvvm库的青年)来说没有什么难度。

作为作者呢,虽然最后我会放出源码的地址,你可以去github上扫一眼代码,但我还是希望你们可以跟我一起,打开个文本编辑器,一个模块一个模块把代码人肉敲出来,这样的感觉是不一样的,就好比是你可能之前就阅读过angular,vue的源码,但你现在还不是在读我的文章么?

第一步 先把骨架搭好, 血肉晚点再填充

还是再上一遍设计图

设计的类不多,一共就5个

//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以达到双向绑定的目的
不妨再看看使用效果

 

有没有觉得SF的API干净利落,清新爽洁!

根据设计图的Step 1,先给已注册的viewModel加上监视,这里我们需要一个Watcher类

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去听候发落。

//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的具体数据结构

//template
{
    "vm_alias":[
        {
            "viewModel":viewModel,
            "element":element,
            "expression":expression,
            "attributeName":attributeName
        }
    ]
}
//如果实际中的DOM Tree是这样的,

    

//那么,Scanner扫描到的结果应该是 { "userVM":[ { "viewModel": userViewModel, "element":

, "expression": "vm.username", "attributeName": "sf-text" }, { "viewModel": userViewModel, "element": , "expression": "vm.username", "attributeName": "sf-value" } ] }

我的实现中特地定一个了一个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会调用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 {
        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;
            }
        })();
    }
}
相关阅读

【教学向】150行代码教你实现一个低配版的MVVM库(1)- 原理篇
【教学向】150行代码教你实现一个低配版的MVVM库(2)- 代码篇
【教学向】再加150行代码教你实现一个低配版的web component库(1) —设计篇
【教学向】再加150行代码教你实现一个低配版的web component库(2) —原理篇

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/87385.html

相关文章

  • 教学】再加150代码教你实现一个低配版的web component(1) —设计

    摘要:为的内置一个方法,用法和原生的事件机制一毛一样。 前言 上两篇Mvvm教程的热度超出我的预期,很多码友留言表扬同时希望我继续出下一篇教程,当时我也半开玩笑说只要点赞超10就兑现承诺,没想到还真破了10,所以就有了今天的文章。 准备工作 熟读 【教学向】150行代码教你实现一个低配版的MVVM库(1)- 原理篇【教学向】150行代码教你实现一个低配版的MVVM库(2)- 代码篇 本篇是在...

    Clect 评论0 收藏0
  • 教学150代码教你实现一个低配版的MVVM(1)- 原理

    摘要:模块则负责维护,以及各个模块间的调度思考题了解了的实现机制,你能否自己动手也试着用百来行代码实现一个库呢好了本教程第一部分设计篇就写到这里,具体请移步下一篇教学向行代码教你实现一个低配版的库代码篇我会用给出一版实现。 适读人群 本文适合对MVVM有一定了解(如有主流框架ng,vue等使用经验配合本文服用则效果更佳),虽然会用这类框架,但是对框架底层核心实现又不太清楚,或者能说出个所以然...

    selfimpr 评论0 收藏0
  • 2017-08-29 前端日报

    摘要:前端日报精选浏览器兼容性问题解决方案配置指南全新的模块化框架,知乎专栏现学现卖中文教学向再加行代码教你实现一个低配版的库原理篇我把最美的青春都献给了代码技术周刊开启浏览器全屏模式如何进行的操作掘金内存分配与垃圾回收写一 2017-08-29 前端日报 精选 浏览器兼容性问题解决方案AlloyTeam ESLint 配置指南全新的redux模块化框架,redux-arena - 知乎专栏...

    atinosun 评论0 收藏0
  • 个人分享--web前端学习资源分享

    摘要:前言月份开始出没社区,现在差不多月了,按照工作的说法,就是差不多过了三个月的试用期,准备转正了一般来说,差不多到了转正的时候,会进行总结或者分享会议那么今天我就把看过的一些学习资源主要是博客,博文推荐分享给大家。 1.前言 6月份开始出没社区,现在差不多9月了,按照工作的说法,就是差不多过了三个月的试用期,准备转正了!一般来说,差不多到了转正的时候,会进行总结或者分享会议!那么今天我就...

    sherlock221 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<