主线程今天不接客
一只巨型计算箱霸占了执行大厅,点击在门外砸门,动画躺在地上数遗言。新来的 Worker 刚把活接走,又抄起计算结果直奔 DOM 工地:“让开,我来改页面!”
取消按钮快把门砸烂了
本文承接上一篇:《到底是谁点了按钮?》。
执行大厅门口,那张告示还在风里晃。
主线程今日不接客
门里面,JavaScript 老大趴在一座数字山下,只剩右手还能动。
“价格乘数量……减满减……再减会员券……”
调用栈小弟抱着最上面那只盘子,腿抖得像两根天线:“还有五百套积分规则。”
“放下。”
“盘子还是规则?”
“遗言。”
门外已经挤成了事故现场。
click 报警盒把门砸得咣咣响:“主人又点了取消!”
动画场记员从门缝塞进半块场记板:“上一帧三秒前就该画了!”
像素工地的布局工人贴在窗户上:“购物车数量变了,到底还让不让重新布局?”
JavaScript 老大被折扣表压得抬不起头:“排队!等这次计算结束!”
“还要多久?”
“快了。”
“三秒前也这么说。”
“那说明很稳定。”
浏览器老大赶到时,整个页面已经像冻住的鱼。
主人点按钮,没有反应。
拖滚动条,没有反应。
只有风扇越转越有感情。
浏览器老大一脚踹开执行大厅的门:“谁把页面做成标本了?”
JavaScript 老大从数字山里伸出一根手指,指向那只巨型计算箱。
箱子上贴着一行代码:
const result = calculateDiscounts(cart, rules);
renderResult(result);
调用栈小弟试着把最上面的盘子抽出来。
盘子纹丝不动。
“函数还没算完。”JavaScript 老大说,“你硬抽一个试试?”
“不了。上次叠盘子,医疗费还没报。”
浏览器老大走到门外。click 抱着事件记录单,场记员举着下一帧,布局工人拖着卷尺,队伍一直排到楼梯口。
“都来找谁?”
三只手一起指向门里。
浏览器老大回头看了一眼那只堵住整扇门的计算箱。
“懂了。不是集体罢工,是连门都进不去。”
浏览器老大拿秒表对着计算箱一按。
三千二百毫秒。
秒表“叮”了一声,吐出一张红色罚单:
Long Task:3200ms
罚单背面印着一行小字:超过 50ms,自动染红。
浏览器老大把红单贴到 JavaScript 老大额头上:“三秒多。主人等你算完,够不够顺便学门外语?”
“看语种。”
把闹钟调早一点
JavaScript 老大从箱子底下摸出一只定时器。
“有办法。”
setTimeout(() => {
const result = calculateDiscounts(cart, rules);
renderResult(result);
}, 0);
定时器胸前挂着一个大大的 0,得意地走进事件大厅。
“看见没有?异步。”JavaScript 老大拍掉袖子上的小数点,“先把计算往后放,主线程不就自由了?”
浏览器老大点点头:“开始演练。”
当前任务结束,执行大厅先喘了口气。门外积压的工作终于动了一下。
紧接着,定时器任务拿到号码牌,昂首挺胸走进执行大厅,又把那只巨型计算箱推了进去。
“各位,我回来了。”
门“砰”地关上。
页面再次冻住。
浏览器老大的眉毛跳了一下:“门还是这扇,箱子还是这只。你只是让它晚点进来?”
“从排班角度看,非常成功。”
定时器隔着门喊:“别甩锅!我只负责晚点送到,没承诺另开一间大厅!”
调用栈小弟又掏出一个 Promise。
“要不放进微任务?”
调用栈小弟把计算箱往 Promise 反应微任务手里一塞。小个子亮出 VIP 牌,绕过普通任务的队伍,直奔执行大厅。
click 报警盒一把抱住门框:“凭什么又是它先?”
“微任务通道。”小个子晃了晃胸牌,“下一个普通任务进门前,先轮到我们。”
浏览器老大眼疾手快,连箱带人拽了回来:“原来堵在普通队伍后面,现在准备堵到普通队伍前面。升职挺快,问题没动。”
JavaScript 老大夺过 Promise,塞回抽屉:“今天谁再说‘异步’,先把要走哪扇门说清楚。”
JavaScript 老大找来一把裁纸刀,准备把计算箱切成几十只小箱子。
“这一箱算完先开门,让点击过去。再算下一箱,再开门。”
调用栈小弟递来进度表:“每只箱子算到哪、剩下什么、下次从哪接着来,都得登记。”
表格铺开,滚过地板,越过楼梯,最后消失在走廊拐角。
JavaScript 老大收起裁纸刀:“有用。但这张表长得像 TCP 老司机的年终总结。”
门外忽然传来刹车声。
一辆小货车停在告示旁。车门打开,一个戴毛线帽的年轻员工跳下来,怀里抱着一块空白工作牌。
“听说这里有活干?”
新分部只接计算
浏览器老大带着新员工穿过一条隔离通道,在执行大厅旁边开了一间分部。
新工作台落地,新储物柜上锁,墙上的钟也自己走了起来。执行大厅的催命铃在隔壁响,分部这边连杯里的咖啡都没抖一下。
门牌刚挂上去:
Dedicated Worker
JavaScript 老大趴在窗口上打量:“新来的,叫什么?”
“Web Worker。”
“会 JavaScript?”
“会。”
“会算折扣?”
“会。”
“会喝咖啡?”
Worker 掏出保温杯:“比你头发多的那种会。”
JavaScript 老大把窗口关上了。
浏览器老大把第一份脚本扔进执行大厅:
const worker = new Worker("./coupon-worker.js", {
type: "module"
});
worker.postMessage({ cart, rules });
worker.addEventListener("message", (event) => {
renderResult(event.data);
});
第二份 coupon-worker.js 从隔离通道滑进分部:
self.addEventListener("message", (event) => {
const { cart, rules } = event.data;
const result = calculateDiscounts(cart, rules);
self.postMessage(result);
});
事件大厅小弟推来一座双向电话亭,执行大厅和分部各占一头。
JavaScript 老大对着自己这边的 Worker 对象按下 postMessage()。
“叮!”
分部那头亮起一盏 message 灯。
Worker 接起电话:“计算箱收到。算完从原路扔回去?”
“不然你游回来?”
巨型箱子穿过电话亭,在分部工作台上重新出现。
JavaScript 老大立刻扑到自己这边的桌上:“等等,我的购物车还在。”
Worker 也打开箱子:“我这里也有一份。”
两人同时拎起一只商品对象,隔窗互看。连商品吊牌上的折痕都一模一样。
“你偷了我的?”JavaScript 老大问。
“你才偷了我的。”
Worker 把自己那份商品价格改成一分钱。
主线程里的价格纹丝不动。
JavaScript 老大又改了自己的数量。Worker 那边也没动。
两人一起蹲下,看向电话亭底部。
里面藏着一台复印机,铭牌上刻着:
structured clone
“难怪。”JavaScript 老大敲敲玻璃,“送一箱,复印一箱。谁也别惦记对方那份。”
JavaScript 老大随手抓起一个函数塞进去。
电话亭当场响起警报:
DataCloneError
复印机把函数原样吐了出来。
“没法复印。”电话亭捏着鼻子说,“下一位。”
HTML 解析器抱着一个 DOM 节点赶来:“这个呢?”
警报叫得更响了。
DOM 节点刚碰到入口,复印机便拉响第二遍警报。
HTML 解析器赶紧把节点抱回来:“知道了,工地财产不外借。”
Worker 摊手:“所以把原料数据给我,别把整间办公室打包。”
货太大,复印机冒烟了
计算分部正式开工。
主线程空了下来。点击报警盒终于进门,动画也重新一帧一帧走动。主人拖动页面时,滚动条不再装死。
浏览器老大很满意,直到第二只数据箱抵达。
这次不是五百件商品,而是一整块等待分析的图像数据。一个巨大的 ArrayBuffer 塞住电话亭,结构化克隆复印机疯狂喷纸,滚轮烫得发红。
Worker 从纸山后探出头:“复印这么大一份,是怕两边都舍不得吗?”
JavaScript 老大也在另一边冒头:“那怎么办?”
Worker 从纸山里刨出一只扳手,敲开电话亭侧板。里面还藏着一条滑轨。
worker.postMessage(
{ buffer: imageBuffer },
[imageBuffer]
);
JavaScript 老大还没听完,便一把推下控制杆。
“嗖——”
图像数据连同控制权一起滑进分部。
JavaScript 老大回头再看自己的 imageBuffer:
console.log(imageBuffer.byteLength); // 0
“空了?”
JavaScript 老大伸手进箱子,摸了个空。
Worker 在分部那头拍了拍完整的数据箱:“滑轨只运一份。箱子和里面的货,现在都在我这边。”
“能退货吗?”
“算完再转移回去。”
“这条滑轨叫什么?”
“转移列表。能上车的货,外号叫 Transferable。”
“为什么不早说?”
“你推杆的动作太有领导气质。”
JavaScript 老大眼睛一亮,把一张 1 + 1 的纸条也绑上滑轨。
纸条先去窗口登记,再装箱,再通知分部。Worker 拆开包装,写下 2,又把结果送回电话亭。
这时,JavaScript 老大已经心算完了,还顺手喝了口咖啡。
浏览器老大夺下那张纸:“开分部、送消息、搬数据,哪样不要时间?这种活也外包,是嫌流程不够长?”
Worker 把滑轨重新锁好:“大箱子走这里。小纸条让复印机顺手带。至于一眼能算完的——”
“闭嘴。”JavaScript 老大把纸条揉成一团,“刚才只是测试员工积极性。”
新员工硬闯 DOM 工地
折扣计算终于结束。
Worker 看着结果,十分满意。
应付金额:4382
节省金额:716
“这种小事还要送回去?”Worker 抄起结果表,“我直接改页面。”
“等等——”JavaScript 老大扑向窗口。
晚了。
Worker 已经冲出分部,直奔 DOM 工地。
HTML 解析器正在修一棵被事件事故撞歪的节点树,远远看见一顶毛线帽飞奔而来。
“让开!”Worker 高举结果表,“我要把总价写进 #total!”
DOM 工地门卫横过栏杆:“证件。”
Worker 递上工作牌。
门卫看了一眼:“WorkerGlobalScope?”
“新部门,前途无量。”
“document 呢?”
Worker 摸了摸左边口袋。
又摸了摸右边口袋。
最后把毛线帽摘下来抖了抖。
什么也没有。
document.querySelector("#total").textContent = result.total;
// ReferenceError: document is not defined
门卫把栏杆又压低了一点:“回去。”
“我会 JavaScript!”
“这里查的是 DOM 权限,不是语法成绩。”
“那我隔着栏杆改一小下?”
“隔着栏杆也叫改。”
HTML 解析器拎着扳手走过来:“你入职时不是分了新工作台、新柜子、新全局环境吗?”
“待遇不错。”
“这棵树没分给你。”
Worker 往分部方向看了一眼。那里有计算箱,有电话亭,有保温杯,唯独没有 window,也没有 document。
“那我借 JavaScript 老大的树用一下?”
门卫指了指工地里密密麻麻的施工队:“一棵树已经够乱了,再放一个从隔壁伸手的,布局工人明天就会拿卷尺上吊。”
Worker 不服:“算完的结果怎么办?”
JavaScript 老大终于追到门口,扶着膝盖喘气。
“发、消、息。”
Worker 按下电话亭按钮:
self.postMessage({
type: "done",
result
});
“叮!”
执行大厅这头的 message 灯亮了。JavaScript 老大接过结果:
worker.addEventListener("message", (event) => {
if (event.data.type !== "done") {
return;
}
total.textContent = event.data.result.total;
savings.textContent = event.data.result.savings;
});
屏幕上的金额终于亮了起来。
Worker 隔着栏杆看了半天:“所以我负责算,你负责碰页面。”
“对。”
“这算职场歧视吗?”
“这算线程隔离。”
“名字稍微体面一点。”
分部开始冒烟
下午六点,Worker 的第一天眼看就要平安结束。
分部突然“砰”地冒出一股黑烟。
屏幕上的“正在计算”还在转圈。JavaScript 老大等了半天,电话亭一声不响。
“算完没有?”
烟里没人回答。
事件大厅小弟从电话亭底下抽出一封皱巴巴的信:“有封 error,门口没有收件人,我还以为你们喜欢黑烟主题。”
JavaScript 老大赶紧补上名字:
worker.addEventListener("error", (event) => {
showError(`计算失败:${event.message}`);
});
演练重来。分部刚一冒烟,错误信便“嗖”地滑到主线程,消息、脚本文件和出错行号一样没少。
JavaScript 老大扫了一眼:“第十八行。”
Worker 从烟里探出头:“一条规则把字符串当函数调用了。小问题。”
“能继续算吗?”
“这个任务不能。”
JavaScript 老大关掉页面上的转圈提示,换成了失败消息。
浏览器老大敲敲电话亭:“这回外面总算知道里面不是在刻苦工作,是炸了。”
刚处理完错误,分部又传出有节奏的敲桌声。
Worker 正对着一份坏数据无限循环。
“还在算?”JavaScript 老大问。
“还在算。”
“算出什么了?”
“下一圈就知道。”
五分钟后。
“现在呢?”
“上一圈对下一圈寄予厚望。”
JavaScript 老大写了封取消信,从电话亭塞进去:
worker.postMessage({ type: "cancel" });
信顺利抵达分部,却只能排在当前任务后面。Worker 还在桌前绕圈,连信封的一角都没看见。
“它什么时候拆信?”JavaScript 老大问。
“忙完这一圈。”事件大厅小弟说。
“这一圈什么时候完?”
Worker 在屋里高喊:“下一圈就知道!”
浏览器老大默默掀开终止闸的保护盖:
worker.terminate();
房间瞬间安静。
桌上留着半行没写完的字:
请等我收完尾再——
“看来终止闸不等下班手续。”JavaScript 老大说。
“也不等遗言。”调用栈小弟补充。
页面重新活了
第二次正式计算开始。
浏览器老大重新打开一间分部。戴“二号”毛线帽的 Worker 接过购物车和折扣规则。
Worker 在隔壁埋头计算。执行大厅没有再被巨型箱子堵住,JavaScript 老大继续接待点击,布局工人的卷尺终于伸开,场记员也一帧一帧打起了板。
计算完成,Worker 把结果发回。
JavaScript 老大接过结果,更新 DOM。
主人点下取消,按钮立刻响应。
拖动滚动条,商品顺畅地滑过屏幕。
动画场记员感动得抱住场记板:“帧终于不是遗照了。”
浏览器老大撕掉门口那张告示。
门外已经没人排队。巨型计算箱在隔壁滚得轰隆作响,执行大厅这边却只偶尔响起一声清脆的 message。
JavaScript 老大给 Worker 倒了杯咖啡:“今天表现不错。除了硬闯 DOM 工地。”
Worker 接过杯子:“新人总得主动一点。”
“下次少主动。”
“那可不一定。”Worker 神秘地压低声音,“我还有个亲戚,也叫 Worker。”
“也会算折扣?”
“不。那位住在网络路口,最爱半路拦车。服务器不在,也敢替人回话。”
隧道外恰好响起断网警报。
URL 的请求车踩下刹车,正准备掉头。
路边一间小岗亭突然亮起了灯。