阿里云主机折上折
  • 微信号
Current Site:Index > The Promise pattern handles asynchronous operations.

The Promise pattern handles asynchronous operations.

Author:Chuan Chen 阅读数:23041人阅读 分类: JavaScript

The Promise pattern is a powerful tool in JavaScript for handling asynchronous operations. It simplifies the callback hell problem through chaining and state management. Whether it's network requests, file reading, or timed tasks, Promises provide a clearer way to organize code logic.

Basic Concepts of Promises

A Promise object represents the eventual completion (or failure) of an asynchronous operation and its resulting value. It has three states:

  • pending: initial state
  • fulfilled: operation completed successfully
  • rejected: operation failed
const promise = new Promise((resolve, reject) => {
  // Asynchronous operation
  setTimeout(() => {
    const success = true;
    if (success) {
      resolve('Operation succeeded');
    } else {
      reject('Operation failed');
    }
  }, 1000);
});

Chaining Promises

The true power of Promises lies in their ability to chain calls, avoiding callback hell:

fetchData()
  .then(processData)
  .then(saveData)
  .catch(handleError);

Each .then() returns a new Promise, allowing continued chaining. .catch() catches errors occurring anywhere in the chain.

Static Methods of Promises

Promises provide several useful static methods:

Promise.all

Waits for all Promises to complete or the first Promise to reject:

Promise.all([
  fetch('/api/users'),
  fetch('/api/posts')
]).then(([users, posts]) => {
  console.log('All data loaded');
}).catch(err => {
  console.error('One request failed', err);
});

Promise.race

Returns the first Promise to complete (whether resolved or rejected):

Promise.race([
  fetch('/api/data'),
  new Promise((_, reject) => 
    setTimeout(() => reject(new Error('Request timeout')), 5000)
  )
]).then(data => {
  console.log('Data fetched successfully');
}).catch(err => {
  console.error('Request timed out or failed', err);
});

Promise.allSettled

Waits for all Promises to complete (regardless of success or failure):

Promise.allSettled([
  Promise.resolve('Success'),
  Promise.reject('Failure')
]).then(results => {
  results.forEach(result => {
    if (result.status === 'fulfilled') {
      console.log('Success:', result.value);
    } else {
      console.log('Failure:', result.reason);
    }
  });
});

Error Handling in Promises

Promises offer multiple ways to handle errors:

// Method 1: Using catch
somePromise()
  .then(handleSuccess)
  .catch(handleError);

// Method 2: Second parameter of then
somePromise()
  .then(handleSuccess, handleError);

// Method 3: Global error handling
window.addEventListener('unhandledrejection', event => {
  console.warn('Unhandled Promise rejection:', event.reason);
});

Promises and async/await

async/await is syntactic sugar built on top of Promises:

async function fetchData() {
  try {
    const response = await fetch('/api/data');
    const data = await response.json();
    return processData(data);
  } catch (error) {
    console.error('Failed to fetch data:', error);
    throw error;
  }
}

Advanced Promise Patterns

Canceling Promises

Although Promises don't natively support cancellation, it can be implemented via wrapping:

function cancellablePromise(promise) {
  let isCancelled = false;
  
  const wrappedPromise = new Promise((resolve, reject) => {
    promise.then(
      value => !isCancelled && resolve(value),
      error => !isCancelled && reject(error)
    );
  });
  
  return {
    promise: wrappedPromise,
    cancel: () => { isCancelled = true; }
  };
}

const { promise, cancel } = cancellablePromise(fetch('/api/data'));
// Later, calling cancel() can "cancel" the Promise

Promise Caching

Avoid repeated requests for the same resource:

const promiseCache = new Map();

function getWithCache(url) {
  if (promiseCache.has(url)) {
    return promiseCache.get(url);
  }
  
  const promise = fetch(url)
    .then(response => response.json())
    .finally(() => promiseCache.delete(url));
    
  promiseCache.set(url, promise);
  return promise;
}

Performance Considerations for Promises

While Promises offer many conveniences, performance considerations are important:

  1. Microtask queue: Promise callbacks execute as microtasks, which may impact performance-sensitive code
  2. Memory usage: Long-pending Promises retain their closure references
  3. Error stacks: Error stacks in Promise chains may be incomplete
// Performance test example
console.time('Promise');
Promise.resolve()
  .then(() => {})
  .then(() => {})
  .then(() => {
    console.timeEnd('Promise'); // Shows execution time of the Promise chain
  });

Common Promise Pitfalls

  1. Forgetting to return values in Promise chains:
// Bad example
somePromise()
  .then(result => {
    processResult(result); // Forgot to return
  })
  .then(processed => {
    // processed will be undefined
  });

// Good example
somePromise()
  .then(result => {
    return processResult(result); // Explicit return
  });
  1. Improper error handling:
// Bad example - then after catch
somePromise()
  .catch(handleError)
  .then(() => {
    // This will still execute even if an error occurred
  });

// Good example - rethrowing errors
somePromise()
  .catch(err => {
    handleError(err);
    throw err; // Or return a rejected Promise
  })
  .then(() => {
    // Only executes if no error occurred
  });

Practical Use Cases for Promises

Sequential Execution of Async Tasks

function executeSequentially(promises) {
  return promises.reduce((chain, promise) => {
    return chain.then(() => promise);
  }, Promise.resolve());
}

executeSequentially([
  () => fetch('/api/step1'),
  () => fetch('/api/step2'),
  () => fetch('/api/step3')
]);

Promise with Retry Mechanism

function retry(fn, retries = 3, delay = 1000) {
  return new Promise((resolve, reject) => {
    const attempt = (n) => {
      fn()
        .then(resolve)
        .catch(err => {
          if (n === 0) {
            reject(err);
          } else {
            setTimeout(() => attempt(n - 1), delay);
          }
        });
    };
    attempt(retries);
  });
}

retry(() => fetch('/api/unstable'), 5, 2000);

Promises and the Event Loop

Understanding Promise behavior in the event loop is crucial:

console.log('Script start');

setTimeout(() => console.log('setTimeout'), 0);

Promise.resolve()
  .then(() => console.log('Promise 1'))
  .then(() => console.log('Promise 2'));

console.log('Script end');

// Output order:
// Script start
// Script end
// Promise 1
// Promise 2
// setTimeout

Unit Testing Promises

Testing Promise code requires special handling:

// Using Jest to test Promises
test('fetchData returns expected data', () => {
  // Ensure the Promise is returned
  return fetchData().then(data => {
    expect(data).toEqual(expectedData);
  });
});

// Or using async/await
test('fetchData returns expected data', async () => {
  const data = await fetchData();
  expect(data).toEqual(expectedData);
});

// Testing error cases
test('fetchData handles errors', async () => {
  await expect(fetchDataWithError()).rejects.toThrow('Request failed');
});

Browser Compatibility for Promises

While modern browsers support Promises, older environments may require polyfills:

<!-- Including a Promise polyfill -->
<script src="https://cdn.jsdelivr.net/npm/promise-polyfill@8/dist/polyfill.min.js"></script>

Or transpilation via tools like Babel:

// babel.config.js
module.exports = {
  presets: [
    ['@babel/preset-env', {
      useBuiltIns: 'usage',
      corejs: 3
    }]
  ]
};

Promises and Web Workers

Promises work well with Web Workers:

// main.js
const worker = new Worker('worker.js');

function workerPromise(message) {
  return new Promise((resolve, reject) => {
    worker.onmessage = e => resolve(e.data);
    worker.onerror = e => reject(e.error);
    worker.postMessage(message);
  });
}

workerPromise('heavy task')
  .then(result => console.log('Worker completed:', result));

// worker.js
self.onmessage = function(e) {
  const result = doHeavyTask(e.data);
  self.postMessage(result);
};

Debugging Techniques for Promises

Debugging Promise chains can be challenging:

  1. Inserting debug points using .then:
somePromise()
  .then(data => {
    console.log('Debug point 1:', data);
    return data;
  })
  .then(processData)
  .then(result => {
    console.log('Debug point 2:', result);
    return result;
  });
  1. Simplifying debugging with async/await:
async function debugFlow() {
  const data = await somePromise();
  debugger; // Can inspect data here
  const processed = await processData(data);
  debugger; // Can inspect processed here
  return processed;
}
  1. Using third-party libraries like bluebird for enhanced debugging:
const Promise = require('bluebird');
Promise.config({ longStackTraces: true });

本站部分内容来自互联网,一切版权均归源网站或源作者所有。

如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn

Front End Chuan

Front End Chuan, Chen Chuan's Code Teahouse 🍵, specializing in exorcising all kinds of stubborn bugs 💻. Daily serving baldness-warning-level development insights 🛠️, with a bonus of one-liners that'll make you laugh for ten years 🐟. Occasionally drops pixel-perfect romance brewed in a coffee cup ☕.