像素工地返工记

JavaScript 刚把窗户加宽一厘米,转头就问:“现在多宽?”布局工人只好重新量房。如此来回八次,卷尺终于弯成了蚊香:“要不您先问完,再动手?”

分享
像素工地返工记

卷尺怎么弯了

本文承接上一篇:《HTML 解析器罢工了》

“都别干了!”

布局工人冲进 DOM 工地,往桌上一拍。

一盘卷尺滚了出来。

滚到一半,打了个结。

“这是什么?” HTML 解析器问。

“卷尺。”

“怎么像蚊香?”

“问 JavaScript!”

JavaScript 老大端着咖啡,从门外悄悄退了半步。

布局工人一把将老大拽回来:“别走!刚才是不是改了八次窗户宽度?”

“只是想让窗户宽八厘米。”

“所以就分成八次,每次加一厘米?”

“怕一步迈太大,页面扯着腰。”

“还每改一次,就立刻问一句‘现在多宽’?”

“这叫谨慎施工。”

“这叫返工八次!”

布局工人拎起卷尺,把 HTML 解析器和 JavaScript 老大带回隔壁的像素工地。

大门一开,布局区里一片狼藉:方格图纸铺了一地,几个页面方块横七竖八地躺着。绘制工人抱着刷子躲在警戒线外,合成员趴在控制台后面,只露出一双眼睛。

HTML 解析器看了一圈:“刚才不是还好好的?”

“刚才你只负责把 DOM 搭出来。” 布局工人捡起卷尺,“接下来还得把它变成屏幕上的东西。”

HTML 解析器拍拍胸口:“树已经交付,后面不归我。”

“光有树有什么用?谁穿什么衣服,站在哪里,画成什么样,最后怎么送上屏幕,全在这边干。”

“这么多人伺候一棵树?”

“本来伺候得挺好。” 布局工人瞪向 JavaScript 老大,“直到有人拽一下窗户,就逼我量一次尺寸。”

JavaScript 老大指了指警戒线外:“我折腾的是卷尺,他们两个躲什么?”

绘制工人把刷子抱得更紧了:“墙的位置变了,后面还画不画,得等布局的新图纸。”

合成员也探出脑袋:“画面改了,最后还拼不拼,得等绘制的新画片。”

“所以八次返工,把所有人都折腾了八次?” HTML 解析器问。

“先别乱定责。” 布局工人捡起一张事故单,“这八轮里,谁真的反复开工,谁只是在等最终结果,得按正常工序查。”

JavaScript 老大往事故监控前一坐:“直接看回放不行吗?”

“连正常顺序都不知道,看见哪台机器响,就会以为哪台机器返工。” 布局工人吹响哨子,“先让页面正常上一次屏。样式管理员,第一站!”

先给节点换衣服

样式管理员推来两辆小车。

左边坐着 DOM 节点,右边堆着 CSS 规则。

<main class="room">
    <h1>像素工地</h1>
    <p class="notice">今日正常施工</p>
</main>
.room { width: 600px; }
.notice { color: orange; }

“DOM 只告诉大家,这里有个 main,里面住着 h1p。” 样式管理员给 .notice 套上一件橙色马甲,“至于穿什么、占多大地方,还得看样式规则。”

HTML 解析器指着右边的小车:“这也是树?”

“CSS 也有自己的规矩和结构。别伸手,刚排好。”

“有名字吗?”

“这套方便浏览器查询的树状账本,常叫 CSSOM。” 样式管理员护住小车,“记住名字就行,只许看,不许乱搬。”

“两棵树直接拼起来?”

“没那么随便。” 样式管理员翻开登记簿,“先看哪些节点需要出现在画面里,再算每个节点最终穿什么。继承、层叠、默认样式,都得对过。”

角落里,一个节点举手:“我穿着 display: none。”

“那你先别进施工队。”

“连地都不给站?”

“你自己要求不参加布局。”

“那 visibility: hidden 呢?” 另一个节点从帘子后面探头。

“位置照留,只是不把你画出来。”

第一个节点不服:“都是看不见,待遇差这么多?”

“一个没来,一个来了但躲起来。能一样吗?”

样式管理员合上登记簿:“DOM 和样式准备好后,浏览器会整理出接下来需要布局和绘制的内容。大家常把这份结构叫渲染树。它和 DOM 有关系,但不是把 DOM 原样复印一遍。”

JavaScript 老大点点头:“衣服穿好,直接拍照?”

像素工地里齐刷刷传来一声:“想得美!”

谁站哪儿

布局工人展开一张巨大的方格纸。

.room,宽六百。靠这里。”

h1,站上面。”

.notice,排在标题下面。”

节点们刚站好,JavaScript 老大伸手把 .room 往外拽了一厘米。

“等等。” 布局工人重新打开卷尺,“房间变宽,里面的人可能也得重新排。”

“只改了一厘米。”

“一厘米也是改。文字会不会换行?旁边还有没有位置?下面的人要不要往下挪?都得看。”

HTML 解析器小声问:“这就是重排?”

“也叫重新布局。” 布局工人沿着墙角重新测量,“元素的尺寸、位置或者页面结构变了,浏览器可能要重新计算一部分甚至更多布局。”

“每次都要把全楼拆了重盖?”

“不一定。改了什么、影响到谁,浏览器自己判断。别一听重排就脑补爆破队。”

刚说完,JavaScript 老大又把房间加宽一厘米。

布局工人闭上眼睛:“还有吗?”

“暂时没了。”

“确定?”

“确定。”

布局工人刚把卷尺拉开,老大又问:“现在多宽?”

空气安静了。

“你刚改完。”

“所以才问。”

“账还没算!”

“可代码现在就要答案。”

布局工人的手开始发抖。

改一下,问一下

事故监控被调了出来。

for (let i = 0; i < 8; i++) {
    room.style.width = `${room.offsetWidth + 1}px`;
}

“就是它!” 布局工人用卷尺指着屏幕,“这一圈先读 offsetWidth,再把宽度加一。麻烦从下一圈开始:上一圈刚改完,下一圈马上又来读。”

第一轮,先问:

“现在多宽?”

拿到答案,宽度加一。

第二轮,上一笔改动还攥在浏览器手里,问题又来了:

“现在呢?”

旧账可能已经不准,只好先把新布局算出来,再交出答案。紧接着,宽度又加一。

第三轮……

“停。” HTML 解析器按住回放键,“卷尺就是这么弯的?”

“第六次时打的结,第八次时冒的烟。”

JavaScript 老大试图解释:“平时浏览器不是会把改动攒一攒吗?”

“会。” 布局工人说,“如果只是连续改,浏览器通常有机会晚点统一处理。可你刚改完就读取尺寸和位置,等于拍着桌子说:别攒了,我现在就要最新结果。”

老大低头看了看咖啡:“那怎么改?”

“先把要问的统一问完,再把要改的统一改完。能记住的答案就别反复量。”

屏幕上的工作单被重新排了一遍:

const width = room.offsetWidth;
room.style.width = `${width + 8}px`;

布局工人看了一眼:“一次量,一次改。卷尺保住了。”

“如果有很多元素?”

“也是先读一批,再写一批。别读、写、读、写,像在门口反复横跳。”

HTML 解析器点头:“懂了。返工的重点不只是‘改了’,而是‘刚改就催着问’。”

“终于有人会做事故总结了。”

量完还得画

布局工人刚交出新图纸,绘制工人就推着油漆车冲了过来。

“墙在哪儿?”

“这里。”

“多宽?”

“图上写了。”

“背景什么颜色?”

样式管理员递来色卡。

“边框呢?阴影呢?文字呢?”

“都在单子上。”

绘制工人抄起刷子,把背景、文字、边框和阴影一项项画出来。

JavaScript 老大凑过去:“要是只把文字从橙色改成蓝色呢?”

布局工人摆摆手:“位置和尺寸没变,通常不用找我。”

绘制工人脸一垮:“找我。颜色变了,得重新画。”

“这叫重绘?”

“对。换颜色,不用重新量房;墙都挪了,后面通常得跟着重新画。所以重排可能带来重绘,但重绘不一定需要重排。”

JavaScript 老大想起门口的事故单:“那刚才布局算了八轮,你也画了八轮?”

“没有。” 绘制工人立刻摆手,“那个循环一口气还没跑完,中间结果根本来不及上屏。布局工人被你逼着反复交出最新尺寸,我通常只等最后一张图纸。”

“所以重新布局一次,不等于当场重绘一次?”

“终于问对了。要是改动分散在不同的画面里,一次次真要展示出来,我才可能跟着反复刷。”

布局工人在事故单上重重盖章:“绘制组,排除八次返工嫌疑。”

JavaScript 老大若有所思,伸手把墙又挪了一厘米。

绘制工人和布局工人同时举起工具。

“放下!” 老大迅速收手,“只是确认理解。”

最后一位只负责拼

合成员终于从控制台上爬起来。

“都画完了?”

“完了。”

“交图层。”

绘制工人把几张透明画片递过去。合成员将它们放到不同轨道上,挪一挪,对一对,最后合成完整页面送上屏幕。

HTML 解析器先把事故单递过去:“绘制组没有画八遍,你呢?”

“连八套画片都没收到,拿什么拼八遍?” 合成员按住印章,“合成组也只处理最终结果。”

事故单上又多了一个大印:排除。

HTML 解析器看得稀奇:“为什么不画在同一张纸上?”

“有些内容适合分层。” 合成员推动操纵杆,“滚动、动画或者重叠时,已经画好的层可以分别移动,再由我拼起来。不是每次都要从墙皮开始刷。”

JavaScript 老大眼睛一亮:“那我让方块移动,只找你?”

“有机会。”

“有机会?”

“用 transform 改位置,或者用 opacity 改透明度,常常可以避开布局,也可能不用重新绘制内容,直接在合成阶段处理。”

“那以后所有动画都加一层!”

合成员一脚踩住控制台的加层按钮:“想都别想。”

“为什么?”

“怎么分层,浏览器会根据情况决定。就算用 transformopacity,也不能保证任何时候都只发生合成。层太多还要占内存、花管理成本。”

绘制工人笑出了声:“还没学会省返工,先想学怎么给大家加班。”

开拍前统一交稿

“可动画总得一帧一帧改。” JavaScript 老大摊开工作单,“不改怎么动?”

浏览器老大从工地门口走了进来,手里拿着一块场记板。

“可以改,但别闭着眼乱撞。要更新下一幅画面,把工作交到开拍前。”

“怎么交?”

requestAnimationFrame。”

一名场记员接过工作单:

requestAnimationFrame(() => {
    box.style.transform = "translateX(100px)";
});

“浏览器准备下一次绘制时,会在合适的时机叫这段回调来干活。” 场记员敲了敲板,“同一轮要改的画面可以凑到一起,节奏也更贴近屏幕真正要展示的画面。”

JavaScript 老大问:“用了它就永远不卡?”

“当然不是。” 场记员翻了个白眼,“你在回调里搬一栋楼,我也救不了。页面所在的标签页藏到后台时,浏览器还可能降低甚至暂停调用,省点力气。”

“那我写个定时器,每十六毫秒跑一次?”

“屏幕什么时候准备画,不听你的秒表。” 浏览器老大收走定时器,“把视觉更新交给 requestAnimationFrame,至少能少一些错拍和白忙。”

布局工人补了一句:“回调里也要守规矩。先读后写,别借着场记板继续反复横跳。”

JavaScript 老大在工作单最上面写下四个字:

统一交稿。

这次不来回返工

像素工地重新开门。

样式管理员给节点分好衣服。

布局工人量出位置和尺寸。

绘制工人画好文字、颜色、边框和阴影。

合成员把不同图层送上屏幕。

JavaScript 老大站在警戒线外,高高举起一张修改单:“房间加宽八厘米,方块向右移动一百像素。还有谁要改,一次说完!”

几个节点飞快补上申请。

老大先统一问完需要的尺寸,把答案记好,再将改动交给下一次开拍前处理。

布局工人只拉开一次卷尺。

绘制工人只推来一次油漆车。

合成员按下按钮,新的页面稳稳出现在屏幕上。

大家等了一拍。

没人追问“现在多宽”。

布局工人感动得眼圈发红:“原来 JavaScript 也能先问完,再动手。”

“能提前量就提前量。” 老大端起咖啡,“动手以后,就别追着你算现账。今天的施工守则,我记住了。”

“念一遍。”

“节点先算样式,再定位置,然后绘制,最后合成。”

“工牌上的名字呢?”

“布局是 Layout,绘制是 Paint,合成是 Composite。”

“继续。”

“改尺寸和位置,可能触发布局;只改颜色这类外观,可能只需重绘;transformopacity 常常更适合交给合成,但没有万能保证。”

“还有呢?”

“别刚改完样式,就立刻读取尺寸。读写分开,动画更新尽量放进 requestAnimationFrame。”

布局工人满意地点头,把那盘蚊香卷尺挂到了事故墙上。

HTML 解析器凑过去:“留着干什么?”

“纪念像素工地最黑暗的八次返工。”

“能点吗?”

“不能。”

“看着挺像。”

“再问就给 DOM 工地寄回去。”

HTML 解析器立刻闭嘴。

工地的灯一盏盏熄灭。

“打扰一下。”

门外,一个 URL 抱着域名探进脑袋:“谁知道这个名字对应哪栋楼?”

众人齐刷刷摇头。

URL 抱紧域名:“那该找谁问路?”

远处,网络问路处的灯亮了。