跳转至

Walkthrough | 手写 Yoga Layout

难度:⭐⭐ 时间:~1.5h 目标:理解 Yoga flexbox 核心,写一个简化版


1. Yoga Layout 是什么

Yoga = Facebook 的 flexbox 引擎。 - 纯 TS 移植(无 WASM) - 4-slot LRU cache - 60+ exports - 支持 RTL

详见 deep-dive-yoga-layout.md


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 抽象

interface Value { unit: Unit; value: number }

4 单位

5.2 Parent reference

parent: MiniNode | null = null

parent 引用

5.3 Flex 计算

// totalFixed + (flexGrow * flexUnit)

flex 分配

5.4 Direction 切换

if (this.style.flexDirection === 'row') { ... } else { ... }

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

if (this.style.flexWrap === 'wrap') {
  // 多行
}

wrap

6.4 加 RTL

if (direction === 'rtl') {
  // mirror
}

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 个关键洞察

  1. Value 抽象 —— 4 单位
  2. Parent reference —— 关键
  3. Flex 计算 —— grow/shrink
  4. Direction —— row/column 切换
  5. Cache —— 性能

8. 对比真实

维度 Mini 真实
行数 120 2578
属性 2 25+
Cache 4-slot LRU
RTL 支持
测量 同步 异步
Yoga 兼容 完整

简化 vs 真实


9. 5 个练习

  1. 加 alignItems —— 4 个值
  2. 加 justifyContent —— 6 个值
  3. 加 flexWrap —— wrap/nowrap
  4. 加 RTL —— mirror
  5. 加 cache —— Map 缓存

5 步


10. 总结

手写 Yoga Layout = 理解 flexbox + Value 抽象 + 树遍历

核心: - 120 行简化版 - flex grow + row/column - Value 抽象 - 2 列布局

下一步: - 看 deep-dive-yoga-layout.md - 加 align / justify - 加 cache