异步编程思想

相关问题

如何解决Promise 地狱问题?

所谓的"Promise 地狱",通常指的是代码中存在多层嵌套的Promise 调用,这种情况会使代码难以理解和维护。解决Promise 地狱的常见方法包括:

  • 链式调用:利用 Promise 的.then()方法可以返回另一个 Promise,你可以通过链式调用的方式来避免深层的嵌套
  • 异步函数 (async/await):使用 ES2017 引入的 async 和 await语法可以使异步代码看起来像同步代码,这样可以极大地提高代码的可读性和可维护性。使用 async 标记的函数总是返回一个 Promise,而 await 关键字可以暂停 async 函数的执行,等待 Promise 解决

Promise.all 和 Promise.race 的区别是什么

  • Promise.all:这个方法接受一个 Promise 对象的数组作为输入,只有当所有的 Promise 对象都变为 fulfilled 状态时,返回的 Promise 才会变为 fulfilled,并将一个数组作为结果传递给处理函数,数组中包含了所有输入 Promise 的结果。如果任何一个输入 Promise 变为 rejected,返回的 Promise 就会立即变为 rejected 状态,且失败的原因会是第一个失败 Promise 的结果
  • Promise.race:这个方法同样接受一个 Promise 对象的数组,但是返回的 Promise 的状态会由第一个改变状态的输入 Promise 决定。也就是说,如果输入数组中的任何一个 Promise 先达到 fulfilled 或 rejected 状态,返回的 Promise 就会立即变为相同的状态,并以那个 Promise 的结果作为返回值

异步处理发展历史

Javascript 的异步编程模型经历了从回调函数到 Promise,再到 async/await 的发展过程,每一步都旨在使异步代码更加清晰和易于管理。

回调函数

  • 回调函数是最初用来处理异步操作的方式。但当异步操作需要连续进行时,就容易出现“回调地狱”,导致代码难以阅读和维护。

Promise

  • 为了解决回调地狱的问题,Promise 被引入为一种更好的异步处理方式。Promise 提供了更好的错误处理机制,并支持链式调用。

Async/Await

  • async/await 是建立在 Promise 之上的,它使异步代码的编写和阅读更接近传统的同步代码

:::info
通过这个发展历程,我们可以看到JavaScript 的异步编程从依赖于回调到使用 Promise,再到通过 async/await 实现更优雅的异步处理方式,这整个过程旨在提高代码的可读性和可维护性,同时降低编写复杂异步逻辑时的错误率。

:::

Promise A+ 规范

  • Promise A+ 规范详细描述了 JavaScript 中 Promise 的行为标准,确保不同的 Promise 实现可以相互兼容。以下是 Promise A+ 规范的所有细节:

相关术语

  • promise: 一个对象或函数,其具有 then 方法,其行为符合本规范。
  • thenable:一个具有 then 方法的对象或函数。
  • value: 任何合法的 JavaScript 值(包括 undefined,一个 thenable,或一个 promise)。
  • exception:一个使用 throw 语句抛出的值。
  • reason:一个表示 promise 被拒绝的原因。

要求

Promise 状态

  • 一个 promise 必须处于以下三种状态之一:pending (等待态),fulfilled(执行态),或 rejected(拒绝态)。
    • pending:可以迁移到 fulfilled 或 rejected 状态。
    • fulfilled:不可迁移到其他状态,必须有一个 value。
    • rejected: 不可迁移到其他状态,必须有一个 reason。

then方法

  • 一个 promise 必须提供一个 then 方法来访问其当前或最终的 value 或 reason
  • promise.then(onFulfilled, onRejected)
    • onFulfilled 和 onRejected 都是可选参数。
    • 如果 onFulfilled 不是函数,必须忽略它。
    • 如果 onRejected 不是函数,必须忽略它。
onFulfilled 的执行
  • 必须在 promise 完成后被调用, 且 promise 的 value 作为其第一个参数。
  • onFulfilled 不得在 promise 完成前被调用。
  • onFulfilled 必须只被调用一次。
onRejected 的执行
  • onRejected 必须在 promise 被拒绝后被调用,且 promise 的 reason 作为其第一个参数。
  • onRejected 不得在promise 被拒绝前被调用。
  • onRejected 必须只被调用一次。
then 方法必须返回一个 promise
promise2 = promisel. then (onFulfilled, onRejected);
// promise2 必须是一个新的 promise
处理返回的值
  • 如果 onFulfilled 或 onRejected 返回一个值 ×,则运行 Promise 解决程序 [[Resolve]] (promise, x).
  • 如果 onFulfilled 或 onRejected 抛出一个异常 e,则 promisez) 必须以e 作为拒绝原因。
  • 如果 onFulfilled 不是一个函数且 promise1 完成, promise2 必须以 promisel 的 value 作为其 value.
  • 如果 onRejected 不是一个函数且 promise1 被拒绝, promise2 必须以 promise1 的 reason 作其 reason.

Promise 解决程序[[Resolve]]

  • 如果 promise 和 × 指向同一对象,则以 TypeError 作为拒绝原因。
  • 如果 × 是一个 promise,采用 × 的状态。
    • 如果 × 是一个对象或函数:
    • 取出 x.then。
    • 如果取 then 时抛出异常,以该异常作为拒绝原因。
    • 如果 then是一个函数,将×作为this调用 then,第一个参数是 resolvePromise,第二个参数是 rejectPromise。
    • 如果 then 不是一个函数,以 × 为值完成 promise。
  • 如果 × 不是对象或函数,以×为值完成 promise。

V8 关于 Promise 实现细节

promise-constructor.tq

手写Promise实现及Promises/A+规范解析

在现代前端开发中,Promise 已经成为了处理异步操作的标准工具之一。然而,虽然我们每天都在使用 Promise,但其背后的实现原理和规范细节却鲜为人知。本文将从零开始,逐步实现一个符合 Promises/A+ 规范的 Promise 库,并深入探讨其工作原理。

一、Promise 的基本概念

Promise 是一种用于处理异步操作的对象。它会返回一个代理对象,该对象在异步操作完成时会有一个确定的状态:fulfilled(已成功)或 rejected(已失败)。无论异步操作是成功还是失败,Promise 都会通知其结果,并提供对结果的处理方法。

1.1 Promise 的状态转移

Promises/A+ 规范定义了三种状态:

  • pending(进行中):初始状态。
  • fulfilled(已成功):异步操作成功完成。
  • rejected(已失败):异步操作失败。
    状态转移只能是单向的:
  • pendingfulfilled
  • pendingrejected
    一旦状态发生转移,就无法再改变。这意味着,一旦 Promise 被解决(resolve)或者拒绝(reject),它将保持该状态,不再受其他操作的影响。

1.2 then 方法

then 方法是 Promise 的核心 API。它接受两个参数:一个成功回调函数和一个失败回调函数。无论 Promise 的最终状态如何,then 方法都会返回一个新的 Promise。

1.3 容器的作用

Promise 的实现需要一个容器,用于存储与该 Promise 相关的状态、回调函数以及其他必要的数据结构。这个容器通常是一个对象,包含以下属性:

  • state:表示 Promise 的当前状态。
  • value:存储最终结果(成功时)或错误信息(失败时)。
  • onFulfilledCallbacks:存储成功回调函数的队列。
  • onRejectedCallbacks:存储失败回调函数的队列。

二、Promise 的基本实现结构

在开始编写代码之前,我们需要明确 Promise 的大致结构。一个基本的 Promise 实现应该包括以下几个部分:

  1. 构造函数:初始化 Promise 的状态和回调队列。
  2. resolve 和 reject 方法:改变 Promise 的状态,并通知相关的回调函数。
  3. then 方法:注册回调函数,并返回一个新的 Promise。
  4. 微任务队列:确保回调函数在合适的时机执行。

三、逐步实现 Promise

3.1 初始化状态和回调队列

我们从构造函数开始,初始化 Promise 的状态和回调队列。

class MyPromise {
  constructor(executor) {
    this.state = 'pending'; // 初始状态
    this.value = undefined; // 存储结果或错误信息
    this.onFulfilledCallbacks = []; // 存储成功的回调
    this.onRejectedCallbacks = []; // 存储失败的回调
    // 立即执行 executor
    try {
      executor((value) => this.resolve(value), (reason) => this.reject(reason));
    } catch (error) {
      this.reject(error);
    }
  }
}

3.2 resolve 方法

resolve 方法用于将 Promise 的状态改为 fulfilled,并将结果存储在 value 属性中。同时,它会触发所有注册的成功回调函数。

class MyPromise {
  // ... 上述代码 ...
  resolve(value) {
    if (this.state !== 'pending') return;
    this.state = 'fulfilled';
    this.value = value;
    // 触发所有成功回调
    this.onFulfilledCallbacks.forEach((callback) => {
      // 使用 setTimeout 模拟微任务
      setTimeout(() => callback(this.value));
    });
    this.onFulfilledCallbacks = []; // 清空队列
  }
}

3.3 reject 方法

reject 方法用于将 Promise 的状态改为 rejected,并将错误信息存储在 value 属性中。同时,它会触发所有注册的失败回调函数。

class MyPromise {
  // ... 上述代码 ...
  reject(reason) {
    if (this.state !== 'pending') return;
    this.state = 'rejected';
    this.value = reason;
    // 触发所有失败回调
    this.onRejectedCallbacks.forEach((callback) => {
      setTimeout(() => callback(this.value));
    });
    this.onRejectedCallbacks = []; // 清空队列
  }
}

3.4 then 方法

then 方法是 Promise 的核心 API,用于注册成功和失败的回调函数。它返回一个新的 Promise,以便支持链式调用。

class MyPromise {
  // ... 上述代码 ...
  then(onFulfilled, onRejected) {
    // 确保 onFulfilled 和 onRejected 是函数
    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : () => {};
    onRejected = typeof onRejected === 'function' ? onRejected : () => {};
    // 返回一个新的 Promise
    return new MyPromise((resolve, reject) => {
      // 根据当前 Promise 的状态,注册相应的回调
      if (this.state === 'fulfilled') {
        setTimeout(() => {
          try {
            const result = onFulfilled(this.value);
            resolve(result);
          } catch (error) {
            reject(error);
          }
        });
      } else if (this.state === 'rejected') {
        setTimeout(() => {
          try {
            const result = onRejected(this.value);
            resolve(result);
          } catch (error) {
            reject(error);
          }
        });
      } else {
        // 如果当前状态是 pending,将回调函数添加到队列中
        this.onFulfilledCallbacks.push(() => {
          try {
            const result = onFulfilled(this.value);
            resolve(result);
          } catch (error) {
            reject(error);
          }
        });
        this.onRejectedCallbacks.push(() => {
          try {
            const result = onRejected(this.value);
            resolve(result);
          } catch (error) {
            reject(error);
          }
        });
      }
    });
  }
}

3.5 执行器函数的错误处理

在构造函数中,我们需要确保执行器函数(executor)中的错误能够被捕获,并将 Promise 的状态设置为 rejected。

class MyPromise {
  constructor(executor) {
    this.state = 'pending';
    this.value = undefined;
    this.onFulfilledCallbacks = [];
    this.onRejectedCallbacks = [];
    try {
      executor((value) => this.resolve(value), (reason) => this.reject(reason));
    } catch (error) {
      this.reject(error);
    }
  }
  // ... 上述代码 ...
}

3.6 处理同步和异步情况

为了让 Promise 的行为更符合规范,我们需要确保所有回调函数在宏任务完成后执行。因此,我们在触发回调时使用 setTimeout 来模拟微任务。

3.7 返回新 Promise

在 then 方法中,我们返回一个新的 Promise,以便支持链式调用。这个新的 Promise 的状态由当前 Promise 的状态和回调函数的执行结果决定。

3.8 处理回调中的错误

在 then 方法中,我们需要确保回调函数中的错误能够被捕获,并将新的 Promise 的状态设置为 rejected。

3.9 处理没有回调的情况

在 then 方法中,如果用户没有提供 onFulfilled 或 onRejected 回调函数,我们需要确保它们不会影响 Promise 的状态。

四、测试和验证

为了确保我们的 Promise 实现符合 Promises/A+ 规范,我们需要进行一系列的测试。以下是一些常见的测试用例:

4.1 测试成功情况

const promise = new MyPromise((resolve, reject) => {
  setTimeout(() => resolve('Success'), 1000);
});
promise.then((value) => {
  console.log(value); // 'Success'
});

4.2 测试失败情况

const promise = new MyPromise((resolve, reject) => {
  setTimeout(() => reject('Error'), 1000);
});
promise.then(null, (reason) => {
  console.log(reason); // 'Error'
});

4.3 测试链式调用

new MyPromise((resolve) => {
  setTimeout(() => resolve(1), 1000);
})
.then((value) => {
  console.log(value); // 1
  return value * 2;
})
.then((value) => {
  console.log(value); // 2
});

4.4 测试错误传播

new MyPromise((resolve) => {
  setTimeout(() => resolve(1), 1000);
})
.then((value) => {
  throw new Error('Error');
})
.then((value) => {
  console.log(value); // 不会执行
}, (error) => {
  console.log(error.message); // 'Error'
});

4.5 测试同步执行

const promise = new MyPromise((resolve) => {
  resolve('Success');
});
promise.then((value) => {
  console.log(value); // 'Success'
});

4.6 测试异步执行

const promise = new MyPromise((resolve) => {
  setTimeout(() => resolve('Success'), 1000);
});
promise.then((value) => {
  console.log(value); // 'Success'
});

五、常见问题解答

5.1 为什么需要使用 setTimeout?

在 Promises/A+ 规范中,所有回调函数必须在当前宏任务完成后执行,以确保代码的同步性和可预测性。使用 setTimeout 可以模拟微任务队列,确保回调函数在合适的时机执行。

5.2 为什么需要返回一个新的 Promise?

then 方法返回一个新的 Promise,以便支持链式调用。每个 then 方法都返回一个新的 Promise,其状态由当前 Promise 的状态和回调函数的执行结果决定。

5.3 为什么需要清空回调队列?

一旦 Promise 的状态发生改变,所有的回调函数都会被触发。为了防止内存泄漏,我们需要清空回调队列。

5.4 为什么需要处理同步和异步情况?

Promises/A+ 规范要求 Promise 的行为必须一致,无论执行器函数是同步还是异步执行。

5.5 为什么需要处理回调中的错误?

如果回调函数中抛出错误,新的 Promise 的状态必须被设置为 rejected,以确保错误能够被正确传播。

async/await

基本语法

在 JavaScript 中,async/await 是一种处理异步操作的语法糖,它使得异步代码的书写更加简洁和直观。async 关键字用于定义一个异步函数,await 则用于等待一个 Promise 对象的解析。

基本用法

async function fetchUser() {
  try {
    const response = await fetch('https://api.github.com/users');
    const data = await response.json();
    return data;
  } catch (error) {
    console.error('Error:', error);
    throw error;
  }
}
fetchUser().then(users => console.log(users));

代码解释

  1. async function fetchUser():定义了一个异步函数 fetchUser
  2. await fetch('https://api.github.com/users'):等待 fetch 请求完成,并将结果赋值给 response
  3. await response.json():等待 response.json() 解析完成,将结果赋值给 data
  4. return data:返回解析后的数据。
  5. catch (error):捕获异步操作中可能发生的错误。

优势

  • 代码可读性async/await 使得异步代码看起来更像同步代码,提高了代码的可读性。
  • 错误处理:可以通过 try/catch 方便地处理异步操作中的错误。
  • 简化回调:避免了多重嵌套的回调函数(callback hell)。

实现原理

async/await 的实现基于 Promise,而其实现底层依赖于 Generator 函数。理解 async/await 的实现原理需要先了解 Generator 函数的相关知识。

Generator 函数

Generator 函数是 ES6 引入的一种特殊的函数,可以暂停和恢复执行。Generator 函数通过 function* 关键字定义,并在函数体内使用 yield 关键字暂停执行。

基本用法
function* generateNumber() {
  yield 1;
  yield 2;
  yield 3;
}
const gen = generateNumber();
console.log(gen.next().value); // 输出 1
console.log(gen.next().value); // 输出 2
console.log(gen.next().value); // 输出 3
代码解释
  1. function* generateNumber():定义了一个 Generator 函数。
  2. yield 1:在函数执行到此处时,暂停执行,并返回 1
  3. gen.next():调用 next() 方法恢复 Generator 的执行,并返回下一个 yield 的值。

将 Generator 转换为 Promise

通过一些库(如 coregenerator),可以将 Generator 函数转换为 Promise。这样,Generator 函数就可以与 async/await 一起使用。

示例代码
const co = require('co');
function* fetchUserGenerator() {
  try {
    const response = yield fetch('https://api.github.com/users');
    const data = yield response.json();
    return data;
  } catch (error) {
    console.error('Error:', error);
    throw error;
  }
}
co(fetchUserGenerator()).then(users => console.log(users));
代码解释
  1. function* fetchUserGenerator():定义了一个 Generator 函数。
  2. yield fetch('https://api.github.com/users'):暂停执行,等待 fetch 请求完成。
  3. yield response.json():暂停执行,等待 response.json() 解析完成。
  4. co(fetchUserGenerator()):将 Generator 函数转换为 Promise

async/await 实现原理

async 函数在底层会被编译为 Generator 函数,并通过 Promise 来处理异步操作。await 关键字的作用相当于 yield,但会自动处理 Promise 的解析。

示例代码
async function fetchUser() {
  try {
    const response = await fetch('https://api.github.com/users');
    const data = await response.json();
    return data;
  } catch (error) {
    console.error('Error:', error);
    throw error;
  }
}
// 编译后的 Generator 代码
function fetchUser() {
  return regeneratorRuntime.async(function* () {
    try {
      const response = yield fetch('https://api.github.com/users');
      const data = yield response.json();
      return data;
    } catch (error) {
      console.error('Error:', error);
      throw error;
    }
  });
}
代码解释
  1. async function fetchUser():定义了一个 async 函数。
  2. await fetch('https://api.github.com/users'):等待 fetch 请求完成,并将结果赋值给 response
  3. await response.json():等待 response.json() 解析完成,将结果赋值给 data
  4. return data:返回解析后的数据。

Generator 函数与 async/await 的结合

Generator 函数和 async/await 可以结合使用,特别是在需要手动控制异步流程的情况下。

示例代码

function* gen() {
  const a = yield Promise.resolve(1);
  const b = yield Promise.resolve(2);
  return a + b;
}
async function run() {
  const g = gen();
  const a = await g.next().value;
  const b = await g.next().value;
  const result = g.next().value;
  return result;
}
run().then(console.log); // 输出 3

代码解释

  1. function* gen():定义了一个 Generator 函数。
  2. const a = yield Promise.resolve(1):暂停执行,返回一个 Promise,并等待其解析。
  3. const b = yield Promise.resolve(2):继续暂停执行,返回另一个 Promise,并等待其解析。
  4. return a + b:返回 ab 的和。
  5. async function run():定义了一个 async 函数。
  6. const g = gen():创建了一个 Generator 实例。
  7. const a = await g.next().value:等待 Generator 的第一个 yield 返回的 Promise
  8. const b = await g.next().value:等待 Generator 的第二个 yield 返回的 Promise
  9. const result = g.next().value:获取 Generator 的返回值。
  10. run().then(console.log):执行 run 函数,并在 Promise 解析后输出结果。

async/await 的优势与不足

优势

  1. 代码简洁async/await 使得异步代码的书写更加简洁,易于阅读。
  2. 错误处理:可以通过 try/catch 方便地处理异步操作中的错误。
  3. 支持同步代码async 函数可以包含同步代码,而 Generator 函数则需要通过 yield 来控制执行。

不足

  1. 兼容性:在旧版本的浏览器或环境中,async/await 可能无法使用,需要通过 Babel 等工具进行转译。
  2. 错误处理:如果不在 async 函数中使用 try/catch,错误可能会被捕获到上层 Promisecatch 中,导致调试困难。
  3. 性能async/await 依赖于 Promise,在某些情况下可能会引入额外的性能开销。

结合 Redux Saga 的使用

Redux Saga 是一个用于管理 Redux 异步操作的库,它通过 Generator 函数来实现复杂的异步流程控制。async/await 也可以与 Redux Saga 结合使用,使得代码更加简洁。

示例代码

function* fetchUserSaga() {
  try {
    const response = yield call(fetch, 'https://api.github.com/users');
    const data = yield response.json();
    yield put({ type: 'FETCH_USER_SUCCESS', data });
  } catch (error) {
    yield put({ type: 'FETCH_USER_FAILURE', error });
  }
}
function* rootSaga() {
  yield takeLatest('FETCH_USER_REQUEST', fetchUserSaga);
}
function makeRootSaga() {
  return all([
    rootSaga(),
  ]);
}
const store = createStore(
  rootReducer,
  applyMiddleware(sagaMiddleware)
);
sagaMiddleware.run(makeRootSaga);
store.dispatch({ type: 'FETCH_USER_REQUEST' });

代码解释

  1. function* fetchUserSaga():定义了一个 Generator 函数,用于处理异步操作。
  2. yield call(fetch, 'https://api.github.com/users'):通过 call effects 调用 fetch 函数。
  3. yield response.json():等待 response.json() 解析完成。
  4. yield put({ type: 'FETCH_USER_SUCCESS', data }):通过 put effects 发送一个成功的动作。
  5. catch (error):捕获异步操作中可能发生的错误,并发送一个失败的动作。
  6. function* rootSaga():定义了一个根 Generator 函数,用于管理多个 Saga
  7. yield takeLatest('FETCH_USER_REQUEST', fetchUserSaga):监听 FETCH_USER_REQUEST 动作,并启动 fetchUserSaga
  8. makeRootSaga():将所有 Saga 组合在一起。
  9. createStoreapplyMiddleware:创建 Redux store,并应用 sagaMiddleware
  10. sagaMiddleware.run(makeRootSaga):启动 Saga
  11. store.dispatch({ type: 'FETCH_USER_REQUEST' }):发送一个动作,触发 Saga

最佳实践

  1. 错误处理:在 async 函数中使用 try/catch 来处理错误,避免错误被捕获到上层 Promisecatch 中。
  2. 避免多重 await:尽量避免在同一行中使用多个 await,以提高代码的可读性。
  3. 使用 await 处理所有异步操作:在 async 函数中,所有异步操作都应该使用 await 来处理,避免混用 Promiseawait
  4. 性能优化:避免在 async 函数中进行复杂的计算或阻塞操作,尽量将这些操作放在 await 之外。

总结

async/await 是 JavaScript 中处理异步操作的一种强大工具,它结合了 PromiseGenerator 函数的优势,使得异步代码的书写更加简洁和直观。通过理解 async/await 的实现原理,可以更好地掌握它的使用场景和局限性。在实际开发中,结合 Generator 函数和 Redux Saga 等工具,可以进一步提升异步流程控制的能力。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值