前端面试题

1. js相关

问题1.1 js超过Number最大值该如何处理

在 JavaScript 中,Number 类型的最大安全整数值是 Number.MAX_SAFE_INTEGER,其值为 2^53 - 1(即 9007199254740991)。
在这里插入图片描述

  • 超过16位时,虽然数值能展示出来,但已经失去了精度。

Number.MAX_SAFE_INTEGER

  • Number.MAX_SAFE_INTEGER 表示 JavaScript 中最大的安全整数。
    值:9007199254740991(即 2^53 - 1)。
  • 在这个值以内的整数可以被精确表示。超过这个值的整数可能会失去精度,导致计算结果不准确。
    在这里插入图片描述

Number.MAX_VALUE

  • Number.MAX_VALUE 表示 JavaScript 中可以表示的最大正数。
    值:1.7976931348623157e+308(即 2^1024 - 1 的近似值)。
  • 这是 JavaScript 可以表示的最大正数。任何大于这个值的数将被视为 Infinity。
  • 主要用于检查一个数是否超出了 JavaScript 的最大表示范围。如果一个数大于 Number.MAX_VALUE,它会被自动转换为 Infinity。

js超过Number最大值的背景

  • 大数据计算
  • 前端表格输入

解决办法

bigInt

BigInt 是一种内置对象,用于表示任意精度的整数。你可以使用 BigInt 来处理超出 Number.MAX_SAFE_INTEGER 的整数
在这里插入图片描述

使用字符串

如果你不需要进行复杂的数学运算,可以将大数存储为字符串,并使用字符串操作来处理它们

使用第三方库

有一些第三方库专门用于处理大数运算,例如 bignumber.js 和 decimal.js。这些库提供了丰富的 API 来处理大数的加、减、乘、除等运算

bignumber.js
const BigNumber = require('bignumber.js')
let a = new BigNumber('12345678901234567')
let b = a.plus(1)
console.log("a:",a,"b:",b)
// a: BigNumber { s: 1, e: 16, c: [ 123, 45678901234567 ] } b: BigNumber { s: 1, e: 16, c: [ 123, 45678901234568 ] }
console.log("a:",a.toString(),"b:",b.toString())
// a: 12345678901234567 b: 12345678901234568
decimal.js
const Decimal = require('decimal.js')
let a = new Decimal('12345678901234567')
let b = a.plus(1)
console.log("a:",a,"b:",b)
// a: 12345678901234567 b: 12345678901234568

问题1.2 如何解决页面请求接口大规模并发问题

【前端】页面瞬间请求一百次,你怎么玩?

问题1.3 前端实现页面截图

背景

  • 列表查看
  • 导出为png
  • 设计类软件出图

方案

1. canvas 、第三方库html2canvas
// html2canvas示例
<template>
  <div class="home">
    home
        <router-view></router-view>
        <button type="primary" class="btn">按钮</button>
        <button @click="takeScreenShot">截图</button>
  </div>
</template>

<script>
import html2canvas from "html2canvas"
export default {
  methods: {
    takeScreenShot(){
      let dom = document.querySelector(".btn")
      html2canvas(dom).then(canvas=>{
        let img = canvas.toDataURL("image/png")
        console.log("img",img);
        
        const link = document.createElement('a')
        link.href = img
        link.download = "shot.png"
        link.click()
      })
    }
  } 
}
</script>
2. dom-to-image

dom-to-image库主要使用的是SVG实现方式,简单来说就是先把DOM转换为SVG然后再把SVG转换为图片

import domtoimage from "domtoimage"
const node = document.getElementById('node');
domtoimage.toPng(node,options).then((dataUrl) => {
    const img = new Image();
    img.src = dataUrl;
    document.body.appendChild(img);
})

toPng方法可传入两个参数node和options。

  • node为要生成截图的dom节点;
  • options为支持的属性配置,具体如下:filter,backgroundColor,width,height,style,quality,imagePlaceholder,cacheBus

问题1.4 移动端适配怎么解决

背景

项目想支持PC端、移动端

方案

  • 根据端来开发不同页面(成本最高)
  • 根据不同端加载不同css样式(可取)
  • 根据响应式,开运行不同的样式规则(常用

需考虑的问题

1. 设置视窗

通过meta(name=“viewport”)设置

<meta name="viewport" content="width=device-width, initial-scale=1.0">
  • name=“viewport”: 指定元标签的名称为 viewport,这是一个特殊的名称,用于定义视口(即用户可见的网页区域)。
  • content 属性:
    • width=device-width: 设置视口宽度等于设备的屏幕宽度。这意味着页面的宽度将根据设备的实际宽度进行调整,从而确保页面内容能够适配各种屏幕尺寸。
    • initial-scale=1.0: 设置初始缩放比例为 1.0,即页面加载时的默认缩放级别。这表示页面将以 1:1 的比例显示,不会有任何缩放。
    • minimum-scale: 设置最小缩放比例。
    • maximum-scale: 设置最大缩放比例。
    • user-scalable: 是否允许用户手动缩放页面(值为 yes 或 no)。
为什么需要设置 <meta name="viewport">

在移动设备上,如果没有设置 viewport 元标签,浏览器会假设页面是为桌面设备设计的,并尝试将整个页面内容压缩到屏幕上,导致字体和元素变得非常小,用户体验很差。通过设置 viewport,可以确保页面在不同设备上都能正确地缩放和布局。

2. 掌握媒体查询
body{
	font-size:16px;
}
//但是在某一些设备尺寸下,这个size是要更改的
//当我的尺寸大于Xx小于XXx的时候,需要什么样式?
@media (min-width:780px)and (max-width:1024px){
body {
	font-size:18px;
}
3. 弹性布局

Flex布局

  • 主轴方向:fex-direction
  • 对齐方式:justify-content、align-items
  • 弹性属性:flex
4.图片响应式
使用max-width/min-widthheight:auto实现

对于img元素,height: auto 通常与 max-width: 100% 结合使用,以确保图片在保持宽高比的同时,能够自适应容器的宽度(不超过容器宽度)

.img {
    max-width: 100%;
    height: auto;
}
使用<picture><source> 标签

HTML5 提供了 <picture> <source> 标签,允许你为不同的屏幕尺寸提供不同的图片源。这有助于优化加载时间和性能。

picture标签

<picture> 标签是 HTML5 中引入的一个元素,用于提供多种图像资源,以便浏览器可以根据设备的特性(如屏幕尺寸、分辨率等)选择最合适的图像。这有助于优化图像加载时间和提高用户体验。

  • 基本结构
    <picture> 标签通常包含一个或多个 <source> 元素和一个<img>元素。浏览器会根据 <source> 元素中的 media 属性来决定使用哪个图像源。如果所有<source>元素都不匹配,则会使用 <img> 元素中的 src 图像。
<picture>
	<source srcset="image-large.jpg"media="(min-width:800px)">
"srcset":Unknown word.
	<source srcset="image-medium.jpg"media="(min-width:400px)">
"srcset":Unknown word.
	<img src="image-small.jpg"alt="Responsive Image">
</picture>
  • 详细解释
    • <picture>: 包含一个或多个<source>元素和一个 <img> 元素。
    • <source>:
      • srcset: 指定图像文件的路径。
      • media: 一个媒体查询,用于指定该图像源适用的条件。如果媒体查询匹配,则使用该图像源。
      • type (可选): 指定图像的 MIME 类型,用于在支持特定格式的浏览器中选择图像。
    • <img>: 如果没有<source>元素匹配,则使用这个图像作为默认图像。
  • 注意事项
    • 确保提供默认图像:始终在<picture>标签中包含一个 <img> 元素,以确保在所有情况下都能显示图像。
    • 媒体查询的顺序:<source> 元素的顺序很重要,浏览器会从上到下依次检查每个 <source> 元素的 media 属性,直到找到匹配的为止。
    • 性能考虑:尽量减少 <source> 元素的数量,以避免过多的 HTTP 请求和计算开销
使用 srcset 属性

<img> 标签的 srcset 属性允许你指定多个图像资源及其对应的视口宽度。浏览器会根据当前视口宽度选择最合适的图像。

<picture>
  <source srcset="image-small.jpg" media="(max-width: 600px)">
  <source srcset="image-medium.jpg" media="(max-width: 1200px)">
  <source srcset="image-large.jpg" media="(min-width: 1201px)">
  <img src="image-default.jpg" alt="Responsive Image">
</picture>
  • srcset: 列出不同宽度的图像文件。
  • sizes: 定义不同视口宽度下的图像显示宽度
使用 CSS 背景图片

如果你需要将图片作为背景图片,并且希望它能够响应式地适应容器,可以使用 CSS 的 background-size 属性。

.responsive-bg {
  background-image: url('image.jpg');
  background-size: cover; /* 或者使用 contain */
  background-position: center;
  width: 100%;
  height: 300px; /* 设置一个固定的高度 */
}
  • cover: 使背景图片覆盖整个容器,可能会裁剪部分图片。
  • contain: 使背景图片完整显示在容器内,可能会留有空白区域
使用 Flexbox 或 Grid 布局

Flexbox 和 Grid 布局可以帮助你更好地控制图片和其他元素的布局,使其在不同屏幕尺寸下都能良好地适应

<div class="flex-container">
  <img src="image.jpg" alt="Responsive Image">
</div>

.flex-container {
  display: flex;
  justify-content: center;
  align-items: center;
  width: 100%;
  height: 300px;
}

.flex-container img {
  max-width: 100%;
  height: auto;
}
5. rem

rem单位的基础值有html的font-size决定;

html{
	font-size:16px;
}
/*体响应式改变基础font-size规则*/
@media (min-width:780px) and (max-width:1024px) {
html {
	font-size:18px;
	.header {
		font-size:1rem/*大家告诉我1rem此时等于多少*/
	}
}

总结

html
<DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<meta name="viewport"content="width=device-width,initial-scale=1.0">
	<title>Responsive Design Example</title>
	<link rel="stylesheet"href="styles.css">
</head>
<body>
	<header class="header">
		<h1>Responsive Page</h1>
	</header>
	<main class="container">
		<div class="card">Card 1</div>
		<div class="card">Card 2</div>
		<div class="card">Card 3</div>
		<div class="card">Card 4</div>
	</main>
</body>
</html>
.card {
	flex:1 1 100%;/*默认每行-个元素*/
	background-color:#fof0f0
	padding:20px;
	box-shadow:0 2px 5px rgba(0,0,0,0.1);
}
/*媒体查询*/
@media (min-width:600px){
.card {
	flex:1 1 45%;/*在较大屏幕上每行两个元素*/
}
@media (min-width:900px){
.card {
	flex:1 1 30%;/*在更大屏幕上每行三个元素*/
}

问题1.5 使用同一个链接,如何实现PC打开是wb应用、手机打开是一个H5应用

背景

老板为了省钱,一个链接访问页面,想同时适配 PC、Mobile。

思路

  • 先识别端
  • 端内容渲染器(内容加载器)

方法

方法1

js识别,通过navigator.userAgent识别出端,然后重定向到不同的链接

document.addEventListener('DOMContentLoaded', () => {
  const userAgent = navigator.userAgent || navigator.vendor || window.opera;

  // 检测移动设备
  const isMobile = /android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini/i.test(userAgent.toLowerCase());

  if (isMobile) {
    // 如果是移动设备,重定向到 H5 应用
    window.location.href = 'https://m.yourwebsite.com';
  } else {
    // 如果是 PC 设备,重定向到 Web 应用
    window.location.href = 'https://www.yourwebsite.com';
  }
});
方法2 移动端适配

具体做法同问题4

问题1.6 eb应用中如何对静态资源加载失败的场景做降级处理

场景

  • 图片
  • css文件
  • JavaScript文件
  • CDN
  • 字体文件
  • 服务端渲染失败

方案

图片
  • 一定要有占位图,要有alt属性来描述图片
<img src="image.jpg"alt="Example Image"onerror="handleImageError(this)">

function handleImageError(image){
image.onerror=null;//防止死循环
image.src='placeholder,jpg';/使用占位图
  • 重试机制(404、无权限)
  • 上报服务器,哪些资源加载失败
css文件处理

一般就是css资源没加载到

  • 关键性样式,通过内联
  • 备用样式
  • 上报
<head>
	<style>
	//体内联关键样式
	body {
		font-family:Arial,sans-serif;
	}
	</style>
	<link rel="stylesheet"href="styles.css"onerror="handleCssError()">
</head>


function handleCssError(){
//加载备用样式
const fallbackCss document.createElement('link');
fallbackCss.rel 'stylesheet';
fallbackCss.href 'fallback-styles.css';
document.head.appendChild(fallbackCss);
js文件处理

网络异常,导致加载失败

  • 内联脚本
  • 备用脚本处理
  • 上报
<head>
	<script>
	//内联关键脚本
	function basicFunctionality() {
		console.log('Basic functionality available.')
	}
	basicFunctionality()
	</script>
	<script src="main.js"onerror="handleJsError()"></script>
</head>
function handleJsError()
//加载备用脚本
	const fallbackScript = document.createElement('script');
	fallbackScript.src = 'fallback-main.js';
	document.head.appendChild(fallbackScript);
}
CDN资源加载失败
  • 本地备份,如果cdn出错了,就使用本地备份
  • 动态切换,切到另一个有用的cdn服务
<head>
	<script src="https://cdn.example.com/library.js"onerror="handleCdnError()"></script>
</head>
function handleCdnError(){
	//加载本地备份
	const fallbackScript = document createElement('script');
	fallbackScript.src = 'local-library.js';
	document.head.appendChild(fallbackScript);
	//或者动态切换到另一个CDN
	//const alternativeCdn = document.createElement('script');
	//alternativeCdn.src = 'https://cdn.alternative.com/library.js';
	//document.head.appendChild(alternativeCdn);
字体
  • 使用降级字体apple、微软雅黑(font-family 可以设置多个字体,前面加载失败,会自动使用后者)
  • webfont处理字体问题
@font-face {
	font-family:'CustomFont';
	src:url('customfont.woff2')format('woff2');
	font-display:swap;/*使用swap策略*/
}
body {
	font-family:'CustomFont',Arial,sans-serif;
}
ssr
  • 降级的html用作渲染
  • 切换为CSR

问题1.7 移动端上拉加载,下拉刷新实现方案

上拉加载

长列表,需要通过上拉加载提升性能

实现步骤
  • 滚动事件监听
  • 怎么判断触底
  • 回调触发列表加载更多
<body>
	<div id="list"></div>
	<script>
		const list = document.getELementById('list');
		let page = 1;
		function loadMoreData(page){
			return fetch(`https://example.com/api/data?page=${page}`)
			.then(response => response.json())
			.then(data => {
				data.items.forEach(item => {
					const div = document.createElement('div');
					div.className = 'item';
					div.textContent = item.text;
					list.appendChild(div);
				})
			})
		}
		
		function handleScroll() {
			if (list.scrollTop + list.clientHeight >= list.scrollHeight-10){
				page++;
				loadMoreData(page);
			}
		}
		
		list.addEventListener('scroll',handleScroll);
		//Initial load
		loadMoreData(page);
	</script>
<body>
dom.scrollTop

scrollLeft:对象的最左边到对象在当前窗口显示的范围内的左边的距离,即在出现了横向滚动条的情况下,滚动条拉动的距离。
scrollTop:对象的最顶部到对象在当前窗口显示的范围内的顶边的距离,即在出现了纵向滚动条的情况下,滚动条拉动的距离
在这里插入图片描述

dom.scrollHeight

scrollWidth和scrollHeight这两个属性用来获取指定元素内容层的真实宽度和高度.
当不存在水平或垂直滚动条时,scrollWidth和scrollHeight等于clientWidth和clientHeight,
当存在水平或垂直滚动条时(当内容层的高宽度超过指定元素的高宽度时),scrollWidth和scrollHeight还得在clientWidth和clientHeight的基础上加上内容层增加高度以及减去相应的滚动条宽度。
也可以这样理解,scrollWidth和scrollHeight即可视区域宽高度+被隐藏区域宽高度。
在这里插入图片描述

dom.clientHeight

clientHeight=height+顶部padding + 底部padding - 水平滚动条宽度。
在这里插入图片描述
具体区分,
在这里插入图片描述

判断拉下是否触底
// saveHeight:安全高度,即距离底部还有多高,即视为触底
dom.clientHeight + dom.scrollTop >= dom.crollHeight - saveHeight
scrollTo方法

scrollTo是浏览器对象window的一个方法,该方法的用途是滚动页面元素到指定位置。
语法形式为: scrollTo(xpos,ypos)

下拉刷新

<script>
	const list = document.getElementById('list');
	const refreshIndicator = document.getElementById('refreshIndicator');
	let startY = 0;
	let isPulling = false;
	function loadData() {
		return fetch('https://example.com/api/data')
		.then(response => response.json())
		.then(data => {
			List.innerHTML='';//清空现有数据
			data.items.forEach(item =>{
				const div = document.createElement('div');
				div.className 'item';
				div.textContent item.text;
				list.appendChild(div);
			})
			refreshIndicator.style.display='none';/隐藏刷惭指示器
		});
	}
	list.addEventListener('touchstart', (event)=>{
		if(list.scrollTop ===0){
			startY = event.touches[0].pageY;
			isPulling = true;
		}
	});
	list.addEventListener('touchmove',(event)=>{
		if (isPulling) {
			const currentY event.touches[0].pageY;
			if(currentY >Ytarty){
				refreshIndicatorstyle.display = 'block';
				refreshIndicator.style.height =`${currentY startY}px`;
			}
		}
	});
	list.addEventListener('touchend',()=> {
		if(isPulling) {
			const refreshHeight = parseInt(refreshIndicator.style.height,10);
			if (refreshHeigh > 50){
				LoadData(};
			}else{
				refreshIndicator.style.display = 'none';
				isPulling false;
				refreshIndicator.style.height = '50px';
			}
		})
		//Initial load
		loadData();
</script>

其他仍需要考虑的点

性能优化
  • 节流、防抖
  • 懒加载
用户体验
  • 视觉反馈,下拉刷新的指示器
  • 平滑动画
  • 错误处理
兼容
  • 触摸事件
  • css

问题1.7 window对象上频繁绑定内容,有什么风险

风险分析

  • 命名冲突,window.hello=‘heyi’
  • 全局污染
  • 安全风险,window.hello
  • 性能问题,增加内存开销

解决方案

  • 模块化
  • 命令空间
  • IIFE(形成闭包,形成独立作用域)
  • 开启严格模式

1.8 多个标签页之间同步数据,并实现跨标签页的通信

window 对象的 storage 事件用于监听在同一个源(即相同的协议、域名和端口)下的其他文档对 localStorage 或 sessionStorage 的修改。当一个文档修改了存储数据时,所有其他打开的同一源的文档都会收到这个事件。

通过这种方式,你可以在多个标签页之间同步数据,并实现跨标签页的通信。这对于一些需要多标签页协同工作的应用非常有用,例如在线协作工具或实时聊天应用。

注意事项:必须是同一域名(同源)下的不同页面

++ StorageEvent 对象
当 storage 事件被触发时,会传递一个 StorageEvent 对象作为参数。该对象包含以下属性:

  • key:发生变化的键名。
  • oldValue:键值改变之前的值。如果这个键是新添加的,则为 null。
  • newValue:键值改变之后的新值。如果这个键被删除了,则为 null。
  • url:触发存储变更的文档的 URL。
  • storageArea:指向存储区域(localStorage 或 sessionStorage)的一个引用。

示例:

<!--1.html-->
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Page 1</title>
</head>
<body>
  <h1>Page 1</h1>
  <button id="set-storage">Set Storage</button>
  <div id="message"></div>

  <script>
    // 监听 storage 事件
    window.addEventListener('storage', function(event) {
      if (event.key === 'sharedData') {
        document.getElementById('message').textContent = `Data changed: ${event.newValue}`;
      }
    });

    // 设置 localStorage 数据
    document.getElementById('set-storage').addEventListener('click', function() {
      localStorage.setItem('sharedData', 'Hello from Page 1');
    });
  </script>
</body>
</html>
<!--2.html-->
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Page 2</title>
</head>
<body>
  <h1>Page 2</h1>
  <button id="set-storage">Set Storage</button>
  <div id="message"></div>

  <script>
    // 监听 storage 事件
    window.addEventListener('storage', function(event) {
      if (event.key === 'sharedData') {
        document.getElementById('message').textContent = `Data changed: ${event.newValue}`;
      }
    });

    // 设置 localStorage 数据
    document.getElementById('set-storage').addEventListener('click', function() {
      localStorage.setItem('sharedData', 'Hello from Page 2');
    });
  </script>
</body>
</html>

1.9 Set和Map

Set和Map是ES6新增的数据类型

Set

set类似数组,但内部元素唯一不重复
set构造函数的参数: 参数必须是iterator接口的数据类型, 否则会抛出异常.
创建set: new Set()
常用方法/属性:

  • add(): 末尾添加元素
  • has(): 判断set中是否有对应的数据
  • delete(): 删除
  • clear(): 清空
  • size: set元素数量
    作用:
  • 数组去重
arr = [...new Set(arr)]

Map

存储多个键值对数据,键不可重复
创建: new Map()
属性和方法:

  • size: map元素个数
  • set(键, 值): 修改/新增元素
  • get(键): 获取对应键的值
  • has(键): 某个键是否存在
  • delete(键): 删除指定的键
  • clear(): 清空

Map和Object的区别

在 JavaScript 中,Map 和 Object 都可以用来存储键值对,但它们在设计和使用上有一些重要的区别。

下面是 Map 和 Object 的主要区别:

  • 键的类型
    • Object:
      键必须是字符串或符号(Symbol)。如果你使用其他类型的键(如数字、对象等),它们会被自动转换为字符串。
    • Map:
      • 键可以是任何类型,包括对象、函数、基本类型等。
      • 键不会被转换为字符串,保持其原始类型。在添加键值对时,会通过严格相等 === 来判断键属性是否已经存在( 特例:NaN === NaN 返回 false )
  • 迭代顺序
    • Object:
      • 在 ES6 之前,对象的属性迭代顺序是不确定的。
      • 从 ES6 开始,对象的属性迭代顺序是按照插入顺序进行的,但只有当所有属性都是字符串时才保证按插入顺序:
        • 对于大于等于0的整数,会按照大小的顺序进行排序,对于小数和负数会当做字符串处理
        • 对于string类型,按照插入的顺序进行输出
        • 对于Symbol类型,会直接过滤掉,不会进行输出,如果想要输出Symbol类型属性,通过Object.getOwnPropertySymbols()方法
    • Map:
      迭代顺序总是按照键值对的插入顺序。
  • 大小
    • Object:
      没有直接的方法来获取对象中键值对的数量。只能手动计算,通过Object.keys()方法或者通过for…in循环统计。
    • Map:
      提供了 size 属性,可以直接获取键值对的数量。
  • 方法
    • Object:
      • 主要通过点符号 (.) 或方括号 ([]) 来访问和设置属性。
      • 可以使用 Object.keys(), Object.values(), Object.entries() 等方法来获取键、值和键值对。
    • Map:
    • 提供了一系列方法来操作键值对,例如:
      set(key, value): 设置键值对。
      get(key): 获取键对应的值。
      has(key): 检查是否存在某个键。
      delete(key): 删除某个键值对。
      clear(): 清空所有键值对。
      forEach(callback, thisArg): 遍历所有的键值对。
      keys(), values(), entries(): 返回迭代器,分别用于遍历键、值和键值对。
  • JSON序列化
    • Object: Object类型可以通过JSON.stringify()进行序列化操作
    • Map: Map结构不能直接进行JSON序列化
  • 性能
    • Object:
      • 对于少量的键值对,对象通常具有较好的性能。
      • 当对象非常大时,性能可能会有所下降。
    • Map:
      • 在某些情况下,Map 的性能优于对象,特别是在频繁添加和删除键值对的情况下。
      • Map 是专门设计来处理大量数据的,因此在大数据集上的性能更好。
  • 使用场景
    • Object
      • 仅做数据存储,并且属性仅为字符串或者Symbol类型
      • 需要进行序列化转换为json传输时
      • 当做一个对象的实例,需要保留自己的属性和方法时
    • Map
      • 会频繁更新和删除键值对时
      • 存储大量数据时,尤其时key类型未知的情况下
      • 需要频繁进行迭代处理

1.10 js 循环机制

  • 执行同步代码。
  • 执行一个宏任务(执行栈中没有就从任务队列中获取)。
  • 执行过程中如果遇到微任务,就将它添加到微任务的任务队列中。
  • 宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行)。
  • 当前宏任务执行完毕,开始检查渣染,然后渣染线程接管进行渣染。
  • 渣染完毕后,JavaScript线程继续接管,开始下一个循环。
    在这里插入图片描述

2. vue相关

2.1 数据双向绑定的原理

概念描述

Vue.js 的数据双向绑定是其核心特性之一,它使得数据模型和视图能够自动保持同步。当数据发生变化时,视图会自动更新;反之,当用户在视图上进行操作时,数据也会自动更新
vue中通过v-model指令实现数据双向绑定,这个其实是个语法糖,相当于v-bind和@update结合。

实现原理

vue类中,构造函数的参数包含根节点id(el),数据对象(data)

首次渲染
  • 初始化数据
    • 将数据对象放到this.$data中。
new Vue({
  el: '#app',
  data: {
    message: 'Hello, Vue!'
  }
});
  • 数据劫持
    • 定义数据劫持Object.defineProperty方法,该方法中对this.$data遍历所有下标,为每一个属性设置setter和getter方法。getter中获取属性的所有依赖。setter中将每一次set通知到所有相关依赖。
    • 为了解决新定义的属性没有setter和getter方法,在数据劫持环节中的setter中在调用数据劫持方法,给新增的属性设置setter和getter方法
function observe(data) {
  if (!data || typeof data !== 'object') {
    return;
  }

  // 获取对象的所有键
  Object.keys(data).forEach(key => {
    defineReactive(data, key, data[key]);
  });
}

function defineReactive(obj, key, val) {
  observe(val); // 递归处理嵌套对象

  const dep = new Dep(); // 创建依赖收集器

  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
      // 收集依赖
      if (Dep.target) {
        dep.depend();
      }
      return val;
    },
    set: function reactiveSetter(newVal) {
      if (newVal === val) {
        return;
      }
      val = newVal;
      observe(newVal); // 递归处理新值
      dep.notify(); // 通知所有依赖更新
    }
  });
}
  • 代理属性劫持
    • 把所有的数据,以属性的形式绑定到vm实例上
    • 为了让用户可以直接通过 vm 实例访问 data 中的数据,Vue 会将 data 中的属性代理到 vm 实例上。
    Object.keys(this.$data).forEach(key=> {
    	Object.defineProperty(this, key, {
    		enumerble: true,
    		configutable: true,
    		get(){
    			return this,$data[key]
    		},
    		set(newVal){
    			this.$data[key] = newVal
    		}
    	})
    })
    
  • 模板编译
    Vue 会遍历 DOM 树,根据节点类型(文本节点),提取出模板中的插值表达式(如 {{ message }}), 提取{{}}内部的文本,替换插值中的文本。
function compile(node, vm) {
  if (node.nodeType === 1) { // 元素节点
    const attrs = node.attributes;
    for (let i = 0; i < attrs.length; i++) {
      const attr = attrs[i];
      if (attr.name === 'v-model') {
        handleVModel(node, vm, attr.value);
      }
    }
  } else if (node.nodeType === 3) { // 文本节点
    handleText(node, vm);
  }

  // 递归处理子节点
  const children = node.childNodes;
  for (let i = 0; i < children.length; i++) {
    compile(children[i], vm);
  }
}

function handleText(node, vm) {
  const content = node.textContent;
  if (content.includes('{{')) {
    const exp = content.replace(/\{\{(.+?)\}\}/g, (...args) => {
      const key = args[1].trim();
      new Watcher(vm, key, (newVal) => {
        node.textContent = content.replace(`{{${key}}}`, newVal);
      });
      return vm[key];
    });
    node.textContent = exp;
  }
}

function handleVModel(node, vm, exp) {
  const value = vm[exp];
  new Watcher(vm, exp, (newVal) => {
    node.value = newVal;
  });

  node.addEventListener('input', (e) => {
    vm[exp] = e.target.value;
  });

  node.value = value;
}
data数据变化
  • 当数据变化时,setter方法会通过依赖收集Dep中的notify方法,通知所有相关依赖watcher,watcher更新DOM(模板编译)
class Dep {
  constructor() {
    this.subs = [];
  }

  addSub(sub) {
    this.subs.push(sub);
  }

  depend() {
    if (Dep.target) {
      this.addSub(Dep.target);
    }
  }

  notify() {
    this.subs.forEach(sub => sub.update());
  }
}

Dep.target = null;

class Watcher {
  constructor(vm, key, cb) {
    this.vm = vm;
    this.key = key;
    this.cb = cb;
    this.value = this.get(); // 初始化时收集依赖
  }

  get() {
    Dep.target = this;
    const value = this.vm[this.key];
    Dep.target = null;
    return value;
  }

  update() {
    this.cb(this.vm[this.key]);
  }
}
页面中input数据发生变化
  • 捕获输入框的v-model中的value
  • 监听input元素的input输入,当输入框的值发生变化时,会触发 input 事件,更新 data 中的数据,并通过 watcher 机制更新视图。
// 在 handleVModel 函数中已经实现了这一点
function handleVModel(node, vm, exp) {
  const value = vm[exp];
  new Watcher(vm, exp, (newVal) => {
    node.value = newVal;
  });

  node.addEventListener('input', (e) => {
    vm[exp] = e.target.value;
  });

  node.value = value;
}

2.2 数据发生变化,但试图未更新

1. 如果你添加了一个新属性到已经创建的实例上,并且这个新属性不是响应式的,那么视图将不会更新。

举例:对data中的info对象添加新属性sex,点击按钮发现data更新了,但视图并未更新:

<template>
  <div class="home">
    <div v-for="(item, key, index) in info" :key="index">
      {{ key + ":" + item }}
    </div>
    <div v-for="(item, index) in list" :key="index">
      {{ item }}
    </div>
    <button @click="add">增加</button>
  </div>
</template>

<script>
export default {
  name: "HomeView",
  data() {
    return {
      info: {
        name: "张三",
        age: 18,
      },
      list: [1, 23, 4, 5],
    };
  },
  methods: {
    add() {
    //info对象新增属性sex
      this.info.sex = "男";
    },
  },
};
</script>

在这里插入图片描述
解决办法:

  • this.$set(this.someObject, ‘newProperty’, value) 来确保新属性是响应式的
  • this.$forceUpdate()用于强制组件重新渲染
  • 直接对info对象重新赋值,这种不常用,只是为了说明vue可以对对象本身、已有属性的变化进行响应式处理,但对象属性的新增和删除响应式。
//this.$set(this.someObject, 'newProperty', value) 来确保新属性是响应式的
methods: {
    add() {
    //info对象新增属性sex
      this.$set(this.info, "sex", "男")
    },
  },
  
  //this.$forceUpdate()强制组件重新渲染
  methods: {
    add() {
    //info对象新增属性sex
     this.info.sex = "男";
     this.$forceUpdate()
    },
  },

2. 通过以下方式修改数组的,无法被响应式检测到:

  • 当你利用索引直接设置一个项时,例如:vm.items[indexOfItem] = newValue
  • 当你修改数组的长度时,例如:vm.items.length = newLength

解决办法:

  • 对于上述变更,Vue 提供了一些方法来修改数组,这些方法会确保视图得到更新:
push()
pop()
shift()
unshift()
splice()
sort()
  • 当然通过this.$forceUpdate()依然可以解决

3. 如果把上述问题代码写到了mounted中,而不是按钮回调函数中,也会出现上述问题

解决办法

  • 按照上面的方法可以解决
  • 把代码写到created、beforeMount中也能解决,原因是此时视图还没渲染

4. Vue 异步执行 DOM 更新:这意味着,当你修改数据后,视图不会立即更新。如果你需要等待视图更新后再执行某些操作,可以使用 Vue 的 this.$nextTick() 方法。

<template>
  <div class="home">
    <div class="city">{{ city }}</div>
    <button @click="changeCity">变更city</button>
  </div>
</template>

<script>
export default {
  name: "HomeView",
  data() {
    return {
      city: "北京",
    };
  },
  methods: {
    changeCity() {
    //修改完city后,data变更了,但是视图变更是异步的,如果立马要获取元素上的信息,得到的还是原来的。
      this.city = "郑州";
      let oCity = document.querySelector(".city");
      let cityContext = oCity.textContent;
      console.log(1, this.city, cityContext);
    },
  },
};

在这里插入图片描述
解决办法:

  • 使用this.$nextTick
    • this.$nextTick 将回调延迟到下次DOM更新循环之后执行。在修改数据之后立即使用它,然后等待DOM更新。
    • 通俗的讲,this.$nextTick中的回调函数,会在本轮数据变更导致的DOM更新结束后,再执行,因此可以获得重新渲染后的新内容。
methods: {
    changeCity() {
      this.city = "郑州";
      let oCity = document.querySelector(".city");
      this.$nextTick(() => {
        let cityContext = oCity.textContent;
        console.log(1, this.city, cityContext);
      });
    },
  },

css相关

3.1 BFC

前端BFC
BFC只是解决margin合并,浮动造成的一些问题,但不是唯一解决办法:
比如margin合并,通过给父元素设置border/padding也能解决;
比如浮动带来的布局问题,还可以通过clear,:after之类的解决。

3.2 重绘和重排

浏览器渲染流程
  • 拿到html代码后解析生成dom树,拿到css文件后解析生成css树
  • DOM树和CSSOM结合在一起,排除了不可见元素(如带有display: none;的元素)后,形成渲染树(Render Tree),这个过程比较耗费性能
  • 根据渲染树,浏览器计算每个节点在屏幕上的确切位置和大小。
    这个过程也称为“布局”或“重排”,它决定了每个元素的几何形状,比如宽度、高度以及相对于其他元素的位置
  • 一旦布局完成,浏览器就开始绘制每个节点,将其转换成实际的像素,这个过程也称为“绘制”或“重绘”。绘制阶段包括填充颜色、绘制边框、文本等内容
  • 页面显示
重排(Reflow)

重排:当一个元素的位置、尺寸等发生改变的时候,浏览器需要重新计算该元素的几何属性并且摆放到正确的位置的过程叫做重排,也叫回流。

重排是一个非常昂贵的操作,因为它可能会影响整个文档树,并且通常会引发后续的重绘

const element = document.getElementById('example');
element.style.width = '200px'; // 改变宽度,触发重排

以下是一些常见的触发重排的操作:

  • 改变窗口大小:调整浏览器窗口的大小。
  • 改变字体大小:修改 font-size 属性。
  • 添加或删除可见的 DOM 元素:插入或移除元素。
  • 内容变化:例如文本内容的变化(比如背景色大小、文本内容增多导致的大小变更)。
  • 激活 CSS 伪类:如 :hover。
  • 查询某些属性:例如 offsetTop、offsetLeft、getComputedStyle 等。
重绘(Repaint)

重绘是指浏览器在不改变布局的情况下,更新页面上的元素的外观,例如改变颜色、背景、边框等。重绘不涉及改变元素的位置或大小,仅仅是重新绘制元素的外观。相对于重排,重绘是一项较为轻量的操作

重排一定会触发重绘,而重绘可以单独发生,不一定伴随重排。

const element = document.getElementById('example');
element.style.width = '200px'; // 改变宽度,触发重排

以下是一些常见的触发重绘的操作:

  • 改变颜色:修改 color 或 background-color。
  • 改变透明度:修改 opacity。
  • 改变背景图片:修改 background-image。
  • 改变 visibility 属性:显示或隐藏元素。
减少重排重绘
  • 通过display: none,将元素设置为不可见,处理完样式等后,再恢复可见,避免中途每一次修改都会触发重排重绘。
  • 使用类名批量修改样式:
    • 将要进行样式更改的元素添加/删除CSS类,而不是直接操作样式属性,可以将多个更改合并到一次重排和重绘中
  • 批量修改样式:
    • 尽量将多个样式修改合并到一个操作中,而不是多次单独修改。
    • 使用 文档片段requestAnimationFrame 来批量处理样式修改。如果需要频繁地向页面中添加大量DOM节点,可以先将他们添加到文档片段中,最后再统一插入文档中,这样可以减少回流的次数。
const element = document.getElementById('example');
requestAnimationFrame(() => {
	element.style.width = "200px"
});
  • 尽量缩小处理的元素范围,能精确到具体元素的,就尽量不要再其父元素上进行处理
  • 避免频繁查询布局信息:某些CSS属性会触发同步布局,例如 offsetWidth 和 getBoundingClientRect。尽量避免在循环中使用这些属性。
  • 使用CSS Grid和Flex布局:使用Grid和Flex布局可以减少对元素位置的频繁调整,从而减少重排
  • 使用 transform 和 opacity:
    • 使用 transform 和 opacity 进行动画效果,因为它们不会触发重排。
    • 例如,使用 translate 而不是 left 和 top 来移动元素。
  • 避免复杂的 CSS 选择器:
    • 使用简单的 CSS 选择器,避免复杂的嵌套选择器,以减少样式计算的时间。
  • 虚拟化长列表:
    • 对于长列表,使用虚拟滚动技术(如 vue-virtual-scroller)来只渲染可见的部分,减少 DOM 元素的数量。
  • CSS 动画:
    • 使用 CSS 动画而不是 JavaScript 动画,因为 CSS 动画由浏览器的合成线程处理,更高效。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值