JavaScript’s Data Template and Event Delegation

在列表里动态添加几行数据,而且还要给加入的行添加某些按钮,比如google的搜索结果页这样:

这样的情况下,我们通常第一个想法就是用 document.createElement 来构建DOM节点,然后将数据写入到节点上,再给按钮绑上事件,然后再一个一个去 appendChild ,这样在节点数比较少的时候还靠谱,如果操作一多,每个节点都有大堆属性,样式,内容,甚至事件要构建和绑定,就很麻烦了。

模板

金大为在D2 4th上有个模板前端的讲座,他在博客上也有相应的文章

第一步,我们实现一个最简单的引擎:

如下构建一段HTML片段(这里textarea是一个容器,模板可以在任何你想保存的地方保存):

<textarea id="J_Templ">
<li uid="$uid" class="user">
	<img src="$avatar" title="$user的头像" />
	<a href="$href">$user</a>
	<p>$status</p>
	<div class="operation">
		<a class="J_Add">加好友</a>
		<a class="J_Block">列入黑名单</a>
	</div>
</li>
</textarea>

最基本的数据是这样子的:

var user = {
    uid:    "1",
    user:   "frank",
    href:   "http://yyfrankyy.com",
    avatar: "http://en.gravatar.com/userimage/3248517/79c0b1355435ae5333ee8525e1c1e0fb.png",
    status: "Hello, Template."
}

然后用 replace 来遍历替换:

function render(templ, data, container){
    for(var item in data){
        templ = templ.replace(new RegExp("\\$" + item, "g"), data[item]);
    }
    container.innerHTML += templ;
}

使用时,找到占位符,执行替换,再塞到容器里:

var templ = document.getElementById("J_Templ").value,
    container = document.getElementById("container");
//渲染
render(templ, user, container);

以上就是最简单的一个JavaScript模板了。在这里,我们只需要保证数据结构是正确的,模板结构也是我们想要的就可以了。

这个简单的模板给我们带来几个好处:

  1. 需求变更的时候,我们需要改动以下地方:1、在模板里加入相应的占位符。2、嗯,没了。
  2. 充分利用JavaScript引擎对文本数据的处理,数据对象只需要查找一次,对模板执行多次遍历,数据存在时就替换,不存在时保留占位符,逻辑实在太简单了。

如果再智能一点,我们可以做一些其他工作,让这个模板支持在JSON对象里查找节点。比方说:我们完全可以这样把 $users.frank.name 变成 users["frank"]["name"],为了提高匹配度,我们用大括号 {,} 把占位符圈起来,接下来的占位符会是这样子:${users.frank.name}

新的模板引擎

var TemplateBuilder = function(tmpl, data) {
    var obj = [], matchs, i = 0, output = tmpl;

    //将所有匹配选项先存入数组
    while (matchs = /\\$\\{([^}]*?)\\}/g.exec(output))
        obj[i++] = matchs[1];

    var l = obj.length;
    while (l--) {
        var tree = obj[l],
             arr = tree.split("."),
             arrLength = arr.length;
             target = data[arr[0]],
             i = 1;

            if (target) {

                while (i++ < arrLength){
                    //如果下级匹配不到,即认为到达JSON树的终点
                    if (!target[arr[i]]) break;
                    target = target[arr[i]];
                }

                //TODO: 这里还可以加入类型检查
                output = output.replace(new RegExp("#\\{" + tree + "\\}", "g"), target);
            }

    }

    return output;
}

到这里,我们可以把这样的新数据替换到新模板里去了。

新模板:

<textarea id="J_Templ">
    <li uid="${users.frank.uid}" class="user">
        <img src="${users.frank.avatar}" title="${users.frank.user}的头像" />
        <a href="${users.frank.href}">${users.frank.user}</a>
        <p>${users.frank.status}</p>
        <div class="operation">
            <a class="J_Add">加好友</a>
            <a class="J_Block">列入黑名单</a>
        </div>
    </li>
</textarea>

新数据:

var users = {
    frank: {
        uid:    "1",
        user:   "frank",
        href:   "http://yyfrankyy.com",
        avatar: "http://en.gravatar.com/userimage/3248517/79c0b1355435ae5333ee8525e1c1e0fb.png",
        status: "Hello, Template."
    }
}
  • 如果还想在复杂一点,我们定义一些变量,让模板更加可重用一点;
  • 或者最起码加一个 if 判断,有些东西这一行我不想让他出现了;
  • 还可以考虑加入循环,给模板再定义一个占位符,找到这个循环占位符的时候,里面的子模板拿出来重复replace几次,然后一次性替换掉整块的循环占位符;
  • 嗯,我想把这个模板变成脚本库,那把前面这个统一的前缀 $ 也改造一下……

STOP!!记住,我们只是想做一个简单的可根据我自己定义的规则做一下相应替换的模板。他最初的目的,是为了减少业务变更给数据展现带来的复杂性。这样的场合非常适用于已经被抽象出来的模块层,比如操作列表,比如Gadgets。其他东西,在用数据对象同步替换数据模板的基础上,爱怎么加怎么加,根据具体的业务需求去定制。

事件代理/委托(Event Delegation)

这里还要再讨论的一个话题,在动态添加的模板文本,变成DOM结构之后,有一些按钮要有一些相应的事件。

如果你只想做业务逻辑,而不是在抽历程一个框架或者模块的时候,我的直接建议是内联。

<a onclick="alert('hello?')">SayHello</a>

因为内联是管理成本最低的,他同时也带来相应的诸多后期维护的隐患,解决这些隐患就是完全不顾这些内联代码,一有改动就修改模板,毕竟模板已经是抽象好用于不断重用的最小单元, 他的维护成本是可以预估的。

现在,我们还没开始写代码,就开始考虑:嗯,这个代码以后可能会有谁谁谁来改,或者需求变更来再次加入另外一些东西,那么我们的事件应该是用 addEventListener/attachEvent 来绑定,他的“可维护性”应该是好的,他或者是可以配置的,或者直接就用一些框架封装好的事件绑定,而当你正好用上这些函数的时候,单独用发现不行,我必须得再去调用框架的其他库,因为他是内部依赖的……

问题是,我最开始只想给这个按钮加一个事件,这个事件就是发个请求,告诉服务器删掉这行数据,数据在删除之后,删掉这行元素,同步一下视图状态就可以了。

有些情况下,我们真想把这些模板单独就构建起来一个可重用的模块,因为实际的需求里,实在太多此类情况,那么可以考虑使用事件代理/委托了:

引用NCZ的这段代码:

document.onclick = function(event){
    //IE doesn't pass in the event object
    event = event || window.event;

    //IE uses srcElement as the target
    var target = event.target || event.srcElement;    

    switch(target.id){
        case "help-btn":
            openHelp();
            break;
        case "save-btn":
            saveDocument();
            break;
        case "undo-btn":
            undoChanges();
            break;
        //others?
    }
};

是的,感谢 event.target 这个东西,事件不冒泡了的情况下,我们可以很容易找到当前操作的对象,而同样不必关心这个对象是个虾米东东,而执行对这行数据所需要的参数,直接在最初的数据对象里传入就安了~

这里,我们做一个妥协,把模板转成DOM对象,因为只有DOM对象才能 addEventListener/attachEvent ……两种处理:

  • 要么,自己把模板进行查找,把最外层的东西处理成DOM,这样做好像有点那个……
  • 要么,在模板外再加一个容器,在这个容器里 addEventListener/attachEvent

End

至此,我们的两个话题可以融在一起,事件代理/委托只是给模板提供了绑定事件的方式。整个思路,是把核心任务放在处理数据本身,只关心数据和操作数据的事件。

数据模板充分减少了我对从HTML文本到DOM节点树的转换过程,将构建DOM树的过程交给浏览器自己去搞,我们所要做的,只是在HTML里呈现数据,加一些交互事件,其他跟DOM结构相关的东西,比如样式,比如语义,或者加入一些自定义的属性,可以较纯粹地分离开。

Hi, 2010

00-09,从中学到大学到走上社会,跨过了最青春,最疯狂,成长最快的10年。这10年里,学会了加法。

接下来这10年,学乘法。每一个目标,再勇敢一点;每一个选择,再坚决一点;每一个脚印,再踏实一点。

Hi, 2010.

[翻译] Rendering: repaint, reflow/relayout, restyle

原文:http://www.phpied.com/rendering-repaint-reflowrelayout-restyle/

翻完长长一篇之后非常囧的发现已经有同事做了笔记:http://www.zhuoqun.net/html/y2009/1430.html

多好的5个”R”开头的单词。

这一节讲讲渲染(Rendering),即在Life of Page 2.0中提到的,在下载资源及其后面的时间。

当得到一整页的HTML,CSS,可能还有一些JavaScript,浏览器到底是怎么把这些东西显示在屏幕上的?

渲染过程

不同浏览器这个过程是不一样的,一旦页面代码下载完,这部分在不同浏览器上或多或少是类似的,如下图:

  • 浏览器把HTML源码(标签集合)进行分析,构建DOM节点树——这个树表示了各个HTML标签之间的关系,标签内的文字也表示为文本节点。这个DOM节点树的根节点是 documentElement (即 <html> 标签)
  • 浏览器解析CSS代码,一般CSS会有不少应该解析的hacks,以及带浏览器前缀的声明,诸如 -moz-webkit 等等,不被理解的的前缀会被忽略掉。样式信息由以下各层组成:基本的样式由用户代理(UA,通常是浏览器)提供,可能还有用户样式(浏览器自定义),然后是页面作者提供的——外部的(<link/>),导入的(@import),内联的(<style/>)各种样式,最后才是HTML标签中 style 属性所声明的样式。
  • 接下来是构建渲染树(Render Tree)。渲染树类似于DOM节点树,但还包括样式。所以当你用 display: none 隐藏 div 时,这个 div 就不在渲染树中了,其他不可见的元素也是一样道理,还有 head 标签以及里面的东西。另外,一些DOM元素可能表现在渲染树上的多个节点上:比如文本节点, <p> 中的每行文本节点都需要一个渲染节点。渲染树的一个节点叫做框(frame),或者盒(box)(跟CSS中的盒模型是类似的,详见 the box model)。每个渲染节点都拥有CSS盒模型的属性——width, height, border, margin, 等等。
  • 一旦渲染树构建完成,浏览器就开始把渲染树的节点逐一绘制(paint)/描绘(draw)到屏幕上。

树和森林

我们举个例子:
HTML source:

<html>
    <head>
        <title>Beautiful page</title>
    </head>
    <body>
        <p>
            Once upon a time there was
            a looong paragraph...
        </p>
        <div style="display: none">
            Secret message
        </div>
        <div><img src="..." /></div>
            ...
    </body>
</html>

这段HTML文档的DOM树每个标签都只有一个子节点,每段文本里也只有一个文本节点(为了简化,我们暂时忽略空白也是一个文本节点)。

documentElement (html)
    head
        title
    body
        p
            [text node]
        div
            [text node]
        div
            img
        ...

DOM节点树可见部分就会转换为渲染树。先去掉一些东西——head 部分和隐藏的 div ,但还要加上多行文本导致的额外的节点(框/盒)。

渲染视图

body
    p
        line 1
        line 2
        line 3
        ...
    div
        img
        ...

渲染树的根节点就是一个包含所有可见元素的框(盒)。你可以认为是在浏览器窗口里的一部分,即页面显示出来的部分。Webkit专门把可见区域称为 RenderView ,跟CSS里的 initial containing block 是一致的,即页面从(0,0)到(window.innerWidth,window.innerHeight)那部分。

这里就需要考虑文档流是如何不断按照渲染树结构把内容显示在屏幕上的。

重绘和reflow

当然屏幕上最起码有一个已经显示着的页面(空白页面(about:blank)也算)。然后,改变页面内容通常会导致渲染树发生以下两种情况:

  1. 重新构建部分渲染树(或者整个树),重新计算节点的数量/规模。这一步称之为reflow,或排版/布局。(或者就直接按照标题5个“R”之一的relayout,不好意思。[译者注:妈的!])必须指出的,至少会有一次reflow的过程——页面第一次布局。
  2. 大部分情况下的是要更新一下屏幕,节点几何属性的调整或是诸如改变背景颜色这样的样式改变。屏幕的更新过程称之为重绘(repaint)或者redraw

重绘和reflow相当地消耗性能,界面反应迟钝,影响了用户体验。

什么情况下会触发reflow和重绘

对页面元素的某些改动,会导致渲染树的重新构建,从而导致reflow和重绘,比如:

  • 添加,删除,更新DOM节点
  • 通过 display: none 用隐藏DOM节点(导致reflow和重绘)或者用 visibility: hidden (只导致重绘,因为位置不变)。
  • 在页面移动DOM节点,或者使用动画。
  • 添加样式表,调整 style 属性
  • 调整窗口大小,改变字体大小,还有(oh, OMG, no![译者注:求中文翻译])滚动页面

再来看看下面的例子:

var bstyle = document.body.style; // cache 

bstyle.padding = "20px"; // reflow, repaint
bstyle.border = "10px solid red"; // another reflow and a repaint 

bstyle.color = "blue"; // repaint only, no dimensions changed
bstyle.backgroundColor = "#fad"; // repaint 

bstyle.fontSize = "2em"; // reflow, repaint 

// new DOM element - reflow, repaint
document.body.appendChild(document.createTextNode('dude!'));

某些情况下的reflow会特别消耗资源。比如:如果你正在折腾的是渲染树上的某节点,而该节点又刚好是body元素的直接子节点,其他地方可能不需要动到,而此刻你正想把这个节点(可能是页面顶部的div),用动画下移到屏幕其他地方,这时就相当消耗资源了。

聪明的浏览器

由于渲染树导致的重绘和reflow导致的高性能消耗,浏览器为了降低这一负面效果,采取的一项策略是不立即执行重绘/reflow,最起码,延迟一点点时间。浏览器会构造起一个由脚本导致的引发reflow的操作列表,然后成批地执行,部分引发reflow的操作被合并到一起。浏览器定时的执行队列里一系列操作,然后统一渲染,直到短暂的延迟时间到,或者一次执行的操作序列已满。

但有些时候,脚本可能会防止浏览器优化reflow,让浏览器马上执行队列中所有操作。这种情况在引发某些样式改变时会触发,比如:

  1. offsetTop, offsetLeft, offsetWidth, offsetHeight
  2. scrollTop/Left/Width/Height
  3. clientTop/Left/Width/Height
  4. getComputedStyle(), or currentStyle in IE

以上所有属性本质上都是对节点的样式操作,只要执行了,浏览器马上会给出最新的值。为了做到这一点,浏览器执行所有还在队列中未执行的操作,更新队列,忍痛把reflow给做了!

比如,连续(循环)获取和改变样式(如下)就是非常糟糕的做法:

// no-no!
el.style.left = el.offsetLeft + 10 + "px";

减少重绘和reflow

降低reflow/重绘给用户体验带来负面效应的策略——简而言之就是减少reflow、重绘以及减少对样式的改变,让浏览器能够自己优化reflow。那该怎么做?

  • 千万别一个一个属性地去改样式。更合理的,可维护性更好的做法,是改变元素的类名(class)。但是那样做对静态的样式比较有效,如果样式是动态的,可以修改 cssText 属性,防止每次轻易的改动都要去改变元素的 style 属性
// bad var left = 10,
top = 10;
el.style.left = left + "px";
el.style.top = top + "px";
// better
el.className += " theclassname";
// or when top and left are calculated dynamically...
// better
el.style.cssText += "; left: " + left + "px; top: " + top + "px;";
  • 需要成批处理DOM的改变时让他们先“离开一会”。“离开一会”意味着先脱离DOM树:
    • 使用 documentFragment 来存储临时性改变
    • 克隆你想要改变的节点,改好后再换成原来的
    • 用 display: none (一次reflow,重绘)隐藏元素,做100次改变,再显示出来(又一次reflow,重绘)。只使用两次代替可能的100次reflow。
  • 不要频繁地计算样式。如果你需要一个计算结果,算一次,缓存起来,然后以后都在缓存值中取。再看一次上面这个“no-no”的例子。
// no-no!
for(big; loop; here) {
    el.style.left = el.offsetLeft + 10 + "px";
    el.style.top = el.offsetTop + 10 + "px";
}
// better
var left = el.offsetLeft,
top = el.offsetTop esty = el.style;
for(big; loop; here) {
    left += 10;
    top += 10;
    esty.left = left + "px";
    esty.top = top + "px";
}
  • 一般情况下,考虑渲染树以及多少东西会被这次改变影响到。比如在演示body的直接子元素的动画时,用absolute定位,防止影响到太多其他节点。最多就是移动时盖过区域的那部分节点可能需要重绘,但不需要reflow。

工具

一年前,没有东西可以让告诉我们浏览器在绘制和渲染时发生了什么事情(不是我们没有意识到,当然事实上就是MS基本不提供开发工具给我们,不知道被埋在MSDN哪里了 :P )。不过现在已经好很多了!

最开始,Firefox nightlies 提供了 MozAfterPaint event,出现了像这个扩展(by Kyle Scholz),mozAfterPaint很酷,但是他只告诉我们重绘的过程。

DynaTrace Ajax 和最近Google的 SpeedTracer(多好的两个”trace”!)出现了,这两个工具在监视reflow和重绘的领域表现太完美了——第一个用在 IE 上,第二个用在 Webkit 浏览器上。[译者问:可怜的safari能用么]

去年某时Douglas Crockford提到我们可能在CSS上做了一些还不知道原因的傻事,而我的确做了。由我发起的一个改进IE6字体的项目导致CPU突发到100%,10-15分钟后,浏览器终于完成了重绘。

好了,工具有了,我们不会再用CSS做这样恶心的事情了。

接下来,额,该说说工具了……用好像Firebug这样优秀的工具来说明除DOM树之外的渲染树,很cool吧?

完整的例子

我们快速示范一下这些工具,看看在我们修改样式(此时不会修改渲染树)和触发reflow(影响布局[译者注:即开始影响渲染树])以及导致最终重绘时发生了什么事情。

我们通过两种方式做同一件事:第一种方式,我们改变一些样式(不会影响布局),然后检查一下style属性,完全跟刚刚的改变没有任何关系。

bodystyle.color = 'red';
tmp = computed.backgroundColor;
bodystyle.color = 'white';
tmp = computed.backgroundImage;
bodystyle.color = 'green';
tmp = computed.backgroundAttachment;

另外的做法,这次我们直接修改style属性,全部一次性改完后检查:

bodystyle.color = 'yellow';
bodystyle.color = 'pink';
bodystyle.color = 'blue';
tmp = computed.backgroundColor;
tmp = computed.backgroundImage;
tmp = computed.backgroundAttachment;

两个例子中,还需要用到这些变量:

var bodystyle = document.body.style;
var computed;
if (document.body.currentStyle) {
    computed = document.body.currentStyle;
} else {
    computed = document.defaultView.getComputedStyle(document.body, '');
}

现在,这两次变化在单击页面时执行,测试页面在这里:restyle.html (点击“dude”)。

第二个测试跟第一个一样,这次我们改变了布局信息:

// touch styles every time
bodystyle.color = 'red';
bodystyle.padding = '1px';
tmp = computed.backgroundColor;
bodystyle.color = 'white';
bodystyle.padding = '2px';
tmp = computed.backgroundImage;
bodystyle.color = 'green';
bodystyle.padding = '3px';
tmp = computed.backgroundAttachment; 

// touch at the end
bodystyle.color = 'yellow';
bodystyle.padding = '4px';
bodystyle.color = 'pink';
bodystyle.padding = '5px';
bodystyle.color = 'blue';
bodystyle.padding = '6px'; 

tmp = computed.backgroundColor;
tmp = computed.backgroundImage;
tmp = computed.backgroundAttachment;

这次改变布局的测试称之为“relayout test”,the source is here.

以下图表是上“restyle test”中通过DynaTrace获得的。

页面加载完了之后,我点击一次页面执行第一段脚本(不断请求样式信息,大约2秒),然后再点击一次执行第二段脚本(延迟请求样式信息,大概4秒)

工具显示了页面是怎么加载的,IE图标表示onload。鼠标光标是点击页面后的渲染信息。在任何一个地方放大(在Timeline区域单击,鼠标不放左/右移动),可以看到以下更多细节(很酷吧):

这里,你可以在蓝色部分看到很详细的JavaScript活动,绿色部分是渲染活动。现在,在这个简单的例子中,告诉我们执行渲染部分比执行脚本要多消耗了多少时间!在平时的Ajax/富应用中,JavaScript并不是性能瓶颈,访问、处理以及渲染DOM节点才是最耗性能的。

嗯,现在运行“relayout test”,这个例子中元素改变了位置,检查一下“PurePaths”,这里有关于时间线上的更多信息。我高亮了第一次点击,这里JavaScript活动显示出来一个等待处理的布局任务。

再一次,放大到另外一个区域,你可以看到“drawing”栏,那里有一个新的“calculating flow layout”(正在计算布局),我们除了重绘之外,还有一段reflow测试。

现在,我们在Chrome下用 SpeedTracer 测试一下。

这是第一个“restyle test”放大后的结果(我估计会因为这些放大而受到诅咒[译者注:例子产生的结果值太小,放大太多]),如下图:

概览部分,我们可以看到一次单击和一次渲染。在第一次单击中,导致了50%时间重新计算样式,为什么?这里,是因为我们在每次样式改动时都去获取样式信息。

展开事件然后显示隐藏的部分(灰色线条被SpeedTracer隐藏掉了,原因是他们并不慢),我们可以看到实际上发生了什么事情——第一次点击之后,计算了三次样式,在那之后——只有一次了。

现在,我们看看“relayout test”,概览部分的事件是一样的:

但是详细的那部分告诉我们第一次点击时导致了三次的reflow(因为我们要计算样式信息),第二次点击导致一次reflow。这里完美地告诉我们到底发生了什么事情。

两个工具有一点点不同——SpeedTracer不告诉我们布局任务还在重绘的队列中排队,DynaTrace里有。DynaTrace没有告诉我们“restyle”(修改样式)和“reflow/relayout”(重布局)的细微区别,而SpeedTracer做到了。也有可能IE里这两者根本是不区分开的?DynaTrace也没有显示出三次的reflow,而是区分开了change-and-touch(改变和访问)和change-then-touch(改变然后访问),也可能这就是IE实际上在做的?

运行这些简单的例子多次,确认了IE实际上并不在乎你改变之后的样式信息。

另外,经过多次重复测试,得出以下数据:

  • Chrome里,修改样式后访问样式消耗的总时间,相比只修改样式消耗的时间,在restyle test中是2.5倍,而relayout test中是4.42倍。
  • Firefox中,分别是1.87倍和1.64倍。
  • IE6/IE8中,无区别。

通过大部分浏览器的测试,改变样式只占同时改变样式和布局的一半时间(现在我觉得,我应该对比只修改样式和只修改布局的时间[译者注:。。。]),IE6是特殊的,修改布局比单独修改样式多花费了4倍时间。

最后的话

感谢你阅读这么一长篇的文章,去玩一玩不同的工具,看看那些神秘的reflow!作为总结,我们再回顾一遍这些术语:

  • 渲染树:DOM树的可见部分
  • 节点在渲染树中成为或者
  • 重新计算渲染树(或其中一部分)称之为reflow(in Mozilla),其他浏览器中称之为layout(布局,布是谓语)
  • 按照结果更新屏幕称之为重绘(IE/DynaTrace中称为redraw)
  • SpeedTracer介绍了“style recalculation”(重新计算样式,不包括形状的改变)的概念,跟“layout”(布局)区分开。

如果你对这个话题很感兴趣,可以参考下面一些额外的文章。这些文章,尤其前三篇,会更加深入,更加接近浏览器。而作为一个开发,我也只能谈到这里。

译者注:写在最后

Stoyan Stefanov的这个性能优化系列文章已经完整地跟了下来,陆陆续续也翻译了一些,最初的想法是整个系列都转过来(目前还木有跟作者联系),后来发现很多方面涉及到的问题作者并没有详细讲,比如Server端配置,Network层面的一些细节优化我也无法从原文中学到什么东西,所以最后决定先挑一些topic深入进去,边译边学,边学边用。

国内的前端性能研究从D2上秦歌的演讲才开始正式冒头,自己也对这一块非常感兴趣,响应一下秦歌的号召(也响应了Stoyan Stefanov),意在整理一些综合性的性能优化的方案,并深入探讨其范畴和详细机制,顺便推广一些优秀的前端性能监测和分析工具,具体的方法则需要靠多方基础多方阅读去掌控了。

翻译这些文章的过程中,对作者的原意理解得更加深入了一些,他们广泛的知识面,对细节的把握,对全局的掌控(Server-Network-Client,每个细节都能找到他们的踪影),无一不是值得我们学习的。

 1 2 3 4 5 6 7 ...24 25 26 Next »