浏览器工作原理——动手实现一个toy-browser(三):排版和渲染

接上一篇博客浏览器工作原理——动手实现一个toy-browser(二):生成DOM树和计算CSS,本篇主要介绍浏览器工作流程的排版和渲染环节。

1 浏览器工作原理——排版生成带位置的DOM

1.1 根据浏览器属性进行排版

1.1.1 css的几代布局演变
  1. 传统布局
  • 正常流
  • float浮动
  • position定位
  1. flex弹性布局
  2. grid网格布局
1.1.2 flex网格布局相关属性
  • flex-direction: row(水平排布)
    • Main(排版时元素的延伸方向): width, x, left, right
    • Cross: height, y, top, bottom
  • flex-direction: column(垂直排布)
    • Main: height, y, top, bottom
    • Cross: width, x, left, right
      使用主轴和交叉轴的概念对排版方向进行抽象,以便能够适应多种书写方式。
1.1.3 排版的准备工作
  1. 设定flex相关属性(flexDirection, justifyContent, alignContent, alignItems, flexWrap)的默认值
  2. 根据flexDirection和flexWrap的值设定抽象的mainSize,mainStart,mainEnd,mainSign,mainBase等变量,方便后续代码的编写
function getStyle(element) {
  if(!element.style) {
    element.style = {}
  }
  for(let prop in element.computedStyle) {
    element.style[prop] = element.computedStyle[prop].value
    // 带px的样式值转成像素值
    if(element.style[prop].toString().match(/(px)$/)) {
      element.style[prop] = parseInt(element.style[prop])
    }
    // 数值型样式值转数值
    if(element.style[prop].toString().match(/^[0-9]+(\.[0-9]+)?$/)) {
      element.style[prop] = parseFloat(element.style[prop])
    }
  }
  return element.style
}

 var elementStyle = getStyle(element)
  if(elementStyle.display !== "flex") {
    return // 暂时只处理flex布局
  }
  var items = element.children.filter(item => {return item.type === "element"}) // 将文本节点滤除
  items.sort((a, b) => {// 按flex的order属性升序排序
    return (a.order || 0) - (b.order || 0)
  })

  var style = elementStyle
  ;["width", "height"].forEach(size => {// 宽高缺失值的处理
    if(style[size] === "auto" || style[size] === "") {
      style[size] = null
    }
  })

  // 给flex的各属性设置默认值
  if(!style.flexDirection || style.flexDirection === "auto") {
    style.flexDirection = "row"
  }
  if(!style.justifyContent || style.justifyContent === "auto") {
    style.justifyContent = "flex-start"
  }
  if(!style.alignContent || style.alignContent === "auto") {
    style.alignContent = "flex-start"
  }
  if(!style.alignItems || style.alignItems === "auto") {
    style.alignItems = "stretch"
  }
  if(!style.flexWrap || style.flexWrap === "auto") {
    style.flexWrap = "nowrap"
  }

  // 根据flexDirection和flexWrap的值设置相关抽象参数
  let mainSize, mainStart, mainEnd, mainBase, mainSign, 
      crossSize, crossStart, crossEnd, crossBase, crossSign
  if(style.flexDirection === "row") {
    mainSize = "width"
    mainStart = "left"
    mainEnd = "right"
    mainSign = +1
    mainBase = 0

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

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

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

    crossSize = "width"
    crossStart = "left"
    crossEnd = "right"
  }
  if(style.flexWrap === "wrap-reverse") {
    var tmp = crossStart 
    crossStart = crossEnd
    crossEnd = tmp
    crossSign = -1
    crossBase = crossSize === "width" ? style.width : style.height
  }else{
    crossSign = +1
    crossBase = 0
  }

  // 未设置mainSize,进行auto sizing,计算所有子元素排进一行的mainSize
  let isAutoMainSize = false
  if(!style[mainSize]) {
    style[mainSize] = 0
    for(let item of element.children) {
      let itemStyle = getStyle(item)
      if(itemStyle[mainSize] !== null && itemStyle[mainSize] !== void(0)) {
        style[mainSize] += itemStyle[mainSize]
      }
    }
    isAutoMainSize = true
  }

1.2 收集元素进行

  1. 根据主轴尺寸将元素排进行
  2. 如果设置了nowrap属性,所有元素强行分配进第一行
  // flex的分行算法
  let flexLine = []
  let flexLines = [flexLine]
  let mainSpace = style[mainSize]
  let crossSpace = 0
  // 将元素排进行
  for(let item of items) {
    let itemStyle = getStyle(item)
    if(itemStyle[mainSize] === null || itemStyle[mainSize] === void(0)) {
      itemStyle[mainSize] = 0
    }
    if(itemStyle[crossSize] === null || itemStyle[crossSize] === void(0)) {
      itemStyle[crossSize] = 0
    }
    if(itemStyle.flex) {// 元素具有flex属性,可伸缩,一定能放进当前行
      flexLine.push(item)
    }else if(style.flexWrap === "nowrap" && isAutoMainSize) {//不换行且mainSize已计算出,元素依次排入一行
      mainSpace -= itemStyle[mainSize]
      if(itemStyle[crossSize] !== null && itemStyle[crossSize] !== void(0)) {
        crossSpace = Math.max(itemStyle[crossSize], crossSpace)
      }
      flexLine.push(item)
    }else{// 排入多行
      if(itemStyle[mainSize] > style[mainSize]) {
        itemStyle[mainSize] = style[mainSize]
      }
      if(mainSpace < itemStyle[mainSize]) {// 主轴剩余空间放不下当前元素,结束当前行,另起一行
        flexLine.mainSpace = mainSpace
        flexLine.crossSpace = crossSpace
        flexLines.push(flexLine)

        flexLine = [item]
        mainSpace = element[mainSize]
        crossSpace = 0
      }else{
        flexLine.push(item)
      }
      // 更新主轴剩余空间和交叉轴空间
      mainSpace -= itemStyle[mainSize]
      if(itemStyle[crossSize] !== null && itemStyle[crossSize] !== void(0)) {
        crossSpace = Math.max(itemStyle[crossSize], crossSpace)
      }
    }
  }
  flexLine.mainSpace = mainSpace
  if(style.flexWrap === "nowrap" || isAutoMainSize) {
    // 如果style(即elementStyle)定义了crossSize,将其直接作为flexLine的crossSpace
    flexLine.crossSpace = (style[crossSize] !== undefined) ? style[crossSize] : crossSpace
  }else{
    flexLine.crossSpace = crossSpace
  }

1.3 计算主轴方向

  1. 如果主轴的剩余空间小于0,将所有flex项的mainBase设为0,然后将剩余元素等比例缩小并确定位置
  2. 如果主轴的剩余空间大于0, 循环处理每个flexLine
  3. 统计每个flexLine的所有flex项
  4. 如果flex项数量大于0,将所有flex项的mainBase按flex比例分配主轴剩余空间,依次确定主轴位置
  5. 否则根据justifyContent确定flexLine中各元素的位置
  // 计算主轴方向
  if(mainSpace < 0) {
    // 排不下,将所有flex元素的mainSize设为0,非flex元素等比例压缩
    let scale =  style[mainSize]/ (style[mainSize] - mainSpace)
    let currentBase = mainBase
    flexLine.forEach((item)=> {
      let itemStyle = getStyle(item)
      if(itemStyle.flex) {
        itemStyle[mainSize] = 0
      }
      itemStyle[mainSize] *= scale
      itemStyle[mainStart] = currentBase
      itemStyle[mainEnd] = itemStyle[mainStart] + mainSign*itemStyle[mainSize]
      currentBase = itemStyle[mainEnd]
    })
  }else{
    // 排得下,遍历flexLines,根据flexLine的flex项数量决定使用按flex比例排版还是依据justifyContent排版
    flexLines.forEach(items=>{
      let mainSpace = items.mainSpace
      let flexTotal = 0
      for(let i=0; i<items.length; i++) {
        let item = items[i]
        let itemStyle = getStyle(item)
        if(itemStyle.flex) {
          flexTotal += itemStyle.flex
        }
      }
      let currentBase = mainBase
      if(flexTotal > 0) {
        for(let i=0; i<items.length; i++) {
          let item = items[i]
          let itemStyle = getStyle(item)
          itemStyle[mainStart] = currentBase
          if(itemStyle.flex) {
            // 将mainSpace空间按flex比例分配
            itemStyle[mainSize] = mainSpace * itemStyle.flex / flexTotal
          }
          itemStyle[mainEnd] = itemStyle[mainStart] + mainSign*itemStyle[mainSize] 
          currentBase = itemStyle[mainEnd]
        }
      }else{
        // 根据justifyContent确定currentBase和step,实现非flex元素的排版
        let step = 0 // 间距
        if(items.justifyContent === "flex-end") {
          currentBase = mainSpace*mainSign + mainBase
        }
        if(items.justifyContent === "center") {
          currentBase = mainSpace/2*mainSign + mainBase
        }
        if(items.justifyContent === "space-between") {
          step = mainSign*mainSpace/(items.length-1)
        }
        if(items.justifyContent === "space-around") {
          step = mainSign*mainSpace/items.length
          currentBase = mainBase + step/2
        }
        for(let i=0; i<items.length; i++) {
          let item = items[i]
          let itemStyle = getStyle(item)
          itemStyle[mainStart] = currentBase
          itemStyle[mainEnd] = itemStyle[mainStart] + mainSign*itemStyle[mainSize]
          currentBase = itemStyle[mainEnd] + step
        }
      }
    })
  }

1.4 计算交叉轴方向

  1. 根据一行中最大元素尺寸计算行高
  2. 根据alignContent和各行行高确定各行的基准位置和间隔
  3. 根据alignItems确定行内元素具体位置

1.2~1.4节相关代码


  // 计算交叉轴方向
  if(!style[crossSize]) { // 未定义交叉轴尺寸,累加各行得到elementStyle[crossSize]
    crossSpace = 0
    style[crossSize] = 0
    for(let i=0; i<flexLines.length; i++) {
      style[crossSize] +=  flexLines[i][crossSpace]
    }
  }else{// 已定义交叉轴尺寸,减去各行剩余空间得到整个剩余空间
    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 lineHeight = style[crossSize] / flexLines.length
  // 根据alignContent属性确定crossBase和step
  let step 
  if(style.alignContent === "flex-start") {
    crossBase += 0
    step = 0
  }
  if(style.alignContent === "flex-end") {
    crossBase += crossSpace*crossSign
    step = 0
  }
  if(style.alignContent === "center") {
    crossBase += (crossSpace/2)*crossSign
    step = 0
  }
  if(style.alignContent === "space-between") {
    step = (crossBase/ (flexLines.length-1))*crossSign
    crossBase += 0
  }
  if(style.alignContent === "space-around") {
    step = (crossBase/ flexLines.length)*crossSign
    crossBase += step/2
  }
  if(style.alignContent === "stretch") {
    crossBase += 0
    step = 0
  }
  // 对flexLine进行交叉轴排版
  flexLines.forEach(items => {
    let lineCrossSize = style.alignContent === "stretch" ? 
                        items.crossSpace + crossSpace/items.length : // 将剩余空间平均分配给每行
                        items.crossSpace // 每行的交叉轴空间
    // 对行内元素进行交叉轴排版                     
    for(let i=0; i<items.length; i++) {
      let item = items[i]
      let itemStyle = getStyle(item)
      let align = itemStyle.alignSelf || style.alignItems
      if(item === 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*lineCrossSize // 此处与视频不同
        itemStyle[crossSize] = crossSign*lineCrossSize
      }
    }
    crossBase  += crossSign*(lineCrossSize+step)
    console.log(items)
  })
}

2 浏览器工作原理——渲染

2.1 绘制单个元素

  1. 绘制需要依赖一个图形环境,出于轻量化依赖少的考虑,课程采用npm的images库
  2. 绘制在一个viewport上进行
  3. 与绘制相关的属性,background-color,border,background-image等

2.2 绘制dom树

  1. 递归调用子元素的绘制方法即可完成整个dom树的绘制
  2. 实际浏览器中,文字绘制是难点,需要依赖字体库,课程中忽略
  3. 实际浏览器中,还会对一些图层做compositing,课程中也予以忽略

2.1~2.2 相关代码

const images = require("images")

function render(viewport, element) {
  if(element.style) {
    let img = images(element.style.width, element.style.height)
    let color = element.style["background-color"]
    if(color) {
      let pattern = /rgb\((\d+), (\d+), (\d+)\)/
      color.match(pattern)
      img.fill(Number(RegExp.$1), Number(RegExp.$2), Number(RegExp.$3))
      viewport.draw(img, element.style.left||0, element.style.top||0)
    }
  }
  if(element.children) {
    for(let child of element.children) {
      render(viewport, child)
    }
  }
}

3 最终渲染结果

利用我们编写的toy-browser程序,下面html文档的最终渲染结果为toy-browser渲染结果

<html lang="en">
        <head>
          <meta charset="UTF-8" />
          <meta name="viewport" content="width=device-width, initial-scale=1.0" />
          <script src="./foo.js"></script>
          <title>Document</title>
          <style>
            body {
              background-color: black;
            }
            .container {
              width: 500px;
              height: 300px;
              display: flex;
              background-color: rgb(255, 255, 255);
              align-items: center;
            }
            .pText {
              width: 200px;
            }
            p.text {
              display: none;
            }
            p.text#name {
              font-size: 20px;
              color: red;
              background-color: blue;
            }
            body div #myImg {
              flex: 1;
              height: 200px;
              background-color: rgb(255, 0, 0);
            }
            body div #hisImg2 {
              flex: 2;
              height: 300px;
              background-color: rgb(0, 255, 0);
            }
         /*   .classImg {
              margin: 10px;
            }
            .myClass {
              border: 2px;
            } */
          </style>
        </head>
        <body>
          <div class="container">
            <div class="pText">
              <p class="text">Hello world</p>
              <p class="text" id="name">My name is blateyang</p>
            </div>
            <div id="myImg" class="classImg  myClass"></div>
            <div id="hisImg2" class="classImg  myClass"></div>
          </div>
        </body>
      </html>

4 toy-browser总结

通过三周的学习和实践,我们对浏览器的工作原理有了较为清晰深入地认识。知道了如何利用有限状态机对HTML文档进行词法和语法分析将其转换成一颗DOM树,CSS规则又是在何时被添加到DOM节点中以及如何与节点进行匹配的,如何根据flex属性对DOM节点进行弹性布局生成带位置信息的DOM树,如何将带位置信息和样式信息的DOM树渲染成网页位图。当然上述流程是对浏览器工作流程的一个简化,实际的浏览器工作流程还包括在发送请求时对请求的分析处理(是否跨域、是否发送预检请求)、生成网页位图后因执行js代码引发的重绘和回流等其它工作,布局排版的实现也仅仅是实现了基本的flex布局,还有很多可以扩展完善的地方,比如在flex布局中增加对margin、padding、border等属性的支持。本文的主要目的是通过实现一个简易的toy-browser,加深对浏览器工作原理的认识和理解,有兴趣的朋友可以在此基础上继续完善。

5 本节代码github地址

如需查看完整代码,请移步github

PS:如果你对本文有任何疑问,可以留言评论或私信,我看到了会尽量回复。如果对你有所帮助,欢迎点赞转发_

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值