下面我来说说我们实际期望怎样的编程方式。
假设一个这样的需求:
页面上有一些文本是highlight的。例如,javaeye的文章如果是点击 google搜索结果过来的,javaeye的后台会自动判断出关键字,并为这些关键字包裹上标记(关键字)。
我们现在希望有这样一个功能,就是允许开启/关闭highlight。
如果关闭的话,那么大家通常可以想到的做法,就是检索所有的.hilite1的元素,然后去掉这个class。
$('span.hilite1').forEach(function(e){e.className=''});
但是这样做好之后,我们就无法再次开启highlight了!所以,我们要换种做法。一种方式是遍历所有的.hilite1然后替换为.hilite0。这样下次就可以找回来。不过正如上一篇文章中所说的,我们其实有更加经济的做法。
我们在body上加入一个class: ,然后把样式改为: body.hiliteEnabled span.hilite1 {...} ,这样,当要关闭highlight的时候,差不多就只需要 document.body.className = '' ,就可以了。
Ok,我们完成这个功能了。
这个时候,有用户希望我们提供另一个很cool的功能,即在页面下方加入一个slider,然后用户可以拖动slider改变highlight部分文字的字体大小。
这就类似于我上一篇中提到的批量修改的问题了。我们有两种做法:
A.
$('#fontSizeSlider').onchange = function() { var size = this.value; $('span.hilite1').forEach(function(e){ e.style.fontSize = size; }); }
B.
$('#fontSizeSlider').onchange = function() { var size = this.value; getStyle('span.hilite1').fontSize = size; } function getStyle(selector){ //示意代码 return document.styleSheets[0].cssRules[0].style; }
单纯看的话,托各种支持selector query的library的福,A做法是很简单的。B做法也不复杂。
但是还记得我们第一个开启关闭的功能么?如果highlight被关闭了,显然,从用户的角度上说,应该也禁用修改font size的效果。乍一看这个很简单,改成select出 body.hiliteEnabled span.hilite1 然后遍历就好了。
对于B做法来说,确实如此,你只需确保getStyle()返回的是针对 body.hiliteEnabled span.hilite1 的样式对象即可。
但是注意这点对于A做法是不够的!因为你给所有的body.hiliteEnabled span.hilite1都加上了inline style,这个是不会自动消失的。所以你需要在 document.body.className = '' 之后加上清理语句 resetFontSize(false) 而在再次开启的语句 document.body.className = 'hiliteEnabled' 之后也需要加上 resetFontSize(true) 。该函数的代码如下:
function resetFontSize(flag) { var v = flag ? $('#fontSizeSlider').value : null; $('span.hilite1').forEach(function(e){ e.style.fontSize = v; }); }
这个味道就不太好。这倒不是因为两个功能被交织了起来。我们原来修改body.className其实隐含了关闭/开启highlight的语义,所以fontSize生效与否受到它的影响是正常的。你可以把“document.body.className = ''; resetFontSize(false)”纳入一个disableHilite()函数中。
这里的问题实际是,每次你加入一个与highlight有关的新功能(例如我们下次可能允许大家定制hilite的颜色),你就需要修改disableHilite()/enableHilite(),加上新功能的清理和初始化代码的调用。这显然味道很不好。
各位可能会想到观察者模式了!
是的,你可以把hilite启用和禁用做成一个事件,然后其他功能都来订阅这个事件,并调用各自的初始化和清理代码。
不错不是嘛。问题都迎刃而解了!
不过还有一个问题。在这里,我们开启/禁用,只是一个二选一的问题。但是我们也可能遇到(制造出)更复杂的需求。比如假设是论坛帖子,除了整个页面的开启/禁用之外,每个回复都可以单独开启禁用hilite(即每个article元素上可以有.hiliteEnabled或.hiliteDisabled,如果没有任何一个class,则看body上是否有.hiliteEnabled),局域的设置override上层设置。这时,你就惨了,因为你的初始化/清理代码是针对整个页面写的,你必须改造成针对一个区域进行初始化和清理。你可能需要把用于遍历的selector作为事件的一个信息来传递。你的事件触发也需要重新写过,可能要让disableHilite()/enableHilite()能够接受一个参数指定操作范围,显然这个参数最好也用css selector。
Ok,这是我生造的需求,所以你会觉得不合理,不过对于程序员来说,需求一般总是不合理的。呵呵。我们这里只是举例。
其实我们从上面可以看到一个线索,那就是hilite启用与否,实际上可以取决于某个selector的模式匹配,因为我们通常把带有语义的开关存放在元素的class属性中。对于最初的简单需求来说:
匹配body.hiliteEnabled span.hilite1,就启用hilite以及hilite相关的功能,
不匹配body.hiliteEnabled span.hilite1,就禁用hilite以及hilite相关的功能。
每个hilite功能(如动态改变fontSize)去监听我们自制的hilite事件来进行初始化和清理,其实也可以等价于监听这一匹配的变化(如果我们能够监听的话)。
对于我们下面人为制造的需求,其实可以转化为:
(注:article元素表示整个页面中每个单独的帖子)
匹配body article.hiliteEnabled span.hilite1,启用hilite,
匹配body.hiliteEnabled article span.hilite1,也启用hilite,
除非匹配body article.hiliteDisabled span.hilite1,则禁用hilite。
如果写成一个单一的selector,就是:
article.hiliteEnabled span.hilite1, body.hiliteEnabled article:not(.hiliteDisabled) span.hilite1
一连串复杂的逻辑,其实就可以简化为对于这一模式匹配的监听。
这样我们期望中的代码就呼之欲出了:
首先,启用和禁用hilite,就是简单的直接对元素(body或article)上设置className。
然后我们这样写:
var hiliteSelector = new Selector('article.hiliteEnabled span.hilite1, body.hiliteEnabled article:not(.hiliteDisabled) span.hilite1'); function initHiliteFontSizeFeature() { hiliteSelector.addEventListener('match', function(evt){ var hiliteSpan = evt.target; hiliteSpan._syncFontSize = function(evt) { hiliteSpan.style.fontSize = evt.target.value; }; hiliteSpan.style.fontSize = $('#fontSizeSlider').value; $('#fontSizeSlider').addEventListener('change', hiliteSpan._syncFontSize, false); }, false); hiliteSelector.addEventListener('unmatch', function(evt){ var hiliteSpan = evt.target; $('#fontSizeSlider').removeEventListener('change', hiliteSpan._syncFontSize, false); hiliteSpan._syncFontSize = null; hiliteSpan.style.fontSize = null; }, false); }
也就是,如果有一个Selector API提供给我们监听match/unmatch事件的话,要做的事情就非常简单了!
理想中,Selector会产生match/unmatch事件,并自动dispatch到所有匹配的节点上。
不过,我们现在并没有这样的API……querySelector及各种library提供的,都是一次性取出符合条件的节点,而没有监听的功能。
实际上,在现有浏览器内使用JavaScript来实现这一API,是相当困难的。但是,我们知道,浏览器内部肯定有等价的功能,因为stylesheet的应用就是遵循这样的机制的。而且我们知道,IE的htc和Mozilla的XBL,正是利用这样一种机制的!未来的XBL2规范,也是如此!
在下一篇blog中,我会拿htc、xbl1和xbl2,来实现我们上面提到的例子。