同步(Synchronous) :
同步意味着代码按照从上到下的顺序依次执行,每个操作都必须完成后,才能继续下一个操作。
例如:
function syncOperation() {
console.log('开始同步操作 1');
let sum = 0;
for (let i = 1; i <= 1000000; i++) {
sum += i;
}
console.log('计算完成,总和为:' + sum);
console.log('继续同步操作 2');
}
在上述代码中,计算总和的循环是一个耗时操作,在这个操作完成之前,后续的 console.log('继续同步操作 2'); 不会被执行。
异步(Asynchronous) :
异步操作允许程序在一个操作仍在进行时继续执行其他任务,而不必等待该操作完成。
常见的异步操作包括 setTimeout 、 setInterval 、网络请求(如 XMLHttpRequest 、 fetch )、文件读取/写入、Promise.then 、 MutationObserver等。
function asyncWithTimeout() {
console.log('开始异步操作');
setTimeout(() => {
console.log('2 秒后执行的异步操作');
}, 2000);
console.log('异步操作未完成,继续执行其他代码');
}
这里,setTimeout 中的回调函数会在 2 秒后执行,而在这 2 秒内,后续的 console.log('异步操作未完成,继续执行其他代码'); 会立即执行。
网络请求(使用 fetch )例子:
function asyncFetchData() {
console.log('发起网络请求前');
fetch('https://example.com/data')
.then(response => response.json())
.then(data => {
console.log('获取到的数据:', data);
})
.catch(error => {
console.error('请求出错:', error);
});
console.log('发起网络请求后,继续执行其他操作');
}
在网络请求发送后,程序不会等待响应返回,而是继续执���后续的代码。当响应返回并处理完成后,相应的回调函数才会被调用。
文件读取例子(假设使用 Node.js 环境):
const fs = require('fs');
function asyncReadFile() {
console.log('开始读取文件前');
fs.readFile('example.txt', 'utf-8', (err, data) => {
if (err) {
console.error('读取文件出错:', err);
return;
}
console.log('读取到的文件内容:', data);
});
console.log('开始读取文件后,继续执行其他操作');
}
文件读取是一个异步操作,不会阻塞后续代码的执行。
异步的原理:
在 JavaScript 中,异步操作的实现原理主要基于事件循环(Event Loop)和任务队列(Task Queue)。
事件循环(Event Loop) :
事件循环是 JavaScript 引擎用于管理异步任务执行的机制。它不断地检查执行栈是否为空,如果为空,就会从任务队列中取出任务并执行。
任务队列(Task Queue) :
任务队列分为宏任务队列(Macro Task Queue)和微任务队列(Micro Task Queue)。
宏任务包括 setTimeout 、 setInterval 、 IO 操作(如网络请求、文件读取)等。微任务包括 Promise.then 、 MutationObserver 等。
当一个异步操作开始时,它不会立即执行其回调函数,而是将回调函数放入相应的任务队列中。
宏任务(Macro Task) :
setTimeout:用于在指定的毫秒数后执行一个函数。它会将指定的回调函数放入宏任务队列,并在设定的时间间隔过去后,将其添加到执行栈中执行。
setTimeout(() => {
console.log('setTimeout 执行');
}, 2000);
setInterval:以指定的时间间隔重复执行一个函数。与 setTimeout 类似,每次执行的回调函数都会被放入宏任务队列。
setInterval(() => {
console.log('setInterval 执行');
}, 1000);
IO 操作(如网络请求、文件读取):当发起网络请求或读取文件时,这些操作不会阻塞主线程,而是在操作完成后将相应的回调函数放入宏任务队列等待执行。
宏任务的特点是执行时间相对较长,并且它们的执行顺序是按照放入队列的先后顺序来的。
微任务(Micro Task) :
当创建一个 Promise 对象时,它的初始状态为 pending ,表示正在进行中,结果尚未确定。
如果在后续的操作中,Promise 成功完成并得到了一个结果,那么它的状态会从 pending 变为 fulfilled ,并且会触发 then 方法中第一个回调函数(处理成功结果的回调)。这个回调函数会被放入微任务队列。
例如:
const myPromise = new Promise((resolve) => {
setTimeout(() => {
resolve('成功的值');
}, 1000);
});
myPromise.then(result => {
console.log(result);
});
在上述代码中,setTimeout 内部的 resolve 函数在 1 秒后将 Promise 的状态变为 fulfilled ,此时 then 中的回调函数就会被放入微任务队列。
如果 Promise 失败了,即发生了错误或出现异常,导致其状态从 pending 变为 rejected ,那么会触发 then 方法中第二个回调函数(处理失败结果的回调,通常通过 catch 方法注册),这个回调函数同样会被放入微任务队列。
例如:
const myPromise = new Promise((_, reject) => {
reject('失败的原因');
});
myPromise.catch(error => {
console.error(error);
});
需要注意的是,微任务队列的执行优先级高于宏任务队列。当当前的同步任务和当前宏任务执行完毕后,会先清空微任务队列中的所有任务,然后再去执行下一个宏任务。
这一特性使得 Promise.then 能够在适当的时候及时处理异步操作的结果,并且保证了异步操作结果处理的顺序和及时性。
MutationObserver:用于监视 DOM 树的变化,当检测到变化时,其回调函数会被放入微任务队列。
微任务的特点是会在当前宏任务执行完毕后,优先于下一个宏任务执行。这确保了微任务能够更快地处理一些需要及时响应的操作,并且微任务的执行顺序是按照它们被放入队列的先后顺序来的。
例如:
console.log('同步任务 1');
setTimeout(() => {
console.log('宏任务 1');
}, 0);
Promise.resolve().then(() => {
console.log('微任务 1');
});
console.log('同步任务 2');
在这个例子中,首先输出 '同步任务 1' 和 '同步任务 2' ,然后执行微任务 '微任务 1' ,最后执行宏任务 '宏任务 1' 。
总的来说,异步的原理是通过将异步操作的回调函数放入任务队列,并由事件循环机制来协调执行,从而实现非阻塞的程序执行,提高程序的性能和响应性。
同步操作的优点:
逻辑简单直观,代码的执行顺序清晰明了,易于理解和调试。
不存在复杂的回调函数或异步处理逻辑,代码结构相对简洁。
例如:
function syncSum(a, b) {
let result = a + b;
console.log(`同步计算结果: ${result}`);
return result;
}
同步操作的缺点:
如果某个操作耗时较长,会导致整个程序阻塞,用户界面无响应,影响用户体验。
在处理大量并发或复杂的 I/O 操作时,效率较低,无法充分利用系统资源。
例如,如果有一个长时间的计算或网络请求:
function longSyncOperation() {
let sum = 0;
for (let i = 0; i < 1000000000; i++) { // 长时间的计算
sum += i;
}
console.log(`完成长时间同步计算`);
}
异步操作的优点:
可以在耗时操作进行的同时,让程序继续执行其他任务,提高程序的响应性和性能。
能够更好地处理并发和 I/O 密集型任务,充分利用系统资源,提高程序的效率。
例如,使用 fetch 发送网络请求时:
fetch('https://example.com/data')
.then(response => response.json())
.then(data => {
console.log(`获取到数据: ${JSON.stringify(data)}`);
})
.catch(error => {
console.error(`请求出错: ${error}`);
});
console.log('在网络请求发送后,继续执行其他操作');
异步操作的缺点:
异步代码的逻辑相对复杂,需要处理回调函数、Promise 、async/await 等异步处理机制,增加了代码的理解和编写难度。
由于异步操作的执行顺序不确定,可能会导致竞态条件和难以预测的错误,增加了调试的难度。
例如,多个异步操作之间的依赖关系处理不当可能会导致问题:
let data1;
let data2;
fetch('https://example1.com/data')
.then(response1 => response1.json())
.then(result1 => {
data1 = result1;
if (data1) {
fetch('https://example2.com/data')
.then(response2 => response2.json())
.then(result2 => {
data2 = result2;
// 处理 data1 和 data2 的逻辑
})
.catch(error => {
console.error(`第二个请求出错: ${error}`);
});
}
})
.catch(error => {
console.error(`第一个请求出错: ${error}`);
});