前端面试基础知识整理【Day-3】

目录

前言

浏览器渲染篇

1.关键路径渲染

2.回流和重绘(绘制)的区别

[3.script 标签的 defer 和 async](#3.script 标签的 defer 和 async)

代码篇

1.手写一个EventBus(订阅-发布模式)

回顾篇

1.为什么做动画时,改变left比改变transform属性更消耗性能


前言

往期回顾:

前端面试基础知识整理【Day-1】-CSDN博客

前端面试基础知识整理【Day-2】-CSDN博客

浏览器渲染篇

1.关键路径渲染

关键路径渲染指的是浏览器把代码变成屏幕上像素经过的六个阶段,任何一个阶段卡住,页面就会白屏或者卡顿,下面是六个阶段:

  1. 构建DOM树:解析HTML,生成DOM节点树
  2. 构建CSSOM树,解释CSS,生成CSS规则树
  3. 生成渲染树:(DOM + CSSOM)才是渲染树,display:none的节点不会出现在渲染树中,但是visibility:hidden的节点会出现在渲染树中(因为它占据空间)
  4. 回流:计算每个节点在屏幕上的位置和大小(即几何信息
  5. 重绘(绘制):填充像素(颜色、背景、边框等等)
  6. 合成:浏览器将不同的层合并,最终显示在屏幕上

所以,根据上面的六个阶段,如果想要优化浏览器页面加载速度可以从以下几个方面入手:

  1. 减少关键资源请求数(少写几个CSS、JS文件)
  2. 减少关键资源体积(例如将png格式的图片改为webp格式的图片)
  3. 异步加载JS文件,只在需要的时候才动态import文件

2.回流和重绘(绘制)的区别

回流必定引起重绘,但是重绘不一定引起回流

这句话很好理解:"回流计算的是节点的几何信息(大小、位置),重绘计算的是节点的(颜色、背景色、阴影等),可以说,重绘是对回流计算的节点进行美化的一个过程"

因此,当某个节点大小改变时,那么颜色、背景色这些属性对应的也要进行重绘来填充

而某个节点的颜色改变时,只需要重绘填充颜色即可,至于节点的大小、位置并没有改变,所以回流不会发生

下面是对回流重绘的一个解释:

回流

定义 :当元素的几何属性(宽、高、位置、显示隐藏)发生变化时,浏览器需要重新计算元素的几何信息

性能:消耗资源非常多,一个元素的大小变化在整个DOM树节点上的影响是全局性的

触发条件

  • 改变窗口大小
  • 改变字体大小
  • 添加/删除DOM节点
  • 改变width、height、margin、padding
  • 读取offsetWidth、offsetHeight、scrollTop等属性时

重绘

定义:当元素的外观属性发生变化,但不影响布局时

性能:资源消耗较少

触发条件

  • color
  • background-color
  • visibility
  • outline

3.script 标签的 defer 和 async

在HTML中,**<script>**标签默认会阻塞DOM的解析,当浏览器执行到<script>标签时,会从服务器下载并执行JS,执行完JS之后再解析HTML

为了不阻塞页面显示,<script> 引入了两个属性:"defer "和"async"

两个属性改变的是**<script>** 标签的下载时机执行时机执行顺序

特性 <script> <script async> <script defer>
HTML解析 立即停止,等待JS代码下载和执行 不停止,JS并行下载 不停止,JS并行下载
JS执行时机 下载完立即执行 下载完立即执行 等到HTML全部解析完 (DOMContentLoaded前)执行
执行顺序 (多个<script>标签) 按代码书写顺序 谁先下完执行谁 严格按代码书写顺序
适用场景 关键且依赖顺序的脚本 独立的脚本 (埋点、广告) 依赖DOM或其它脚本的方案

一句话概括:

<script async>:下载完立即中断HTML去执行,谁下载快谁先被执行,但如果a.js依赖于b.js,但是b.js大一点下得慢,a.js先执行就会报错

<script defer>:下载完成并等到HTML解析完成之后再执行,执行顺序严格按照代码顺序,并且可以保证DOM被渲染好了(不用写window.onload)

现在的工程项目中,通常把<script>放在<head>里并加入<defer>,这是最安全、性能最好的选择

在以前的HTML老代码中,可能会将JS代码写在<body>标签的最后,或者在JS里手动添加"window.onload"这很啰嗦,但在现代项目中,直接使用defer关键字即可

一个老项目代码:

html 复制代码
<!DOCTYPE html>
<html>
<head>
    <script>
        window.onload = function() {
            const box = document.getElementById('box');
            box.innerText = "DOM加载完成";
        }
    </script>
</head>
<body>
    <div id="box">准备改变内容</div>
    <script src="vue.js"></script>
    <script src="plugin.js"></script>
    <script src="app.js"></script>
</body>
</html>

经过现代化工程修改后的代码:

javascript 复制代码
<!DOCTYPE html>
<html>
<head>
    <title>现代化工程代码</title>
    <script defer src="vue.js"></script>
    <script defer src="plugin.js"></script>
    <script defer src="app.js"></script>
</head>
<body>
    <div id="box">准备改变内容</div>
</body>
</html>

代码篇

1.手写一个EventBus(订阅-发布模式)

javascript 复制代码
class EventBus {
  constructor() {
    this.events = {};
  }

  on(eventName, callback) {
    if (!this.events[eventName]) {
      this.events[eventName] = [];
    }
    this.events[eventName].push(callback);
  }

  emit(eventName, ...args) {
    if (this.events[eventName]) {
      const callbacks = [...this.events[eventName]];
      callbacks.forEach(callback => {
        callback(...args);
      })
    }
  }

  off(eventName, callback) {
    if (this.events[eventName]) {
      this.events[eventName] = this.events[eventName].filter(cb => {
        return cb !== callback && cb.originalCallback != callback;
      })
    }
  }

  once(eventName, callback) {
    const onceWrapper = (...args) => {
      this.off(eventName, onceWrapper);
      callback(...args);
    }
    onceWrapper.originalCallback = callback;
    this.on(eventName, onceWrapper);
  }

}


const bus = new EventBus();

const fn1 = (name, age) => console.log(`fn1: ${name}, ${age}`);
const fn2 = (name, age) => console.log(`fn2: ${name}, ${age}`);

console.log("1.on和emit方法展示")
bus.on('sayHi', fn1);
bus.on('sayHi', fn2);
bus.emit('sayHi', 'Alice', 30);

console.log("2.off方法展示")
bus.off('sayHi', fn1);
bus.emit('sayHi', 'Bob', 25);

console.log("3.once方法展示")
const onceOn = (name, age) => console.log(`onceOn: ${name}, ${age}`);
bus.once('greet', onceOn);
bus.emit('greet', 'Charlie', 28);
bus.emit('greet', 'David', 35);

console.log("4.once还没被执行就被off掉")
const onceOff = (name, age) => console.log(`onceOff: ${name}, ${age}`);
bus.once('welcome', onceOff);
bus.off('welcome', onceOff);
bus.emit('welcome', 'Eve', 22);

运行结果:

回顾篇

1.为什么做动画时,改变left比改变transform属性更消耗性能

本问题用"回流/重绘"原理解释:

  1. 首先,改变left属性必然会引起回流,此时性能消耗巨大,此外,还会引起重绘,所以性能消耗更大了
  2. 其次,改变transform属性不会引起节点的几何属性变化,也不会引起节点的外观属性变化,故不会引起回流或者重绘
  3. 最后,使用transform,浏览器会使用GPU加速渲染,大大提升效率
相关推荐
IT_陈寒9 小时前
Python开发者必知的5大性能陷阱:90%的人都踩过的坑!
前端·人工智能·后端
codingWhat9 小时前
介绍一个手势识别库——AlloyFinger
前端·javascript·vue.js
代码老中医9 小时前
2026年CSS彻底疯了:这6个新特性让我删掉了三分之一JS代码
前端
不会敲代码19 小时前
Zustand:轻量级状态管理,从入门到实践
前端·typescript
踩着两条虫9 小时前
VTJ.PRO 双向代码转换原理揭秘
前端·vue.js·人工智能
扉川川9 小时前
OpenClaw 架构解析:一个生产级 AI Agent 是如何设计的
前端·人工智能
远山枫谷9 小时前
一文理清页面/组件通信与 Store 全局状态管理
前端·微信小程序
codingWhat9 小时前
手撸一个「能打」的 React Table 组件
前端·javascript·react.js
HelloReader9 小时前
Tauri 应用安全从开发到发布的威胁防御指南
前端
bluceli9 小时前
WebAssembly实战指南:将高性能计算带入浏览器
前端·webassembly