JavaScript 异步编程

目录

请简述 JavaScript 异步编程

JavaScript 异步编程是一种处理非阻塞操作的编程方式,允许程序在等待某些操作完成时继续执行其他代码。这对于处理如网络请求、文件操作等可能耗时的任务特别重要。以下是 JavaScript 异步编程的主要方法和概念:

  1. 回调函数 (Callbacks)
    • 最基本的异步编程方式
    • 将一个函数作为参数传递给另一个函数,在操作完成时调用
    • 容易导致“回调地狱“,使代码难以维护
  2. Promise
    • ES6 引入的异步编程解决方案
    • 代表一个异步操作的最终完成或失败
    • 使用 .then() 和 .catch() 方法处理结果和错误
    • 可以链式调用,避免回调地狱
  3. Async/Await
    • 基于 Promise 的语法糖,使异步代码更易读
    • async 关键字用于声明异步函数
    • await 关键字用于等待 Promise 解决
    • 使异步代码看起来像同步代码
  4. 事件循环 (Event Loop)
    • JavaScript 运行时的核心机制
    • 管理异步操作的执行顺序
    • 包括宏任务队列和微任务队列
  5. 定时器函数
    • setTimeout() 和 setInterval() 用于延迟执行或定期执行代码
  6. 事件监听器
    • 用于响应用户交互或其他事件
  7. Fetch API
    • 用于进行网络请求的现代接口
    • 返回 Promise,便于处理响应
  8. Web Workers
    • 允许在后台线程中运行脚本,不影响主线程性能

Promise 专题

要点总结

  1. Promise的状态一经改变就不能再改变
  2. .then.catch都会返回一个新的Promise
  3. catch不管被连接到哪里,都能捕获上层的错误。
  4. Promise中,返回任意一个非 promise 的值都会被包裹成 promise 对象,例如return 2会被包装为return Promise.resolve(2)
  5. Promise 的 .then 或者 .catch 可以被调用多次, 当如果Promise内部的状态一经改变,并且有了一个值,那么后续每次调用.then或者.catch的时候都会直接拿到该值
  6. .then 或者 .catch 中 return 一个 error 对象并不会抛出错误,所以不会被后续的 .catch 捕获
  7. .then 或 .catch 返回的值不能是 promise 本身,否则会造成死循环
  8. .then 或者 .catch 的参数期望是函数,传入非函数则会发生值透传
  9. .then方法是能接收两个参数的,第一个是处理成功的函数,第二个是处理失败的函数,再某些时候你可以认为catch.then第二个参数的简便写法。
  10. .finally方法也是返回一个Promise,他在Promise结束的时候,无论结果为resolved还是rejected,都会执行里面的回调函数。

打印 new Promise 的效果

Promise { <fulfilled>: value }   // 注意:不是 Promise { <resolved> } 
Promise { <pending> }
Promise { <rejected>: reason }  

没有 resolve不会执行 then

const promise = new Promise((resolve, reject) => {
  console.log(1);
  console.log(2);
});
promise.then(() => {
  console.log(3);
});
console.log(4);

//**************************************
// output:
// 1 2 4
//
// 3 不会被输出,因为 Promise里面没有 resolve
//**************************************

注意 resolve 后会执行 then

const promise1 = new Promise((resolve, reject) => {
  console.log("promise1");
  resolve("resolve1");
});
const promise2 = promise1.then((res) => {
  console.log(res);
});
console.log("1", promise1);
console.log("2", promise2);

//**************************************
// output: 下面序号表示每次打印的顺序
// ① promise1
// ② 1 Promise { <resolved> }  // 因为 promise1 是 resolved 状态
// ③ 2 Promise { <pending> }   // 因为 promise2 是 pending 状态
// ④ resolve1  // 因为 promise1 是 resolved 状态,所以 promise2 的 then 方法会执行
//**************************************

setTimeout 里面嵌入 Promise 的场景

图片&文件

打印效果:

'start'
'timer1'
'timer2'
'timer3'  // 这个还是最后执行,setTimeout 里面嵌入 setTimeout
'start'
'timer1'
'promise'
'timer2'

catch不管被连接到哪里,都能捕获上层的错误,并且catch()也会返回一个Promise

const promise = new Promise((resolve, reject) => {
  reject("error");
  resolve("success2");
});
promise
  .then((res) => {
    console.log("then1: ", res);
  })
  .then((res) => {
    console.log("then2: ", res);
  })
  .catch((err) => {
    console.log("catch: ", err);
  })
  .then((res) => {
    console.log("then3: ", res);
  });

// Output:
// catch:  error
// then3:  undefined

return 2 会被包装成resolve(2)

Promise.resolve(1)
  .then((res) => {
    console.log(res);
    return 2; // 等价于 resolve(2)
  })
  .catch((err) => {
    return 3;
  })
  .then((res) => {
    console.log(res);
  });

// output: 1 2

resolve 后每个 then 和 catch 能被调用多次

Promise 的 .then 或者 .catch 可以被调用多次,但这里 Promise 构造函数只执行一次。或者说 promise 内部状态一经改变,并且有了一个值,那么后续每次调用 .then 或者 .catch 都会直接拿到该值。

const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    console.log("timer");
    resolve("success");
  }, 1000);
});
const start = Date.now();
promise.then((res) => {
  console.log(res, Date.now() - start);
});
promise.then((res) => {
  console.log(res, Date.now() - start);
});

// Output:
// timer
// success 1003
// success 1003 or success 1004

return new Error('error!!!') 不走 reject

Promise.resolve()
  .then(() => {
    return new Error("error!!!");
  })
  .then((res) => {
    console.log("then: ", res);
  })
  .catch((err) => {
    console.log("catch: ", err);
  });

// Output:
// then:  Error: error!!!

走 catch 的场景

Promise.resolve()
  .then(() => {
    return Promise.reject(new Error("error!!!"));
    // or
    throw new Error("error!!!");
  })
  .then((res) => {
    console.log("then: ", res);
  })
  .catch((err) => {
    console.log("catch: ", err);
  });

// Output:
// catch:  Error: error!!!

死循环

const promise = Promise.resolve().then(() => {
  return promise;
})
promise.catch(console.err)

值穿透

// 发生了 值穿透
Promise.resolve(1)
  .then(2)
  .then(Promise.resolve(3))
  // console.log 是一个函数,它被作为参数传递给 .then()
  // 当 Promise 解析时,.then() 会调用这个函数,并将前一个 Promise 的值作为参数传入
  // 所以这里的 console.log 会打印出 1
  .then(console.log);
  • .then 或者 .catch 的参数期望是函数,传入非函数则会发生值透传
  • .then()执行传入的函数参数,并把参数传递给它

下面打印出 4

Promise.resolve(1)  
    .then(2)  
    .then(Promise.resolve(3))  
    .then(() => {  
        return Promise.resolve(4);  
    }).then(console.log)  
  
// 打印结果 4

需要注意是否有 reject 函数

.then()方法的第二个参数reject也是可以捕获错误

Promise.reject("err!!!")
  .then(
    (res) => {
      // 这里不会执行
      console.log("success", res);
    },
    // 这里会执行: error err!!!
    (err) => {
      console.log("error", err);
    },
  )
  .catch((err) => {
    console.log("catch", err);
  });

finally 虽然一定会执行,但还是有顺序的

Promise.resolve("2")
  .finally(() => {
    console.log("f2");
    return "3";
  })
  .then((res) => {
    console.log(res);
  });
// 打印结果 f2 2

Promise.resolve("1")
  .finally(() => {
    console.log("f1");
    throw new Error("f1 error");
  })
  .then((res) => {
    console.log(res);
  })
  .catch((err) => {
    console.log(err);
  });

// 打印结果:
// f1
// VM327:10 Error: f1 error
//     at <anonymous>:4:11
//     at <anonymous>

finally()也是微任务队列

function promise1() {
  return new Promise((resolve) => {
    console.log("p1");
    resolve("1");
  });
}
function promise2() {
  return new Promise((resolve, reject) => {
    console.log("p2");
    reject("err");
  });
}
promise1()
  .then((res) => console.log(res))
  .catch((err) => console.log(err))
  // 第一轮,不会将 finally 里的函数放入微任务队列
  .finally(() => console.log("f1"));

promise2()
  .then((res) => console.log(res))
  .catch((err) => console.log(err))
  .finally(() => console.log("f2"));

// p1 p2 1 err f1 f2

.catch()函数能够捕获到.all()最先的那个异常,并且只执行一次

[!danger]

  • 并不是只要有异常就完事了,每个都会执行
  • catch 只捕获最新的那个异常,并且执行一次
  • Promise.all().then()结果中数组的顺序和Promise.all()接收到的数组顺序一致
function runAsync(x) {
  return new Promise((resolve) =>
    setTimeout(() => resolve(x, console.log(x)), 1000),
  );
}
function runReject(x) {
  return new Promise((resolve, reject) =>
    setTimeout(() => reject(`Error: ${x}`, console.log(x)), 1000 * x),
  );
}
Promise.all([runAsync(1), runReject(4), runAsync(3), runReject(2)])
  .then((res) => console.log(res))
  .catch((err) => console.log(err));

1; // 1s后输出
3; // 1s后输出
2; // 2s后输出
Error: 2; // 2s后输出
4; // 4s后输出

再看下面一段代码,第 4s 即使 reject 了,也不再触发17 行了

function runAsync(x) {
  return new Promise((resolve) =>
    setTimeout(() => resolve(x, console.log(x)), 1000),
  );
}
function runReject(x) {
  return new Promise((resolve, reject) =>
    setTimeout(() => reject(`Error: ${x}`, console.log(x)), 1000 * x),
  );
}
Promise.all([runAsync(1), runReject(4), runAsync(3), runReject(2)])
  .then(
    (res) => console.log(res),
    // 添加 reject 回调函数参数
    (reason) => console.log("reject", reason),
  )
  // 这段代码不执行
  .catch((err) => console.log("catch:", err));

1; // 1s 后输出 1
3; // 1s 后输出 3
2; // 2s 后输出 2
reject Error: 2; // 2s 后输出 Error: 2
4; // 4s 后输出 4

现在,让我们解释为什么上面 catch 没有被触发:

  1. Promise.all 的特性:
    • 当其中任何一个 Promise 被拒绝时,Promise.all 就会立即拒绝,并返回第一个被拒绝的 Promise 的原因。
    • 在这个例子中,runReject(2) 会在 2 秒后首先被拒绝。
  2. then 方法的处理:
    • 您在 then 方法中提供了两个回调函数:
      • 一个用于成功(第一个参数)
      • 一个用于失败(第二个参数)。
    • 当 Promise.all 被拒绝时,它会调用 then 中的第二个回调函数(失败回调)。
    • 在这个例子中,失败回调 (reason) => console.log("reject", reason) 被调用。
  3. 错误被处理:
    • 一旦错误在 then 的第二个回调中被处理,JavaScript 认为这个错误已经被“捕获“和“处理“了。
    • 因此,错误不会继续传播到后面的 catch 语句。
  4. catch 的作用:
    • ==catch 主要用于捕获在前面的 then 链中未被处理的错误。==
    • 在这个例子中,错误已经在 then 的第二个回调中被处理了,所以 catch 不会被触发

总结就是:Promise.all 只处理一个异常,其他的都被吞了

race会获取最新有结论的任务,然后走后面then或者 catch,其他的正常执行,但不走后面的then和 catch 了,执行结果会被抛弃

function runAsync(x) {
  return new Promise((r) => setTimeout(() => r(x, console.log(x)), 1000));
}
Promise.race([runAsync(1), runAsync(2), runAsync(3)])
  // 只需要有一个promise实例率先改变状态,新的Promise状态就跟着改变
  // 然后就会调用then方法,绑定回调函数
  // 之后就不会在理会其他promise实例的状态,但还是会继续执行
  // 只不过不走后面 then 和 catch 方法了
  .then((res) => console.log("result: ", res))
  .catch((err) => console.log(err));

// 1
// 'result: ' 1
// 2
// 3

await async2() 会立即同步执行 async2

async function async1() {
  console.log("1");
  // 这里 async2() 会立即执行,所以立即同步打印 3
  await async2();
  console.log("2");
}
async function async2() {
  console.log("3");
}
async1();
console.log("4");

// 打印结果 1 3 4 2

并不需要 sync 一定返回一个 promise,然后才执行下面的东西

async function async1() {
  console.log("1");
  await async2();
  console.log("2");
}

// 并不需要 async2 一定返回一个 promise
// 下面这个没有返回值,那么返回 undefined
async function async2() {
  // 下一轮宏任务,所以最后执行
  setTimeout(() => {
    console.log("3");
  }, 0);
  console.log("4");
}
async1();
console.log("5");

1;
4;
5;
2;
3;

注意 setTimeout(fn,0) 的第一次解析的顺序

async function async1() {
  console.log("1");
  await async2();
  console.log("2");
  setTimeout(() => {
    console.log("3");
  }, 0);
}
async function async2() {
  setTimeout(() => {
    console.log("4");
  }, 0);
  console.log("5");
}

async1();

// 先于第 5 行解析到
setTimeout(() => {
  console.log("6");
}, 0);

console.log("7");

// 1 5 7 2 4 6 3

async 的返回值

async function fn() {
  // return await 1234
  // 等同于 相等于 resolve(123)
  return 123;
}
fn().then((res) => console.log(res));

await new Promise() 注意有没有 resolve 或者 reject

async function async1() {
  console.log("1");
  // Promise是没有返回值的,也就是它的状态始终是pending状态
  // 因此相当于一直在await,后面的 3 和 4 不会被打印
  await new Promise((resolve) => {
    console.log("2");
    // 下面的注释放开就能正常打印所有的 log
    // resolve("22");
  });
  console.log("3");
  return "4";
}
console.log("5");
async1().then((res) => console.log(res));
console.log("6");

// 5 1 2 6

如果在async函数中抛出了错误,则终止错误结果,不会继续向下执行

async function async1() {
  await async2();
  // 下面都是执行了,因为 async2 报错了
  console.log("2");
  return "3";
}
async function async2() {
  return new Promise((resolve, reject) => {
    console.log("3");
    reject("e4");
  });
}
async1().then((res) => console.log(res));

// 3
// Uncaught (in promise) e4