解密 Flex 排版流程

366 阅读7分钟

Flex 布局

2009年,W3C 提出了一种新的方案----Flex 布局,可以简便、完整、响应式地实现各种页面布局。目前,它已经得到了所有浏览器的支持,这意味着,现在就能很安全地使用这项功能。

那么浏览器又是如何实现这样的一套神奇的布局方式呢?下面的内容就带你详细了解一套简单易懂的布局代码,看看每个属性是如何作用于对应的 flex 元素,并如何计算出每个元素的位置并排版,最终实现一个完整的 flex 布局

主要流程

Flex 布局的主要流程可以分为以下几个方面

  1. 根据 flex 属性或默认值,初始化变量,浏览器通过这一步确认修改 flex 的默认行为
  1. 确定主轴和交叉轴及其方向、偏移起点终点等变量,比如确定主轴的变量是 width 还是 height
  1. 将元素收集进 FLexLine 弹性行,这一步会计算初始的 mainSpace 和 crossSpace,为后面计算主轴和交叉轴,每个元素的位置,空隙的大小等,提供初始量
  1. 根据主轴的几个变量, 确定起点偏移量和终点偏移量,确定方向,确定起点,将 flex 弹性元素放入主轴方向计算,形成 FlexLine 弹性行,同时会把 mainSpace 用到行内,让空隙按照符合设置的方式填充进剩余空间。也可以根据 flex-grow 和 flex-shrink 分配剩余的空隙

  1. 根据交叉轴的几个变量,确定起点偏移量和终点偏移量,确定方向,确定起点,将 FlexLine 弹性行放入交叉轴方向计算,形成 FlexBox 弹性盒子
function flexLayout(element) {
  // 获取 style
    const style  = getStyle(element)
 // 初始化 flex 属性
    const initStyle = initSetting(style)
 // 
    const flexProp: FlexProp = getFlexProperty(initStyle)

    const flexLine: FlexLine = {
        orderItems: [],
        mainSpace: 0,
        crossSpace: 0,  
    }
    const flexLines: FlexLines = [flexLine]

    const orderItems = getOrderItems(element);
    const autoMainSize = computeAutoMainSize(initStyle, orderItems, element)
    const lastFlexLine: FlexLine = collectItems(
        initStyle,
        orderItems,
        flexLine,
        flexLines,
        flexProp,
        autoMainSize
    )
    computeMainSpace(
        initStyle,
        orderItems,
        lastFlexLine,
        flexLines,
        flexProp
    )
    computeCrossSpace(initStyle, flexLines, flexProp)
}

流程分解

1. 设置默认值

影响 flex 布局里的主要属性的默认值分配,按照如下表格初始化,父元素未定义的就用默认值

flex-directionrow
flex-wrapno-wrap
justify-contentstretch
align-contentflex-start
align-itemsstretch
// getStyle 获取元素 style 
const style = getStyle(element)
// style 是节点的样式,style 中将 px 和 数字都转为数字
// 子元素 list
const items = element.node.children
const orderOfItems = Array.from(items)
orderOfItems.sort((item1, item2) => {
  const { order1 } = getStyle(item1);
  const { order2 } = getStyle(item2);
  return order1 - order2
})

if (!style.flexDirection || style.flexDirection === 'auto') {
  style.flexDirection = 'row'
}
if (!style.alignItems || style.alignItems === 'auto') {
  style.alignItems = 'stretch'
}
if (!style.justifyContent || style.justifyContent === 'auto') {
  style.justifyContent = 'flex-start'
}
if (!style.flexWrap || style.flexWrap === 'auto') {
  style.flexWrap = 'no-wrap'
}
if (!style.alignContent || style.alignContent === 'auto') {
  style.alignContent = 'stretch'
}

2. 确定变量并初始化

  1. 确定变量
    1. 根据 flex-direction 确定主轴和交叉轴:
    • row:从左到右,width 为主轴,height 为交叉轴
    • row-reverse:从右到左,width 为主轴,height 为交叉轴
    • column:从上到下,height 为主轴,width 为交叉轴
    • column-reverse:从下到上,height 为主轴,width 为交叉轴
    1. 确定以下几个变量:
      1. mainSize 主轴尺寸:主轴的空间大小
      2. crossSize 交叉轴尺寸:交叉轴的空间大小
      1. mainBase 主轴起始线:主轴所有元素开始排版的起点
      2. crossBase 交叉轴起始线:交叉轴所有 flexLine 开始排版的起点
      1. mainStart 主轴偏移起点:根据 step 和 mainSize 计算,每一个元素在进 flexLine 前的起点
      2. mainEnd 主轴偏移终点:根据 step 和 mainSize 计算,每一个元素在进 flexLine 后的终点
      1. mainSign 主轴排布方向,决定了元素往什么方向排布在主轴上
      2. crossStart 交叉轴偏移起点:根据 step 和 crossSize计算,每一个 flexLine 的起点
      1. crossEnd 交叉轴偏移终点:根据 step 和 crossSize计算,每一个 flexLine 前的终点点
      2. crossSign 交叉轴排布方向,决定了元素往什么方向排布在交叉轴上

下图的是默认情况的主轴和交叉轴及其变量

let mainSize, mainStart, mainEnd, mainSign, mainBase,
        crossSize, crossStart, crossEnd, crossSign = 1, crossBase
  1. 初始化数值
  • mainSize 和 crossSize 指的是弹性盒子的实际大小,也就是带上一系列偏移量、margin、padding 等后的实际大小
  • 这里的 crossSign 和 mainSign 是可以根据 x 轴和 y 轴的方向改变的
  • wrap-reverse 实际改变的是交叉轴,因此需要交叉轴本身改换 crossSign 和 crossStart、crossEnd,只是 crossBase 不改变,因为 crossBase 依据主轴的起点
// 为什么要用 left right 表示 Start 和 End? 是因为 left right 表示了元素一定的偏移量,实际上用的不是CSS 里的 left right 属性
if (style.flexDirection === 'row') {
  mainSize = 'width'
  mainStart = 'left'
  mainEnd = 'right'
  mainSign = +1
  mainBase = 0

  crossSize = 'height'
  crossStart = 'top'
  crossEnd = 'bottom'
}
if (style.flexDirection === 'row-reverse') {
  mainSize = 'width'
  mainStart = 'right'
  mainEnd = 'left'
  mainSign = -1
  mainBase = style.width

  crossSize = 'height'
  crossStart = 'top'
  crossEnd = 'bottom'
}
if (style.flexDirection === 'column') {
  mainSize = 'height'
  mainStart = 'top'
  mainEnd = 'bottom'
  mainSign = +1
  mainBase = 0

  crossSize = 'width'
  crossStart = 'left'
  crossEnd = 'right'
}
if (style.flexDirection === 'column-reverse') {
  mainSize = 'height'
  mainStart = 'top'
  mainEnd = 'bottom'
  mainSign = -1
  mainBase = style.height

  crossSize = 'right'
  crossStart = 'left'
}


if (style.flexWrap === 'wrap-reverse') {
  // 影响交叉轴,reverse 实际上让交叉轴反转了
  let tmp = crossStart
  crossStart = crossEnd
  crossEnd = tmp
  crossSign = crossSign * -1
} else {
  crossBase = 0
  crossSign = crossSign
}

3. 将元素收集进行内(hang)(注意不光是 flex 元素,是所有子元素都先收集进行内,然后再分配空间)

    1. 遍历元素,计算每个元素占据的 mainSpace 和 crossSpace,mainSpace 被占用并递减,crossSpace 选出改行元素中最大 crossSize 的一个作为该弹性行的 crossSpace,如果该弹性行 mainSpace 已经是负数,就换下一行,重置 mainSpace 和 crossSpace,以此类推,直到所有元素分配到行内
    2. 元素有 flex 或 flex-shrink 属性的情况:元素直接进行内,因为 flex-shrink 默认为 0,所以此时不减小 mainSpace,行内占用空间之后计算
    1. 设置为 no-wrap 的情况:元素直接进行内,正常计算 mainSpace 和 crossSpace,只是不会因为 mainSpace 为负而换行
    2. 每个 flexLine 都有 mainSpace 和 crossSpace,mainSapce 是减去所有的主轴上元素的 mianSize 的空隙空间,crossSpace 是行高,或行内最高
    1. isAutoMainSize 的情况,是指父元素没有 mainSize 的情况下,通过堆叠子元素的mainSize 来计算一个初始对的 mainSize

// 收集元素进行(hang)(flex-line)
// 父元素没有 mainSize 的情况,需要算出 mainSize
let isAutoMainSize = false
if (!style[mainSize]) {
  style[mainSize] = 0
  for (let i = 0; i < items.length; i++) {
    let item = items[i]
    let itemStyle = getStyle(item)
    if (itemStyle[mainSize] !== null || itemStyle[mainSize] !== (void 0)) {
      style[mainSize] = style[mainSize] + itemStyle[mainSize]
    }
  }
  isAutoMainSize = true;
}

let flexLine = {
  items: [],
  mainSpace: 0,
  crossSpace: 0,
}
let flexLines = [flexLine]
let mainSpace = style[mainSize]
let crossSpace = 0

for(let i =0; i < items.length; i++) {
  let item = items[i]
  let itemStyle = getStyle(item)
  let itemMainSize = itemStyle.flexBasis || itemStyle[mainSize]
  if (itemMainSize === null) {
    itemMainSize = 0
  }
  if (itemStyle.flex){
    flexLine.items.push(item)
  } else if (style.flexWrap === 'nowrap' && isAutoMainSize) {
    mainSpace -= itemMainSize
    if (itemStyle[crossSize] !== null && itemStyle[crossSize]) {
      crossSpace = Math.max(crossSpace, itemStyle[crossSize])
    }
    flexLine.items.push(item)
  } else {
    // 当 item 元素的主轴大小 大于 父元素主轴大小,item 的主轴会被改成父元素一样大
    if (itemMainSize > style[mainSize]) {
      itemMainSize = style[mainSize]
    }
    if (mainSpace < itemMainSize) {
      // 收集行内剩余的 mainSpace 和 crossSpace,之后清空并换行
      flexLine.mainSpace = mainSpace
      flexLine.crossSpace = crossSpace

      flexLine = {
        items: [],
        mainSpace: 0,
        crossSpace: 0
      }
      flexLines.push(flexLine)
      flexLine.items.push(item)

      mainSpace = style[mainSize]
      crossSpace = 0
      continue;
    } else {
      flexLine.items.push(item)
    }
    if (itemStyle[crossSize] !== null && itemStyle[crossSize] !== (void 0)) {
      crossSpace = Math.max(crossSpace, itemStyle[crossSize])
    }
    mainSpace -= itemMainSize
  }
}
flexLine.mainSpace = mainSpace

if (style.flexWrap === 'nowrap' || isAutoMainSize) {
  flexLine.crossSpace = (style[crossSize] !== undefined) ? style[crossSize] : crossSpace
} else {
  flexLine.crossSpace = crossSpace
}

4. 根据主轴的几个变量,确定起点偏移量和终点偏移量,确定方向,确定起点,将 Flex 弹性元素放入主轴方向计算,形成 FlexLine 弹性行

    1. mainSpace 小于 0 ,即no-wrap 的情况:
      • flex-shrink 奏效,按照 shrink 的设置比例缩小元素,越大缩的越小,改变了元素的 mainSize,
      • 根据父元素的 mainSize 和 mainSpace 可以算出基础缩放系数,通过 flex-shrink 分配基础缩放系数
// compute the main axis size, justify-content
if (mainSpace < 0) {
  // 只发生在 no-wrap 的单行情况,否则一定会分行出来
  // scale 可以通过 shrink 指定,shrink 越大,缩的越小,默认为 1
  // flex: 0, 1, auto; grow shrink basis
  let flexShrinkTotal = 0
  for (let i = 0; i < orderOfItems.length; i++) {
    let item = orderOfItems[i];
    let itemStyle = this.getStyle(item)
    if((itemStyle.flexShrink !== null) && (itemStyle.flexShrink !== void 0)) {
      flexShrinkTotal += itemStyle.flexShrink
    } else {
      flexShrinkTotal += 1
    }
  }
  let scale = style[mainSize] / (style[mainSize] - mainSpace)
  let currentMain = mainBase
  for (let i = 0; i < orderOfItems.length; i++) {
    let item = orderOfItems[i];
    let itemStyle = this.getStyle(item)
    if((itemStyle.flexShrink !== null) && (itemStyle.flexShrink !== void 0)) {
      scale = scale / flexShrinkTotal * itemStyle.flexShrink
    }
    itemStyle[mainSize] = itemStyle[mainSize] * scale
    // 循环计算各个元素在主轴的位置
    itemStyle[mainStart] = currentMain
    itemStyle[mainEnd] = itemStyle[mainStart] + mainSign * itemStyle[mainSize]
    currentMain = itemStyle[mainEnd]
  }
}
    1. mainSpace 大于 0
    • 循环每个 flexLine,分配 mainSpace。此时 mainSpace 的分配有两种形式
      • 有 flex-grow 元素的情况,需要优先分配 mainSpace 给 grow 元素,并且按照 flex-grow 的大小分配,数值越大的元素分配到的等分的 mainSpace 越多,最终会改变元素的 mainSize,然后将剩余空间全部占据
      • 按照 justify-content 属性的设置分配 mainSpace,确定空隙 space 的大小以及 mainBase 起始点,循环每个元素的 mainStart 和 mainEnd 偏移量,并结合计算出的空隙 space,以此累加到每行的 mainBase 上,从而使每个元素偏移 mainBase 排版,确定每个在行内的弹性元素的平行行内的位置
else if (mainSpace >= 0) {
  // 处理每个 flexLine,收集 flex 元素
  flexLines.forEach(function (flexLine) {
    let mainSpace = flexLine.mainSpace
    let flexTotal = 0
    for (let i = 0; i < flexLine.items.length; i++) {
      let item = flexLine.items[i];
      let itemStyle = getStyle(item)

      if((itemStyle.flex !== null) && (itemStyle.flex !== void 0)) {
        flexTotal += itemStyle.flex
      }
    }
    if (flexTotal > 0) {
      // 处理有 flex: 1 属性的元素,即处理 flex-grow
      let currentMain = mainBase
      for (let i = 0; i < flexLine.items.length; i++) {
        let item = flexLine.items[i];
        let itemStyle = getStyle(item)
        if (itemStyle.flex) {
          itemStyle[mainSize] = (mainSpace / flexTotal) * itemStyle.flex
        }
        // 循环
        itemStyle[mainStart] = currentMain
        itemStyle[mainEnd] = itemStyle[mainStart] + mainSign * itemStyle[mainSize]
        currentMain = itemStyle[mainEnd]
      }
    } else {
      let currentMain = mainBase
      let step = 0
      if (style.justifyContent === 'flex-start') {
        currentMain = mainBase
        step = 0
      }
      if (style.justifyContent === 'flex-end') {
        // 起点在 Base,第一个元素需要在空白之后,所以第一个元素在 mainSpace 之后 
        currentMain = mainBase + mainSpace * mainSign
        step = 0
      }
      if (style.justifyContent === 'flex-center') {
        currentMain = mainBase + mainSpace / 2 * mainSign
        step = 0
      }
      if (style.justifyContent === 'space-between') {
        currentMain = mainBase
        step = mainSpace / (items.length - 1) * mainSign
      }
      if (style.justifyContent === 'space-around') {
        step = mainSpace / items.length * mainSign
        currentMain = mainBase + step / 2
      }
      if (style.justifyContent === 'stretch') {

      }
      for (let i = 0; i < items.length; i++) {
        let item = items[i];
        let itemStyle = getStyle(item)
        itemStyle[mainStart] = currentMain
        itemStyle[mainEnd] = itemStyle[mainStart] + mainSign * itemStyle[mainSize]
        currentMain = itemStyle[mainEnd] + step;
      }
    }
  })
}

\

5. 根据交叉轴的几个变量,确定起点偏移量和终点偏移量,确定方向,确定起点,将 FlexLine 放入交叉轴方向计算,形成 FlexBox 弹性盒子

  • 根据父元素的 crossSpace 和 align-content 的布局方式确定交叉轴的起始线、以及空隙 space
  • 循环加到 crossStart 和 crossEnd 上,并以此类推加到起始线上,偏移起始线,确定每一个弹性行的位置
  • 每个弹性行内再根据每行的 crossSpace 和 alignItems 属性,根据弹性元素的大小以及 crossStart 和 crossEnd,偏移每个元素对 crossBase 的位置,计算其垂直行内的位置
  • crossBase 再加上每行的 crossSpace ,一直循环到每一行及其行内元素都排版完

计算交叉轴
// compute the cross axis sizes, align-items, align-self
if (!style[crossSize] ) {
  // 没有 crossSize 就累加
  crossSpace = 0
  style[crossSize] = 0
  for (let i = 0; i < flexLines.length; i++) {
    style[crossSize] += flexLines[i].crossSpace
  }
} else {
  // 有 crossSize ,crossSpace 就等于 crossSize 减去每行的 crossSpace,得到空隙的空间
  crossSpace = style[crossSize]
  for (let i = 0; i < flexLines.length; i++) {
    crossSpace -= flexLines[i].crossSpace;
  }
}

if (style.flexWrap === 'wrap-reverse') {
  crossBase = style[crossSize]
} else {
  crossBase = 0
}

// 交叉轴大小
// let lineSize = style[crossSize] / flexLines.length

let step
if (style.alignContent === flexStart) {
  crossBase += 0
  step = 0
}
if (style.alignContent === center) {
  crossBase += crossSign * crossSpace / 2
  step = 0
}
if (style.alignContent === flexEnd) {
  crossBase  +=crossSign * crossSpace
  step = 0
}
if (style.alignContent === spaceBetween) {
  crossBase += 0
  step = crossSpace / (flexLines.length - 1)
}
if (style.alignContent === spaceAround) {
  step = crossSpace / (flexLines.length)
  crossBase += crossSign * step / 2
}
flexLines.forEach(function (flexLine) {
  let lineCrossSize = style.alignContent = stretch
  // 各行均匀分配
  ? flexLine.crossSpace + crossSpace / flexLines.length
  : flexLine.crossSpace
  for (let i = 0; i < flexLine.items.length; i++) {
    const item = flexLine.items[i];
    let itemStyle = getStyle(item)

    // alignItems 是全局的,alignSelf 是只作用于本身的,alignSelf 优先级较高 
    let align = itemStyle.alignSelf || style.alignItems

    if (itemStyle[crossSize] == null) {
      itemStyle[crossSize] = (align === stretch)
        ? lineCrossSize
      : 0
    }

    if (align === 'flex-start') {
      itemStyle[crossStart] = crossBase
      itemStyle[crossEnd] = itemStyle[crossStart] + crossSign * itemStyle[crossSize]
    }
    if (align === 'flex-end') {
      itemStyle[crossEnd] = crossBase + crossSign * lineCrossSize
      itemStyle[crossStart] = itemStyle[crossEnd] - crossSign * itemStyle[crossSize]
    }
    if (align === 'center') {
      itemStyle[crossStart] = crossBase + crossSign * (lineCrossSize - itemStyle[crossSize]) / 2
      itemStyle[crossEnd] = itemStyle[crossStart] + crossSign * itemStyle[crossSize]
    }
    if (align === 'stretch') {
      itemStyle[crossStart] = crossBase
      itemStyle[crossEnd] = crossBase + crossSign
        * (
        (itemStyle[crossSize] !== null) && (itemStyle[crossSize] !== (void 0))
        ? itemStyle[crossSize] 
        : lineCrossSize
      )

      itemStyle[crossSize] = crossSign * (itemStyle[crossEnd] - itemStyle[crossStart])
    }
  }
  crossBase += crossSign * (lineCrossSize + step)
})

flex-direction

flex-direction 确定了主轴的方向,一共四个方向,row、column、row-reverse、column-reverse

flex-wrap

flex-wrap 给定了交叉轴的方向,可以反转或取消交叉轴

wrap 就是正常流的方向,从左到右,从上到下

wrap-reverse 就是反方向,从下到上,从右到左

justify-content

justify-content 计算的是弹性元素平行于弹性行行内的方向的位置,或者说平行于主轴方向的空隙排布

flex-start 和 flex-end 分别代表主轴的起点和终点,注意 flex-end 会把剩余的空隙放在最前面再计算其他元素的位置

center 表示主轴方向平均分配空隙,二分至起点和终点

space-around 是将空隙除以元素数得到 step-space,并且以 mainBase + step-space 除以 2 的地方为起点

space-between 则还是以 mainBase 为起点,空隙均匀分在每个夹缝里

align-contents

align-content 计算的是弹性行 flexLine 平行于交叉轴的方向上的位置

flex-start 和 flex-end 分别代表交叉轴的起点和终点

center 表示平均分配空隙,二分至起点和终点

stretch 表示所有 flexLine 在交叉轴方向等比放大

space-between 和 space-around 分别表示空隙均匀分在每个夹缝里,以及将空隙除以元素数得到 step-space,并且以 crossSpace + step-space 除以 2 的地方为起点

align-items

align-content 计算的是弹性行 flexLine 平行于交叉轴的方向上的位置

flex-start 和 flex-end 分别代表在行内的起点和终点

center 表示在行内平均分配空隙,二分至起点和终点

stretch 表示所有元素在行内的交叉轴方向等比放大

baseline 表示所有元素在行内的以文字基线位置为标注对齐

align-self

与 align-items 同理都是控制行内空隙排布,区别在于 align-self 只作用于被设置的单个 flex 子元素上,默认值 auto,继承 align-items 的属性,但优先级比 align-items 高

\

flex-shrink

flex-shrink 决定了当主轴空隙为负,即元素溢出时,元素主轴方向是否缩放以及缩放的大小,数字占比越大缩小的越多,值必须是自然数。下图元素 2 表示了 flex-shrink 为 0 就不收缩,其余元素都是 flex-shrink 默认为 1 的元素都等比例收缩

flex-grow

flex-shrink 决定了当主轴空隙为正,即行内有空间时,元素主轴方向是否缩放以及缩放的大小,数字占比越大放大的越大,值必须是自然数

flex-basis

flex-basis 指的是元素占用的主轴的空间,默认值是 auto,即元素本来的大小

order

order 属性设置了排版时的顺序。数字越小排的靠前,值可以为负数。

\

通过应用理解

比如我们现在设置一个 flex container,里面有若干个 flex item

首先获取 mainSize 和 crossSize,默认是 width 和 height,然后排版起点默认以 flex-start ,也就是以父元素最左边为 mainBase 起点,从左向右排版为正方向,mainSign 为 +1,表示元素是递增偏移 mainBase

在排版开始后,元素入行,mainBase 偏移并递减 mianSpace,直到 mainSpace 为负数,就开新行,flex line 记录下每行的 mainSpace,根据每行最大的 crossSize 确定 flex line 的 crossSize

循环每个 flex line,根据 mainSpace 和决定分配 mainSpace 的 justify-content 属性,决定如何排序 flex 子元素和普通子元素以及空隙

循环每个 flex line,根据 crossSpace 和决定分配 crossSpace 的 align-content 属性,决定每行的垂直排序起点。接着再根据 align-items 分配每行的起点开始的空隙,完成每行的垂直排序,最终完成垂直排版

结合 Cocos

Cocos 拖拽模板是为了拖入多个拖拽选项而准备的,拖拽到里面的选项可能大小不一,为了使得选项能够正确排版,就想到了 flex 布局结合到 cocos 里面的方式,在这个过程中发现 flex 布局引入 cocos 是时需要考虑锚点xy轴方向等问题

锚点的问题:

cocos 默认是正中心 (0.5, 0.5),浏览器内默认是左上角(0,1)

可以通过计算定位偏移量解决,就在计算实际 cc.Vec2 的时候用 mainStart 和 mainSize 结合计算出中心点即可

xy 轴方向的问题:

cocos 默认是 xy 轴正方向都是向上向右,浏览器内默认 xy 轴正方向是向下向右

可以通过对初始化数值的 crossSign 和 mainSign 配置解决,即想要从哪个方向排版,就将 mainSign 和 crossSign 改为对应方向的符号,比如 flexDirection 为 row 的情况

注意

目前因为考虑到 Scale 值影响动画原因以及没有缩放子节点的需求,因此需要去掉 flex-shrink 和 flex-grow 及类似的属性,同时对溢出的部分进行空间分配,使得溢出时布局仍然正确

最终返回的是一组 cc.Vec2 数据,根据 id 对其对应的元素进行分配位置,重新移动和排版

需要用到父元素偏移量等手段影响实际的布局,就在计算出的 cc.Vec2 加上父元素及设定的偏移量

实现上的细节

  • mainBase 的计算依靠 flex-direction,一般就是 0 或 mainSize
  • step 或者说 space 的计算(空隙),space-between 是均匀分布空隙,mainSpace 除以元素数减 1,space-around 是起点终点各加上一个 step space 除 2
  • flex-flow 是 flex-direction 和 flex-wrap 的简写 row、no-warp
  • flex 是 flex-grow、flex-shrink、flex-basis 的简写,默认值 0、1、auto
  • 当没有设定 width 时,需要计算 主轴的 mainSize 并主动把主轴方向的父元素撑大,代码里的isAutoMainSize 就处理了这个情况,isAutoMainSize 有点类似于 no-wrap 的情况
  • crossSize 的计算,和 flexLine 有关,如果没有初始值,会变成各个 flexLine 的累计值,同 isAutoMain
  • crossBase 的计算,step 计算,crossSpace 的计算类似于上面 mainBase 等的计算
  • stretch 比较特殊,justify-content 和 align-items 中的 stretch 可以改变元素 mainSize 和 crossSize 的大小

实现的效果

链接

总结

通过将实际的属性抽象成 flex 里的主轴和交叉轴上的属性, flex 布局成为了一套足够简单和灵活的布局方式,最终开放给用户的 API 也足够简单易懂,能够让人快速上手并且做出符合需求的布局效果。通过实现和还原 flex 布局过程,能够更加深入的了解到 flex 布局的逻辑,以及为什么 flex 能够覆盖多重的布局场景

参考文章:

  1. developer.mozilla.org/zh-CN/docs/… 《Basic_Concepts_of_Flexbox》
  2. developer.mozilla.org/zh-CN/docs/… 《Controlling_Ratios_of_Flex_Items_Along_the_Main_Ax》
  1. www.ruanyifeng.com/blog/2015/0… 《Flex 布局教程:语法篇》