批量批改style采取哪种方式好(续篇)

批量修改style采取哪种方式好(续篇)
前篇见批量修改style采取哪种方式好,主要是回答fins的提问。

下面我来说说我们实际期望怎样的编程方式。

假设一个这样的需求:

页面上有一些文本是highlight的。例如,javaeye的文章如果是点击google搜索结果过来的,javaeye的后台会自动判断出关键字,并为这些关键字包裹上标记(<span class="hilite1">关键字</span>)。

我们现在希望有这样一个功能,就是允许开启/关闭highlight。

如果关闭的话,那么大家通常可以想到的做法,就是检索所有的.hilite1的元素,然后去掉这个class。

$('span.hilite1').forEach(function(e){e.className=''});

但是这样做好之后,我们就无法再次开启highlight了!所以,我们要换种做法。一种方式是遍历所有的.hilite1然后替换为.hilite0。这样下次就可以找回来。不过正如上一篇文章中所说的,我们其实有更加经济的做法。

我们在body上加入一个class: <body class="hiliteEnabled"...> ,然后把样式改为: 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,来实现我们上面提到的例子。
1 楼 fins 2008-02-24  
非常受用! 谢谢!

另外 您能不能就 XBL多谈一谈呢? 那片太简短了 网络上相关的资料也不多

还有 很像听听您对 xforms 的看法批量批改style采取哪种方式好(续篇)

2 楼 hax 2008-08-24  
因为懒惰所以没有继续写htc、xbl。不过需要补充一下,本文所说的这样一种对于selector的match和unmatch监控,恕我孤陋寡闻,其实在我写作本文之前已经有实现了。那就是jQuery的live query插件。可看http://blog.brandonaaron.net/2007/08/19/new-plugin-live-query/。

其实现方法大体是用定时器检查dom的变化,从而触发match/unmatch。