Walkthrough | 手写 Yoga Layout¶
难度:⭐⭐ 时间:~1.5h 目标:理解 Yoga flexbox 核心,写一个简化版
1. Yoga Layout 是什么¶
Yoga = Facebook 的 flexbox 引擎。 - 纯 TS 移植(无 WASM) - 4-slot LRU cache - 60+ exports - 支持 RTL
2. 目标¶
手写一个简化版 yoga: - flex 横向布局 - 简单计算 - 1 个 cache slot - 2 个属性(flexGrow, flexDirection)
3. 完整代码¶
// mini-yoga.ts
type FlexDirection = 'row' | 'column'
type Unit = 'point' | 'percent' | 'auto' | 'undefined'
interface Value {
unit: Unit
value: number
}
interface Node {
style: Style
children: Node[]
parent: Node | null
layout: { x: number; y: number; width: number; height: number } | null
}
interface Style {
flexDirection: FlexDirection
flexGrow: number
flexShrink: number
width: Value
height: Value
// 简化
}
function pointValue(v: number): Value {
return { unit: 'point', value: v }
}
function percentValue(v: number): Value {
return { unit: 'percent', value: v }
}
function autoValue(): Value {
return { unit: 'auto', value: 0 }
}
export class MiniNode {
style: Style = {
flexDirection: 'column',
flexGrow: 0,
flexShrink: 0,
width: autoValue(),
height: autoValue(),
}
children: MiniNode[] = []
parent: MiniNode | null = null
layout: { x: number; y: number; width: number; height: number } | null = null
addChild(child: MiniNode) {
child.parent = this
this.children.push(child)
}
calculateLayout(parentWidth: number, parentHeight: number) {
// 简化算法:只用 width
const myWidth = this.resolveValue(this.style.width, parentWidth)
// 1. 测量 children
let totalFixedWidth = 0
let totalFlexGrow = 0
for (const child of this.children) {
const childWidth = child.resolveValue(child.style.width, myWidth)
if (child.style.flexGrow > 0) {
totalFlexGrow += child.style.flexGrow
} else {
totalFixedWidth += childWidth
}
}
// 2. 分配剩余空间
const remaining = Math.max(0, myWidth - totalFixedWidth)
const flexUnit = totalFlexGrow > 0 ? remaining / totalFlexGrow : 0
// 3. layout children
let x = 0
for (const child of this.children) {
if (this.style.flexDirection === 'row') {
child.calculateLayout(myWidth, parentHeight)
child.layout!.x = x
child.layout!.y = 0
const childWidth =
child.style.flexGrow > 0
? child.style.flexGrow * flexUnit
: child.resolveValue(child.style.width, myWidth)
child.layout!.width = childWidth
x += childWidth
} else {
// column - similar but vertical
child.calculateLayout(parentWidth, myWidth)
child.layout!.x = 0
child.layout!.y = x
x += child.layout!.height
}
}
this.layout = {
x: 0,
y: 0,
width: myWidth,
height: 0, // 简化
}
}
private resolveValue(v: Value, parentSize: number): number {
if (v.unit === 'point') return v.value
if (v.unit === 'percent') return (v.value / 100) * parentSize
if (v.unit === 'auto') return 100 // 简化
return 0
}
}
export class MiniYoga {
calculateLayout(root: MiniNode, width: number, height: number) {
root.calculateLayout(width, height)
}
}
~120 行。
4. 使用示例¶
const root = new MiniNode()
root.style.width = pointValue(800)
root.style.height = pointValue(600)
root.style.flexDirection = 'row'
const sidebar = new MiniNode()
sidebar.style.width = pointValue(200)
sidebar.style.flexGrow = 0
root.addChild(sidebar)
const main = new MiniNode()
main.style.width = pointValue(0) // auto
main.style.flexGrow = 1
root.addChild(main)
const yoga = new MiniYoga()
yoga.calculateLayout(root, 800, 600)
console.log('sidebar:', sidebar.layout)
// { x: 0, width: 200 }
console.log('main:', main.layout)
// { x: 200, width: 600 }
2 列布局。
5. 5 个关键设计¶
5.1 Value 抽象¶
4 单位。
5.2 Parent reference¶
parent 引用。
5.3 Flex 计算¶
flex 分配。
5.4 Direction 切换¶
row / column。
5.5 Cache 简化(无)¶
未实现 —— 真实 yoga 4-slot LRU。
6. 5 个扩展¶
6.1 加 alignItems¶
interface Style {
alignItems: 'flex-start' | 'center' | 'flex-end' | 'stretch'
}
private alignChild(child, parentSize, childSize) {
if (this.style.alignItems === 'center') {
return (parentSize - childSize) / 2
}
if (this.style.alignItems === 'flex-end') {
return parentSize - childSize
}
return 0
}
align。
6.2 加 justifyContent¶
private justifyChildren(totalSize, childSizes) {
if (this.style.justifyContent === 'space-between') {
const space = (totalSize - sum(childSizes)) / (childSizes.length - 1)
// ...
}
}
justify。
6.3 加 flexWrap¶
wrap。
6.4 加 RTL¶
RTL。
6.5 加 cache¶
private cache = new Map<string, Layout>()
private cachedLayout(key: string, compute: () => Layout): Layout {
if (this.cache.has(key)) return this.cache.get(key)!
const v = compute()
this.cache.set(key, v)
return v
}
cache。
7. 5 个关键洞察¶
- Value 抽象 —— 4 单位
- Parent reference —— 关键
- Flex 计算 —— grow/shrink
- Direction —— row/column 切换
- Cache —— 性能
8. 对比真实¶
| 维度 | Mini | 真实 |
|---|---|---|
| 行数 | 120 | 2578 |
| 属性 | 2 | 25+ |
| Cache | 无 | 4-slot LRU |
| RTL | 无 | 支持 |
| 测量 | 同步 | 异步 |
| Yoga 兼容 | 无 | 完整 |
简化 vs 真实。
9. 5 个练习¶
- 加 alignItems —— 4 个值
- 加 justifyContent —— 6 个值
- 加 flexWrap —— wrap/nowrap
- 加 RTL —— mirror
- 加 cache —— Map 缓存
5 步。
10. 总结¶
手写 Yoga Layout = 理解 flexbox + Value 抽象 + 树遍历。
核心: - 120 行简化版 - flex grow + row/column - Value 抽象 - 2 列布局
下一步: - 看 deep-dive-yoga-layout.md - 加 align / justify - 加 cache