很多時候,寫代碼就像砌磚頭,只要我們不關心蓋樓的原因、建築的原理、土木工程基礎和工程經驗,就算我們砌了100棟高樓,我們也就只是一個砌磚工人,永遠也成為不了一個工程師,更別說建築師了。而那些包工頭也只會把我們當成勞動力罷了。——左耳朵耗子
avalon在兼容舊式IE上做了大量工作,從而讓它更接地氣,完美地運行於國內的各種奇葩瀏覽器中。
首先是Object.defineProperties的模擬,正因為有這東西,才能讓avalon是純事件驅動地同步視圖,而不用臟檢測,從而獲得更高的性能。
//IE6-8使用VBScript類的set get語句實現
if (!defineProperties && window.VBArray) {
window.execScript([
"Function parseVB(code)",
"\tExecuteGlobal(code)",
"End Function"
].join("\n"), "VBScript")
function VBMediator(accessingProperties, name, value) {
var accessor = accessingProperties[name]
if (arguments.length === 3) {
accessor(value)
} else {
return accessor()
}
}
defineProperties = function(name, accessingProperties, normalProperties) {
var className = "VBClass" + setTimeout("1"),
buffer = []
buffer.push(
"Class " + className,
"\tPrivate [__data__], [__proxy__]",
"\tPublic Default Function [__const__](d, p)",
"\t\tSet [__data__] = d: set [__proxy__] = p",
"\t\tSet [__const__] = Me", //鏈式調用
"\tEnd Function")
//添加普通屬性,因為VBScript對象不能像JS那樣隨意增刪屬性,必須在這里預先定義好
for (name in normalProperties) {
buffer.push("\tPublic [" + name + "]")
}
buffer.push("\tPublic [" + 'hasOwnProperty' + "]")
//添加訪問器屬性
for (name in accessingProperties) {
if (!(name in normalProperties)) { //防止重復定義
buffer.push(
//由於不知對方會傳入什么,因此set, let都用上
"\tPublic Property Let [" + name + "](val" + expose + ")", //setter
"\t\tCall [__proxy__]([__data__], \"" + name + "\", val" + expose + ")",
"\tEnd Property",
"\tPublic Property Set [" + name + "](val" + expose + ")", //setter
"\t\tCall [__proxy__]([__data__], \"" + name + "\", val" + expose + ")",
"\tEnd Property",
"\tPublic Property Get [" + name + "]", //getter
"\tOn Error Resume Next", //必須優先使用set語句,否則它會誤將數組當字符串返回
"\t\tSet[" + name + "] = [__proxy__]([__data__],\"" + name + "\")",
"\tIf Err.Number <> 0 Then",
"\t\t[" + name + "] = [__proxy__]([__data__],\"" + name + "\")",
"\tEnd If",
"\tOn Error Goto 0",
"\tEnd Property")
}
}
buffer.push("End Class") //類定義完畢
buffer.push(
"Function " + className + "Factory(a, b)", //創建實例並傳入兩個關鍵的參數
"\tDim o",
"\tSet o = (New " + className + ")(a, b)",
"\tSet " + className + "Factory = o",
"End Function")
window.parseVB(buffer.join("\r\n")) //先創建一個VB類工廠
return window[className + "Factory"](accessingProperties, VBMediator) //得到其產品
}
option元素的value值的提取。在規范中,如果用戶沒有顯式定義value,則會對其innerHTML進行兩邊對空白操作,作為value值。但如何判定用戶是否顯示定義value值呢,IE67是沒有hasAttribute方法,此外還有其他兼容問題,而jQuery的做法太羅索。看avalon的實現:
var roption = /^<option(?:\s+\w+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]+))?)*\s+value[\s=]/i
var valHooks = {
"option:get": function(node) {
//在IE11及W3C,如果沒有指定value,那么node.value默認為node.text(存在trim作),但IE9-10則是取innerHTML(沒trim操作)
if (node.hasAttribute) {
return node.hasAttribute("value") ? node.value : node.text
}
//specified並不可靠,因此通過分析outerHTML判定用戶有沒有顯示定義value
return roption.test(node.outerHTML) ? node.value : node.text
},
//.....
}
舊式IE下高性能獲對所有綁定屬性:
//IE67下,在循環綁定中,一個節點如果是通過cloneNode得到,自定義屬性的specified為false,無法進入里面的分支,
//但如果我們去掉scanAttr中的attr.specified檢測,一個元素會有80+個特性節點(因為它不區分固有屬性與自定義屬性),很容易卡死頁面
if (!"1" [0]) {
var cacheAttr = createCache(512)
var rattrs = /\s+(ms-[^=\s]+)(?:=("[^"]*"|'[^']*'|[^\s>]+))?/g,
rquote = /^['"]/,
rtag = /<\w+\b(?:(["'])[^"]*?(\1)|[^>])*>/i
var getAttributes = function(elem) {
if (elem.outerHTML.slice(0, 2) == "</") {//處理舊式IE模擬HTML5新元素帶來的偽標簽
return []
}
var str = elem.outerHTML.match(rtag)[0]
var attributes = [],
match,
k, v;
if (cacheAttr[str]) {
return cacheAttr[str]
}
while (k = rattrs.exec(str)) {
v = k[2]
var name = k[1].toLowerCase()
match = name.match(rmsAttr)
var binding = {
name: name,
specified: true,
value: v ? rquote.test(v) ? v.slice(1, -1) : v : ""
}
attributes.push(binding)
}
return cacheAttr(str, attributes)
}
}
avalon允許使用script, noscript, textaea作為子模塊的容器,但script節點需要修改type屬性,textarea要手動display:none,noscript無疑是最好的選擇,但noscript在IE78中竟然抽風了,在chrome下也有坑。avalon被逼又正則一番了……
var rnoscripts = /<noscript.*?>(?:[\s\S]+?)<\/noscript>/img
var rnoscriptText = /<noscript.*?>([\s\S]+?)<\/noscript>/im
if (el.tagName === "NOSCRIPT" && !(el.innerHTML || el.fixIE78)) { //IE7-8 innerText,innerHTML都無法取得其內容,IE6能取得其innerHTML
var xhr = getXHR() //IE9-11與chrome的innerHTML會得到轉義的內容,它們的innerText可以
xhr.open("GET", location, false) //謝謝Nodejs 亂燉群 深圳-純屬虛構
xhr.send(null)
//http://bbs.csdn.net/topics/390349046?page=1#post-393492653
var noscripts = DOC.getElementsByTagName("noscript")
var array = (xhr.responseText || "").match(rnoscripts) || []
var n = array.length
for (var i = 0; i < n; i++) {
var tag = noscripts[i]
if (tag) { //IE6-8中noscript標簽的innerHTML,innerText是只讀的
tag.style.display = "none" //http://haslayout.net/css/noscript-Ghost-Bug
tag.fixIE78 = (array[i].match(rnoscriptText) || ["", " "])[1]
}
}
}
A,IMG標簽的src, href路徑的轉義,這個真是夠隱秘啊!
if (!W3C && (method === "src" || method === "href")) {
val = val.replace(/&/g, "&")//處理IE67自動轉義的問題
}
oninput事件在IE6-9的兼容問題:
if (W3C) { //先執行W3C
element.addEventListener("input", updateVModel)
data.rollback = function() {
element.removeEventListener("input", updateVModel)
}
} else {
removeFn = function(e) {
if (e.propertyName === "value") {
updateVModel()
}
}
element.attachEvent("onpropertychange", removeFn)
data.rollback = function() {
element.detachEvent("onpropertychange", removeFn)
}
}
if (DOC.documentMode === 9) { // IE9 無法在切剪中同步VM
var selectionchange = function(e) {
if (e.type === "focus") {
DOC.addEventListener("selectionchange", updateVModel)
} else {
DOC.removeEventListener("selectionchange", updateVModel)
}
}
element.addEventListener("focus", selectionchange)
element.addEventListener("blur", selectionchange)
var rollback = data.rollback
data.rollback = function() {
rollback()
element.removeEventListener("focus", selectionchange)
element.removeEventListener("blur", selectionchange)
}
}
}
此外還有許多許多,但都是見諸於jQuery源碼的常見問題,我就不便貼出來了,它們的實現也與jQuery的相差無幾。可見兼容舊式IE是多么頭痛糾結的一件事。但由於OA的要求,甲方的要求,公司上頭的要求,我們總是奔於疲命。jQuery幫我們搞定了瀏覽器的兼容問題,但業務上的復雜性,讓我們的代碼在DOM與業務邏輯上兩頭跳。用了avalon后,我們就能從搬磚似的DOM操作上解放出來,研究設計模式,算法,分層架構等具有更高附加值的東西。從碼農到工程師到架構師的道路邁進!
