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

前言

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

为什么我们要用Promise

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

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

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

异步任务必须指定回调函数,当主线程开始执行异步任务,就是执行对应的回调函数。而我们可能会写出一个回调金字塔,维护大量的callback将是一场灾难:

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)时的回调函数(该参数可以省略)。一旦状态改变,就调用相应的回调函数。

扫描二维码关注公众号,回复: 3717922 查看本文章

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

class Promise {
    constructor(executor) {
        // 初始化state为pending
        this.state = 'pending'
        // 成功的值
        this.value = undefined
        // 失败的原因
        this.reason = undefined
        // 异步操作,我们需要将所有then中的成功调用保存起来
        this.onResolvedCallbacks = []
        // 异步操作,我们需要将所有then中的失败调用保存起来
        this.onRejectedCallbacks = []
        let resolve = value => {
            // 检验state状态是否改变,如果改变了调用就会失败
            if (this.state === 'pending') {
                // resolve调用后,state转化为成功态
                this.state = 'fulfilled'
                // 储存成功的值
                this.value = value
                // 执行成功的回调函数
                this.onResolvedCallbacks.forEach(fn => fn)
            }
        }

        let reject = reason => {
            // 检验state状态是否改变,如果改变了调用就会失败
            if (this.state === 'pending') {
                // reject调用后,state转化为失败态
                this.state === 'rejected'
                // 储存失败的原因
                this.reason = reason
                // 执行失败的回调函数
                this.onRejectedCallbacks.forEach(fn => fn)
            }
        }
        // 如果executor执行报错,直接执行reject
        try {
            executor(resolve, reject)
        } catch (err) {
            reject(err)
        }
    }
    // then 方法 有两个参数onFulfilled onRejected
    then(onFulfilled, onRejected) {
        // 状态为fulfilled,执行onFulfilled,传入成功的值
        if (this.state === 'fulfilled') {
            onFulfilled(this.value)
        }
        // 状态为rejected,执行onRejected,传入失败的原因
        if (this.state === 'rejected') {
            onRejected(this.reason)
        }
        // 当状态state为pending时
        if (this.state === 'pending') {
          // onFulfilled传入到成功数组
          this.onResolvedCallbacks.push(()=>{
            onFulfilled(this.value);
          })
          // onRejected传入到失败数组
          this.onRejectedCallbacks.push(()=>{
            onRejected(this.reason);
          })
        }
    }
}
复制代码

如果需要实现链式调用和其它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. BAT前端经典面试问题:史上最最最详细的手写Promise教程
  3. JavaScript Promise:简介
  4. JavaScript 运行机制详解:再谈Event Loop
  5. Promise 对象
  6. async 函数

猜你喜欢

转载自juejin.im/post/5bcedb78e51d457b7d135746