博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
从零到一编写MVVM
阅读量:7177 次
发布时间:2019-06-29

本文共 14400 字,大约阅读时间需要 48 分钟。

简介

公司H5页面较多,为了开发效率,通过查阅资料和angular源码,就写了这个小框架,这个只适用于小项目,运行效率和速度上还存在这一些问题,只能做到全量渲染,如果有时间,可以不断的完善它。

分析

它关键点就 Object.defineProperty 在这个方法,通过  get set  来达到数据变更更新视图。

Object.defineProperty(data, key, {    get: () => {        return data[key]    },    set: (val) => {        data[key] = val    }})复制代码

代理数组方法,来达到更新的目的。

defValue(obj, key, val, enumerable) {    Object.defineProperty(obj, key, {        value: val,        enumerable: !!enumerable,        configurable: true,        writable: true    })}let arrayProto = Array.prototype;let arrayMethods = Object.create(arrayProto);[    'push',    'pop',    'shift',    'unshift',    'splice',    'sort',    'reverse'].forEach(method => {    let original = arrayMethods[method];    this.defValue(arrayMethods, method,  function() {        let result = original.apply(this, arguments);        return result;    })})复制代码

模板编译

这边采用的是angular5+的模板方式。

用到了两个比较关键的函数 with、eval ,这两个函数的运行速度很慢,暂时想不出怎么去解析表达式,目前正在看angular的源码,看能不能发现更牛的黑科技,来提升这个小框架的运行速度和灵活性。  

export class Compile {    constructor(ref, value, dep) {        this.vm = value;        this.ref = ref;        this.dep = dep;        this.ref.style.display = 'none';        this.compileElement(this.ref);        this.ref.style.display = 'block';    }    ref;    vm;    dep;    eventReg = /\((.*)\)/;    attrReg = /\[(.*)\]/;    valueReg = /\{\{((?:.|\n)+?)\}\}/;    compileElement(ref, vm = this.vm) {        let childNodes = ref.childNodes;        if (!childNodes.length) return;        Array.from(childNodes).every(node => {            return this.compileNode(node, vm);        })    }    compileNode(node, vm = this.vm) {        let text = node.textContent;        if (node.nodeType === 1) {            Array.from(node.attributes).every(attr => {                //事件                if (this.eventReg.test(attr.nodeName)) {                    this.compileEvent(node, attr, vm)                }                //属性                if (this.attrReg.test(attr.nodeName)) {                    this.compileAttr(node, attr, vm);                    this.dep.add(() => {                        this.compileAttr(node, attr, vm)                    })                }                //模板 *if                if (attr.nodeName === '*if') {                    this.compileIf(node, attr, vm);                    this.dep.add(() => {                        this.compileIf(node, attr, vm)                    })                    node.removeAttribute(attr.nodeName)                }                //模板 *for                if (attr.nodeName === '*for') {                    let comment = document.createComment(attr.nodeValue)                    comment.$node = node;                    node.parentNode.insertBefore(comment, node);                    node.parentNode.removeChild(node);                    let nodes = this.compileFor(comment, attr);                    this.dep.add(() => {                        this.compileFor(comment, attr, nodes);                    })                }                return true;            })        }        //绑值表达式 {
{}} /\s*(\.)\s*/ if (node.nodeType === 3 && this.valueReg.test(text)) { node.$textContent = node.textContent.replace(/\s*(\.)\s*/, '.'); this.compileText(node, vm); this.dep.add(() => { this.compileText(node, vm) }) } if (node.childNodes && node.childNodes.length && !~Array.from(node.attributes).map(attr => attr.nodeName).indexOf('*for')) { this.compileElement(node, vm); } return true; } getForFun(exg) { let exgs = exg.split(/;/); let vs; let is = undefined; if (exgs instanceof Array && exgs.length) { vs = exgs[0].match(/let\s+(.*)\s+of\s+(.*)/); let index = exgs[1].match(/let\s+(.*)\s?=\s?index/); if (index instanceof Array && index.length) { is = index[1].trim(); } } return new Function('vm', ` return function (fn) { for (let ${vs[1]} of vm.${vs[2]}){ fn && fn(${vs[1]}, vm.${vs[2]}.indexOf(${vs[1]}), vm, '${vs[1]}', '${is}') } } `) } compileFor(comment, attr, arr = []) { let node = comment.$node; if (arr instanceof Array && arr.length) { arr.every(n => { comment.parentNode.removeChild(n); return true; }); arr.length = 0; } this.getForFun(attr.nodeValue)(this.vm)((a, b, c, d, e) => { let copy = node.cloneNode(true); copy.removeAttribute('*for'); copy.style.removeProperty('display'); if (!copy.getAttribute('style')) copy.removeAttribute('style'); comment.parentNode.insertBefore(copy, comment); arr.push(copy); let data = Object.create(this.vm.__proto__); data[d] = a; data[e] = b; this.compileNode(copy, data); }); return arr; } compileIf(node, attr, vm = this.vm) { let bo = !!this.compileFun(attr.nodeValue, vm); node.style.display = bo ? 'block' : 'none'; } compileText(node, vm = this.vm) { let textContent = node.$textContent; let values = textContent.match(new RegExp(this.valueReg, 'ig')); values.every(va => { textContent.replace(va, value => { let t = value.match(this.valueReg); let val = this.isBooleanValue(this.compileFun(t[1], vm)); textContent = textContent.replace(t[0], val) }); return true; }); node.textContent = textContent; } compileFun(exg, vm) { let fun = new Function('vm', ` with(vm){
return eval("${exg.replace(/'/g, '\\\'').replace(/"/g, '\\\"')}")} `); return fun(vm); } isBooleanValue(val) { switch (val) { case true: return String(true); case false: return String(false); case null: return String(); case void 0: return String(); default: return String(val) } } compileEvent(node, attr, vm = this.vm) { let event = attr.nodeName.match(this.eventReg)[1]; switch (event) { case 'model': if (node instanceof HTMLInputElement || node instanceof HTMLTextAreaElement) { switch (node.type) { case 'text': node.oninput = (event) => { this.compileFun(`${attr.nodeValue}='${event.target.value}'`, vm) }; break; case 'textarea': node.oninput = (event) => { this.compileFun(`${attr.nodeValue}='${event.target.value}'`, vm) }; break; case 'checkbox': node.onchange = (event) => { this.compileFun(`${attr.nodeValue}=${event.target.checked}`, vm) }; break; case 'radio': node.onchange = (event) => { this.compileFun(`${attr.nodeValue}='${event.target.value}'`, vm) }; break; } } break; default: node[`on${event}`] = (event) => { vm.__proto__.$event = event; this.compileFun(attr.nodeValue, vm); Reflect.deleteProperty(vm.__proto__, '$event'); }; } node.removeAttribute(attr.nodeName) } compileAttr(node, attr, vm = this.vm) { let event = attr.nodeName.match(this.attrReg)[1]; switch (event) { case '(model)': case 'model': if (node instanceof HTMLInputElement || node instanceof HTMLTextAreaElement) { switch (node.type) { case 'text': case 'textarea': node.value = this.compileFun(attr.nodeValue, vm); break; case 'checkbox': node.checked = !!this.compileFun(attr.nodeValue, vm); break; case 'radio': if (node.value === String(this.compileFun(attr.nodeValue, vm))) { node.checked = true; } break; } } break; case 'value': if (node instanceof HTMLInputElement || node instanceof HTMLTextAreaElement) { break; } default: let attrs = event.split(/\./); let attrValue = this.compileFun(attr.nodeValue, vm); if (attrs[0] in node && attrs.length === 1) { node[attrs[0]] = attrValue; break; } if (attrs.length >= 2) { switch (attrs[0]) { case 'attr': node.setAttribute(attrs[1], attrValue); break; case 'class': if (!!attrValue) { node.classList.add(attrs[1]); } else { node.classList.remove(attrs[1]); } break; case 'style': let val = attrs[2] ? (attrValue ? (attrValue + attrs[2]) : '') : (attrValue || ''); if (val) { node.style[attrs[1]] = val; } else { node.style.removeProperty(attrs[1]) } break; } } } node.removeAttribute(attr.nodeName) }}复制代码

  1. 支持所有的dom事件,比如 (click)等。
  2. 支持属性的值绑定,[attr.xxx]、[class.xxx]、[style.xxx]等。
  3. 支持所有的表达式,比如三元等
  4. 支持双向绑定, [(model)]  和angular的有点不同,用法一样,这样写的原因在于属性的key所有的字母会转为小写,这个比较坑。比如这个 [innerHTML]  也无法使用,有空了再去解决。
  5. 支持dom事件传递当前event对象,比如 (click)="test($event)" 。
        

订阅与发布

在编译的过程中,将需要变更的dom通过订阅的方式保存起来,数据变更后通过发布来达到视图的更新

class Dep {    constructor() {    }    subs = [];    //添加订阅    add(sub) {        this.subs.unshift(sub);    }    remove(sub) {        let index = this.subs.indexOf(sub);        if (index !== -1) {            this.subs.splice(index, 1);        }    }    //更新    notify() {        this.subs.forEach(sub => {            if (sub instanceof Function) sub();        });    }}复制代码

可以看出,更新的方式是全量更新。

这边再需要一个类将这几个类关联起来

export class MVVM {    constructor(id, value) {        if (!id) throw `dom节点不能为空`;        if (!value) throw `值不能为空`;        this.vm = value;        this.ref = id;        this.dep = new Dep();        if (!(this.ref instanceof Element)) {            this.ref = window.document.querySelector(`${this.ref}`)        }        /**         * 解析         */        new Compile(this.ref, this.vm, this.dep);        /**         * 值变更检测         */        this.def(this.vm)    }    vm;    ref;    dep;    defValue(obj, key, val, enumerable) {        Object.defineProperty(obj, key, {            value: val,            enumerable: !!enumerable,            configurable: true,            writable: true        })    }    copyAugment(target, src, keys) {        for (let i = 0, l = keys.length; i < l; i++) {            let key = keys[i];            this.defValue(target, key, src[key]);        }    }    def(data) {        if (!data || typeof data !== 'object') {            return;        }        if (data instanceof Array) {            let arrayProto = Array.prototype;            let arrayMethods = Object.create(arrayProto);            [                'push',                'pop',                'shift',                'unshift',                'splice',                'sort',                'reverse'            ].forEach(method => {                let original = arrayMethods[method];                let that = this;                this.defValue(arrayMethods, method,  function() {                    let result = original.apply(this, arguments);                    that.dep.notify();                    return result;                })            })            this.copyAugment(data, arrayMethods, Object.getOwnPropertyNames(arrayMethods))            Object.keys(data).forEach(key => {                this.def(data[key]);                data[`_${key}`] = data[key];                Object.defineProperty(data, key, {                    get: () => {                        return data[`_${key}`]                    },                    set: (val) => {                        this.def(val);                        data[`_${key}`] = val;                        this.dep.notify()                    }                })            })        } else {            Object.keys(data).forEach(key => {                this.def(data[key]);                data[`_${key}`] = data[key];                Object.defineProperty(data, key, {                    get: () => {                        return data[`_${key}`]                    },                    set: (val) => {                        this.def(val);                        data[`_${key}`] = val;                        this.dep.notify()                    }                })            })        }    }}复制代码

写到这,算是完成了,再写个测试用例。

测试用例

class Test {    constructor(id) {        this.a = 1;        this.b = 2;        this.list = [            {id: 1, name: '一'},            {id: 2, name: '二'},            {id: 3, name: '三'},            {id: 4, name: '四'},            {id: 5, name: '五'},        ];        new MVVM(id, this);    }    test(event,data){        console.info(event);    }    bo(data){        return data;    }}new Test("#body");复制代码

{

{a?'1111':Math.random() + Math.abs(a-200) + 'a'}}

{

{ a + b }} {
{ a * b }}

{

{index}} {
{i}}

*if

复制代码

这个小框架也就能完成简单繁琐的任务,建议不要在大型项目中使用,写H5页面搓搓有余的,还是有些不足的地方,循环模板 *for  内部不能使用当前环境下(即this)的方法,后续有空修复。如果有不足的地方欢迎留言。

转载地址:http://frdzm.baihongyu.com/

你可能感兴趣的文章
销傲中国式销售过程管理系统功能概述
查看>>
IDEA 学习笔记之 Java项目开发深入学习(1)
查看>>
重建二叉树 (剑指offer第六题)
查看>>
爬虫基础 pyquery 详解
查看>>
QT creator+OpenCV2.4.2+MinGW 在windows下开发环境配置
查看>>
Allegro PCB Design GXL (legacy) 设置十字大光标
查看>>
数据结构--图的定义和存储结构
查看>>
[C#参考]委托机制
查看>>
linux常用命令
查看>>
自然杂志上的影评
查看>>
SQL Server 存储过程
查看>>
Appium自动化测试1 - 安装部署
查看>>
广州.NET微软技术俱乐部微信群各位技术大牛的blog
查看>>
《Redis设计与实现》之第九章:数据库
查看>>
10月10日学习内容整理:socketserver模块,ftp作业讲解
查看>>
P1352 没有上司的舞会
查看>>
Bzoj 1648: [Usaco2006 Dec]Cow Picnic 奶牛野餐 深搜,bitset
查看>>
关于《淘宝技术这十年》
查看>>
c#事件机制
查看>>
冒泡排序
查看>>