当我们谈论Promise时,我们说些什么

本文深入探讨了JavaScript中Promise的概念,解释了为何需要使用Promise,如何实现和使用它解决异步编程中的回调地狱问题。通过具体示例展示了Promise在HTTP请求、错误处理、并行与顺序操作中的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

各类详细的Promise教程已经满天飞了,我写这一篇也只是用来自己用来总结和整理用的。如果有不足之处,欢迎指教。

为什么我们要用Promise

JavaScript语言的一大特点就是单线程。单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。

为了解决单线程的堵塞问题,现在,我们的任务可以分为两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)。

  • 同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;
  • 异步任务指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。

异步任务必须指定回调函数,当主线程开始执行异步任务,就是执行对应的回调函数。而我们可能会写出一个回调地狱。

step1(function (value1) {
  step2(value1, function(value2) {
    step3(value2, function(value3) {
      step4(value3, function(value4) {
        // ...
      });
    });
  });
});
复制代码

而Promise 可以让异步操作写起来,就像在写同步操作的流程,而不必一层层地嵌套回调函数。

(new Promise(step1))
  .then(step2)
  .then(step3)
  .then(step4);
复制代码

简单实现一个Promise

关于Promise的学术定义和规范可以参考Promise/A+规范,中文版【翻译】Promises/A+规范

Promise有三个状态pendingfulfilledrejected: 三种状态切换只能有两种途径,只能改变一次:

  • 异步操作从未完成(pending) => 成功(fulfilled)
  • 异步操作从未完成(pending) => 失败(rejected)

Promise 实例的then方法,用来添加回调函数。

then方法可以接受两个回调函数,第一个是异步操作成功时(变为fulfilled状态)时的回调函数,第二个是异步操作失败(变为rejected)时的回调函数(该参数可以省略)。一旦状态改变,就调用相应的回调函数。

下面是一个写好注释的简单实现的Promise的实现:

const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'

class AJPromise {
  constructor(executor) {
    this.state = PENDING
    this.value = undefined
    this.reason = undefined
    this.onResolvedCallbacks = []
    this.onRejectedCallbacks = []

    let resolve = value => {
      // 确保 onFulfilled 异步执行
      setTimeout(() => {
        if (this.state === PENDING) {
          this.state = FULFILLED
          this.value = value
          // this.onResolvedCallbacks.forEach(fn => fn)
          // 可以将 value 操作后依次传递
          this.onResolvedCallbacks.map(cb => (this.value = cb(this.value)))
        }
      })
    }

    let reject = reason => {
      setTimeout(() => {
        if (this.state === PENDING) {
          this.state = REJECTED
          this.reason = reason
          // this.onRejectedCallbacks.forEach(fn => fn)
          this.onRejectedCallbacks.map(cb => (this.reason = cb(this.reason)))
        }
      })
    }

    try {
      //执行Promise
      executor(resolve, reject)
    } catch (err) {
      reject(err)
    }
  }

  then(onFulfilled, onRejected) {
    if (this.state === FULFILLED) {
      onFulfilled(this.value)
    }

    if (this.state === REJECTED) {
      onRejected(this.reason)
    }

    if (this.state === PENDING) {
      typeof onFulfilled === 'function' &&
        this.onResolvedCallbacks.push(onFulfilled)

      typeof onRejected === 'function' &&
        this.onRejectedCallbacks.push(onRejected)
      // 返回 this 支持then方法可以被同一个 promise 调用多次
      return this
    }
  }
}
复制代码

如果需要实现链式调用和其它API,请查看下面参考文档链接中的手写Promise教程。

优雅的使用Promise

使用Promise封装一个HTTP请求

function get(url) {
  return new Promise(function(resolve, reject) {
    var req = new XMLHttpRequest();
    req.open('GET', url);

    req.onload = function() {
      if (req.status == 200) {
        resolve(req.responseText);
      }
      else {
        reject(Error(req.statusText));
      }
    };

    req.onerror = function() {
      reject(Error("Network Error"));
    };

    req.send();
  });
}
复制代码

现在让我们来使用这一功能:

get('story.json').then(function(response) {
  console.log("Success!", response);
}, function(error) {
  console.error("Failed!", error);
})

// 当前收到的是纯文本,但我们需要的是JSON对象。我们将该方法修改一下
get('story.json').then(function(response) {
  return JSON.parse(response);
}).then(function(response) {
  console.log("Yey JSON!", response);
})

// 由于 JSON.parse() 采用单一参数并返回改变的值,因此我们可以将其简化为:
get('story.json').then(JSON.parse).then(function(response) {
  console.log("Yey JSON!", response);
})

// 最后我们封装一个简单的getJSON方法
function getJSON(url) {
  return get(url).then(JSON.parse);
}
复制代码

then() 不是Promise的最终部分,可以将各个then链接在一起来改变值,或依次运行额外的异步操作。

Promise.then()的异步操作队列

当从then()回调中返回某些内容时:如果返回一个值,则会以该值调用下一个then()。但是,如果返回类promise 的内容,下一个then()则会等待,并仅在 promise 产生结果(成功/失败)时调用。

getJSON('story.json').then(function(story) {
  return getJSON(story.chapterUrls[0]);
}).then(function(chapter1) {
  console.log("Got chapter 1!", chapter1);
})
复制代码

错误处理

then() 包含两个参数onFulfilled, onRejectedonRejected是失败时调用的函数。
对于失败,我们还可以使用catch,对于错误进行捕捉,但下面两段代码是有差异的:

get('story.json').then(function(response) {
  console.log("Success!", response);
}, function(error) {
  console.log("Failed!", error);
})

get('story.json').then(function(response) {
  console.log("Success!", response);
}).catch(function(error) {
  console.log("Failed!", error);
})
 // catch 等同于 then(undefined, func)
get('story.json').then(function(response) {
  console.log("Success!", response);
}).then(undefined, function(error) {
  console.log("Failed!", error);
})
复制代码

两者之间的差异虽然很微小,但非常有用。Promise 拒绝后,将跳至带有拒绝回调的下一个then()(或具有相同功能的 catch())。如果是 then(func1, func2),则 func1func2 中的一个将被调用,而不会二者均被调用。但如果是 then(func1).catch(func2),则在 func1 拒绝时两者均被调用,因为它们在该链中是单独的步骤。看看下面的代码:

asyncThing1().then(function() {
  return asyncThing2();
}).then(function() {
  return asyncThing3();
}).catch(function(err) {
  return asyncRecovery1();
}).then(function() {
  return asyncThing4();
}, function(err) {
  return asyncRecovery2();
}).catch(function(err) {
  console.log("Don't worry about it");
}).then(function() {
  console.log("All done!");
})
复制代码

以下是上述代码的流程图形式:

蓝线表示执行的 promise 路径,红路表示拒绝的 promise 路径。与 JavaScript 的 try/catch 一样,错误被捕获而后续代码继续执行。

并行和顺序:两者兼得

假设我们获取了一个story.json文件,其中包含了文章的标题,和段落的下载地址。

1. 顺序下载,依次处理
getJSON('story.json').then(function(story) {
  addHtmlToPage(story.heading);

  return story.chapterUrls.reduce(function(sequence, chapterUrl) {
    // Once the last chapter's promise is done…
    return sequence.then(function() {
      // …fetch the next chapter
      return getJSON(chapterUrl);
    }).then(function(chapter) {
      // and add it to the page
      addHtmlToPage(chapter.html);
    });
  }, Promise.resolve());
}).then(function() {
  // And we're all done!
  addTextToPage("All done");
}).catch(function(err) {
  // Catch any error that happened along the way
  addTextToPage("Argh, broken: " + err.message);
}).then(function() {
  // Always hide the spinner
  document.querySelector('.spinner').style.display = 'none';
})
复制代码
2. 并行下载,完成后统一处理
getJSON('story.json').then(function(story) {
  addHtmlToPage(story.heading);

  // Take an array of promises and wait on them all
  return Promise.all(
    // Map our array of chapter urls to
    // an array of chapter json promises
    story.chapterUrls.map(getJSON)
  );
}).then(function(chapters) {
  // Now we have the chapters jsons in order! Loop through…
  chapters.forEach(function(chapter) {
    // …and add to the page
    addHtmlToPage(chapter.html);
  });
  addTextToPage("All done");
}).catch(function(err) {
  addTextToPage("Argh, broken: " + err.message);
}).then(function() {
  document.querySelector('.spinner').style.display = 'none';
})
复制代码
3. 并行下载,一旦顺序正确立即渲染
getJSON('story.json').then(function(story) {
  addHtmlToPage(story.heading);

  // Map our array of chapter urls to
  // an array of chapter json promises.
  // This makes sure they all download parallel.
  return story.chapterUrls.map(getJSON)
    .reduce(function(sequence, chapterPromise) {
      // Use reduce to chain the promises together,
      // adding content to the page for each chapter
      return sequence.then(function() {
        // Wait for everything in the sequence so far,
        // then wait for this chapter to arrive.
        return chapterPromise;
      }).then(function(chapter) {
        addHtmlToPage(chapter.html);
      });
    }, Promise.resolve());
}).then(function() {
  addTextToPage("All done");
}).catch(function(err) {
  // catch any error that happened along the way
  addTextToPage("Argh, broken: " + err.message);
}).then(function() {
  document.querySelector('.spinner').style.display = 'none';
})
复制代码

async / await

async函数返回一个 Promise 对象,可以使用then方法添加回调函数。当函数执行的时候,一旦遇到await就会先返回,等到异步操作完成,再接着执行函数体内后面的语句。

基本用法

我们可以重写一下之前的getJSON方法:

// promise 写法
function getJSON(url) {
    return get(url).then(JSON.parse).catch(err => {
        console.log('getJSON failed for', url, err);
        throw err;
    })
}

// async 写法
async function getJSON(url) {
    try {
        let response = await get(url)
        return JSON.parse(response)
    } catch (err) {
        console.log('getJSON failed for', url, err);
    }
}

复制代码

注意:避免太过循环

假定我们想获取一系列段落,并尽快按正确顺序将它们打印:

// promise 写法
function chapterInOrder(urls) {
  return urls.map(getJSON)
    .reduce(function(sequence, chapterPromise) {
      return sequence.then(function() {
        return chapterPromise;
      }).then(function(chapter) {
        console.log(chapter)
      });
    }, Promise.resolve())
}
复制代码

*不推荐的方式:

async function chapterInOrder(urls) {
  for (const url of urls) {
    const chapterPromise = await getJSON(url);
    console.log(chapterPromise);
  }
}
复制代码

推荐写法:

async function chapterInOrder(urls) {
  const chapters = urls.map(getJSON);

  // log them in sequence
  for (const chapter of chapters) {
    console.log(await chapter);
  }
}
复制代码

参考资料

  1. 异步函数 - 提高 Promise 的易用性
  2. 你能手写一个Promise吗?Yes I promise。
  3. JavaScript Promise:简介
  4. JavaScript 运行机制详解:再谈Event Loop
  5. Promise 对象
  6. async 函数

转载于:https://juejin.im/post/5bcedb78e51d457b7d135746

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值