更優(yōu)雅的事件觸發(fā)兼容
更新時(shí)間:2011年10月24日 01:05:12 作者:
對(duì)于JS框架開發(fā)中的客戶端(瀏覽器)兼容難題,各位想必都不陌生。平常,我們都用if去面對(duì)接口不一致以及成堆的bug。然而,這里介紹的方法卻可以讓兼容更加優(yōu)雅
問(wèn)題種種
做底層接口兼容,無(wú)非就是利用if,判斷客戶端支持哪個(gè)接口的問(wèn)題。最著名的例子就是事件:
var addEvent = function(e, what, how) {
if (e.addEventListener) e.addEventListener(what, how, false)
else if (e.attachEvent) e.attachEvent('on' + what, how)
}
這里考慮了給元素綁定事件時(shí)可能遇到的兩種狀況——標(biāo)準(zhǔn)的W3C DOM接口以及DHTML提供的接口。當(dāng)然這個(gè)例子還很粗糙,但足夠說(shuō)明問(wèn)題了。
原先的方法是在兼容層調(diào)用有現(xiàn)場(chǎng)判斷并進(jìn)入相應(yīng)的if分支。很顯然,這種“現(xiàn)場(chǎng)判斷”的方法效率并不高。后來(lái),人們采用這樣的辦法:
if (MSIE) {
addEvent = function(e, what, how) {
e.attachEvent('on' + what, how);
}
} else {
addEvent = function(e, what, how) {
e.addEventListener(what, how);
}
}
在一次判斷后給addEvent綁定不同的代碼,從而免去了運(yùn)行時(shí)的分支判斷。
很可惜,這個(gè)問(wèn)題也不小。首先把“采用attachEvent”和“客戶端是MSIE”綁定在一起是個(gè)很過(guò)時(shí)的想法。假如微軟哪天良心發(fā)現(xiàn)了怎么辦?這事情現(xiàn)在就發(fā)生了——IE9明確支持了DOM接口,甚至DOM3都支持。結(jié)果,就這個(gè)“良心發(fā)現(xiàn)”的舉動(dòng)會(huì)毀掉許多前端庫(kù),他們必須被迫修改代碼(如同IE8來(lái)時(shí)那樣)。況且這種做法沒(méi)有考慮“未知的客戶端”——據(jù)我所知,Google發(fā)布Chrome后也導(dǎo)致不少類庫(kù)重寫代碼。
特性檢測(cè)
那究竟該怎么做?特性檢測(cè)就可以最大限度地避免“新客戶端”帶來(lái)的麻煩——通過(guò)一組在類庫(kù)初始化時(shí)定義的代碼來(lái)檢測(cè)客戶端擁有的特性,并利用這一組檢測(cè)值綁定類庫(kù)代碼:
var supportsAddEventListener = !!(checkerElement.addEventListener);
if (supportsAddEventListener) {
addEvent = function(e, what, how) {
e.addEventListener(what, how);
}
} else if (supportsAttachEvent) {
addEvent = function(e, what, how) {
e.attachEvent('on' + what, how);
}
}
特性檢測(cè)實(shí)際上是將“使用某個(gè)客戶端”和“支持某個(gè)特性”進(jìn)行解耦——讓if分支直接針對(duì)“特性有無(wú)”(接口是否一致)判斷,從而消除客戶端制造商“良心發(fā)現(xiàn)”造成的“好心辦壞事”。事實(shí)上這么做也是符合歷史潮流之選——當(dāng)標(biāo)準(zhǔn)接口逐漸普及,客戶端之間漸漸“表征一致”時(shí),為什么不做個(gè)一致的兼容層接口呢?
跌落
讓我們重新看看這些代碼。通常,一條利用特性檢測(cè)進(jìn)行兼容的代碼往往是這樣:
if (new_interface_detected) {
comp = function() {uses_new_interface};
} else if (old_interface_detected) {
comp = function() {uses_old_interface};
} else {
throw new Error('Unadaptable!')
}
換言之,過(guò)程是:
如果客戶端支持新接口,就將兼容層綁定到新接口上
否則,如果客戶端支持老接口/不一致接口,就將兼容層綁定到老接口上
否則,如果可以的話,給出錯(cuò)誤回饋
亦即,兼容層程序是從高空“掉”下來(lái),如果客戶端支持“高級(jí)”特性(新接口、標(biāo)準(zhǔn)接口)就將它“接住”——兼容層就有了歸宿;否則繼續(xù)向下掉——哦,老接口接住了,就用老接口;如果一直沒(méi)人接住,于是——啪——摔倒了地上,并且用最后一口氣喊一聲:“你用的客戶端太小眾,我拿你沒(méi)辦法了!”
這和什么比較像?
事實(shí)上,如果你了解JavaScript對(duì)象系統(tǒng)的機(jī)理,你就可以類比:這不就是原型嘛!原型系統(tǒng)就是利用了這種跌落——尋找某個(gè)成員,如果它在這個(gè)對(duì)象里定義了,就返回之;否則沿著原型鏈向上搜(沒(méi)錯(cuò),這次是向上的),如此重復(fù),直到真的連原型鏈都到頭的時(shí)候,返回個(gè)undefined。
說(shuō)做就做!這里同樣用addEvent為例。首先,我們定義一個(gè)空驅(qū)動(dòng),它里面什么都不包含:
var nullDriver = {}
然后,就是創(chuàng)建個(gè)對(duì)象,并且把原型鏈指向它。在ECMA V5時(shí)代,我們可以用Object.create,可惜,現(xiàn)在還有N多老客戶端(否則做什么兼容?。宰约篶raft個(gè)函數(shù):
var derive = Object.create ? Object.create: function() {
var T = function() {};
return function(obj) {
T.prototype = obj;
return new T
}
}()
這個(gè)用法你可能會(huì)覺得很詭異,但它工作起來(lái)一點(diǎn)問(wèn)題沒(méi)有,速度也不慢——能達(dá)到Object.create的一半。我們就用這個(gè)derive開動(dòng):
var dhtmlDriver = derive(nullDriver);
var dhtmlDriverBugfix = derive(dhtmlDriver);
這里的bugfix是針對(duì)一些“bug”和特殊情況定義的特別Driver。這里你可以忽略它。好了,DHTML里面addEvent是什么來(lái)著?
if (supportsAttachEvent) {
dhtmlDriver.addEvent = function(e, what, how) {
e.attachEvent('on' + what, how)
}
}
然后呢?位于原型鏈最前端的應(yīng)該是W3C的標(biāo)準(zhǔn)驅(qū)動(dòng)啊,寫上!
var w3cDriver = derive(dhtmlDriverBugfix);
var w3cDriverBugfix = derive(w3cDriver);
if (supportsAddEventListener) {
w3cDriver.addEvent = function(e, what, how) {
e.addEventListener(what, how)
}
}
最后,我們就放個(gè)東西上去做最后調(diào)用的接口。(因?yàn)閣3cDriverBugfix太難看……)
var driver = derive(w3cDriverBugfix);
然后就調(diào)用好了???,這就讓那些長(zhǎng)得嚇人的分支判斷變得簡(jiǎn)單有效,但不失fallback本色:在支持addEventListener上調(diào)用addEvent等價(jià)于調(diào)用w3cDriver.addEvent,而在不支持addEventListener的客戶端上就會(huì)跌落到底下,比如調(diào)用dhtmlDriver.addEvent。另外,進(jìn)行bugfix也很容易——可以在專門的“bugfix”層進(jìn)行hook,而原有層絲毫不受影響。
等等,繼承這么多層
會(huì)很慢么?誠(chéng)然,那么深的原型鏈肯定會(huì)慢,不過(guò)我有辦法。還記得給對(duì)象的屬性寫入時(shí)會(huì)發(fā)生什么事情嗎?
var ego = function(x) {return x}
for (var each in driver) {
if (! (each in nullDriver)) {
driver[each] = ego(driver[each])
}
}
沒(méi)錯(cuò),原來(lái)高企在原型鏈上面的方法會(huì)“嘩”的一下掉到最下面!這回不用沿著原型鏈向上搜了,直接從最底端獲取屬性即可。這里用ego函數(shù)的原因是防止一些瀏覽器“優(yōu)化掉”這里的代碼。
總結(jié)
雖然這里談兼容,可是,它的精華卻在語(yǔ)言特性上——利用原型繼承,我們可以很優(yōu)雅地完成這個(gè)令人頭疼的操作。是的,框架的美感不應(yīng)該只在外表,其內(nèi)部——即使是最最令人煩的內(nèi)部——也同樣要優(yōu)雅。
這里的技術(shù)可以在dess中找到。
來(lái)自:typeof.net
做底層接口兼容,無(wú)非就是利用if,判斷客戶端支持哪個(gè)接口的問(wèn)題。最著名的例子就是事件:
復(fù)制代碼 代碼如下:
var addEvent = function(e, what, how) {
if (e.addEventListener) e.addEventListener(what, how, false)
else if (e.attachEvent) e.attachEvent('on' + what, how)
}
這里考慮了給元素綁定事件時(shí)可能遇到的兩種狀況——標(biāo)準(zhǔn)的W3C DOM接口以及DHTML提供的接口。當(dāng)然這個(gè)例子還很粗糙,但足夠說(shuō)明問(wèn)題了。
原先的方法是在兼容層調(diào)用有現(xiàn)場(chǎng)判斷并進(jìn)入相應(yīng)的if分支。很顯然,這種“現(xiàn)場(chǎng)判斷”的方法效率并不高。后來(lái),人們采用這樣的辦法:
復(fù)制代碼 代碼如下:
if (MSIE) {
addEvent = function(e, what, how) {
e.attachEvent('on' + what, how);
}
} else {
addEvent = function(e, what, how) {
e.addEventListener(what, how);
}
}
在一次判斷后給addEvent綁定不同的代碼,從而免去了運(yùn)行時(shí)的分支判斷。
很可惜,這個(gè)問(wèn)題也不小。首先把“采用attachEvent”和“客戶端是MSIE”綁定在一起是個(gè)很過(guò)時(shí)的想法。假如微軟哪天良心發(fā)現(xiàn)了怎么辦?這事情現(xiàn)在就發(fā)生了——IE9明確支持了DOM接口,甚至DOM3都支持。結(jié)果,就這個(gè)“良心發(fā)現(xiàn)”的舉動(dòng)會(huì)毀掉許多前端庫(kù),他們必須被迫修改代碼(如同IE8來(lái)時(shí)那樣)。況且這種做法沒(méi)有考慮“未知的客戶端”——據(jù)我所知,Google發(fā)布Chrome后也導(dǎo)致不少類庫(kù)重寫代碼。
特性檢測(cè)
那究竟該怎么做?特性檢測(cè)就可以最大限度地避免“新客戶端”帶來(lái)的麻煩——通過(guò)一組在類庫(kù)初始化時(shí)定義的代碼來(lái)檢測(cè)客戶端擁有的特性,并利用這一組檢測(cè)值綁定類庫(kù)代碼:
復(fù)制代碼 代碼如下:
var supportsAddEventListener = !!(checkerElement.addEventListener);
if (supportsAddEventListener) {
addEvent = function(e, what, how) {
e.addEventListener(what, how);
}
} else if (supportsAttachEvent) {
addEvent = function(e, what, how) {
e.attachEvent('on' + what, how);
}
}
特性檢測(cè)實(shí)際上是將“使用某個(gè)客戶端”和“支持某個(gè)特性”進(jìn)行解耦——讓if分支直接針對(duì)“特性有無(wú)”(接口是否一致)判斷,從而消除客戶端制造商“良心發(fā)現(xiàn)”造成的“好心辦壞事”。事實(shí)上這么做也是符合歷史潮流之選——當(dāng)標(biāo)準(zhǔn)接口逐漸普及,客戶端之間漸漸“表征一致”時(shí),為什么不做個(gè)一致的兼容層接口呢?
跌落
讓我們重新看看這些代碼。通常,一條利用特性檢測(cè)進(jìn)行兼容的代碼往往是這樣:
復(fù)制代碼 代碼如下:
if (new_interface_detected) {
comp = function() {uses_new_interface};
} else if (old_interface_detected) {
comp = function() {uses_old_interface};
} else {
throw new Error('Unadaptable!')
}
換言之,過(guò)程是:
如果客戶端支持新接口,就將兼容層綁定到新接口上
否則,如果客戶端支持老接口/不一致接口,就將兼容層綁定到老接口上
否則,如果可以的話,給出錯(cuò)誤回饋
亦即,兼容層程序是從高空“掉”下來(lái),如果客戶端支持“高級(jí)”特性(新接口、標(biāo)準(zhǔn)接口)就將它“接住”——兼容層就有了歸宿;否則繼續(xù)向下掉——哦,老接口接住了,就用老接口;如果一直沒(méi)人接住,于是——啪——摔倒了地上,并且用最后一口氣喊一聲:“你用的客戶端太小眾,我拿你沒(méi)辦法了!”
這和什么比較像?
事實(shí)上,如果你了解JavaScript對(duì)象系統(tǒng)的機(jī)理,你就可以類比:這不就是原型嘛!原型系統(tǒng)就是利用了這種跌落——尋找某個(gè)成員,如果它在這個(gè)對(duì)象里定義了,就返回之;否則沿著原型鏈向上搜(沒(méi)錯(cuò),這次是向上的),如此重復(fù),直到真的連原型鏈都到頭的時(shí)候,返回個(gè)undefined。
說(shuō)做就做!這里同樣用addEvent為例。首先,我們定義一個(gè)空驅(qū)動(dòng),它里面什么都不包含:
var nullDriver = {}
然后,就是創(chuàng)建個(gè)對(duì)象,并且把原型鏈指向它。在ECMA V5時(shí)代,我們可以用Object.create,可惜,現(xiàn)在還有N多老客戶端(否則做什么兼容?。宰约篶raft個(gè)函數(shù):
復(fù)制代碼 代碼如下:
var derive = Object.create ? Object.create: function() {
var T = function() {};
return function(obj) {
T.prototype = obj;
return new T
}
}()
這個(gè)用法你可能會(huì)覺得很詭異,但它工作起來(lái)一點(diǎn)問(wèn)題沒(méi)有,速度也不慢——能達(dá)到Object.create的一半。我們就用這個(gè)derive開動(dòng):
復(fù)制代碼 代碼如下:
var dhtmlDriver = derive(nullDriver);
var dhtmlDriverBugfix = derive(dhtmlDriver);
這里的bugfix是針對(duì)一些“bug”和特殊情況定義的特別Driver。這里你可以忽略它。好了,DHTML里面addEvent是什么來(lái)著?
復(fù)制代碼 代碼如下:
if (supportsAttachEvent) {
dhtmlDriver.addEvent = function(e, what, how) {
e.attachEvent('on' + what, how)
}
}
然后呢?位于原型鏈最前端的應(yīng)該是W3C的標(biāo)準(zhǔn)驅(qū)動(dòng)啊,寫上!
復(fù)制代碼 代碼如下:
var w3cDriver = derive(dhtmlDriverBugfix);
var w3cDriverBugfix = derive(w3cDriver);
if (supportsAddEventListener) {
w3cDriver.addEvent = function(e, what, how) {
e.addEventListener(what, how)
}
}
最后,我們就放個(gè)東西上去做最后調(diào)用的接口。(因?yàn)閣3cDriverBugfix太難看……)
var driver = derive(w3cDriverBugfix);
然后就調(diào)用好了???,這就讓那些長(zhǎng)得嚇人的分支判斷變得簡(jiǎn)單有效,但不失fallback本色:在支持addEventListener上調(diào)用addEvent等價(jià)于調(diào)用w3cDriver.addEvent,而在不支持addEventListener的客戶端上就會(huì)跌落到底下,比如調(diào)用dhtmlDriver.addEvent。另外,進(jìn)行bugfix也很容易——可以在專門的“bugfix”層進(jìn)行hook,而原有層絲毫不受影響。
等等,繼承這么多層
會(huì)很慢么?誠(chéng)然,那么深的原型鏈肯定會(huì)慢,不過(guò)我有辦法。還記得給對(duì)象的屬性寫入時(shí)會(huì)發(fā)生什么事情嗎?
復(fù)制代碼 代碼如下:
var ego = function(x) {return x}
for (var each in driver) {
if (! (each in nullDriver)) {
driver[each] = ego(driver[each])
}
}
沒(méi)錯(cuò),原來(lái)高企在原型鏈上面的方法會(huì)“嘩”的一下掉到最下面!這回不用沿著原型鏈向上搜了,直接從最底端獲取屬性即可。這里用ego函數(shù)的原因是防止一些瀏覽器“優(yōu)化掉”這里的代碼。
總結(jié)
雖然這里談兼容,可是,它的精華卻在語(yǔ)言特性上——利用原型繼承,我們可以很優(yōu)雅地完成這個(gè)令人頭疼的操作。是的,框架的美感不應(yīng)該只在外表,其內(nèi)部——即使是最最令人煩的內(nèi)部——也同樣要優(yōu)雅。
這里的技術(shù)可以在dess中找到。
來(lái)自:typeof.net
相關(guān)文章
解決layui的radio屬性或別的屬性沒(méi)顯示出來(lái)的問(wèn)題
今天小編就為大家分享一篇解決layui的radio屬性或別的屬性沒(méi)顯示出來(lái)的問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2019-09-09
10個(gè)必備的JavaScript?async/await工具函數(shù)分享
當(dāng)談到異步編程時(shí),async/await是JavaScript中常用的功能之一,本文為大家整理了10個(gè)常用的await和async函數(shù)示例,感興趣的小伙伴可以參考一下2023-12-12
支付寶小程序?qū)崿F(xiàn)省市區(qū)三級(jí)聯(lián)動(dòng)
這篇文章主要為大家詳細(xì)介紹了支付寶小程序?qū)崿F(xiàn)省市區(qū)三級(jí)聯(lián)動(dòng),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-06-06
小程序hover-class點(diǎn)擊態(tài)效果實(shí)現(xiàn)
這篇文章主要介紹了小程序hover-class點(diǎn)擊態(tài)效果實(shí)現(xiàn),小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2019-02-02
關(guān)于封裝axios網(wǎng)絡(luò)請(qǐng)求降低代碼耦合度詳解
在項(xiàng)目中直接使用Axios或其他第三方庫(kù)來(lái)發(fā)送網(wǎng)絡(luò)請(qǐng)求獲取數(shù)據(jù)時(shí),會(huì)導(dǎo)致代碼與網(wǎng)絡(luò)請(qǐng)求的邏輯耦合度過(guò)高,導(dǎo)致難以維護(hù),所以本文將講解如何將網(wǎng)路請(qǐng)求的代碼進(jìn)行封裝來(lái)進(jìn)行解耦操作,文中通過(guò)代碼示例和圖文講解的非常詳細(xì),需要的朋友可以參考下2024-05-05
js實(shí)現(xiàn)隨機(jī)div顏色位置 類似滿天星效果
這篇文章主要為大家詳細(xì)介紹了js實(shí)現(xiàn)隨機(jī)div顏色位置,類似滿天星效果,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-10-10
在javascript中執(zhí)行任意html代碼的方法示例解讀
關(guān)于javascript的eval()函數(shù)無(wú)法執(zhí)行html代碼的問(wèn)題,下面為大家介紹下一種在javascript中執(zhí)行任意html代碼的方法,感興趣的朋友不要錯(cuò)過(guò)2013-12-12
js拆分字符串并將分割的數(shù)據(jù)放到數(shù)組中的方法
這篇文章主要介紹了js拆分字符串并將分割的數(shù)據(jù)放到數(shù)組中的方法,涉及javascript中split方法及數(shù)組的操作技巧,需要的朋友可以參考下2015-05-05

