普通视图

发现新文章,点击刷新页面。
今天 — 2025年12月10日技术

每日一题-统计计算机解锁顺序排列数🟡

2025年12月10日 00:00

给你一个长度为 n 的数组 complexity

在房间里有 n 台 上锁的 计算机,这些计算机的编号为 0 到 n - 1,每台计算机都有一个 唯一 的密码。编号为 i 的计算机的密码复杂度为 complexity[i]

编号为 0 的计算机密码已经 解锁 ,并作为根节点。其他所有计算机必须通过它或其他已经解锁的计算机来解锁,具体规则如下:

  • 可以使用编号为 j 的计算机的密码解锁编号为 i 的计算机,其中 j 是任何小于 i 的整数,且满足 complexity[j] < complexity[i](即 j < i 并且 complexity[j] < complexity[i])。
  • 要解锁编号为 i 的计算机,你需要事先解锁一个编号为 j 的计算机,满足 j < i 并且 complexity[j] < complexity[i]

求共有多少种 [0, 1, 2, ..., (n - 1)] 的排列方式,能够表示从编号为 0 的计算机(唯一初始解锁的计算机)开始解锁所有计算机的有效顺序。

由于答案可能很大,返回结果需要对 109 + 7 取余数。

注意:编号为 0 的计算机的密码已解锁,而 不是 排列中第一个位置的计算机密码已解锁。

排列 是一个数组中所有元素的重新排列。

 

示例 1:

输入: complexity = [1,2,3]

输出: 2

解释:

有效的排列有:

  • [0, 1, 2]
    • 首先使用根密码解锁计算机 0。
    • 使用计算机 0 的密码解锁计算机 1,因为 complexity[0] < complexity[1]
    • 使用计算机 1 的密码解锁计算机 2,因为 complexity[1] < complexity[2]
  • [0, 2, 1]
    • 首先使用根密码解锁计算机 0。
    • 使用计算机 0 的密码解锁计算机 2,因为 complexity[0] < complexity[2]
    • 使用计算机 0 的密码解锁计算机 1,因为 complexity[0] < complexity[1]

示例 2:

输入: complexity = [3,3,3,4,4,4]

输出: 0

解释:

没有任何排列能够解锁所有计算机。

 

提示:

  • 2 <= complexity.length <= 105
  • 1 <= complexity[i] <= 109

别让页面 “鬼畜跳”!Google 钦点的 3 个性能指标,治好了我 80% 的用户投诉

2025年12月9日 23:48

💥告别卡顿!前端性能优化第一课:Google钦点的三大核心指标,你真的懂吗?

欢迎来到前端性能优化专栏的第一课!在这个“用户体验至上”的时代,一个卡顿、缓慢、乱跳的网站,就像一辆抛锚在高速公路上的跑车,再酷炫也只会让人抓狂。别担心,Google已经为你准备好了一份“体检报告”——核心Web指标(Core Web Vitals)

今天,我们就来揭开这份报告的神秘面纱,用最通俗易懂的方式,让你彻底搞懂这三大指标,迈出性能优化的第一步!

✨ LCP(Largest Contentful Paint):最大内容绘制

🚀 衡量:加载性能(你的“第一印象”)

LCP,直译过来是最大内容绘制。它衡量的是用户在访问页面时,视口中最大的那块可见内容(图片或文本块)完成渲染所需的时间

简单来说,它回答了一个核心问题:用户觉得你的页面“加载完成”了吗?

想象一下,你打开一个电商网站,最想看到的是商品大图和价格。LCP就是衡量这个“最重要的东西”多久能出现在你面前。它直接反映了用户对页面加载速度的感知。

指标 衡量维度 优秀标准
LCP 用户感知的加载速度 <= 2.5秒

如果你的 LCP 超过 2.5 秒,用户可能就开始感到不耐烦了。优化 LCP,就是让你的“门面”以最快的速度展示给客人!

LCP 示意图

⚠️ CLS(Cumulative Layout Shift):累积布局偏移

🛡️ 衡量:视觉稳定性(告别“鬼畜”跳动)

CLS,累积布局偏移,听起来有点拗口,但它的作用非常直观:它衡量页面加载过程中元素的非预期移动

你一定遇到过这种情况:正准备点击一个按钮,结果它上面的广告突然加载出来,把按钮挤下去了,你点了个空!这就是布局偏移(Layout Shift)。

CLS 的分数就是用来量化这种“鬼畜”跳动有多严重的。分数越低,代表你的页面布局越稳定,用户体验越丝滑。

指标 衡量维度 理想值
CLS 页面布局稳定性 < 0.1

小贴士: 布局偏移通常是由没有设置尺寸的图片、动态插入的广告或内容导致的。想要 CLS 达标,请给你的元素预留好“坑位”!

⚡️ INP(Interaction to Next Paint):交互到下次绘制

🔄 衡量:整体交互响应性(从“第一印象”到“全程流畅”)

INP,交互到下次绘制,是性能指标家族的新晋“网红”。它衡量的是用户进行点击、触摸或键盘输入后,浏览器需要多长时间才能在屏幕上绘制出视觉反馈。

它取代了老前辈 FID(First Input Delay,首次输入延迟) ,为什么呢?

为什么用 INP 替代 FID? INP 的优势
FID 的局限性 仅测量首次输入延迟,忽略了用户在后续操作中遇到的卡顿。
INP 的全面性 监控用户整个访问周期内的所有交互,更全面。
更真实的用户体验 INP 选取最慢的一次交互作为代表值,反映了“整个使用过程中”的流畅度,而不是仅仅看“第一印象”。

简单来说,FID 就像面试官只看你的简历,而 INP 则是全程跟拍你的工作表现。一个真正流畅的网站,不应该只是第一次点击快,而是从头到尾都快!

INP 与 FID 比较图

🔬 实验室数据 vs. 🌍 现场数据:性能优化的“双重奏”

搞懂了三大指标,接下来我们聊聊如何获取这些数据。性能数据主要分为两大类:实验室数据现场数据

类型 来源(数据渠道) 优点 缺点
实验室数据 (Lab Data) Lighthouse 可控、快速、方便复现问题 非真实用户环境,可能与实际体验有偏差
现场数据 (Field Data) CrUX/Web Vitals 真实用户体验,数据最可靠 不易复现问题,需要时间积累数据

🔧 实验室数据工具

实验室数据就像你在实验室里用精密仪器做的测试,环境是固定的。

  • Lighthouse (Chrome 内置) :Chrome 开发者工具里就能找到,它能快速给你打分,支持 LCP/CLS/INP 评分。
  • PageSpeed Insights:Google 官方工具,它会结合实验室数据(Lighthouse)和现场数据(CrUX),给你一份一站式的性能报告。

🛠️ 现场数据工具

现场数据则是你的网站在真实用户、真实网络、真实设备上跑出来的“实战成绩”。

  • Google Search Console:提供网站整体的核心指标健康报告,是 SEO 优化的重要参考。
  • web-vitals JavaScript 库:这是前端工程师的“秘密武器”。它是一个轻量级的库,可以让你在用户浏览器中实时收集 LCP、CLS、INP 数据,并上报到你的分析后台。

💡 使用 web-vitals 收集性能数据(实战代码)

通过这个库,你可以将真实用户的性能数据发送到你的服务器进行分析,建立自己的性能监测体系(RUM,Real User Monitoring)。

import { onLCP, onCLS, onINP } from 'web-vitals'

function sendToAnalytics(metric) {
  // 将性能指标数据转换为 JSON 字符串
  const body = JSON.stringify(metric)
  
  // 使用 navigator.sendBeacon 或 fetch 发送数据,确保在页面关闭前发送成功
  ;(navigator.sendBeacon && navigator.sendBeacon('/analytics', body)) ||
    fetch('/analytics', { body, method: 'POST', keepalive: true })
}

// 监听并上报三大核心指标
onLCP(sendToAnalytics)
onCLS(sendToAnalytics)
onINP(sendToAnalytics)

✅ 总结:性能优化的“三字真经”

好了,总结一下我们今天学到的前端性能优化的“三字真经”:

  1. LCP:核心内容尽快可见(加载速度要快!)
  2. CLS:页面布局稳定(别再乱跳了!)
  3. INP:交互响应及时(操作要流畅!)

通过 LighthouseSearch Console 以及 Web Vitals 库,我们不仅能建立起一套完善的性能监测体系,还能精准地识别并修复那些让用户抓狂的体验瓶颈。

提升网页质量,不仅能让用户开心,还能让你的网站在 Google 搜索中获得更好的排名。

🤝 了解 CDP (Chrome DevTools Protocol):browser-use 背后的隐藏功臣

作者 卤代烃
2025年12月9日 23:07

Chrome DevTools Protocol (CDP) 是 Chromium 浏览器调试工具的核心通信协议:它基于 JSON 格式,可以通过 WebSocket 实现客户端与浏览器内核之间的双向实时交互。

基于 CDP 的开源产品有许多,其中最有名的应该是 Chrome Devtools FrontendPuppeteerPlaywright 了。

Chrome Devtools Frontend 就是前端开发者天天按 F12 唤起的调试面板,而 Puppeteer 和 Playwright 是非常有名的浏览器自动化操作工具,如今的 agent browser tool(例如 playwright-mcpbrowser-usechrome-devtools-mcp)也是基于它们构建的。可以说每个开发者都在使用 CDP,但因它的定位比较底层,大家常常又意识不到他的存在。

Chrome Devtools Frontend Puppeteer

CDP 有自己的官方文档站和相关的 Github 地址,秉承了 Google 开源项目的一贯风格,简洁,克制,就是没什么可读性。文档和项目都是根据源码变动自动生成的,所以只能用来做 API 的查询,这也导致如果没有相关的领域知识,直接阅读相关文档或 deepwiki 是拿不到什么有建设性内容的。

上面的那些吐槽,也是我写本文的原因,互联网上介绍 CDP 的博文太少了,也没什么系统性的架构分析,所以不如我自己来写丰富一下 AI 的语料库(bushi)。

协议格式

首先 CDP 协议是一个典型的 CS 架构,这里我们拿 Chrome Devtools 为例:

  • Chrome Devtools:就是 Client,用来做调试数据的 UI 展示,方便用户阅读
  • CDP:就是连接 Client-Server 的 Protocol,定义 API 的各种格式和细节
  • Chromium/Chrome:就是 Server,用来产生各种数据

CDP 协议的格式基于 JSON-RPC 2.0 做了一些轻量的定制。首先是去掉了 JSON 结构体中的 "jsonrpc": "2.0" 这种每次都要发送的冗余信息。可以看下面几个 CDP 的实际例子:

首先是常规的 JSON RFC Request/Response,细节不用关注,就看整体的格式:

Target.setDiscoverTargets

// Client -> Chromium
{
  "id":2
  "method": "Target.setDiscoverTargets",
  "params": {"discover":true,"filter":[{}]},
}

// Chromium -> Client
{
  "id": 2,
  "result": {}
}

可以看到这就是一个经典的 JSON RFC 调用,用 id 串起 request 和 response 的关系,然后 request 中通过 methodparams 把请求方法和请求参数带上;response 通过 result 带上响应结果。

关于 JSON RFC Notification(Event)的例子如下,定义也很清晰,就不展开了:

Target.targetCreated

{
  "method": "Target.targetCreated",
  "params": {
    "targetInfo": {
      "targetId": "12345",
      "type": "browser",
      "title": "",
      "url": "",
      "attached": true,
      "canAccessOpener": false
    }
  }
}

众所周知,JSON RFC 只是一套协议标准,它其实可以跑在任意的支持双向通讯的通信协议上。目前 CDP 的主流方案还是跑在 WebSocket 上(也可以用本地 pipe 的方式连接,但用的人少),所以用户可以借助任意的 Websocket 开源库搭建出合适的产品。


Domain 整体分类

如果直接看 CDP 的文档,会发现它的目录侧边栏只有一列,那就是 Domains,然后下面有一堆看起来很熟悉的名词:DOM,CSS,Console,Debugger 等等...

CDP Domains Chrome Devtools Frontend

其实这些 Domain 都可以和 Chrome Devtools 联系起来的。所以我们可以从 Chrome Devtools 的各种功能反推 CDP 中的各种 Domain 作用:

  • Elements:会用到 DOM,CSS 等 domain 的 API
  • Console:会用到 Log,Runtime 等 domain 的 API
  • Network:会用到 Network 等 domain 的 API
  • Performance:会用到 Performance,Emulation 等 domain 的 API
  • ......

那么到这里就有一个比较直观的认识了。我们再返回看 CDP 本身,CDP 其实可以分为两大类,然后下面有不同的 Domain 分类:

  • Browser Protocol:浏览器相关的协议,之下的 Domain 都是平台相关的,比如说 Page,DOM,CSS,Network,都是和浏览器功能相关
  • JavaScript Protocol:JS 引擎相关的协议,主要围绕 JS 引擎功能本身,比如说 Runtime,Debugger,HeapProfiler 等,都是比较纯粹的 JS 语言调试功能

deepwiki 给出的 CDP Domains 分类

了解了 Domain 的整体分类,下一步我们探索一下 Domain 内部的运行流程。

Domain 内部通信

理解某个 Domain 的运行流程,还是老办法,对照着 Chrome Devtools Frontend 的某个调试面板反推,这样理解起来是最快的。

这里我们拿 Console 面板为例,这个基本上是 Web 开发者日常使用频率最高的功能了。

从 UI 面板上看有很多功能,有筛选,分类,分组等各种高级功能,但绝大部分的功能都是前端上的实现,联系到背后和 Console 相关的 CDP 协议,其实主要就 5 条:


举一个真实的例子,我们在 Console 面板先发起一个不合规的网络请求,然后再 log 一句话:

  • 首先每个页面打开 Devtools 的时候,会默认调用 Log.enable 启动 log 监听
  • 手动 fetch 一个不合规的地址时,浏览器会先做安全检查,通过 Log.entryAdded 提示不合规
  • 发起一个真实的网络请求,失败后会通过 Runtime.exceptionThrown 提示 Failed to fetch
  • 最后手动调用 console API,CDP 会发一个 Runtime.consoleAPICalled 的调用 log event

把上面的的例子抽象一下,其实所有的 Domain 的调用流程基本都是一样的:

  • 通过 Domain.enable 开启某个 Domain 的调试功能
  • 开启功能后,就可以在这个阶段发送相关的 methods 调用,也可以监听 Chrome 发来的各种 event
  • 通过 Domain.disable 关闭这个 Domain 的调试功能

部分 Domain 并没有 enable/disable 这两个 methods,具体情况具体分析

Target: 特殊的 Domain

上面介绍了 Domain 的分类和 Domain 内部运转的整体流程,但是有一个 Domain 非常的特殊,那就是 Target

type 分类

Target 是一个较为抽象的概述,它指的是浏览器中的可交互实体

  • 我创建了一个浏览器,那么它本身就是一个 type 为「browser」的 Target
  • 浏览器里有一个标签页,那么这个页面本身就是一个 type 为「page」的 Target
  • 这个页面里要做一些耗时计算创建了一个 Worker,那么它就是一个 type 为「worker」的 Target

目前从 chromium 源码上可以看出,Target 的 type 有以下几种:

  • browser,browser_ui,webview
  • tab,page,iframe
  • worker,shared_worker,service_worker
  • worklet,shared_storage_worklet,auction_worklet
  • assistive_technology,other

从上面的 target type 可以看出,Target 整体是属于一个 scope 比较大的实体,基本上是以进程/线程作为隔离单位分割的,每个 type 下可能包含多个 CDP domain,比如说 page 下就有 Runtime,Network,Storage,Log 等 domain,其他类型同理。

交互流程

Target 的内部分类清晰了,那么还剩重要的一环:如何和 Target 做交互

CDP 这里的逻辑是,先发个请求,向 Target 发起交互申请,然后 Target 就会给你一个 sessionId,之后的交互就在这个 session 信道上进行。CDP 在这里也对 JSON-RPC 2.0 做了一个轻量定制,它们把 sessionId 放在了 JSON 的最外层,和 id 同一个层级:

{
  method: "SystemInfo.getInfo",
  id: 9,
  sessionId: "62584FD718EC0B52B47067AE1F922DF1"
}

我举个实际的例子看 session 的交互流程。

假设我们想从 browser Target 上获取一些系统消息,先假设我们事先已经知道了 browser 的 targetId,那么一个完整的 session 通信如下:

这里为了聚焦 session 的核心交互逻辑,下面的 CDP message 删除了不必要的信息

  1. Client 通过 Target.attachToTarget API 向 browser 发起会话请求,拿到 sessionId
// Client —> Chromium
{
  "method": "Target.attachToTarget",
  "params": {
    "targetId": "31a082d2-ba00-4d8f-b807-9d63522a6112", // browser targetId
    "flatten": true // 使用 flatten 模式,后续将会把 sessionId 和 id 放在同一层级
  },
  "id": 8
}

// Chromium —> Client
{
  "id":8,
  "result": {
    "sessionId": "62584FD718EC0B52B47067AE1F922DF1" // 拿到这次对话的 sessionId
  }
}
  1. Client 带上上一步给的 sessionId,发送一条获取系统信息的 CDP 调用并获取到相关消息
// Client —> Chromium
{
  "method": "SystemInfo.getInfo", // 获取系统信息的方法
  "id": 9,
  "sessionId": "62584FD718EC0B52B47067AE1F922DF1" // sessionId 和 id 同级,在最外层
}

// Chromium —> Client
{
  "id": 9,
  "sessionId": "62584FD718EC0B52B47067AE1F922DF1"
  "result": { /* ... */ },
}
  1. 不想在这个 session 上聊了,调用 Target.detachFromTarget 直接断开连接,自此这个会话就算销毁了
// Client —> Chromium
{
  "method": "Target.detachFromTarget",
  "id": 11,
  "sessionId":"62584FD718EC0B52B47067AE1F922DF1"
}

// Chromium —> Client
{
  "id": 11,
  "result": {}
}

上面的流程可以用下面的图表示:

当然涉及 Target 生命周期的相关 Methods 和 Event 还有很多,一一讲解也不现实,感兴趣的同学可以自己探索。

一对多

除了上述的特性,Target 还有一个特点,那就是一个 Target 允许多个 session 连接。这意味着可以有多个 Client 去控制同一个 Target。这在现实中也是很常见的。比如说对于一个网页实体,它既可以被 Chrome Devtools(Client1)调试,也可以同时被 puppeteer(Client2)连接做自动化控制。当然这也会带来了一些资源访问的并发问题,在实际应用场景上需要万分的小心。

综合案例

综上所述,我们可以看一个实际的例子,把上面的内容都囊括起来。

下面的案例我是用 puppeteer 创建了一个 url 为 about:blank 的新网页时,底层的 CDP 调用流程。调用的源文件可以访问右边的超链接下载:create_about_blank_page.har,har 文件可用 Chrome Devtools Network 导入查看:


首先是最开始的 Target 创建流程。注意下图红框和红线里的内容:

  • 首先调用 Target.createTarget 创建一个 page(在调用 createTarget 时,会同步生成一个 tab Target,我们可以忽略这个行为,不影响后续理解)
  • page Target 创建好后,在响应 Target.createTarget methods 的同时,还会发送一个 Target.targetCreated 的 event,里面有这个 page Target 的详细 meta info,例如 targetId,url,title 等
  • page Target 的 meta info 变动时,会下发 Target.targetInfoChanged event 同步信息变化
  • page Target 下发一个 Target.attachedToTarget 的 event,告知 client 这次连接的 sessionId,这样后续的一些 domain 操作就可以带上 sessionId 保证信道了

Target 创建好后,就要开启这个 page 下的各个 Domain 了:

  • Network.enable:开启 Network Domain 的监听,比如说各种网络请求的 request/response 的细节
  • Page.enable:开启 Page Domain 的监听,比如说 navigation 行为的操纵
  • Runtime.enable:开启 Runtime Domain 的监听,比如说要在 page 里 evaluate 一段注入函数
  • Performance.enable:开启 Performance Domain 的监听,比如说一些 metrics 信息
  • Log.enable:开启 log 相关信息的监听,比如说各种 console.log 信息

开启相关 Domain 后,就可以监听这个 page Target 的相关 event 或者主动触发一些方法,如下图所示:

  • 我们主动执行 Page.getNavigationHistory methods,获取当前页面的 history 导航记录
  • 我们监听到 Runtime.consoleAPICalled event 的触发,拿到了一些 console 信息

相关的细节还有很多就不一一列举了,感兴趣的同学可以看上面的 har 源文件,我相信全部看完后就会对 CDP 有个清晰的认知了。

编码建议

就目前(2025.12)而言,Code Agent 和 DeepResearch 等常见的 AI 辅助编程工具在 CDP 领域上表现并不是很好,主要的原因有 3 点:

  • 预训练语料少:从前文可知,CDP 因为协议过于底层,所以相关的使用案例和代码非常少,模型预训练时语料很少,导致幻觉还是比较严重的
  • 文档质量一般:CDP 文档写的太简洁了,基本就是根据出入参自动生成的类型文档,只能用来查询核实一下,想从中获得完整的概念,对 AI 和人来说还是太难了
  • API 动态迭代:CDP 虽然开源出来了,但其本质还是一个为 Chromium 服务的私有协议,其 latest 版本一直在动态迭代中,所以这种动态变化也影响了 AI 的发挥

综合以上原因,我的一个策略是「小步快跑,随时验证」。方案就是对于自己想实现的功能,先让 AI 出一个大致的方案,但是不要直接在自己的迭代的项目里直接写,而是先生成一个可以快速验证相关功能的最小 DEMO,然后亲自去验证这个方案是否符合预期。

「亲自验证 DEMO 可行性」 这一步非常重要,因为 AI 直出的 CDP 解决方案可靠性并不高,不像 AI -> UI 有较高的容错率和置信度,只有在 DEMO 上验证成功的方案才有迁移到正式项目的价值。

另一个解决方案,就是 puppeteer 等优秀项目上吸取经验。puppeteer 底层也是调用 CDP,而且它迭代了十余年,对一些常见案例已经沉淀了一套成熟的解决方案。通过学习它内部的 CDP 调用流程,可以学习到很多文档未曾描述的运用场景。下一篇 Blog,我们就分析一下 puppeteer 的源码架构,让我们在调用过程中更加得心应手。

如何设置你的 PWN 环境

2025年12月9日 22:36

你首先需要的是 Linux Shell ,因为大多数 pwn 挑战通常是 64 位 ELF 可执行文件 。我会写两个不同的部分,分别针对 Windows 和 MacOS。如果你已经用过任何 Linux 发行版,这部分你就没问题了!对于剩下的 Windows/MacOS 用户,我强烈建议安装 Debian Bookworm 作为首选发行版,因为它目前使用 glibc 2.36,尚未实现完整的 RELRO,  且利用起来稍微容易一些。

Windows

对于正在阅读本文的 Windows 用户,你可以按照链接的说明书轻松下载 WSL。如果你懒得看,这里有一份你应该执行的命令列表。

安装使用 Debian for WSL

wsl.exe --update
wsl --set-default-version 2
wsl install -d Debian

就是这样!从现在起,你可以通过开始菜单中的 Debian 搜索 Debian,或者在命令行中运行 Debian,进入你的 Debian shell。关于上述 WSL 2 的要求,WSL 2 允许你在 WSL 内部的集成桌面模式下使用 GUI 应用。这其实没什么特别的意义,除了你可以让你的调试器在一个新窗口里弹出,不用 tmux,这对我来说简直是个大胜利!!

补充一点,你可能需要更新系统包 ,并下载文本编辑器,以避免在 WSL 终端和其他窗口之间切换。你可以用以下命令完成: sudo apt update && sudo apt dist-upgrade && sudo apt(-get) install <editor of choice> 。

如果你想要完全懒散的体验和最少的 Linux shell 作,你可以挂载你的 Windows 文件系统 ,并在 Downloads 目录中使用以下命令(不区分大小写!): cd /mnt/c/users/your\ name/downloads

macOS

不幸的是,对于 MacOS 用户来说,我觉得没有像 Windows 用户那样的集成桌面。所以,我建议你按照这个指南作,尽可能多地在终端里作 ,你会在其中花费比预期更多的时间。好消息是,有 Lima,我们可以用它帮助简化创建 x64 Linux 壳,即使是在 Apple Silicon 上。

这里有一份快速入门的推荐清单。

brew install lima
limactl start --name=x64-debian template://debian --arch=x86_64
limactl shell x64-debian 
sudo apt install tmux

遇到终端颜色的问题,解决方案完全取决于你的终端模拟器 ,所以你自己找解决方案。

额外加分:可写主机文件系统!

LiMactl 编辑 x64-debian 以编辑配置 添加以下不安全的配置:

mounts:
- location: "~"
  writable: true

现在你拥有了一个运行在 x64 上的 Linux Shell ,可以不受限制地访问你的主机文件系统! 你可以用标签自动补全你的虚拟机名字拼写!

恭喜你通过了 Linux 安装!繁琐的部分现在开始了......我们先着手安装调试器和基础工具。

工具安装


恭喜你完成了Linux安装!乏味的部分现在开始了...让我们先安装调试器和基本工具。

实用工具


pwntools
  1. sudo apt install python3 安装python
  2. sudo apt-get install python3-pwntools 安装pwntools
  3. pwn checksec /lib/x86_64-linux-gnu/libc.so.6 验证它是否工作
one gadget
  1. sudo apt install rubygems 安装gem
  2. gem install one_gadget 安装one gadget
  3. one_gadget /lib/x86_64-linux-gnu/libc.so.6 验证它是否工作
rop gadget
  1. sudo -H python3 -m pip install ROPgadget 安装ROPgadget
  2. ROPgadget /lib/x86_64-linux-gnu/libc.so.6 验证它是否工作(警告:输出量很大!!)

调试器


强烈推荐bata-gef,你也应该使用它!

我不建议盲目运行安装脚本。相反,克隆仓库并在你的.gdbinit中source它的gef.py

cd ~
git clone https://github.com/bata24/gef
vi ~/.gdbinit

#.gdbinit
source /path/to/gef.py

对于没有集成桌面模式的MacOS用户,使用我们之前安装的tmux。只需运行以下命令:

  1. tmux 启动tmux
  2. [Ctrl] + B 进入命令模式
  3. ] 启用滚动
  4. [Ctrl] + C 重置状态
  5. [Ctrl] + B,然后<arrow> 切换焦点标签页

反编译器/反汇编器


太好了!现在我们有了开始编写和调试_pwn_脚本所需的一切,这技术上就是我们需要的全部,但有时,挑战不提供源代码。在这种情况下,需要进行一些逆向工程工作。让我们下载**Ghidra**。

设置相当简单,你只需要一个Java运行时,我推荐Temurin,你可以在

这里 找到它。

一旦你安装了运行时,只需运行ghidraRunghidraRun.bat来启动Ghidra

使用Ghidra

Ghidra实际上相当不错且用户友好,你所要做的就是创建一个空项目,然后拖放你的挑战二进制文件到其中,并给它一点时间来生成反汇编和反编译!

容器化和服务器复制


恭喜你走到这一步!你已经准备好处理几乎所有pwn挑战了。然而,我们可以做得更好。

有时,善意的挑战作者会提供libc、ld和/或Dockerfile。这些文件特别有用,因为它们帮助你确保你的挑战使用与服务器相同的库,确保你永远不会遇到_"在我的机器上可以运行"_的问题!所以,我将介绍如何处理这些文件的各种组合。

给定了Libc、ld和Dockerfile


这是最好的情况,因为你不需要做太多。这里有一个你可以运行的命令来使用给定的libcld

./ld-linux-x86-64.so.2 --library-path . ./chal

这将运行链接器,并告诉它使用你目录中的库来启动挑战。就这样! 你现在正在使用给定的库。但是,我们如何复制服务器呢?

首先,我们必须安装Docker。过程相当长,但请相信

文档 并遵循它。

一旦你安装了Docker,你所要做的就是运行以下命令:

docker run -p YOUR_PORT:CONTAINER_PORT -d --privileged $(docker build -q .)

这将构建容器,然后以特权运行它,这通常是_pwn.red/jail_模板所需要的。请将CONTAINER_PORT占位符替换为容器提供挑战的端口,将YOUR_PORT替换为宿主机上的任何未使用端口。默认情况下,这将绑定到所有接口

设置完成后,你可以像这样连接到容器并运行挑战:

nc 127.0.0.1 YOUR_PORT

就这样!你已经成功复制了服务器使用的库,以及服务器本身!完成后,你应该通过运行docker ps找到容器哈希,然后运行docker stop <hash>来清理正在运行的容器。

只提供了Dockerfile


有时,作者只提供Dockerfile。这很糟糕,但没关系,因为我们可以自己提取libcld。我们首先使用上述步骤启动容器,然后执行以下操作:

docker cp 1a2b:/lib/x86_64-linux-gnu/libc.so.6 ./libc.so.6
docker cp 1a2b:/lib/x86_64-linux-gnu/ld-linux-x86-64 ./ld-linux-x86-64.so.2

记得将1a2b替换为容器哈希的前4个字符,启动时会显示给你。有时,Dockerfile会从另一个Linux发行版复制文件到一个文件夹来提供服务,以便使用该发行版的libc。在这种情况下,只需在路径前加上前缀。使用一个容器执行COPY --from=debian:bookworm / /srv的例子,我们只需在/lib前加上/srv来从服务器的文件中复制。

结论


通过以上所有设置和安装,你应该拥有所有工具,以及开始处理pwn挑战的基本知识。快乐pwning! 如果你现在想了解更多关于pwn的一般知识,请阅读我的博客文章

3577. 统计计算机解锁顺序排列数

作者 stormsunshine
2025年6月9日 06:15

解法

思路和算法

使用已解锁的计算机密码将未解锁的计算机解锁的条件是:已解锁的计算机的编号和密码复杂度分别小于未解锁的计算机的编号和密码复杂度。由于初始时只有编号为 $0$ 的计算机密码已解锁,编号 $0$ 是最小编号,因此对于其他任意编号 $i$,是否可以使用编号为 $0$ 的计算机密码解锁编号为 $i$ 的计算机的情况如下。

  • 当 $\textit{complexity}[i] > \textit{complexity}[0]$ 时,可以使用编号为 $0$ 的计算机密码解锁编号为 $i$ 的计算机。

  • 当 $\textit{complexity}[i] \le \textit{complexity}[0]$ 时,不能使用编号为 $0$ 的计算机密码解锁编号为 $i$ 的计算机。

如果存在 $1 \le i < n$ 的整数 $i$ 满足 $\textit{complexity}[i] \le \textit{complexity}[0]$,则对于任意可以被编号为 $0$ 的计算机密码解锁的计算机的编号 $j$,必有 $\textit{complexity}[j] > \textit{complexity}[0]$,因此 $\textit{complexity}[j] > \textit{complexity}[i]$,即编号为 $j$ 的计算机密码不能解锁编号为 $i$ 的计算机。因此,编号为 $i$ 的计算机无法被解锁,此时无法解锁所有计算机。

如果 $1 \le i < n$ 的所有整数 $i$ 都满足 $\textit{complexity}[i] > \textit{complexity}[0]$,则所有计算机都能被编号为 $0$ 的计算机密码解锁。由于初始时编号为 $0$ 的计算机密码已解锁,因此其余 $n - 1$ 台计算机可以按任意顺序解锁,排列数是 $(n - 1)!$。

代码

###Java

class Solution {
    static final int MODULO = 1000000007;

    public int countPermutations(int[] complexity) {
        long permutations = 1;
        int n = complexity.length;
        for (int i = 1; i < n; i++) {
            if (complexity[i] <= complexity[0]) {
                return 0;
            }
            permutations = permutations * i % MODULO;
        }
        return (int) permutations;
    }
}

###C#

public class Solution {
    const int MODULO = 1000000007;

    public int CountPermutations(int[] complexity) {
        long permutations = 1;
        int n = complexity.Length;
        for (int i = 1; i < n; i++) {
            if (complexity[i] <= complexity[0]) {
                return 0;
            }
            permutations = permutations * i % MODULO;
        }
        return (int) permutations;
    }
}

###C++

const int MODULO = 1000000007;

class Solution {
public:
    int countPermutations(vector<int>& complexity) {
        long long permutations = 1;
        int n = complexity.size();
        for (int i = 1; i < n; i++) {
            if (complexity[i] <= complexity[0]) {
                return 0;
            }
            permutations = permutations * i % MODULO;
        }
        return permutations;
    }
};

###Python

MODULO = 1000000007

class Solution:
    def countPermutations(self, complexity: List[int]) -> int:
        permutations = 1
        n = len(complexity)
        for i in range(1, n):
            if complexity[i] <= complexity[0]:
                return 0
            permutations = permutations * i % MODULO
        return permutations

###C

const int MODULO = 1000000007;

int countPermutations(int* complexity, int complexitySize) {
    long long permutations = 1;
    for (int i = 1; i < complexitySize; i++) {
        if (complexity[i] <= complexity[0]) {
            return 0;
        }
        permutations = permutations * i % MODULO;
    }
    return permutations;
}

###Go

const MODULO = 1000000007

func countPermutations(complexity []int) int {
    permutations := 1
    n := len(complexity)
    for i := 1; i < n; i++ {
        if complexity[i] <= complexity[0] {
            return 0
        }
        permutations = permutations * i % MODULO
    }
    return permutations
}

###JavaScript

const MODULO = 1000000007;

var countPermutations = function(complexity) {
    let permutations = 1;
    let n = complexity.length;
    for (let i = 1; i < n; i++) {
        if (complexity[i] <= complexity[0]) {
            return 0;
        }
        permutations = permutations * i % MODULO;
    }
    return permutations;
};

###TypeScript

const MODULO = 1000000007;

function countPermutations(complexity: number[]): number {
    let permutations = 1;
    let n = complexity.length;
    for (let i = 1; i < n; i++) {
        if (complexity[i] <= complexity[0]) {
            return 0;
        }
        permutations = permutations * i % MODULO;
    }
    return permutations;
};

复杂度分析

  • 时间复杂度:$O(n)$,其中 $n$ 是数组 $\textit{complexity}$ 的长度。需要遍历数组一次。

  • 空间复杂度:$O(1)$。

脑筋急转弯(Python/Java/C++/Go)

作者 endlesscheng
2025年6月8日 12:06

题目说:用计算机 $j$ 解锁计算机 $i$ 的前提是 $j<i$ 且 $\textit{complexity}[j] < \textit{complexity}[i]$。

观察

  • 一开始就解锁的只有计算机 $0$。
  • 第一轮,被 $0$ 解锁的计算机(记作集合 $A$),密码复杂度比 $\textit{complexity}[0]$ 大。
  • 第二轮,被集合 $A$ 中的计算机解锁的计算机(记作集合 $B$),密码复杂度更大,所以也比 $\textit{complexity}[0]$ 大。
  • 第三轮,被集合 $B$ 中的计算机解锁的计算机(记作集合 $C$),密码复杂度更大,所以也比 $\textit{complexity}[0]$ 大。
  • 依此类推,所有被解锁的计算机的密码复杂度都要比 $\textit{complexity}[0]$ 大。

定理:当且仅当计算机 $0$ 右边的所有计算机的密码复杂度都比 $\textit{complexity}[0]$ 大,才能解锁所有计算机。

充分性:如果计算机 $0$ 右边的所有计算机的密码复杂度都比 $\textit{complexity}[0]$ 大,根据题意,仅用计算机 $0$ 便可解锁所有计算机。

必要性:如果可以解锁所有的计算机,那么计算机 $0$ 右边的所有计算机的密码复杂度都比 $\textit{complexity}[0]$ 大。考虑其逆否命题,即如果在计算机 $0$ 的右边存在计算机 $A$,满足 $\textit{complexity}[i] \le \textit{complexity}[0]$,那么不可能解锁计算机 $A$,更不可能解锁所有计算机。为了解锁计算机 $A$,我们需要在其左边找比 $\textit{complexity}[i]$ 更小的计算机。不断往左找,计算机的密码复杂度只会更小,直到找到一台被计算机 $0$ 解锁的计算机 $B$。$B$ 的密码复杂度必须比 $\textit{complexity}[0]$ 大,但为了能解锁计算机 $A$,$B$ 的密码复杂度又要 $< \textit{complexity}[i] \le \textit{complexity}[0]$,矛盾,所以不可能解锁计算机 $A$。

根据定理,如果计算机 $0$ 右边的所有计算机的密码复杂度都比 $\textit{complexity}[0]$ 大,那么我们可以按照任意顺序解锁这 $n-1$ 台计算机,方案数为 $n-1$ 个不同物品的全排列个数,即

$$
(n-1)!
$$

注意取模。关于模运算的知识点,见 模运算的世界:当加减乘除遇上取模

###py

class Solution:
    def countPermutations(self, complexity: List[int]) -> int:
        MOD = 1_000_000_007
        ans = 1
        for i in range(1, len(complexity)):
            if complexity[i] <= complexity[0]:
                return 0
            ans = ans * i % MOD
        return ans

###java

class Solution {
    public int countPermutations(int[] complexity) {
        final int MOD = 1_000_000_007;
        long ans = 1;
        for (int i = 1; i < complexity.length; i++) {
            if (complexity[i] <= complexity[0]) {
                return 0;
            }
            ans = ans * i % MOD;
        }
        return (int) ans;
    }
}

###cpp

class Solution {
public:
    int countPermutations(vector<int>& complexity) {
        const int MOD = 1'000'000'007;
        long long ans = 1;
        for (int i = 1; i < complexity.size(); i++) {
            if (complexity[i] <= complexity[0]) {
                return 0;
            }
            ans = ans * i % MOD;
        }
        return ans;
    }
};

###go

func countPermutations(complexity []int) int {
const mod = 1_000_000_007
ans := 1
for i := 1; i < len(complexity); i++ {
if complexity[i] <= complexity[0] {
return 0
}
ans = ans * i % mod
}
return ans
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(n)$,其中 $n$ 是 $\textit{complexity}$ 的长度。
  • 空间复杂度:$\mathcal{O}(1)$。

更多相似题目,见下面思维题单的「§5.2 脑筋急转弯」。

分类题单

如何科学刷题?

  1. 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
  2. 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
  3. 单调栈(基础/矩形面积/贡献法/最小字典序)
  4. 网格图(DFS/BFS/综合应用)
  5. 位运算(基础/性质/拆位/试填/恒等式/思维)
  6. 图论算法(DFS/BFS/拓扑排序/基环树/最短路/最小生成树/网络流)
  7. 动态规划(入门/背包/划分/状态机/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
  8. 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
  9. 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
  10. 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
  11. 链表、二叉树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA/一般树)
  12. 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)

我的题解精选(已分类)

欢迎关注 B站@灵茶山艾府

分类讨论

作者 tsreaper
2025年6月8日 12:05

解法:分类讨论

因为只能用小 complexity 解锁大 complexity,所以后续解锁的电脑一定满足 complexity[i] > complexity[0]

换句话说,如果存在 i > 0,使得 complexity[i] <= complexity[0],那就无法解锁该电脑,答案为 $0$。

否则,所有电脑都能通过电脑 $0$ 解锁,因此后续编号可以任意排列,答案为 $(n - 1)!$。

复杂度 $\mathcal{O}(n)$。

参考代码(c++)

class Solution {
public:
    int countPermutations(vector<int>& complexity) {
        int n = complexity.size();

        // 检查是否有无法解锁的电脑
        for (int i = 1; i < n; i++) if (complexity[i] <= complexity[0]) return 0;

        // 计算 (n - 1)!
        const int MOD = 1e9 + 7;
        long long ans = 1;
        for (int i = 1; i < n; i++) ans = ans * i % MOD;
        return ans;
    }
};
昨天 — 2025年12月9日技术

How Fiber Network Works

作者 yukang
2025年12月10日 01:05

I just got back from CKCon in beautiful Chiang Mai 🌴, where I gave a talk on the Fiber Network. To help everyone wrap their heads around how Fiber (CKB’s Lightning Network) actually moves assets, I hacked a visual simulation with AI.

To my surprise, people didn’t just understand it—they loved it! 🎉

Here is the “too long; didn’t read” version. But first, go ahead and play with the dots yourself, 👉 Play the Simulation: fiber-simulation

We all love Layer 1 blockchains like Bitcoin or CKB for their security, but let’s be honest: they aren’t exactly built for speed.

Every transaction has to be shouted out to the entire world and written down by thousands of nodes. On CKB, you’re waiting about 8 seconds for a block; on Bitcoin, it’s 10 minutes! Plus, the fees can get nasty if you’re just trying to buy a coffee. ☕️

So, how do we fix this?

The Lightning Network is a scalable, low-fee, and instant micro-payment solution for P2P payments.

The secret sauce isn’t actually new. Even Satoshi Nakamoto hinted at this “high-frequency” magic in an early email:

Intermediate transactions do not need to be broadcast. Only the final outcome gets recorded by the network.

A Lightning Network consists of Peers and Channels. A peer can send, receive, or forward a payment. A Channel is used for communication between two Peers.

Imagine you and a friend want to trade money back and forth quickly:

  1. Opening the Channel: You both put some money into a pot and sign a Funding Tx. This goes on the blockchain (L1).
  2. The Fun Part (Off-Chain): Now that the channel is open, you can send money back and forth a million times instantly! You just update the balance sheet between you two (using HTLCs and signatures). No one else needs to know, and no blockchain fees are paid yet.
  3. Closing the Channel: When you’re done, you agree on the final balance, sign a Shutdown Tx, and tell the blockchain.

Everything in the middle? That’s off-chain magic. ✨

Now, if Fiber was just about paying your direct neighbor, it would be boring. The real power comes from the Network.

This means Alice can pay Bob even if they don’t have a direct channel between them. The payment can travel through one or more intermediate nodes. As long as there is a path with enough liquidity, the payment will reach its destination instantly.

All data is wrapped in Onion Packets (yes, like layers of an onion). The nodes in the middle serve as couriers, but they are blindfolded:

  • They don’t know who sent the money.
  • They don’t know who is receiving it.
  • They only know “pass this to the next guy.”

They simply follow a basic rule: they forward the Hash Time Lock, and if the payment succeeds, they earn a tiny fee for their trouble. Easy peasy.

The “Not So Easy” Part 😅

While the idea is simple, building it is… well, an engineering adventure. We’re dealing with cryptography, heavy concurrency, routing algorithms, and a whole jungle of edge cases. But hey, that’s what makes it fun!

We’ve poured the last two years into building Fiber, and I’m proud to say it’s finally GA ready.

If you want to geek out on the details, check these out:

Here is the full presentation from my talk:
CKB Fiber Network Engineering Updates

webpack和vite区别及原理实现

作者 光影少年
2025年12月9日 17:52

WebpackVite 都是用于构建现代前端应用的构建工具,它们在原理和实现上有显著的区别。下面我将详细比较它们的异同,帮助你了解两者的工作原理以及各自的优势。


一、Webpack 和 Vite 的核心区别

特性 Webpack Vite
构建速度 较慢,特别是大型项目 快,几乎是即时的
构建原理 通过打包所有资源,生成最终的 bundle 采用按需编译,利用浏览器原生支持 ES 模块
开发模式 一开始就进行全部的打包,编译速度较慢 通过浏览器原生支持 ES Modules,只有请求的模块才会被处理
构建产物 生成一个或多个 bundle 文件 基于 ES Module 按需加载,不同于 Webpack 完整的打包
支持类型 支持所有 JavaScript,CSS,图片,字体等 主要支持 ES 模块,针对现代浏览器优化
使用体验 配置复杂,适用于各种需求和优化 配置简单,适合快速开发,但功能不如 Webpack 灵活

二、Webpack 原理和实现

1. 传统的打包工具

Webpack 是一个 模块打包器,它将所有的静态资源(JavaScript、CSS、图片等)当作模块处理,并生成一个或多个 bundle 文件,最终这些文件将被浏览器加载。

2. 打包过程:

Webpack 的打包过程主要包含以下几个阶段:

  1. 解析阶段(Parsing)

    • Webpack 从入口文件(entry)开始,递归地解析每一个依赖,生成依赖图。
    • 在解析时,Webpack 会调用 loader 对不同类型的文件进行预处理(如 Babel 转译、Sass 编译等)。
  2. 构建阶段(Building)

    • Webpack 会通过 loaderplugin 处理所有模块,生成最终的 AST(抽象语法树)
    • 使用 module bundling 将所有模块合并成一个或多个文件(bundle)。
  3. 优化阶段(Optimization)

    • Webpack 会对生成的 bundle 进行优化,如:分割代码(Code Splitting)、压缩(Terser)等。
  4. 输出阶段(Output)

    • 最终将 bundle 输出到指定的目录,并生成相应的文件供浏览器使用。

3. Webpack 需要时间打包所有资源

由于 Webpack 会将所有资源都打包成一个或多个文件,所以当你做 webpack --mode development 命令时,它必须编译所有文件,这就导致开发过程中启动时间较长。


三、Vite 原理和实现

1. 基于浏览器原生支持的 ES Modules

Vite 的核心原理是利用浏览器原生支持 ES Modules,它并不像 Webpack 那样进行完整的打包,而是通过 按需加载 来提高构建速度。

2. Vite 开发流程:

Vite 的开发过程分为两个阶段:

开发阶段:
  1. 按需编译

    • 当你启动 Vite 时,它不会一次性打包整个项目,而是仅对 首次请求的模块 进行编译和服务。比如,只有用户第一次访问某个页面时,Vite 才会编译该页面依赖的 JavaScript 和 CSS。
  2. 热模块替换(HMR)

    • Vite 提供了 即时的热模块替换,当你在开发过程中修改了某个模块,Vite 会只编译并替换该模块,而不是重新打包整个项目。这大大提高了开发体验。
构建阶段:
  1. 生产构建(build)

    • 在生产环境下,Vite 使用 Rollup(一个现代的 JavaScript 打包工具)进行最终的打包,将所有模块合并成一个优化过的 bundle,进行代码拆分,压缩等优化,生成最终的静态文件。

3. 不需要一直打包全部资源

Vite 的按需编译和快速响应机制,使得开发过程非常迅速。只有在页面访问时,才会处理该页面的依赖,避免了 Webpack 那种完全打包的性能消耗。


四、Webpack 与 Vite 优缺点对比

特性 Webpack Vite
构建速度 较慢(尤其是大型项目时) 极快,尤其是冷启动和热更新
配置复杂性 配置较为复杂,需要处理许多细节 配置简单,开箱即用,少配置即可
开发体验 开发中每次更改都会触发完整编译 热更新速度快,修改后的内容即时反应
支持的功能 功能强大,支持的插件丰富,几乎无所不包 适合现代前端开发,特性较为简洁和聚焦
构建产物 生成一个或多个较大的 bundle 生成多个按需加载的小文件
适用场景 适合中大型复杂项目,支持更多自定义需求 适合中小型项目、现代前端框架(如 React/Vue)

五、总结

  1. Webpack:

    • 适用于复杂的前端项目,支持插件和加载器的灵活扩展。
    • 在开发时,启动和热更新较慢,尤其是大型项目。
    • 配置复杂,需要更多的手动配置来实现项目定制。
  2. Vite:

    • 更适合现代前端开发,特别是对开发速度和用户体验有高要求的场景。
    • 使用浏览器原生的 ES Modules 来实现按需编译和即时热更新,开发体验极佳。
    • 适用于现代前端框架(如 Vue、React),并在生产环境中使用 Rollup 进行高效构建。

📌 推荐场景:

  • Webpack 适合 大型、复杂的前端项目,尤其是有多种技术栈、框架,或者需要更多自定义构建的项目。
  • Vite 更适合 快速开发、现代化前端应用,尤其是小型或中型项目,或者想要享受极速开发体验的团队。

React Scheduler为何采用MessageChannel调度?

2025年12月9日 17:43
特性 setTimeout requestAnimationFrame requestIdleCallback MessageChannel
本质 定时器宏任务 动画回调钩子 空闲期回调钩子 消息通信宏任务
执行时机 延迟指定毫秒后(不精确) 下一帧渲染之前(与刷新率同步) 浏览器空闲时(一帧的末尾) 下一轮事件循环(作为宏任务)
主要设计目的 延迟执行代码 实现流畅动画 执行低优先级后台任务 不同上下文间通信
关键优势 通用、灵活、兼容性好 动画流畅、节能(后台暂停) 不阻塞渲染与交互,利用空闲时间 延迟极短且稳定,可精准控制任务切片
关键缺陷 时序不精确,嵌套有最小延迟(如4ms) 依赖渲染周期,执行频率固定(~16.7ms) 执行时机不可控,可能长期得不到调用 非用于调度,是“创造性”用法
React调度的适用性 ❌ 延迟不可控,不适合精细调度 ❌ 依赖渲染节奏,无法在帧中多次调度 ❌ 时机不可靠,无法满足及时响应需求 ✅ 在事件循环中及时插入任务,实现可中断调度的理想选择

📝 各API功能与特性详解

下面我们来具体看看每个API的核心工作机制和适用场景。

  1. setTimeout

    • 功能:最基础的异步定时器,用于在指定的延迟(毫秒)后,将回调函数推入任务队列等待执行
    • 执行机制:它设置的是一个“最小延迟”,而非精确时间。回调的实际执行时间会受到主线程上其他任务(如同步代码、微任务、UI渲染)的阻塞,延迟可能远大于设定值
    • 使用场景:适用于对时间精度要求不高的延迟操作,如防抖/节流、轮询检查等。
  2. requestAnimationFrame

    • 功能:专为动画设计的API,其回调函数会在浏览器下一次重绘(即绘制下一帧)之前执行
    • 执行机制:与显示器的刷新率(通常是60Hz,约16.7ms/帧)同步。浏览器会自动优化调用,在页面不可见时(如标签页被隐藏)会自动暂停,以节省资源
    • 使用场景实现任何需要平滑过渡的动画效果,是替代setTimeout做动画的最佳实践
  3. requestIdleCallback

    • 功能:允许你在浏览器空闲时期调度低优先级任务
    • 执行机制:在一帧处理完用户输入、requestAnimationFrame回调、布局和绘制等关键任务后,如果还有剩余时间,才会执行它的回调回调函数会接收一个IdleDeadline参数,告诉你当前帧还剩余多少空闲时间
    • 使用场景:适合执行一些非紧急的后台任务,如数据上报、非关键的数据预取等。
  4. MessageChannel

    • 功能:用于在不同浏览器上下文(如两个iframe、主线程与Web Worker)间建立双向通信的通道
    • 执行机制:调用port.postMessage()方法,会向消息队列添加一个宏任务。这个任务会在当前事件循环的微任务执行完毕后、下一次事件循环中执行。
    • 使用场景:主要应用于跨上下文通信。它在React调度中的用法,是利用其能产生一个在下一轮事件循环中尽早执行的宏任务的特性。

⚙️ 为什么React最终选择了MessageChannel?

React调度器的核心目标是:实现可中断的并发渲染,将长任务切成小片,在每一帧中插入执行,同时能快速响应高优先级更新。这就要求调度器能主动、及时地“让出”主线程。

结合上表和分析,其他API不适用于此的原因如下:

  • setTimeout延迟不稳定且不可控。其最小延迟(如4ms)在密集调度时会造成浪费,更严重的是,延迟时间可能被拉长,导致调度器无法在预期时间内“苏醒”并交还主线程,影响页面响应
  • requestAnimationFrame调用频率被锁死在屏幕刷新率(约16.7ms一次) 。这意味着即便一帧中有大量空闲时间,调度器也无法插入更多任务切片,无法充分利用帧内的空闲资源
  • requestIdleCallback执行时机“太被动”且不稳定。它依赖于浏览器的“空闲通知”,但空闲期可能很短或很久都不出现。对于需要主动、可预测地进行任务切片的调度器来说,这不可靠,同时,其兼容性也不够理想。

MessageChannel的优势正在于解决了上述问题:

  1. 主动且及时的调度:通过port.postMessage(),React可以主动在下一个事件循环中创建一个宏任务来继续工作。这让调度器能精确地在每个5ms左右的时间片(这是React设定的切片时间目标-7)结束后“中断”自己,及时归还主线程给浏览器进行渲染或处理交互。
  2. 更高的执行频率:它不依赖屏幕刷新率,可以在同一帧内的多次事件循环中连续调度,从而更密集、更充分地利用一帧之内的计算资源。
  3. 避免微任务的弊端:为什么不直接用Promise(微任务)?因为微任务会在当前事件循环中连续执行直到队列清空,这同样会长时间阻塞主线程,达不到“可中断”的目的

💎 总结

React选择MessageChannel,是基于其能产生一个在事件循环中及时、稳定执行的宏任务这一特性。这为React实现主动、可中断、基于时间片的任务调度提供了最合适的底层机制,从而在实现并发渲染的同时,保障了浏览器的渲染和用户交互能获得最高优先级的响应。

前端基础数据中心:从混乱到统一的架构演进

2025年12月9日 17:41
一、问题是怎么来的 做过 B 端系统的同学应该都有体会——基础数据无处不在。港口、船舶、航线、货币、字典……这些数据在几乎每个页面都会用到,要么是下拉选择,要么是代码翻译,要么是表格筛选。 我们项目一
❌
❌