简介
公司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) }}复制代码
- 支持所有的dom事件,比如 (click)等。
- 支持属性的值绑定,[attr.xxx]、[class.xxx]、[style.xxx]等。
- 支持所有的表达式,比如三元等
- 支持双向绑定,
[(model)]
和angular的有点不同,用法一样,这样写的原因在于属性的key所有的字母会转为小写,这个比较坑。比如这个[innerHTML]
也无法使用,有空了再去解决。 - 支持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)的方法,后续有空修复。如果有不足的地方欢迎留言。