《掌握同步和异步:JavaScript 程序设计的进阶之路》
2024-08-07 08:56 阅读(276)

同步(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}`);
});