Skip to content

03 - JS 异步与 Promise

目录


一、同步与异步

  • JS 是单线程语言,只能同时做一件事
  • 同步代码:按顺序逐行执行,阻塞后续代码
  • 异步代码:不阻塞主线程,通过回调函数 / Promise 在适当时机执行

异步应用场景

  1. 网络请求:AJAX、Fetch、Axios 等
  2. 定时任务setTimeoutsetInterval
  3. 事件处理:用户点击、滚动等
  4. 文件操作FileReader 异步读取文件
  5. 动画渲染requestAnimationFrame
  6. 复杂计算:Web Worker 后台线程
  7. 数据库交互:IndexedDB
  8. 实时通信:WebSocket、SSE
  9. 并发控制Promise.allasync/await

二、Event Loop 事件循环

为何需要 Event Loop

JS 是单线程的,但需要处理异步操作(网络请求、定时器等)。事件循环通过任务队列非阻塞 I/O 实现了并发执行。

核心组成

1. 调用栈(Call Stack)

  • 存储同步任务的执行上下文
  • 先进后出(LIFO)结构

2. Web APIs(浏览器环境)

  • 处理异步操作(setTimeoutfetch、DOM 事件)
  • 异步任务完成时,将回调函数推入任务队列

3. 任务队列

  • 宏任务队列(Macrotask Queue)setTimeoutsetIntervalI/O、UI 渲染、DOM 事件回调
  • 微任务队列(Microtask Queue)Promise.thenMutationObserverqueueMicrotask()process.nextTick(Node.js)

事件循环执行流程

  1. 执行同步代码(调用栈)
  2. 调用栈清空后,执行所有微任务
  3. (给浏览器一次 DOM 渲染的机会)
  4. 执行一个宏任务
  5. 重复 1-4

执行示例

js
console.log('1'); // 同步
setTimeout(() => console.log('2'), 0); // 宏任务
Promise.resolve().then(() => console.log('3')); // 微任务
console.log('4'); // 同步
// 输出:1 → 4 → 3 → 2
js
console.log('1');
setTimeout(() => {
  console.log('2');
  Promise.resolve().then(() => console.log('3'));
}, 0);
setTimeout(() => console.log('4'), 0);
Promise.resolve().then(() => console.log('5'));
// 输出:1 → 5 → 2 → 3 → 4

Call Stack 执行过程图示

Event Loop总体流程

Call Stack执行过程1Call Stack执行过程2

执行 setTimeout 后,Web APIs 等待计时完成:

setTimeout执行后

同步代码执行完后,Event Loop 检查 Callback Queue:

Event Loop检查队列

DOM 事件与 Event Loop

DOM事件和Event Loop

  • JS 是单线程
  • 异步(setTimeout、Ajax 等)使用回调,基于 Event Loop
  • DOM 事件也使用回调(触发时机不同),同样基于 Event Loop

async/await 顺序问题

async await顺序

  • 初始化 Promise 时,传入的回调函数立刻执行
  • 同步代码执行完毕(Call Stack 清空)
  • 执行微任务
  • 尝试触发 DOM 渲染
  • 触发 Event Loop,执行宏任务

三、宏任务与微任务

分类

  • 宏任务setTimeoutsetInterval、Ajax 网络请求的回调、DOM 事件回调(click、scroll 等)
  • 微任务Promise.then/catchasync/await(对前端来说)

微任务比宏任务执行更早

js
console.log(100);
setTimeout(() => console.log(200));       // 宏任务
Promise.resolve().then(() => console.log(300)); // 微任务
console.log(400);
// 输出:100 → 400 → 300 → 200

宏任务 vs 微任务与 DOM 渲染的关系

  • 微任务:DOM 渲染触发(ES 规范内,JS 引擎处理,无需浏览器帮助)
  • 宏任务:DOM 渲染触发(浏览器处理)

微任务队列示意宏任务队列示意

Event Loop 完整渲染流程

每一次 Call Stack 结束后:

DOM渲染与Event Loop

JS Event Loop vs 浏览器 Event Loop

特性JS Event Loop浏览器 Event Loop
主要任务处理 JS 代码执行处理 JS + 渲染 + 用户输入
任务队列微任务 + 宏任务还包括渲染队列、用户输入队列
渲染不直接涉及包括重排/重绘任务

四、Promise

三种状态

状态含义触发条件
pending初始状态,进行中创建后的默认状态
fulfilled成功完成执行 resolve(value)
rejected失败执行 reject(reason)

状态变化不可逆pending → fulfilledpending → rejected

Promise状态示意

状态与回调的关系

  • pending 状态:不触发 thencatch
  • fulfilled 状态:触发后续 .then 回调
  • rejected 状态:触发后续 .catch 回调
js
const p1 = Promise.resolve(100);
p1.then(data => console.log('data1=' + data))
  .catch(err => console.error('err1=' + err));

const p2 = Promise.reject('错误');
p2.then(data => console.log('data2=' + data))
  .catch(err => console.error('err2=' + err)); // 触发这里

then 和 catch 对状态的影响

  • 链式处理中,正常 → 触发后续 then
  • 只要抛错 → 触发后续 catch
  • 不管 then 还是 catch 处理完毕,只要不继续抛错,之后状态就是 fulfilled

Promise 面试题

Promise面试题1

打印:1 3

Promise面试题2

打印:1 2 3

Promise面试题3

打印:1 2

手写 Promise 加载图片

js
function loadImg(url) {
  return new Promise((resolve, reject) => {
    const img = document.createElement('img');
    img.onload = () => resolve(img);
    img.onerror = () => reject(new Error(`图片加载错误:${url}`));
    img.src = url;
  });
}

loadImg('https://example.com/img.jpg')
  .then(img => {
    console.log(`图片宽高:${img.width}, ${img.height}`);
    return img;
  })
  .then(img => {
    document.body.append(img);
  })
  .catch(err => console.error(err));

五、async / await

目的

解决嵌套异步回调地狱(callback hell),使用同步写法彻底消灭回调函数。

基本用法

js
async function loadImg1() {
  const img1 = await loadImg('https://example.com/img1.jpg');
  return img1;
}

(async function() {
  try {
    const img1 = await loadImg1();
    console.log(img1);
  } catch (ex) {
    console.error(ex);
  }
})();

与 Promise 的关系

  • async 函数返回的都是 Promise 对象
    js
    async function fn1() { return 100; }
    console.log(fn1()); // Promise {<fulfilled>: 100}
    // 相当于 Promise.resolve(100)
  • await 相当于 Promise 的 then
  • try...catch 代替 Promise 的 catch

await 的行为

js
// await Promise 对象:等待 resolve 后继续
(async function() {
  const p = Promise.resolve(100);
  const res = await p;
  console.log(res); // 100
})();

// await 非 Promise:直接返回值
(async function() {
  const res = await 100;
  console.log(res); // 100
})();

// await rejected Promise:抛错,需 try...catch
(async function() {
  const p = Promise.reject('some err');
  try {
    const res = await p;
  } catch (ex) {
    console.error(ex); // 'some err'
  }
})();

总结

  • async 封装 Promise
  • await 处理 Promise 成功(相当于 .then
  • try...catch 处理 Promise 失败(相当于 .catch

异步本质

await 是同步写法,但本质仍是异步调用,基于 Event Loop:

js
async function async1() {
  console.log('async1 start');
  await async2();
  console.log('async1 end'); // 相当于 .then 回调,是微任务
}
async function async2() {
  console.log('async2');
}
console.log('script start');
async1();
console.log('script end');
/*
输出:
script start
async1 start
async2
script end
async1 end
*/

forEach vs for...of 中的 await

js
// ❌ forEach:并发执行(同时触发所有异步,1秒后同时打印)
async function test1() {
  [1, 2, 3].forEach(async x => {
    const res = await multi(x);
    console.log(res); // 1, 4, 9 同时打印
  });
}

// ✅ for...of:串行执行(每隔 1 秒打印一个)
async function test2() {
  for (let x of [1, 2, 3]) {
    const res = await multi(x);
    console.log(res); // 1, 4, 9 依次打印
  }
}

async/await面试题

答案:Promise(100), 100


六、for await...of

用于遍历异步可迭代对象(async iterable),逐个等待 Promise 解析。

js
function createPromise(val) {
  return new Promise(resolve => setTimeout(() => resolve(val), 1000));
}

async function fetchData() {
  const promises = [
    createPromise('数据1'),
    createPromise('数据2'),
    createPromise('数据3')
  ];

  for await (const data of promises) {
    console.log(data); // 依次打印 "数据1", "数据2", "数据3"
  }
}
fetchData();

对比Promise.all 一次性等待所有完成,返回结果数组:

js
Promise.all([p1, p2, p3]).then(res => console.log(res)); // [数据1, 数据2, 数据3]

七、手写 Promise

js
class MyPromise {
  state = 'pending';
  value = undefined;
  reason = undefined;
  resolveCallbacks = [];
  rejectCallbacks = [];

  constructor(func) {
    const resolveHandler = (value) => {
      if (this.state === 'pending') {
        this.state = 'fulfilled';
        this.value = value;
        this.resolveCallbacks.forEach(fn => fn());
      }
    };
    const rejectHandler = (reason) => {
      if (this.state === 'pending') {
        this.state = 'rejected';
        this.reason = reason;
        this.rejectCallbacks.forEach(fn => fn());
      }
    };
    try {
      func(resolveHandler, rejectHandler);
    } catch (err) {
      rejectHandler(err);
    }
  }

  then(fn1, fn2) {
    fn1 = typeof fn1 === 'function' ? fn1 : v => v;
    fn2 = typeof fn2 === 'function' ? fn2 : e => { throw e; };

    if (this.state === 'pending') {
      return new MyPromise((resolve, reject) => {
        this.resolveCallbacks.push(() => {
          try { resolve(fn1(this.value)); } catch (err) { reject(err); }
        });
        this.rejectCallbacks.push(() => {
          try { resolve(fn2(this.reason)); } catch (err) { reject(err); }
        });
      });
    }
    if (this.state === 'fulfilled') {
      return new MyPromise((resolve, reject) => {
        try { resolve(fn1(this.value)); } catch (err) { reject(err); }
      });
    }
    if (this.state === 'rejected') {
      return new MyPromise((resolve, reject) => {
        try { resolve(fn2(this.reason)); } catch (err) { reject(err); }
      });
    }
  }

  catch(fn) {
    return this.then(v => v, fn);
  }
}

// 静态方法
MyPromise.resolve = value => new MyPromise(resolve => resolve(value));
MyPromise.reject = reason => new MyPromise((_, reject) => reject(reason));

MyPromise.all = function(promiseList = []) {
  return new MyPromise((resolve, reject) => {
    const resultList = [];
    let count = 0;
    promiseList.forEach(p => {
      p.then(data => {
        resultList.push(data);
        if (++count >= promiseList.length) resolve(resultList);
      }).catch(err => reject(err));
    });
  });
};

MyPromise.race = function(promiseList = []) {
  return new MyPromise((resolve, reject) => {
    let settled = false;
    promiseList.forEach(p => {
      p.then(data => {
        if (!settled) { resolve(data); settled = true; }
      }).catch(err => reject(err));
    });
  });
};

<< 返回首页