公司H5頁面較多,爲了開發效率,經過查閱資料和angular源碼,就寫了這個小框架,這個只適用於小項目,運行效率和速度上還存在這一些問題,只能作到全量渲染,若是有時間,能夠不斷的完善它。javascript
它關鍵點就 Object.defineProperty
在這個方法,經過 get set
來達到數據變動更新視圖。java
Object.defineProperty(data, key, {
get: () => {
return data[key]
},
set: (val) => {
data[key] = val
}
})複製代碼
代理數組方法,來達到更新的目的。node
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的源碼,看能不能發現更牛的黑科技,來提高這個小框架的運行速度和靈活性。 bash
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)
}
}複製代碼
[(model)]
和angular的有點不一樣,用法同樣,這樣寫的緣由在於屬性的key全部的字母會轉爲小寫,這個比較坑。好比這個 [innerHTML]
也沒法使用,有空了再去解決。(click)="test($event)"
。在編譯的過程當中,將須要變動的dom經過訂閱的方式保存起來,數據變動後經過發佈來達到視圖的更新app
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();
});
}
}複製代碼
能夠看出,更新的方式是全量更新。框架
這邊再須要一個類將這幾個類關聯起來dom
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");複製代碼
<p [attr.data-id]="a" [style.width.px]="a" [class.test]="bo(false)" [style.z-index]="a" (click)="test($event.target,a)"></p>
<p>{{a?'1111':Math.random() + Math.abs(a-200) + 'a'}}</p>
<p>{{ a + b }} {{ a * b }}</p>
<p *for="let i of list;let index = index;">
<span>{{index}}</span>
<a href="javascript:void 0" (click)="test($event,i)">{{i}}</a>
</p>
<p *if="e">*if</p>
<input [(model)]="a" type="text">
<input type="checkbox" [(model)]="b">
<input type="radio" value="1" name="radio1" [(model)]="a">
<input type="radio" value="2" name="radio1" [(model)]="a">
<input type="radio" value="3" name="radio1" [(model)]="a">
<input type="radio" value="4" name="radio1" [(model)]="a">複製代碼
這個小框架也就能完成簡單繁瑣的任務,建議不要在大型項目中使用,寫H5頁面搓搓有餘的,仍是有些不足的地方,循環模板 *for
內部不能使用當前環境下(即this)的方法,後續有空修復。若是有不足的地方歡迎留言。測試