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

栏目: JavaScript · 发布时间: 6年前

内容简介:各类详细的JavaScript语言的一大特点就是单线程。单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。为了解决单线程的堵塞问题,现在,我们的任务可以分为两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)。

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

下面是一个写好注释的简单实现的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 路径,红路表示拒绝的 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);
  }
}
复制代码

以上所述就是小编给大家介绍的《当我们谈论Promise时,我们说些什么》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

长尾理论

长尾理论

[美]克里斯·安德森 / 中信出版集团股份有限公司 / 2015-8-1 / 59.00元

互联网时代,大众市场不再一统天下,小众市场也可以呼风唤雨。 在《长尾理论》一书中,克里斯·安德森详细阐释了长尾的精华所在,指出商业和文化的未来不在于传统需求曲线上那个代表“畅销商品”的头部,而是那条代表“冷门商品”的经常被人遗忘的长尾。尽管我们仍然对热门商品着迷,但它们对消费者的吸引力已经大不如从前,因为市场已经大大分化。黄金电视节目的收视率几十年来一直在萎缩,若是在七八十年代,现在的一档最......一起来看看 《长尾理论》 这本书的介绍吧!

随机密码生成器
随机密码生成器

多种字符组合密码

Base64 编码/解码
Base64 编码/解码

Base64 编码/解码

SHA 加密
SHA 加密

SHA 加密工具