从同步到异步:重新理解 JavaScript 的执行机制

从同步到异步:重新理解 JavaScript 的执行机制

昨天学习了 JavaScript 中同步任务、异步任务、事件循环以及 Promise 的基本使用。本文用几个小例子,把这些知识串起来,帮助自己建立一条清晰的理解路径。

前言

刚开始学 JavaScript 异步时,很容易产生一个疑问:

为什么代码明明写在前面,却不是马上执行?

比如下面这段代码:

js 复制代码
console.log('start');

setTimeout(() => {
  console.log('222');
}, 1000);

console.log('end');

它的输出顺序是:

txt 复制代码
start
end
222

这说明 JavaScript 并不是简单地"从上到下等每一行都执行完"。当遇到定时器、事件、网络请求这类耗时任务时,它会先把这些任务交出去,继续执行后面的同步代码。

这就是理解异步的入口。

JavaScript 为什么需要异步?

JavaScript 的一个重要特点是:主线程是单线程的。

也就是说,同一时间主线程只能做一件事。下面这种同步代码会按照顺序立即执行:

js 复制代码
let a = 1;
let b = 2;
let c = 3;

console.log(a + b + c);

同步任务的特点是简单、直接、执行顺序清晰。

但是在真实开发中,很多任务并不是马上就能完成的,比如:

  • setTimeout 定时器
  • 用户点击、输入等事件
  • fetch 网络请求
  • 文件读取
  • 数据库操作

如果 JavaScript 遇到这些耗时任务时一直等待,页面就会卡住,用户也无法继续操作。所以 JavaScript 采用了异步机制:先执行同步任务,耗时任务完成后再回来处理它们的回调。

JavaScript 的执行机制

可以把 JavaScript 的执行过程理解成三步:

  1. 主线程先执行所有同步代码。
  2. 遇到异步任务时,把它交给对应的环境处理,比如浏览器的定时器模块、网络模块。
  3. 等同步代码执行完以后,再通过事件循环取出可以执行的异步回调。

还是看这个例子:

js 复制代码
console.log('start');

setTimeout(() => {
  console.log('222');
}, 1000);

console.log('end');

执行过程是:

  1. 打印 start
  2. 遇到 setTimeout,注册一个 1 秒后的回调。
  3. 不等待定时器,继续向下执行。
  4. 打印 end
  5. 大约 1 秒后,同步代码已经执行完,事件循环把定时器回调拿出来执行。
  6. 打印 222

所以最终结果是 start -> end -> 222

Promise:管理异步任务的容器

异步任务多了以后,只靠回调函数会让代码变得很难读。ES6 提供了 Promise,它可以更清晰地表达一个异步任务最终成功还是失败。

一个基本的 Promise 写法如下:

js 复制代码
const p = new Promise((resolve, reject) => {
  console.log('许下诺言');

  setTimeout(() => {
    reject('网络错误');
  }, 2000);
});

p
  .then((data) => {
    console.log(data);
    console.log('成功了');
  })
  .catch((err) => {
    console.log(err);
  })
  .finally(() => {
    console.log('终于到达了这里');
  });

这里有几个关键点:

  • new Promise() 需要传入一个函数,这个函数叫 executor
  • executor 会立刻执行,它本身是同步执行的。
  • resolve 表示异步任务成功,后续会进入 .then()
  • reject 表示异步任务失败,后续会进入 .catch()
  • .finally() 不关心成功还是失败,最后都会执行。

可以把 Promise 理解成一个"异步任务的状态容器"。

它一开始是等待状态,之后要么成功,要么失败:

txt 复制代码
pending -> fulfilled
pending -> rejected

成功时调用:

js 复制代码
resolve(result);

失败时调用:

js 复制代码
reject(error);

fetch 本身就返回 Promise

网络请求是最常见的异步任务之一。浏览器提供的 fetch 方法,底层返回的就是一个 Promise。

html 复制代码
<button id="loadBtn">请求数据</button>
<pre id="output"></pre>

<script>
  const output = document.querySelector('#output');
  const loadBtn = document.querySelector('#loadBtn');

  function log(message) {
    console.log(message);
    output.textContent += `${message}\n`;
  }

  loadBtn.addEventListener('click', () => {
    output.textContent = '';

    log('start');

    fetch('https://jsonplaceholder.typicode.com/users/1')
      .then((res) => {
        log('请求完成,开始把响应转成 JSON');
        return res.json();
      })
      .then((user) => {
        log(`用户名称:${user.name}`);
        log(`用户邮箱:${user.email}`);
      })
      .catch((err) => {
        log(`请求失败:${err.message}`);
      })
      .finally(() => {
        log('finally:不管成功失败都会执行');
      });

    log('end');
  });
</script>

点击按钮后,startend 会先打印出来。请求完成后,才会进入后面的 .then()

这里也能再次看到异步代码的执行特点:

txt 复制代码
先执行同步代码
再等待异步任务完成
最后执行异步回调

用 Promise 封装一个 sleep

JavaScript 本身没有内置的 sleep 函数,但可以用 Promise 加 setTimeout 实现一个简单版本:

js 复制代码
function sleep(t) {
  const p = new Promise((resolve) => {
    setTimeout(() => {
      resolve();
    }, t);
  });

  return p;
}

sleep(2000).then(() => {
  console.log('2s 后再做');
});

这段代码的意思是:

  1. 调用 sleep(2000)
  2. 返回一个 Promise。
  3. 2 秒后调用 resolve()
  4. Promise 状态变成成功。
  5. 执行 .then() 中的代码。

这也是 Promise 很实用的地方:它可以把一个异步过程包装成更容易组织的链式调用。

小结

这次学习主要理解了几个点:

  • JavaScript 主线程是单线程的,同步任务会优先执行。
  • 定时器、事件、网络请求都属于常见异步任务。
  • 异步任务不会阻塞后面的同步代码。
  • 事件循环会在同步代码执行完成后,再调度异步回调。
  • Promise 是 ES6 中管理异步任务的重要机制。
  • resolve 对应成功,.then() 会执行。
  • reject 对应失败,.catch() 会执行。
  • .finally() 不管成功失败都会执行。
  • fetch 返回 Promise,所以可以使用 .then().catch().finally()
  • 可以用 Promise 封装自己的异步工具函数,比如 sleep

学异步时,最重要的不是死记 API,而是先建立执行顺序的感觉:

txt 复制代码
同步代码先走
异步任务先挂起
同步执行完后
事件循环再调回调

理解了这条主线,再去学习 async/await、并发请求、错误处理,就会顺很多。

相关推荐
半个落月1 小时前
JavaScript 同步异步与 Promise 详解 —— 从 Event Loop 到手写 sleep
javascript
触底反弹1 小时前
深入理解 JavaScript 同步与异步:从 Event Loop 到 async/await
javascript
浮生望2 小时前
JavaScript 异步编程核心:从同步阻塞到 Promise 事件循环
javascript·promise
假如让我当三天老蒯2 小时前
暂时性死区是否和闭包是相背的呢(自学用)
前端·javascript
渣波2 小时前
前端开发主页面小技巧
前端·javascript
小林ixn2 小时前
前端必知:JS同步异步与Promise,终于有人讲明白了!
javascript·面试
bonechips2 小时前
JS:同步与异步,从单线程到 Promise 的编程之路
前端·javascript
先吃饱再说2 小时前
为什么 `setTimeout` 会“插队”?JS 事件循环与 Promise 通关笔记
前端·javascript·promise
Web打印2 小时前
打印PDF面单顺序会乱 使用HttpPrinter连接打印机打印PDF快递面单,顺序会乱,请问有没有碰到过这样的问题呢?是怎么解决的
javascript