給IE打補丁技巧之CSS Expression

[url=http://msdn.microsoft.com/en-us/library/ms537634.aspx]CSS Expression[/url]是自IE5開始提供的特性,雖然因安全性、性能問題臭名昭著,到IE8也終於[url=http://blogs.msdn.com/b/ie/archive/2008/10/16/ending-expressions.aspx]壽終正寢[/url]。回過頭看,與XMLHttpRequest一樣,CSS Expression的理念確實也有先驅之功,從CSS Expression也可看出由jQuery發揚光大的用CSS selector綁定行爲的編程方式的雛形。不過雛形只能是雛形。由於設計上的缺陷,CSS Expression不堪大用,通常只侷限爲patch一些CSS特性,例如min-width/max-width。

不過CSS Expression在patch IE方面其實還可以發揮更大的功用。Dean Edwards首創了[url=http://dean.edwards.name/weblog/2005/06/base64-sexy/]一次性執行experssion的模式[/url],巧妙的利用了IE的內建Selector機制,同時又避免了experssion被反覆計算的性能問題。這種模式被許多patch所使用。例如Peter Nederlof的[url=http://www.xs4all.nl/~peterned/csshover.html]hover/active/focus僞類補丁[/url]。

但是這個模式仍然有不足。Dean使用的是behavior屬性。而單個CSS屬性只能被用一次,即在一個元素上,最後根據cascade規則只會有一個behavior聲明會勝出,因此你無法爲一個元素同時啓用多個特性(即調用多個行爲)。

Peter Nederlof因爲要綁定三個行爲(分別對應於三個僞類),因此徵用了3個不常用的css property(text-kashida, text-kashida-space, text-justify)。

顯然,可以徵用的css屬性是很有限的(比方說你不能把background這樣常用的屬性給搞壞了,所以西方人也就是會欺負一下日本人專用的CSS屬性)。如果能使用任意自定義的css屬性的話,就好了。實際上IE對於不認識的property也是可以通過currentStyle返回其cascade之後的值的,而且expression對於自定義property也是有效的。

但是custom property存在一些問題:

1. 無法用runtimeStyle來override自定義屬性
2. 即使刪除包含expression的stylesheet,expression仍然有效
3. 在expression調用的方法裏也不能調用removeExpression來強行刪除表達式(會扔異常)

下面我講一下我對這個問題的研究和解決方法。

根據我的研究,expression的機制與普通CSS屬性不同,是設置在每個元素的style上的。

[code]
span {
behavior: expression(test1(this));
pie-test: expression(test2(this));
}
[/code]

通常我們可以從currentStyle.propName來得到屬性值,但是這樣只是得到計算後的結果,且currentStyle上並無getExpresssion方法。但是調用span.style.getExpression('behavior')或('pie-test')會得到'test1(this)'和'test2(this)'

因此可以認爲,對於這個rule,相當於對每個span都調用了一次 .style.setExpression('behavior', 'test1(this)') 以及 .style.setExpression('pie-test', 'test2(this)')

且通過對應的 .style.removeExpression 可以清除expression。

但是方法調用有一個限制,就是如果當前在expression所調用的函數(這裏就是test2()函數)中,是不能調用 .style.set/removeExpression 方法的,會扔出異常。

設置runtimeStyle.behavior時,也會清除expression,相當於調用了style.removeExpression('behavior'),但是沒有上面描述的限制。然而對於自定義屬性無效。甚至任何時候runtimeStyle.setExpression('pie-test', null),也不會覆蓋style上已經設定的expression。

以上。

顯然一個解決方案是在expression之外調用 .style.removeExpression 。

示例代碼:

function test2(e) {
setTimeout(function () {
e.style.removeExpression('pie-test')
}, 1)
...
}


這是有效的。問題是定時器只能保證最終自定義屬性上的expression會被移除,但是不能確保expression只被執行一次(實測下來會執行多少次是不確定的,一次到數次都有可能)。

當然,我們可以給每個元素加上標誌位來判斷是否已經執行過,不過這還是挺麻煩的。


另一個解決方案是先把元素存起來,然後統一刪除expression。

示例代碼:

var spans = []
function test(e) {
spans.push(e)
...
}


<head>
...
<script defer>
for (var i = 0; i < spans.length; i++)
spans[i].style.removeExpression('pie-test')
spans = []
</script>


經過實測defer的script能確保正好執行一次。不過有趣的是如果打印spans.length會發現是在不斷增加的,也就是並非所有元素上的test2執行完成後才執行defer的腳本,而是恰好一次test2,一次循環。這個也太巧合了,我對這個行爲吃不太準。另外這個方式不能應對後續動態加入的匹配元素,喪失了使用css expression的最大好處之一。


最終的方案是利用CSS的規則。

實際上不是所有expression都會被反覆執行。IE其實有基本的優化,比如對於expression(0)就只會計算一次,不會反覆求值,因爲很容易判斷該值是不會變化的。

雖然如前所述不能在test2裏直接調用 .style.setExpression ,但是可以通過改變match條件來override expression,如示例代碼:

<style>
span {
pie-test: expression(test2(this));
}
span.pie-test {
pie-test: expression(true);
}
</style>
<script>
function test2(e) {
e.className += ' pie-test'
...
}
</script>


經過測試該方式可以完美達到只執行一次的目標!

該方法還有個好處,如果要再次執行test2,只需要從元素的class中刪除pie-test即可。

後續的blog中我會展示一下使用這個模式來給IE打一些重要的特性補丁。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章