
promise+async/await+任务队列
promise+事件队列
·
文章目录
Promise 理解应用
- Promise概念
- Promise是异步编程的一种解决方案,它代表了一个异步操作的最终完成或者失败及其结果值。简单来说,它是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。一个Promise对象有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。状态的改变是单向的,一旦从pending变为fulfilled或者rejected,就不会再改变。
- Promise参数
- 构造函数参数:Promise构造函数接受一个函数作为参数,这个函数有两个参数,分别是
resolve
和reject
。resolve
函数用于将Promise的状态从pending
转换为fulfilled
,并传递成功的值。reject
函数用于将Promise的状态从pending
转换为rejected
,并传递失败的原因。例如:
const myPromise = new Promise((resolve, reject) => { // 模拟异步操作,例如一个定时器 setTimeout(() => { const success = true; if (success) { resolve('操作成功'); } else { reject('操作失败'); } }, 1000); });
- then和catch方法参数:
then
方法用于处理Promise成功的情况,它接受一个函数作为参数,这个函数会在Promise状态变为fulfilled
时被调用,并且传入resolve
传递的值。catch
方法用于处理Promise失败的情况,它接受一个函数作为参数,这个函数会在Promise状态变为rejected
时被调用,并且传入reject
传递的错误信息。例如:
myPromise.then((result) => { console.log(result); }).catch((error) => { console.log(error); });
- 构造函数参数:Promise构造函数接受一个函数作为参数,这个函数有两个参数,分别是
- Promise应用场景
- 网络请求:在前端进行Ajax请求或者在后端发送HTTP请求时,使用Promise可以方便地处理请求的成功和失败情况。例如,使用
fetch
API进行网络请求时,它返回的就是一个Promise对象。
fetch('https://example.com/api/data') .then((response) => response.json()) .then((data) => console.log(data)) .catch((error) => console.log('请求出错:', error));
- 文件读取(在Node.js等环境中):读取文件是一个异步操作,通过将文件读取操作封装在Promise中,可以更好地处理读取成功和失败的情况。
const fs = require('fs').promises; fs.readFile('example.txt', 'utf8') .then((data) => console.log(data)) .catch((error) => console.log('文件读取失败:', error));
- 网络请求:在前端进行Ajax请求或者在后端发送HTTP请求时,使用Promise可以方便地处理请求的成功和失败情况。例如,使用
- Promise优缺点
- 优点
- 可读性好:相比于传统的回调函数嵌套(回调地狱),Promise通过链式调用
then
和catch
方法,使异步代码的结构更加清晰,易于理解和维护。例如,对比以下回调地狱代码和Promise代码: - 回调地狱代码
setTimeout(() => { console.log('第一步'); setTimeout(() => { console.log('第二步'); setTimeout(() => { console.log('第三步'); }, 1000); }, 1000); }, 1000);
- Promise代码
const step1 = new Promise((resolve) => { setTimeout(() => { console.log('第一步'); resolve(); }, 1000); }); const step2 = step1.then(() => { return new Promise((resolve) => { setTimeout(() => { console.log('第二步'); resolve(); }, 1000); }); }); const step3 = step2.then(() => { return new Promise((resolve) => { setTimeout(() => { console.log('第三步'); resolve(); }, 1000); }); }); step3;
- 错误处理方便:可以在链的末尾使用
catch
方法统一处理所有的错误,而不是在每个回调函数中都进行错误处理。
- 可读性好:相比于传统的回调函数嵌套(回调地狱),Promise通过链式调用
- 缺点
- 代码仍然有一定复杂度:虽然相比回调地狱已经有了很大的改进,但是对于复杂的异步操作,Promise链可能会变得很长,仍然有一定的理解成本。
- 无法取消Promise:一旦创建并开始执行一个Promise,就无法取消它。
- 优点
- Promise解决的问题
- 主要解决了“回调地狱”的问题,让异步代码的书写更加符合线性思维,提高了代码的可读性和可维护性。同时,它提供了统一的错误处理机制,使得在异步操作中处理错误更加方便。
Promise all、race 实例
-
Promise.all
方法应用实例- 定义和用途:
Promise.all
方法接收一个可迭代对象(通常是一个数组),其中包含多个Promise
实例。它返回一个新的Promise
,这个新Promise
会在所有传入的Promise
都成功完成(resolved
)时才成功完成,并且它的结果是一个包含所有传入Promise
结果的数组。如果其中任何一个Promise
被拒绝(rejected
),那么整个Promise.all
返回的Promise
就会立即被拒绝,并且返回第一个被拒绝的Promise
的原因。
- 示例场景:同时获取多个API数据
- 假设我们有三个API端点,分别用于获取用户信息、产品信息和订单信息。我们可以使用
Promise.all
来同时发送请求并等待所有数据返回。
function getUserInfo() { return new Promise((resolve, reject) => { // 模拟一个API请求,1秒后返回用户信息 setTimeout(() => { const userInfo = {name: "John", age: 30}; resolve(userInfo); }, 1000); }); } function getProductInfo() { return new Promise((resolve, reject) => { // 模拟一个API请求,1.5秒后返回产品信息 setTimeout(() => { const productInfo = {name: "Product A", price: 19.99}; resolve(productInfo); }, 1500); }); } function getOrderInfo() { return new Promise((resolve, reject) => { // 模拟一个API请求,0.8秒后返回订单信息 setTimeout(() => { const orderInfo = {orderId: "12345", status: "pending"}; resolve(orderInfo); }, 800); }); } const promises = [getUserInfo(), getProductInfo(), getOrderInfo()]; Promise.all(promises) .then((results) => { console.log("All data fetched successfully"); const [userInfo, productInfo, orderInfo] = results; console.log("User Info:", userInfo); console.log("Product Info:", productInfo); console.log("Order Info:", orderInfo); }) .catch((error) => { console.error("Error fetching data:", error); });
- 解释:
- 首先定义了三个函数
getUserInfo
、getProductInfo
和getOrderInfo
,它们都返回一个Promise
,在Promise
的resolve
回调中模拟从API获取数据并返回。 - 然后将这三个
Promise
放入一个数组promises
中,调用Promise.all
并传入这个数组。 - 当所有的
Promise
都成功完成(即所有的API数据都获取成功)时,Promise.all
返回的Promise
的then
方法被调用,并且传入的results
参数是一个包含三个API数据结果的数组。我们可以使用数组解构来获取每个数据并进行处理。 - 如果其中任何一个
Promise
被拒绝(例如API请求出错),Promise.all
返回的Promise
的catch
方法会被调用,并且传入第一个被拒绝的Promise
的错误信息。
- 首先定义了三个函数
- 假设我们有三个API端点,分别用于获取用户信息、产品信息和订单信息。我们可以使用
- 定义和用途:
-
Promise.race
方法应用实例- 定义和用途:
Promise.race
方法同样接收一个可迭代对象(通常是一个数组),其中包含多个Promise
实例。它返回一个新的Promise
,这个新Promise
会在可迭代对象中的任意一个Promise
完成(resolved
或者rejected
)时就完成,并且它的结果就是这个最先完成的Promise
的结果。
- 示例场景:加载资源的竞争
- 假设我们有两种方式加载同一张图片,一种是从本地缓存加载,另一种是从网络服务器加载。我们可以使用
Promise.race
来看看哪种方式先完成。
function loadFromCache() { return new Promise((resolve, reject) => { const cachedImage = localStorage.getItem("imageData"); if (cachedImage) { resolve(cachedImage); } else { reject("Image not found in cache"); } }); } function loadFromServer() { return new Promise((resolve, reject) => { const imageUrl = "https://example.com/image.jpg"; const xhr = new XMLHttpRequest(); xhr.open("GET", imageUrl); xhr.onload = function() { if (xhr.status === 200) { resolve(xhr.responseText); localStorage.setItem("imageData", xhr.responseText); } else { reject("Error loading image from server"); } }; xhr.onerror = function() { reject("Network error"); }; xhr.send(); }); } const loadingPromises = [loadFromCache(), loadFromServer()]; Promise.race(loadingPromises) .then((result) => { console.log("Image loaded successfully:", result); }) .catch((error) => { console.error("Error loading image:", error); });
- 解释:
- 首先定义了两个函数
loadFromCache
和loadFromServer
,它们都返回一个Promise
。loadFromCache
尝试从本地存储中获取图片数据,如果有则resolve
,否则reject
。loadFromServer
通过XMLHttpRequest
从网络服务器加载图片数据,如果加载成功则resolve
并且将数据存储到本地缓存中,否则reject
。 - 然后将这两个
Promise
放入一个数组loadingPromises
中,调用Promise.race
并传入这个数组。 - 当
loadingPromises
中的任意一个Promise
完成时,Promise.race
返回的Promise
就会完成。如果是成功完成,then
方法被调用并传入最先完成的Promise
的结果(也就是加载成功的图片数据)。如果是被拒绝完成,catch
方法被调用并传入最先出现的错误信息。
- 首先定义了两个函数
- 假设我们有两种方式加载同一张图片,一种是从本地缓存加载,另一种是从网络服务器加载。我们可以使用
- 定义和用途:
Async/Await 应用
- Async/Await概念
async/await
是一种基于Promise的更高级的异步编程语法糖。async
函数是一个异步函数,它总是返回一个Promise对象。在async
函数内部,可以使用await
关键字来暂停函数的执行,等待一个Promise被解决(fulfilled)或者被拒绝(rejected)。await
只能在async
函数内部使用。
- Async/Await参数
- async函数参数:
async
函数的参数和普通函数一样,可以接受任意数量和类型的参数,这些参数在函数内部可以正常使用。例如:
async function getData(id) { // 这里的id就是传入的参数 // 可以在函数内部使用这个参数进行异步操作 }
- await表达式参数:
await
后面跟着一个Promise对象或者一个返回Promise对象的表达式。它会暂停async
函数的执行,直到这个Promise被解决或者被拒绝。例如:
async function getData() { const response = await fetch('https://example.com/api/data'); const data = await response.json(); return data; }
- async函数参数:
- Async/Await应用场景
- 几乎所有适合使用Promise的场景都适合使用
async/await
,特别是在需要进行一系列异步操作并且需要按照顺序执行的情况下,async/await
更加方便。例如,在一个需要先登录获取令牌,然后使用令牌获取用户信息的场景中:
async function getUserInfo() { const loginResponse = await login(); const token = loginResponse.token; const userInfoResponse = await getUserInfoWithToken(token); return userInfoResponse; }
- 几乎所有适合使用Promise的场景都适合使用
- Async/Await优缺点
- 优点
- 代码更加简洁直观:
async/await
让异步代码看起来更像同步代码,使得异步操作的流程更加清晰,易于理解和编写。例如,对比Promise链和async/await
代码: - Promise链代码
getData() .then((data1) => { return processData1(data1); }) .then((data2) => { return processData2(data2); }) .then((result) => { console.log(result); }) .catch((error) => { console.log('出错:', error); });
- Async/Await代码
async function main() { try { const data1 = await getData(); const data2 = await processData1(data1); const result = await processData2(data2); console.log(result); } catch (error) { console.log('出错:', error); } } main();
- 错误处理方便:可以使用
try/catch
语句来捕获await
表达式中的错误,这和同步代码中的错误处理方式很相似,更加符合开发者的习惯。
- 代码更加简洁直观:
- 缺点
- 可能导致性能问题(如果滥用):如果在
async
函数中大量使用await
,并且这些异步操作可以并发执行,但是却按照顺序等待每个操作完成,可能会导致性能下降。例如,多个网络请求本可以并发进行,但是使用await
逐个等待就会浪费时间。
- 可能导致性能问题(如果滥用):如果在
- 优点
- Async/Await解决的问题
- 它进一步简化了异步代码的书写,让异步操作在代码结构上更接近同步操作,降低了异步编程的难度。同时,它利用
try/catch
提供了更自然的错误处理方式,使得开发者能够更方便地处理异步操作中的错误。
- 它进一步简化了异步代码的书写,让异步操作在代码结构上更接近同步操作,降低了异步编程的难度。同时,它利用
promise,async/await 在事件队列中应用
- Promise和Async/Await在事件队列中的角色
- Promise:当一个Promise被创建时,它的异步操作(例如定时器、网络请求等)会被放入任务队列(宏任务队列或者微任务队列,具体取决于操作类型)。在JavaScript的事件循环中,一旦主线程空闲,就会从任务队列中取出任务执行。当Promise的状态从
pending
变为fulfilled
或者rejected
时,then
和catch
方法中的回调函数会被放入微任务队列。这意味着它们会在当前宏任务执行完后,下一个宏任务开始前被执行。例如,一个setTimeout
(宏任务)中的Promise的then
方法(微任务):
console.log('start'); setTimeout(() => { const myPromise = new Promise((resolve) => { resolve('Promise resolved'); }); myPromise.then((result) => { console.log(result); }); console.log('setTimeout inner'); }, 0); console.log('end');
- Async/Await:
async
函数本身返回一个Promise。当在async
函数内部遇到await
时,它会暂停函数的执行,将后续代码(实际上是一个隐式的Promise
)放入任务队列。如果await
等待的是一个已经解决(fulfilled
)的Promise,那么函数会继续执行;如果是等待一个未解决的Promise,函数会暂停,直到该Promise被解决。并且await
后面的表达式(如果是一个Promise)的then
方法中的回调函数会被当作微任务处理。例如:
async function asyncFunc() { console.log('async function start'); const result = await new Promise((resolve) => { setTimeout(() => { resolve('Promise resolved'); }, 0); }); console.log(result); console.log('async function end'); } console.log('main start'); asyncFunc(); console.log('main end');
- Promise:当一个Promise被创建时,它的异步操作(例如定时器、网络请求等)会被放入任务队列(宏任务队列或者微任务队列,具体取决于操作类型)。在JavaScript的事件循环中,一旦主线程空闲,就会从任务队列中取出任务执行。当Promise的状态从
- 事件队列的机制
- 宏任务和微任务:JavaScript的事件队列分为宏任务队列和微任务队列。宏任务包括
script
(整体代码)、setTimeout
、setInterval
、I/O
操作、postMessage
、MessageChannel
等。微任务包括Promise.then
、MutationObserver
、process.nextTick
(在Node.js环境)等。当一个宏任务执行时,在执行过程中产生的微任务会被添加到微任务队列中。当当前宏任务执行完毕后,会先清空微任务队列,然后再执行下一个宏任务。 - 事件循环流程:
- 首先执行全局
script
代码,这是第一个宏任务。 - 在执行宏任务过程中,遇到微任务,将微任务添加到微任务队列。
- 宏任务执行完毕后,检查微任务队列,将微任务队列中的所有任务依次执行。
- 不断重复上述过程,从宏任务队列中取出下一个宏任务执行,在执行过程中处理微任务。
- 首先执行全局
- 宏任务和微任务:JavaScript的事件队列分为宏任务队列和微任务队列。宏任务包括
- 事件队列注意要点
- 宏任务和微任务的执行顺序:一定要清楚微任务是在当前宏任务执行完后就立即执行,而不是等到下一个宏任务周期。这可能会导致一些意外的结果,例如在
setTimeout
(宏任务)回调函数中,如果有Promise.then
(微任务),那么Promise.then
会在setTimeout
回调函数执行完后,下一个宏任务开始前就执行。 - 任务的嵌套和排队:如果在一个微任务或者宏任务中又创建了新的宏任务或者微任务,它们会按照事件队列的规则排队等待执行。例如,在一个
Promise.then
(微任务)中又设置了一个setTimeout
(宏任务),那么setTimeout
会在微任务队列清空后,等待下一个宏任务周期才执行。 - 长时间运行的宏任务:如果一个宏任务执行时间过长,会阻塞后续宏任务和微任务的执行。例如,一个
while
循环或者一个复杂的计算任务在宏任务中执行,会导致页面的响应性能下降,因为JavaScript是单线程的,事件循环无法处理其他任务。
- 宏任务和微任务的执行顺序:一定要清楚微任务是在当前宏任务执行完后就立即执行,而不是等到下一个宏任务周期。这可能会导致一些意外的结果,例如在
- 优化事件队列的运行
- 合理安排任务类型:对于可以并行执行的任务,尽量使用宏任务(如
setTimeout
或者setInterval
)来避免阻塞主线程。例如,如果有多个不依赖彼此的网络请求,可以同时发起,而不是顺序等待。 - 减少长时间运行的宏任务:将长时间运行的任务拆分成多个小的任务,或者使用
requestAnimationFrame
(用于动画等需要频繁更新的场景)来优化性能。requestAnimationFrame
会在浏览器下一次重绘之前执行回调函数,它可以保证动画的流畅性,并且不会像setTimeout
那样因为时间间隔不准确而导致画面抖动。 - 及时处理微任务积压:如果微任务队列积压太多任务,会导致宏任务的延迟执行。所以要尽量避免在微任务中进行复杂的计算或者长时间的操作。例如,在
Promise.then
方法中,只进行简单的数据处理和状态更新,避免在其中进行大量的循环或者I/O操作。
- 合理安排任务类型:对于可以并行执行的任务,尽量使用宏任务(如
技术名词解释
I/O操作
- 概念
- 浏览器中的I/O(Input/Output)操作主要是指浏览器与外部设备或其他系统之间的数据输入和输出过程。这包括从网络获取数据(如网页资源、API数据等)、读写本地存储(如Cookie、LocalStorage、IndexedDB等)以及与用户输入设备(如鼠标、键盘)进行交互等。
- 网络I/O操作
- 网页资源加载
- 当浏览器加载一个网页时,需要通过网络I/O从服务器获取HTML、CSS、JavaScript等文件。这个过程涉及到多个步骤。首先,浏览器会解析URL,确定要请求的服务器地址和资源路径。然后,它会建立与服务器的连接,通常是通过HTTP或HTTPS协议。例如,当请求一个网页时,浏览器会发送一个HTTP请求,像这样:
GET /index.html HTTP/1.1 Host: example.com
- 服务器收到请求后,会返回相应的资源。浏览器接收这些资源并进行解析和渲染。这个过程中,网络I/O操作的性能对网页的加载速度有很大影响。如果网络带宽低、延迟高或者服务器响应慢,网页加载就会变慢。
- API数据获取
- 很多网页会使用JavaScript来获取外部API的数据,这也是一种网络I/O操作。例如,一个天气网站可能会通过API获取天气数据。通常会使用
fetch
或XMLHttpRequest
等工具来发送请求。以fetch
为例:
fetch('https://api.example.com/weather') .then((response) => response.json()) .then((data) => console.log(data)) .catch((error) => console.log('请求出错:', error));
- 这里
fetch
发送请求到指定的API地址,等待服务器响应,然后将响应数据解析为JSON格式并进行处理。整个过程中,从发送请求到接收和处理响应都是网络I/O操作的一部分。
- 很多网页会使用JavaScript来获取外部API的数据,这也是一种网络I/O操作。例如,一个天气网站可能会通过API获取天气数据。通常会使用
- 网页资源加载
- 本地存储I/O操作
- Cookie操作
- Cookie是一种存储在用户浏览器中的小文本片段,主要用于存储用户的一些信息,如登录状态、偏好设置等。浏览器在与服务器进行交互时,会根据服务器的要求发送和接收Cookie。例如,当用户第一次登录一个网站时,服务器会在响应头中设置一个Cookie,像这样:
Set - Cookie: sessionid=123456789; Path=/; HttpOnly
- 之后,浏览器在每次请求该网站的资源时,都会在请求头中带上这个Cookie,以便服务器识别用户身份。这种发送和接收Cookie的过程就是本地存储I/O操作的一种。
- LocalStorage和IndexedDB操作
- LocalStorage是一种简单的键值对存储方式,用于在浏览器中持久存储数据。例如,可以使用以下方式在LocalStorage中存储和读取数据:
// 存储数据 localStorage.setItem('username', 'John'); // 读取数据 const username = localStorage.getItem('username');
- IndexedDB则是一种更强大的本地数据库存储方式,用于存储大量的结构化数据,如应用程序的用户数据、离线缓存数据等。它支持复杂的查询和事务处理。例如,要在IndexedDB中存储一个用户对象:
const request = indexedDB.open('myDatabase', 1); request.onsuccess = function (event) { const db = event.target.result; const transaction = db.transaction(['users'], 'readwrite'); const objectStore = transaction.objectStore('users'); const user = {id: 1, name: 'Alice'}; const addRequest = objectStore.add(user); addRequest.onsuccess = function () { console.log('用户数据已存储'); }; };
- 在这些操作中,浏览器与本地存储系统之间的数据交互就是本地存储I/O操作。
- Cookie操作
- 用户输入I/O操作
- 浏览器需要处理用户通过鼠标和键盘等输入设备的输入信息。例如,当用户点击一个按钮时,浏览器会捕获这个鼠标点击事件,这是一个输入I/O操作。在JavaScript中,可以通过事件监听器来处理这种操作,比如:
const button = document.getElementById('myButton'); button.addEventListener('click', function () { console.log('按钮被点击'); });
- 对于键盘输入,同样可以设置事件监听器来处理按键事件。当用户按下一个键时,浏览器会接收这个输入信息并触发相应的事件处理函数,这也是一种输入I/O操作。例如:
document.addEventListener('keydown', function (event) { console.log('按下的键是:', event.key); });
5.如何优化网络 I/O 操作以提高网页加载速度
- 减少请求次数
- 合并资源文件:将多个CSS和JavaScript文件合并为一个文件。例如,在构建工具(如Webpack)中,可以配置模块打包规则,把相关的样式表和脚本文件打包成单个文件。这样,浏览器在加载页面时就不需要多次请求这些小文件,从而减少了网络往返时间。对于图片资源,可以使用CSS精灵(CSS Sprites)技术,将多个小图标合并到一张大图中,通过CSS的背景定位来显示不同的图标,这样只需一次请求就能获取多个图标资源。
- 内联关键资源:对于一些非常小但又对页面初始渲染很关键的CSS和JavaScript代码,可以考虑将它们直接内联到HTML文件中。比如,一些基本的样式规则用来设置页面的布局和字体,或者是初始化页面交互的JavaScript代码。不过要注意,内联过多代码可能会使HTML文件变得臃肿,影响页面的可维护性。
- 优化请求顺序
- 优先加载关键资源:识别出页面加载和渲染所必需的关键CSS和JavaScript文件,将它们放在文档头部
<head>
部分加载。这样可以确保浏览器在开始渲染页面之前就能够获取这些关键资源。例如,对于一个以内容展示为主的网页,布局相关的CSS就是关键资源,应该优先加载。同时,对于一些非关键的JavaScript文件,如用于分析用户行为或者广告展示的脚本,可以将它们延迟加载,使用defer
或async
属性来改变脚本的加载和执行顺序。 - 懒加载非关键资源:对于页面中那些不在初始视窗范围内的图片、视频等资源,采用懒加载(Lazy Loading)技术。懒加载是指在页面滚动到相应位置时才加载这些资源。可以通过JavaScript实现懒加载,例如,使用
IntersectionObserver
API来检测元素是否进入视窗,当元素进入视窗时,再动态地设置src
属性来加载图片。这可以避免一次性加载大量非关键资源,减少页面初始加载的负担。
- 优先加载关键资源:识别出页面加载和渲染所必需的关键CSS和JavaScript文件,将它们放在文档头部
- 采用缓存策略
- 浏览器缓存:合理设置资源的缓存头(Cache - Headers),让浏览器能够缓存经常使用的资源。对于那些不经常变化的静态资源,如样式表、脚本文件和图片等,可以设置较长的缓存时间。例如,通过服务器端配置(如在Apache或Nginx服务器上),将
Cache - Control
头设置为max - age = 31536000
(一年),表示浏览器可以在一年内使用本地缓存的该资源,而无需再次从服务器获取,从而加快了页面的加载速度。 - 内容分发网络(CDN)缓存:将静态资源(如图片、CSS和JavaScript文件)部署到CDN上。CDN是一个分布式服务器网络,它会根据用户的地理位置和网络状况,从离用户最近的服务器缓存中提供资源。这样可以减少资源的传输延迟。例如,当用户请求一个在CDN上缓存的图片时,CDN会自动找到距离用户最近的服务器来提供该图片,而不是从原始服务器获取,大大提高了加载速度。
- 浏览器缓存:合理设置资源的缓存头(Cache - Headers),让浏览器能够缓存经常使用的资源。对于那些不经常变化的静态资源,如样式表、脚本文件和图片等,可以设置较长的缓存时间。例如,通过服务器端配置(如在Apache或Nginx服务器上),将
- 优化网络协议和服务器配置
- 使用HTTP/2或HTTP/3协议:与HTTP/1.1相比,HTTP/2和HTTP/3具有更高的性能。HTTP/2支持多路复用,允许在一个TCP连接上同时发送多个请求和响应,避免了HTTP/1.1中“队头阻塞”的问题。HTTP/3则进一步基于UDP协议,减少了连接建立的延迟。如果服务器和浏览器都支持这些新协议,切换到HTTP/2或HTTP/3可以显著提高网络I/O性能。
- 服务器端性能优化:确保服务器的配置能够高效地处理请求。这包括优化服务器的硬件资源(如CPU、内存和网络带宽),以及软件配置(如数据库查询优化、服务器端脚本执行效率等)。例如,对于一个使用Node.js构建的服务器,可以通过优化数据库连接池、减少不必要的文件I/O操作以及使用高效的中间件来提高服务器的响应速度,从而加快网络I/O操作。
- 数据压缩和优化资源大小
- 文件压缩:在服务器端对CSS、JavaScript和HTML文件进行压缩,如使用Gzip或Brotli等压缩算法。这些算法可以显著减小文件大小,减少网络传输的数据量。大多数现代服务器和浏览器都支持这些压缩算法。例如,通过在服务器端配置(如在Nginx中)启用Gzip压缩,文件大小可能会减小到原来的几分之一,从而加快了文件的传输速度。
- 图片优化:对图片进行优化,包括选择合适的图片格式(如JPEG用于照片,PNG用于具有透明度的图像,WebP是一种更高效的现代格式)、调整图片尺寸以适应实际需求、以及使用图像压缩工具来减小图片的文件大小。例如,对于一个只需要在小尺寸下显示的图标,就不应该使用高分辨率的大图片,并且可以使用图像编辑工具将其转换为更合适的格式和尺寸,以减少网络I/O负担。
postMessage、MessageChannel 应用
- postMessage
- 概念
postMessage
是一种跨域通信机制,它允许在不同窗口(如不同的iframe
、弹出窗口或者不同源的主窗口之间)安全地传递数据。它是HTML5中引入的一个重要的API,用于解决跨域数据传输的问题。
- 主要作用
- 跨域数据传输:在浏览器的安全限制下,不同源(协议、域名、端口号不完全相同)的页面之间通常不能直接访问对方的DOM或者JavaScript变量。
postMessage
提供了一种安全的方式来打破这种限制,实现跨域通信。例如,一个网页包含一个iframe
,iframe
的内容来自于另一个域,通过postMessage
,父窗口可以向iframe
发送消息,iframe
也可以向父窗口发送消息。 - 多窗口通信:在复杂的Web应用中,可能会有多个弹出窗口或者多个
iframe
同时存在。postMessage
可以用于这些窗口之间的相互通信,使得它们能够协调工作。比如,一个在线支付页面可能会弹出一个银行认证窗口,两个窗口之间可以通过postMessage
来传递认证信息和支付结果。
- 跨域数据传输:在浏览器的安全限制下,不同源(协议、域名、端口号不完全相同)的页面之间通常不能直接访问对方的DOM或者JavaScript变量。
- 应用场景
- 微前端架构:在微前端应用中,不同的子应用可能部署在不同的域名下。
postMessage
可以用于主应用和子应用之间的通信,实现数据共享和交互。例如,主应用可以向子应用发送用户的登录状态信息,子应用可以向主应用反馈操作结果。 - 第三方插件集成:当网页集成第三方插件(如地图插件、广告插件等)时,这些插件通常是在
iframe
中加载的。通过postMessage
,网页可以和插件进行通信,例如传递用户的位置信息给地图插件,或者接收广告插件的点击事件反馈。 - 跨域数据共享与协作:在一些需要跨域共享数据的场景中,如跨域的表单提交、多域名下的数据同步等,
postMessage
发挥着重要作用。例如,两个不同域名的网站A和B需要共享用户的部分信息,通过postMessage
可以在安全的前提下实现数据的传输和同步。
- 微前端架构:在微前端应用中,不同的子应用可能部署在不同的域名下。
- MessageChannel
- 概念
MessageChannel
是一个用于在两个不同的JavaScript执行上下文之间传递消息的接口。它创建了一个消息通道,包括两个端口(port1
和port2
),可以通过这两个端口来发送和接收消息。
- 主要作用
- 隔离通信通道:与
postMessage
不同,MessageChannel
提供了一种更加灵活和隔离的通信方式。它可以在同一个页面的不同部分之间(如两个iframe
之间、一个Web Worker
和主线程之间等)建立专用的通信通道,避免消息的混乱和干扰。例如,在一个复杂的Web应用中,可以使用MessageChannel
为不同的模块或者功能组件创建独立的通信路径。 - 异步通信支持:
MessageChannel
支持异步通信,消息可以在不同的时间点发送和接收,并且不会阻塞其他代码的执行。这对于处理复杂的异步任务或者需要独立消息处理流程的场景非常有用。
- 隔离通信通道:与
- 应用场景
- Web Worker通信:
Web Worker
是一种在后台线程中运行JavaScript代码的机制,可以用于执行一些耗时的计算任务而不阻塞主线程。MessageChannel
可以用于主线程和Web Worker
之间的通信。例如,主线程可以通过MessageChannel
向Web Worker
发送数据进行计算,Web Worker
计算完成后再通过MessageChannel
将结果返回给主线程。 - 组件间通信(在复杂页面架构中):在具有多个独立组件的页面中,如单页应用(SPA),可以使用
MessageChannel
来实现组件之间的通信。例如,一个组件可以通过MessageChannel
向另一个组件发送事件通知或者数据更新信息,使得组件之间能够更好地协同工作。
- Web Worker通信:
postMessage 与 MessageChannel实例
- 场景1-微前端架构中三个iframe之间通过postMessage通信实例
- 概念
假设我们有一个主页面index.html
,其中包含三个iframe
,分别是iframe1.html
、iframe2.html
和iframe3.html
,它们的源(域名、端口和协议)可能相同也可能不同。
主页面(index.html)结构示例
<!DOCTYPE html>
<html>
<head>
<title>微前端主页面</title>
</head>
<body>
<iframe id="iframe1" src="iframe1.html"></iframe>
<iframe id="iframe2" src="iframe2.html"></iframe>
<iframe id="iframe3" src="iframe3.html"></iframe>
<script>
window.addEventListener('load', function () {
// 获取iframe元素
const iframe1 = document.getElementById('iframe1');
const iframe2 = document.getElementById('iframe2');
const iframe3 = document.getElementById('iframe3');
// 向iframe1发送消息
const messageToIframe1 = { data: '这是给iframe1的消息' };
iframe1.contentWindow.postMessage(messageToIframe1, '*');
// 监听iframe2发送的消息
window.addEventListener('message', function (event) {
if (event.source === iframe2.contentWindow) {
console.log('收到iframe2的消息:', event.data);
}
});
// 建立iframe3和iframe1之间的通信
iframe3.contentWindow.addEventListener('message', function (event) {
if (event.source === iframe1.contentWindow) {
console.log('iframe3收到iframe1的消息:', event.data);
}
});
iframe1.contentWindow.postMessage({ data: '这是从iframe3请求的消息' }, '*');
});
</script>
</body>
</html>
iframe1.html示例内容
<!DOCTYPE html>
<html>
<head>
<title>iframe1</title>
</head>
<body>
<script>
window.addEventListener('message', function (event) {
console.log('iframe1收到消息:', event.data);
// 向iframe3回复消息
event.source.postMessage({ data: '这是iframe1给iframe3的回复' }, '*');
});
</script>
</body>
</html>
iframe2.html示例内容
<!DOCTYPE html>
<html>
<head>
<title>iframe2</title>
</head>
<body>
<script>
const messageToParent = { data: '这是iframe2给父窗口的消息' };
window.parent.postMessage(messageToParent, '*');
</script>
</body>
</html>
iframe3.html示例内容
<!DOCTYPE html>
<html>
<head>
<title>iframe3</title>
</head>
<body>
<script>
window.addEventListener('message', function (event) {
console.log('iframe3收到消息:', event.data);
});
</script>
</body>
</html>
在这个例子中:
- 主页面加载完成后,向
iframe1
发送消息。iframe1
接收到消息后在控制台打印,并向iframe3
回复消息。 iframe2
在加载时向主页面发送消息,主页面监听到来自iframe2
的消息后在控制台打印。iframe1
向iframe3
发送消息(通过主页面间接建立通信),iframe3
接收到消息后在控制台打印。
- 场景2-在复杂页面架构中MessageChannel实现组件间通信示例
假设我们有一个单页应用(SPA),页面中有两个组件ComponentA
和ComponentB
,我们使用MessageChannel
来实现它们之间的通信。
HTML结构示例(假设是一个简单的div结构来表示组件)
<!DOCTYPE html>
<html>
<head>
<title>复杂页面架构</title>
</head>
<body>
<div id="componentA">组件A</div>
<div id="componentB">组件B</div>
<script>
// 创建MessageChannel
const channel = new MessageChannel();
const componentA = document.getElementById('componentA');
const componentB = document.getElementById('componentB');
// 组件A发送消息
componentA.addEventListener('click', function () {
const message = { text: '这是组件A发送的消息' };
channel.port1.postMessage(message);
});
// 组件B接收消息
channel.port2.onmessage = function (event) {
console.log('组件B收到消息:', event.data);
};
// 将port2传递给组件B的作用域(这里简单地通过全局变量模拟)
window.componentBPort = channel.port2;
</script>
</body>
</html>
在这个例子中:
- 首先创建了
MessageChannel
,它包含两个端口port1
和port2
。 - 当用户点击
ComponentA
时,通过port1
发送一个消息。 port2
被设置为监听消息,当收到消息时,ComponentB
(通过port2
)在控制台打印出消息内容。这里为了简单演示,将port2
挂载到全局变量window.componentBPort
上,实际应用中可能会通过更复杂的组件间数据传递机制将port2
传递给ComponentB
的内部作用域来处理消息。
MutationObserver介绍
- MutationObserver概念
- MutationObserver是一个用于监视DOM(文档对象模型)变化的接口。它提供了一种异步观察DOM树变化的能力,可以监测DOM节点的添加、删除、属性修改以及文本内容的改变等情况。与旧的DOM mutation事件(如
DOMSubtreeModified
)相比,MutationObserver具有更好的性能和更精细的控制,因为它采用了异步回调的方式,并且可以在多个DOM变化后一次性通知,而不是每次变化都触发同步的事件处理。
- MutationObserver是一个用于监视DOM(文档对象模型)变化的接口。它提供了一种异步观察DOM树变化的能力,可以监测DOM节点的添加、删除、属性修改以及文本内容的改变等情况。与旧的DOM mutation事件(如
- MutationObserver参数
- 构造函数参数:
MutationObserver
构造函数接受一个回调函数作为参数。这个回调函数会在DOM发生变化并被观察到的时候被调用。回调函数接收一个MutationRecord
对象数组作为参数,每个MutationRecord
对象代表一次DOM变化,其中包含了变化的类型(如attributes
属性变化、childList
子节点列表变化等)、变化的目标节点等信息。例如:
const observer = new MutationObserver((mutationsList) => { for (const mutation of mutationsList) { console.log('发生了DOM变化'); if (mutation.type === 'childList') { console.log('子节点列表发生变化'); } else if (mutation.type === 'attributes') { console.log('属性发生变化'); } } });
- 观察选项参数:使用
observe
方法来开始观察DOM节点,它接受要观察的DOM节点和一个配置对象作为参数。配置对象可以包含以下属性:childList
:布尔值,设置为true
时,观察目标节点的子节点(添加或删除子节点)的变化。attributes
:布尔值,设置为true
时,观察目标节点的属性变化。characterData
:布尔值,设置为true
时,观察目标节点的文本内容(Text
节点或Comment
节点)变化。subtree
:布尔值,设置为true
时,不仅观察目标节点本身,还观察其后代节点的变化。例如:
const targetNode = document.getElementById('myElement'); const config = { childList: true, attributes: true, subtree: true }; observer.observe(targetNode, config);
- 构造函数参数:
- MutationObserver应用场景
- 动态网页内容监控:在一些具有动态加载内容的网页中,如社交网站的动态消息列表或者新闻网站的实时新闻推送。当新的内容被添加到页面(如一条新的评论、一篇新的新闻文章)时,使用
MutationObserver
可以检测到这些变化,并及时更新页面的其他部分或者执行相关的操作。例如:
const newsFeed = document.getElementById('news - feed'); const observer = new MutationObserver((mutationsList) => { for (const mutation of mutationsList) { if (mutation.type === 'childList' && mutation.addedNodes.length > 0) { console.log('有新的新闻文章添加到新闻列表'); // 可以在这里进行新文章的渲染优化,如懒加载图片等操作 } } }); const config = { childList: true }; observer.observe(newsFeed, config);
- 表单元素变化检测:在一个包含多个表单元素的复杂表单中,使用
MutationObserver
可以检测表单元素的属性变化(如输入框的value
属性、下拉菜单的selected
属性等)或者表单元素的添加和删除。这对于实时验证表单数据、动态更新表单提示信息等操作很有用。例如:
const form = document.getElementById('myForm'); const observer = new MutationObserver((mutationsList) => { for (const mutation of mutationsList) { if (mutation.type === 'attributes' && mutation.attributeName === 'value') { console.log('表单输入框的值发生变化'); // 可以在这里进行实时验证,如检查密码强度等操作 } } }); const config = { attributes: true }; observer.observe(form, config);
- DOM元素样式变化监控:对于一些需要根据元素样式变化来执行动画或者其他视觉效果的场景,
MutationObserver
可以用来观察元素的样式属性(如class
、style
属性)变化。例如,当一个元素的class
被添加或删除,导致其样式改变时,触发相应的动画效果。
const animatedElement = document.getElementById('animated - element'); const observer = new MutationObserver((mutationsList) => { for (const mutation of mutationsList) { if (mutation.type === 'attributes' && mutation.attributeName === 'class') { console.log('元素的class属性发生变化,触发动画'); // 在这里可以启动或停止相关的CSS动画 } } }); const config = { attributes: true }; observer.observe(animatedElement, config);
- 动态网页内容监控:在一些具有动态加载内容的网页中,如社交网站的动态消息列表或者新闻网站的实时新闻推送。当新的内容被添加到页面(如一条新的评论、一篇新的新闻文章)时,使用
任务队列小结
- 任务队列的分类
- 宏任务队列(Macrotask Queue)
- 包含了一系列的宏任务,如
script
(整体代码块)、setTimeout
、setInterval
、I/O
操作、postMessage
、MessageChannel
等。这些任务通常是比较耗时或者需要等待外部资源(如网络、磁盘)的操作。
- 包含了一系列的宏任务,如
- 微任务队列(Microtask Queue)
- 主要包含微任务,像
Promise.then
、MutationObserver
、process.nextTick
(Node.js环境)等。微任务一般是对宏任务执行过程中产生的一些小的、快速完成的任务进行处理。
- 主要包含微任务,像
- 宏任务队列(Macrotask Queue)
- 事件循环(Event Loop)流程
- 首先,JavaScript引擎会从宏任务队列中取出一个宏任务开始执行,这个宏任务可能是JavaScript代码的入口文件(
script
)或者是通过setTimeout
等方式排队等待执行的任务。 - 在执行宏任务的过程中,如果产生了微任务(例如在宏任务中的
Promise
被resolve
或者reject
,触发then
方法;或者MutationObserver
观察到DOM变化等情况),这些微任务会被添加到微任务队列中。 - 当当前的宏任务执行完毕后,引擎会检查微任务队列。如果微任务队列中有任务,会按照先进先出的顺序依次执行微任务队列中的所有任务。
- 当微任务队列中的任务全部执行完毕后,引擎会再次从宏任务队列中取出下一个宏任务进行执行,重复上述过程。
- 首先,JavaScript引擎会从宏任务队列中取出一个宏任务开始执行,这个宏任务可能是JavaScript代码的入口文件(
- 注意要点
- 微任务的执行时机:微任务是在当前宏任务执行完后就立即执行,而不是等到下一个宏任务周期。这就导致了微任务的执行优先级高于下一个宏任务。例如,在
setTimeout
(宏任务)回调函数中,如果有Promise.then
(微任务),那么Promise.then
会在setTimeout
回调函数执行完后,下一个宏任务开始前就执行。 - 任务的嵌套和排队:如果在一个微任务或者宏任务中又创建了新的宏任务或者微任务,它们会按照事件队列的规则排队等待执行。例如,在一个
Promise.then
(微任务)中又设置了一个setTimeout
(宏任务),那么setTimeout
会在微任务队列清空后,等待下一个宏任务周期才执行。 - 长时间运行的宏任务影响:如果一个宏任务执行时间过长,会阻塞后续宏任务和微任务的执行。因为JavaScript是单线程的,事件循环无法处理其他任务。例如,一个复杂的计算任务或者长时间的循环在宏任务中执行,会导致页面的响应性能下降。
- 微任务的执行时机:微任务是在当前宏任务执行完后就立即执行,而不是等到下一个宏任务周期。这就导致了微任务的执行优先级高于下一个宏任务。例如,在
- 不同环境下的特点
- 浏览器环境:浏览器的任务队列机制主要用于处理页面的渲染、用户交互、网络请求等多种任务。网络请求(如
fetch
或XMLHttpRequest
)一般是宏任务,当收到响应并处理Promise
相关操作时会涉及微任务。用户输入事件(如鼠标点击、键盘输入)也是宏任务,而在事件处理过程中可能产生微任务(如更新DOM并触发MutationObserver
)。 - Node.js环境:除了和浏览器类似的定时器(
setTimeout
、setInterval
)等宏任务和Promise
相关的微任务外,还有一些特定的任务类型。例如,文件I/O操作是宏任务,process.nextTick
是微任务,并且它在事件循环中有特殊的优先级,会在当前阶段优先于其他微任务(如Promise.then
)执行。
- 浏览器环境:浏览器的任务队列机制主要用于处理页面的渲染、用户交互、网络请求等多种任务。网络请求(如
更多推荐
所有评论(0)