主线程今天不接客

一只巨型计算箱霸占了执行大厅,点击在门外砸门,动画躺在地上数遗言。新来的 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 的请求车踩下刹车,正准备掉头。

路边一间小岗亭突然亮起了灯。