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

前言

各种详细的Promise教程已经满天飞了,我写这一篇也只是用来本身用来总结和整理用的。若是有不足之处,欢迎指教。javascript

为何咱们要用Promise

JavaScript语言的一大特色就是单线程。单线程就意味着,全部任务须要排队,前一个任务结束,才会执行后一个任务。html

为了解决单线程的堵塞问题,如今,咱们的任务能够分为两种,一种是同步任务(synchronous),另外一种是异步任务(asynchronous)。java

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

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

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

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

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

简单实现一个Promise

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

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

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

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

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

下面是一个写好注释的简单实现的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 函数
相关文章
相关标签/搜索