面试题:如何开发一个进度条组件(暗藏杀机!!!)

起因

之前看了一个问题(如何实现一个进度条),也看了其他人的文章,觉得还有很多的坑没有讲到,如果读者根据作者的文章去实现,可能会导致更大的问题。下面就说一下思路,大家看下自己在哪一层!

第一层

我们最简单的实现就是一个 div 里面套 div,动态改变里面 div 的宽度,就达到了一个进度条的效果

代码:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    #container {
      width: 600px;
      height: 20px;
      display: inline-block;
      border: 1px solid;
      overflow: hidden;
    }
    #progress {
      width: 0;
      background: pink;
      height: 100%;
      transition: width 0.5s linear;
    }
  </style>
</head>
<body>
  <div id="container">
    <div id='progress'></div>
  </div>

  <div>
    <button id='btn'>按钮</button>
  </div>

  <script>
    const btn = document.getElementById('btn')
    const progress = document.getElementById('progress')

    btn.onclick = () => {
      let width = 0
      
      const timer = setInterval(() => {
        if (width === 100) return clearInterval(timer)

        progress.style.width = `${++width}%`
      }, 50);
    }
  </script>
</body>
</html>

性能:

从图上就可以看出来改变 width 会频繁的导致 layout,而一般情况下 layout 又会引起 paint,所以会花费很多的时间。

1.gif

第二层

我们现在到了第二层,所以我们就想到了用 transform,我只去改变他的位置,不触发 layout,那肯定性能好!

代码:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    #container {
      width: 600px;
      height: 20px;
      display: inline-block;
      border: 1px solid;
      overflow: hidden;
    }
    #progress {
      display: inline-block;
      background: pink;
      height: 100%;
      transition: transform 0.5s linear;
      width: 100%;
      transform: translateX(-100%);
    }

  </style>
</head>
<body>
  <div id="container">
    <div id='progress'></div>
  </div>

  <div>
    <button id='btn'>按钮</button>
  </div>

  <script>
    const btn = document.getElementById('btn')
    const progress = document.getElementById('progress')

    btn.onclick = () => {
      let process = 0
      
      const timer = setInterval(() => {
        if (process === 100) return clearInterval(timer)

        progress.style.transform = `translateX(-${100 - ++process}%)`
      }, 50);
    }
  </script>
</body>
</html>

性能:

我们可以看到没有触发 layout,也就没有触发多余的 paint,自然性能就好多了!

2.gif

第三层

这个时候我们可能想到了 will-change 这个属性,简单来说这个属性就是告诉浏览器即将发生哪些变化,浏览器就可以进行优化,从而使得页面更快速更灵敏。

注意:

  1. 这个属性千万不可以滥用,用不好还可能导致性能降低。
  2. will-change 好的使用是在属性要变化之前去添加,但是浏览器通常会为优化设置提供至少 200 毫秒的时间,所以我们要给浏览器留下时间去优化。

参考文档

  1. developer.mozilla.org/zh-CN/docs/…
  2. drafts.csswg.org/css-will-ch…

第四层

我们设置了 'transition: transform'(过渡动画) 或者 'will-change: transform'(优化) 的元素会被提升到合成层去渲染,如下图:

代开控制面板,然后ctrl + shift + p,搜索 layers 打开该面板

image.png

image.png

层爆炸

如下图,我们进度条开始运动的时候,会被提升到合成层进行渲染,但是下面很多数据标签的层叠位置都比进度条高,浏览器为了保证正确的层叠顺序,就会提升比进度条层叠位置高的元素,又因为我们 li 标签设置了 overflow: hidden,导致浏览器无法对额外提升的元素优化(层压缩),所以这个时候就会导致层爆炸的出现!

层爆炸效果

3.gif

优化效果

我们给进度条设置高的 z-index: 999,这个时候进度条的层级就比所有的元素高,所以浏览器也不用隐式提升其他的元素,也就不会导致层爆炸的出现

4.gif

代码:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    #container {
      width: 600px;
      height: 20px;
      display: inline-block;
      border: 1px solid;
      overflow: hidden;
    }

    #progress {
      position: relative;
      display: inline-block;
      background: pink;
      height: 100%;
      transition: transform 0.5s linear;
      width: 100%;
      transform: translateX(-100%);
    }

    .list-item {
      position: relative;
      /* overflow 导致无法层压缩 */
      overflow: hidden;
    }

    .list-item-text {
      position: absolute;
      left: 0;
      top: 0;
    }

  </style>
</head>
<body>
  <div id="container">
    <div id='progress'></div>
  </div>

  <div>
    <button id='btn'>按钮</button>
  </div>

  <hr />
  <!-- 隐式提升的元素 -->
  <ul id='listContainer'></ul>

  <script>
    const btn = document.getElementById('btn')
    const progress = document.getElementById('progress')

    btn.onclick = () => {
      let process = 0
      
      const timer = setInterval(() => {
        if (process === 100) return clearInterval(timer)

        progress.style.transform = `translateX(-${100 - ++process}%)`
      }, 50);
    }
    
    const createData = () => {
      const listContainer = document.getElementById('listContainer')

      let str = ''
      for (let i = 0; i < 1000; i++) {
        str += `<li class='list-item'>
          <div class='list-item-text'>${i}++++++++++++++</div>
          </li>`
      }

      listContainer.innerHTML = str
    }

    createData()
  </script>
</body>
</html>

第五层

假设我们开发了个进度条组件,我们也无法保证其他使用者是否熟知层爆炸的知识,这个时候我们调整了进度条的 z-index,又可能会导致其他样式的错乱,所以这个的取舍得慎重!

最后

一个简单的进度条涉及了浏览器渲染的很多知识,重排,重绘,层提升,层爆炸,层压缩,坑点还是很多的。

之前我看其他开源组件库用的 width 实现进度条,我觉得他在第一层,慢慢学习前端知识之后我才猛然发现,他们在第五层!