簡介
公司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 = /{{((?:.|
)+?)}}/;
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(/lets+(.*)s+ofs+(.*)/);
let index = exgs[1].match(/lets+(.*)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");複製程式碼
<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)的方法,後續有空修復。如果有不足的地方歡迎留言。