2.4 KiB
2.4 KiB
第二章:先看懂 prepare 与 layout 的分工
2.1 同一份输入,有不同的变化频率
聊天消息的文字和字体可能很久不变,但容器宽度会在拖动窗口时连续变化。把所有工作绑在一个函数里,每次 resize 都会重复做许多相同的事情。
Pretext 将它拆成两步:
import { prepare, layout } from '@chenglou/pretext'
const prepared = prepare('Hello 世界,今天一起读源码。', '16px Arial')
const narrow = layout(prepared, 200, 24)
const wide = layout(prepared, 400, 24)
这里 200、400 是内容宽度,24 是调用者指定的每行高度,单位都是 CSS 像素。
| 阶段 | 输入 | 主要工作 | 什么时候重做 |
|---|---|---|---|
prepare() |
文字、字体、选项及语言环境 | 分析断行机会、测量、记录修正数据 | 相关输入变化 |
layout() |
prepared、宽度、行高 | 根据已知宽度决定换行并计数 | 容器宽度或行高变化 |
prepared 可以理解为“已经整理好的排版材料”。它与容器宽度无关,但并不包含任意宽度下现成的布局答案。
2.2 沿源码走一遍
打开 src/layout.ts,定位 prepareInternal():
读取引擎规则与语言
→ analyzeText(...)
→ measureAnalysis(...)
→ prepared handle
再定位 layout(),其核心逻辑是:
const lineCount = countPreparedLines(internal, normalizedWidth)
return { lineCount, height: lineCount * lineHeight }
这是省略类型与内部转换的源码摘义。它没有逐行调用 Canvas,也没有先创建 DOM 再读取高度。
2.3 一段文字被布局多次,省下多少工作
用 P 表示准备成本,用 L 表示一次布局成本,一共布局 k 次:
复用 prepared:P + k × L
每次重新 prepare:k × (P + L)
两种写法相差 (k - 1) × P。这解释了为什么使用方式会直接决定收益:在 resize 回调中重新 prepare,会让原本可以省掉的分析工作再次发生。
另外,重复 prepare 可能命中测量缓存,但仍要分析文本、组装新的 handle。它依然比直接复用已有 handle 做得更多。
本章检查:layout() 是 O(1) 吗?不是。它通常还要扫描分段。快来自减少每一步要做的工作,而不是完全不看文本。