【JavaScript】异步和期约

概述

  • JavaScript 是单线程的,因此异步机制对于避免阻塞主线程(如 UI 呈现)和耗时的操作(如网络请求、文件读取和计时器)非常重要。
  • 回调函数是早期的异步实现,但容易出现"回调地狱"问题。
  • ES6 添加了一个正式的 Promise 引用类型,允许您优雅地定义和组织异步逻辑。
  • ES2017引入了基于承诺的语法糖async/await,使异步代码更接近同步风格。

异步编程

同步和异步

同步:任务按照代码的顺序逐一执行(执行顺序与代码书写顺序一致),每个任务必须等上一个任务完成后才能开始(阻塞)。

更准确地讲,同步行为是指令按顺序严格执行,执行后变量的值也能立即从寄存器或内存获取,同步代码的执行状态可以说是一目了然,可以被清晰地分析。

异步:任务不需要等待其他任务完成即可开始,耗时操作可以在后台处理,主线程继续执行其他任务。

异步行为与系统中断类似,中断是计算机系统中一种机制,允许外部事件或硬件设备打断当前正在运行的程序,并执行一段特定的处理代码(中断服务程序,ISR),当前进程会被暂时挂起,中断处理完成后恢复执行。

异步行为无法知晓代码的执行状态,就像一个黑盒。

对于需要暂停或等待的任务,异步比同步更加高效,因此同步操作适合短时间内可以完成的简单任务,异步操作则适合如网络请求、文件读写等耗时的任务。

旧异步编程模式:回调

在早期的 JavaScript 中,只支持定义回调函数来表明异步操作完成。回调是指通过将函数作为参数传递,在任务完成后调用。下面逐步演示使用回调的异步操作。

(1)异步操作:

javascript 复制代码
function double(value) {
  setTimeout(console.log, 1000, value);
}
double(3);

(2)传递回调函数作为参数并获得异步操作的返回值:

javascript 复制代码
function double(value, callback) {
  setTimeout(() => callback(value), 1000);
}
double(3, (x) => console.log(`Success: ${x}`);

(3)传递失败回调:

javascript 复制代码
function double(value, success, failure) {
  setTimeout(() => {
    try {
      if (typeof value !== 'number') {
        throw 'Must provide number as first argument';
      }
      success(value);
    } catch (e) {
      failure(e);
    }
  }, 1000);
}
const successCallback = (x) => console.log(`Success: ${x}`);
const failureCallback = (e) => console.log(`Failure: ${e}`);
double(3, successCallback, failureCallback);
double('b', successCallback, failureCallback);

到这儿已经初见 Promise 端倪了。

(4)回调地狱

然而使用这种方式获取的异步返回值只在回调函数内可用,回调结束后也就销毁了。而且,如果异步返回值又依赖另一个异步返回值,就会形成嵌套的回调,即回调地狱:

javascript 复制代码
function double(value, callback) {
  setTimeout(() => callback(value*2), 1000);
}
double(3, (x)=>
    double(x, (y) => 
        double(y, (z) => console.log(`Success: ${z}`)
    )
);

Promise

Promise 可以看作是对未知状态的一个约定,对不存在结果的一个替代。

timeline title Promise 规范发展时间轴 section 关键里程碑 2010 : Promises/A 规范发布(CommonJS 项目) 2012 : Promises/A+ 规范分叉
(解决 Promises/A 不足,明确细节) 2015 : ES6 正式支持 Promise 类型
(实现 Promises/A+ 规范)

Promise 基础

new Promise 创建期约

创建新期约时必须传入执行器(executor)函数作为参数,哪怕是空函数,如果不提供执行器函数,就会抛出 SyntaxError。

javascript 复制代码
let p = new Promise((resolve, reject) => {});
setTimeout(console.log, 0, p);   // Promise <pending>

Promise 的三个状态

stateDiagram-v2 Pending --> Fulfilled : resolve() Pending --> Rejected : reject()
  • Pending:初始状态,既未完成,也未失败。
  • Fulfilled 或 Resolved(已完成):操作成功完成,返回结果。
  • Rejected(已失败):操作失败,返回原因。

在待定状态下,Promise 可以落定(settle)为代表成功的兑现状态,或者代表失败的拒绝状态,无论落定为哪种状态都是不可逆的。并且 Promise 的状态是私有的,不能直接被外部 JS 检测到,于是也不能被修改。

Promise 的两大用途

Promise 主要有两大用途。

  1. 表示一个异步操作。前面已经说过了异步操作的三种状态,对于一些用例来说,状态表示已经足够。比如,请求成功,请求状态转为ResolvedFulfilled;请求失败,Promise状态转为Rejected
  2. 生成一个值。Promise状态改变后,程序会获取到一个值。Promise 成功,该值为解决值;Promise 失败,该值为拒绝理由,一般为 Error 对象。

上述两大用途都是通过执行器函数实现的,下节具体介绍。

执行器函数的作用

(1)初始化 Promise 的异步行为

Promise 执行器函数中的代码是同步的、立即执行的。通常用于处理网络请求、文件读取等异步操作。

javascript 复制代码
const promise = new Promise((resolve, reject) => {
  console.log('执行器函数同步执行');
  setTimeout(() => {
    resolve('异步操作完成');
  }, 1000);
});

console.log('Promise 已创建');
promise.then(console.log);

// 执行器函数同步执行
// Promise 已创建
// 异步操作完成

(2)控制 Promise 状态的转换

控制 Promise 状态的转换是通过调用执行器函数的两个函数参数 resolve() 和 reject() 实现的。调用 resolve() 会把状态切换为兑现,调用 reject() 会把状态切换为拒绝且会抛出错误。

javascript 复制代码
let p1 = new Promise((resolve, reject) => resolve());
setTimeout(console.log, 0, p1); // Promise <resolved>
let p2 = new Promise((resolve, reject) => reject());
setTimeout(console.log, 0, p2); // Promise <rejected>
// Uncaught error (in promise)

执行器函数是同步执行的,因为执行器函数是 Promise 的初始化程序。

无论 resolve() 和 reject() 中的哪个被调用,状态转换都不可撤销了,继续修改状态会静默失败。

javascript 复制代码
let p = new Promise((resolve, reject) => {
  resolve();
  reject(); // 没有效果
});
setTimeout(console.log, 0, p); //Promise<resolved>

(3)传递解决值和拒绝理由

resolve(value) 用于将操作的结果值传递给后续的 .then()

reject(reason) 用于将错误或失败的原因传递给后续的 .catch()

Promise 的实例方法

Promise 的实例方法是连接外部同步代码与内部异步代码之间的桥梁。

这些方法可以访问异步操作返回的数据,处理 Promise 成功和失败的结果,连续对 Promise 求值,或者添加只有 Promise 进入终止状态时才会执行的代码。

Thenable 接口

在 ECMAScript 暴露的异步结构中,任何对象都有一个 then() 方法,这个方法被认为实现了 Thenable 接口。

只要对象实现了 .then() 方法(一个接受两个参数的函数 onFulfilled 和 onRejected),它就被认为是一个 Thenable。

Thenable 对象可以通过 Promise.resolve() 转换为真正的 Promise,并且可以与原生 Promise 进行交互。

从 Thenable 到 Promise 的过程: 当你将一个 Thenable 对象传递给 Promise 构造函数时,Promise 会通过调用该对象的 then() 方法,并传递给它两个回调函数(resolvereject),来"转换"它为一个真正的 Promise。如果这个 Thenable 对象调用 resolvereject,那么它就会变成一个已解决或已拒绝的 Promise

javascript 复制代码
let thenable = {
  then: function(resolve, reject) {
    setTimeout(() => resolve('Done!'), 1000); // 模拟异步操作
  }
};

let promise = new Promise((resolve, reject) => {
  thenable.then(resolve, reject);  // 将Thenable对象转化为Promise
});

promise.then((value) => {
  console.log(value);  // 输出 "Done!" after 1 second
});

then()

简介:Promise.prototype.then() 是为Promise实例添加处理程序的主要方法。

签名:then(onFulfilled, onRejected)

参数:onFulfilled 是 Promise 成功时调用的回调函数;onRejected 是 Promise 失败时调用的回调函数。如果想只提供 onRejected 参数,那就要在 onResolved 参数的位置上传入 undefined 或 null,避免创建多余的对象。

返回值:then() 返回一个新的 Promise 实例,这使得可以链式调用 then()。这个新 Promise 实例是通过 Promise.resolve() 包装返回值生成的。

示例:

在 Promise 的执行函数或处理程序中抛出错误会导致拒绝,对应的错误对象会成为拒绝的理由。

javascript 复制代码
const promise = new Promise((resolve, reject) => {
  let success = false;
  if (success) {
    resolve("Success!");
  } else {
    reject("Failure!");
  }
});

let p = promise.then(null, (error) => {
  throw new Error(error);
});

setTimeout(console.log, 0, p);
// Promise {<rejected>: Error: Failure!
// Uncaught (in promise) Error: Failure!

catch()

简介:catch() 用于给Promise添加拒绝处理程序,是一个语法糖,相当于 then(null, onRejected)。

签名:catch(onRejected)

参数:

  • onRejected:拒绝处理程序。

返回值:与 then 一样,使用 Promise.resolve() 包装处理程序返回值生成的新Promise实例。

示例:

javascript 复制代码
const promise = new Promise((resolve, reject) => {
  let success = false;
  if (success) {
    resolve("Success!");
  } else {
    reject("Failure!");
  }
});

let p = promise.catch((value) => value);

setTimeout(console.log, 0, p); // Promise {<fulfilled>: 'Failure!'}

finally()

简介:finally() 是为了避免 onResolved() 和 onRejected() 中出现冗余代码。

签名:finally(onFinally())

参数:

  • onFinally() 在 Promise 转换为解决或拒绝状态时都会执行。

返回值:finally() 返回一个新 Promise 实例,该 Promise 实例大多数情况下表现为父Promise的传递,除了onFinally() 返回一个待定 Promise 或者抛出了错误(显示抛出或返回一个拒绝 Promise),则会返回相应的 Promise(待定或拒绝)。

示例:

javascript 复制代码
let p1 = Promise.resolve("foo");
// 这里都会原样后传
let p2 = p1.finally();
let p3 = p1.finally(() => undefined);
let p4 = p1.finally(() => {});
let p5 = p1.finally(() => Promise.resolve());
let p6 = p1.finally(() => "bar");
let p7 = p1.finally(() => Promise.resolve("bar"));
let p8 = p1.finally(() => Error("qux"));
setTimeout(console.log, 0, p2); // Promise <resolved>: foo
setTimeout(console.log, 0, p3); // Promise <resolved>: foo
setTimeout(console.log, 0, p4); // Promise <resolved>: foo
setTimeout(console.log, 0, p5); // Promise <resolved>: foo
setTimeout(console.log, 0, p6); // Promise <resolved>: foo
setTimeout(console.log, 0, p7); // Promise <resolved>: foo
setTimeout(console.log, 0, p8); // Promise <resolved>: foo

let p9 = p1.finally(() => new Promise(() => {}));
let p10 = p1.finally(() => Promise.reject());
// Uncaught (in promise): undefined
setTimeout(console.log, 0, p9); // Promise <pending>
setTimeout(console.log, 0, p10); // Promise <rejected>: undefined

let p11 = p1.finally(() => {
  throw "baz";
});
// Uncaught (in promise) baz
setTimeout(console.log, 0, p11); // Promise <rejected>: baz

Promise 代码的执行顺序

Promise 代码的执行顺序取决于 JavaScript 中事件循环的机制:

  1. 在 JavaScript 中,首先会执行所有同步代码,也就是栈中立即执行的代码。只有同步代码执行完,事件循环才会开始处理异步代码。
  2. Promise 的回调(.then() 或 .catch())是异步执行的,只有当 Promise 状态改变(从 pending 到 resolved 或 rejected)之后,回调才会被加入到微任务队列中。微任务(Promise 的回调、MutationObserver 等) 在当前执行栈清空后、宏任务开始前执行。
  3. 宏任务(如 setTimeout、setInterval、I/O 操作等) 在微任务队列执行后才执行。

示例:

javascript 复制代码
console.log('Start');  // 同步代码,立即执行

const promise = new Promise((resolve, reject) => {
  console.log('Promise started');  // 同步代码,立即执行
  resolve('Promise resolved');  // 设置 Promise 完成
});

promise.then((result) => {
  console.log(result);  // 微任务,Promise 回调
});

setTimeout(() => {
  console.log('setTimeout');  // 宏任务
}, 0);

console.log('End');  // 同步代码,立即执行

输出:

javascript 复制代码
Start
Promise started
End
Promise resolved
setTimeout

Promise 的错误处理

Note the following when an error is thrown in a Promise:

  1. Promise 中抛出错误会导致Promise的状态转为拒绝;
  2. Promise 中抛出的错误无法被同步的 try...catch 捕获;
  3. Promise 中抛出错误不会阻止后续代码执行。
javascript 复制代码
try { 
  let promise = new Promise((resolve, reject) => {
    throw new Error('error');
  });
  setTimeout(console.log, 0, promise);
} catch (e) {
  console.log(e);
}

// Promise {<rejected>: Error: error
// Uncaught (in promise) Error: error

Promise 合成

Promise 类提供两个将多个 Promise 实例组合成一个 Promise 的静态方法:Promise.all() 和 Promise.race()。而合成后的 Promise 的行为取决于内部 Promise 的行为。

Promise.all()

简介:Promise.all() 方法接收一个可迭代对象(如数组),其中的元素是 Promise 对象或值,并返回一个新的 Promise。

签名:Promise.all(iterable)

参数:

  • iterable:一个可迭代对象(如数组或 Set),其中的元素是 Promise 对象或其他值。非 Promise 的值会被 Promise.resolve() 包装成 Promise。

返回值:返回一个新的 Promise。当所有传入的 Promise 都成功(fulfilled)时,返回的 Promise 将被解决(fulfilled),其值是所有 Promise 解析值组成的数组。如果任意一个传入的 Promise 被拒绝(rejected),返回的 Promise 立即被拒绝,其原因就是第一个被拒绝的 Promise 的拒绝原因。

示例:

javascript 复制代码
Promise.all([
  Promise.resolve(1),
  Promise.resolve(2),
  Promise.resolve(3)
]).then((value) => {
  console.log(value);
}); // [1, 2, 3]

Promise.all([
  Promise.resolve(1),
  Promise.reject(2),
  Promise.reject(3)
]).then((value) => {
  console.log(value);
}).catch((error) => {
  console.log(error);
}); // 2

Promise.race()

简介:Promise.race() 方法接受一个可迭代对象(如数组或 Set),其中的元素是 Promise 对象或值,并返回一个新的 Promise。这个新的 Promise 将由输入的第一个完成(无论是解决 fulfilled 还是拒绝 rejected)的 Promise 决定其状态和返回值。

签名:Promise.race(iterable)

参数:

  • iterable:一个可迭代对象(如数组或 Set),其中的元素是 Promise 对象或其他值。非 Promise 的值会被 Promise.resolve() 包装成 Promise。

返回值:一旦 iterable 中的某个 Promise 最先完成(无论成功或失败),返回的 Promise 会采用该完成的状态和返回值。如果传入的 iterable 是空的,返回的 Promise 处于 pending 状态。如果传入的 iterable 中包含非 Promise 值,则这些值会被立即解析。

示例:

javascript 复制代码
Promise.race([
  new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(1)
    }, 1000)
  }),
  new Promise((resolve, reject) => {
    setTimeout(() => {
      reject(2)
    }, 500)
  }),
  new Promise((resolve, reject) => {
    setTimeout(() => {
      reject(3)
    }, 300)
  })
]).then(value => {
  console.log(value); // Uncaught (in promise) 3
});

async/await

async/await 是在 ES8 规范中引入的新特性,旨在通过语法和行为上的改进,使 JavaScript 能够以更接近同步代码的方式执行异步操作,让我们更方便地编写异步代码,避免依赖回调函数和过度嵌套的 .then() 调用。

async

async 关键字用于声明异步函数,可以用在函数声明、函数表达式、箭头函数和方法上。

使用 async 关键字让函数成为异步函数,但函数内部仍按同步方式执行。

异步函数返回的值会被隐式地包装在一个 Promise 里,但这与 Promise.resolve 又有所不同,对于 Promise 类型的返回值,Promise.resolve 会返回相同的引用,异步函数会返回一个新的 Promise。

javascript 复制代码
const p = new Promise((res, rej) => {
  res(1);
});

async function asyncReturn() {
  return p;
}

function basicReturn() {
  return Promise.resolve(p);
}

console.log(p === basicReturn()); // true
console.log(p === asyncReturn()); // false

与在 Promise 处理程序中一样,在异步函数中抛出错误会返回拒绝的 Promise。不过,拒绝 Promise 的错误不会被异步函数捕获:

javascript 复制代码
async function foo() {
  console.log(1);
  Promise.reject(3);
}
// Attach a rejected handler to the returned promise
foo().catch(console.log);
console.log(2);
// 1
// 2
// Uncaught(inpromise): 3

await

简介

await 操作符被用于等待(或者说是解包,unwrap)一个 Promise 并获取它的解决值,同时暂停异步函数执行。等该 Promise 落定(settled)后,异步函数恢复执行,await 表达式的值变成这个 Promise 的落定值

语法

await expression

expression 可以是一个 Promise、一个 Thenable 对象或者任意非 Thenable 值。expression 的解包方式与 Promise.resolve 相同,总会转换为一个原生 Promise 然后等待它。

  • 若 expression 是 Promise,则它直接被使用;
  • 若 expression 是 Thenable,则 Thenable 会转换为 Promise;
  • 若 expression 是 non-Thenable,则 non-Thenable 会被转换为 fulfilled promise。

返回值

promise 或者 thenable 对象的解决值,或者如果表达式不是 thenable,那么返回表达式本身的值。

异常

如果 promise 或者 thenable 对象被拒绝,那么会抛出拒绝原因。

javascript 复制代码
async function f() {
  // 使用 try...catch 
  // try {
  //   const response = await Promise.reject(30);
  // } catch (e) {
  //   console.error(e); // 30
  // }
  
  // 链接 .catch 处理程序
  const response = await Promise.reject(30).catch((e) => {
    console.error(e); // 30
    return "default desponse";
  });
}

f();

示例

developer.mozilla.org/en-US/docs/...

Control flow effect of await

await 暂停当前函数执行,依赖于 await 的代码会被推入微任务队列(microtask queue),但不会阻塞主线程,主线程继续执行其他任务。即使等待的值是已解决的 Promise 或者非 Promise,也会是这个行为。

javascript 复制代码
async function foo(name) {
  console.log(name, "start");
  await console.log(name, "middle");
  console.log(name, "end");
}

foo("First");
foo("Second");

// First start
// First middle
// Second start
// Second middle
// First end
// Second end

这相当于:

javascript 复制代码
function foo(name) {
  return new Promise((resolve) => {
    console.log(name, "start");
    resolve(console.log(name, "middle"));
  }).then(() => {
    console.log(name, "end");
  });
}

注意点

(1)await 只能被用在异步函数或者模块的顶部。

javascript 复制代码
// 假设这是一个模块文件,例如 topLevelAwait.mjs

// 你可以直接使用 await 在模块顶层
const response = await fetch('https://jsonplaceholder.typicode.com/todos/1');
const data = await response.json();

console.log(data);

模块的顶部(the top level of module)不是指视觉位置上的顶部,而是说模块的顶级作用域,不包含在任何函数、类、对象内部。

(2)异步函数的特质不会扩展到嵌套函数。因此,await 不能出现在嵌套的同步函数内。

javascript 复制代码
// 不允许:await出现在了同步函数中
async function foo() {
  const syncFn = () => {
    return await Promise.resolve('foo');
  };
  console.log(syncFn());
}
相关推荐
Monly213 小时前
Vue:Table合并行于列
前端·javascript·vue.js
子非鱼9214 小时前
使用ES5和ES6求函数参数的和、解析URL Params为对象
前端·javascript·es6
zhanggongzichu4 小时前
零基础Vue入门6——Vue router
前端·javascript·vue.js·vue3·路由·vue router
NoneCoder4 小时前
JavaScript系列(64)--响应式状态管理实现详解
开发语言·javascript·ecmascript
曹二7474 小时前
HTML&CSS&JS
javascript·css·html
狗都不学爬虫_6 小时前
JS逆向案例-ali231补环境 - 14
开发语言·javascript·原型模式
橙子家czzj6 小时前
DES & 3DES 简介 以及 C# 和 js 实现【加密知多少系列_2】
javascript·elasticsearch·c#
hx_11996 小时前
ES6-代码编程风格(数组、函数)
前端·javascript·es6
还是鼠鼠6 小时前
使用 Axios ——个人信息修改与提示框实现
前端·javascript·vscode·ajax·bootstrap·css3·html5