• [Javascript] Promise


    Promise 代表着一个异步操作,这个异步操作现在尚未完成,但在将来某刻会被完成。

    Promise 有三种状态

    • pending : 初始的状态,尚未知道结果
    • fulfilled : 代表操作成功
    • rejected : 代表操作失败

    如果 Promise 操作 fulfilled 或者 rejected ,并且对应的处理函数被声明了,则该处理函数被调用。

    Promise vs 事件监听器(event listener) 

    事件监听器善于处理同一对象上重复发生的事情,例如按键、点击鼠标等。对于这些事件,你只关心添加回调函数之后的发生的事情。当处理结果导向的异步事件时,你的代码可能是

    img1.callThisIfLoadedOrWhenLoaded(function() {
      // loaded
    }).orIfFailedCallThis(function() {
      // failed
    });
    
    // and…
    whenAllTheseHaveLoaded([img1, img2]).callThis(function() {
      // all loaded
    }).orIfSomeFailedCallThis(function() {
      // one or more failed
    });

    要实现同样的功能,Promise 的代码更加简洁,更加直观。

    img1.ready().then(function() {
      // loaded
    }, function() {
      // failed
    });
    
    // and…
    Promise.all([img1.ready(), img2.ready()]).then(function() {
      // all loaded
    }, function() {
      // one or more failed
    });

    Promise 和 事件监听器很相似,除了以下几点

    1. Promise 只经历一次执行,成功或者失败。Promise 不会被执行第二次,也不可以转变执行结果,即执行无法从成功变成失败,反之亦然。而事件监听器可以被反复执行。

    2. 如果 Promise 已经执行完毕,成功或者失败,然后再给 Promise 添加对应的回调函数,那么这个对应的回调函数仍然会被执行。也就是说,Promise 添加回调函数后,回调函数先检查在自己被添加之前事件是否已发生,如果发生则执行,如果没有发生就继续等待。而事件监听器只对自己被添加后的事件负责,忽略在自己被添加前的事件。

    上面两点非常有利于处理结果导向的异步事件。因为你只需要关系事件发生的结果,成功或者失败,而不需了解事件发生的准确时间。

    Promise 的使用

    Promise 早已存在一些库里面,例如

    创建 Promise 

    类库中的 Promise 实现大多都遵循标准行为,Promises/A+。 但是 API 会有些不一样。 Javascript 自带的 Promise 采用类似 RSVP.js 的 API 。

    var promise = new Promise(function(resolve, reject) {
      // do a thing, possibly async, then…
    
      if (/* everything turned out fine */) {
        resolve("Stuff worked!");
      }
      else {
        reject(Error("It broke"));
      }
    });

    Promise 构造函数接受一个回调函数作为参数。在创建 Promise 对象时候,执行该回调函数。根据回调函数的结果,成功或者失败,把执行控制权交给 reolve() 或者 reject() 函数。不过 reolve() 或 reject() 真正被执行的时间是在他们通过 Promise.then() 被添加之后。

    使用 Promise 

    promise.then(function(result) {
      console.log(result); // "Stuff worked!"
    }, function(err) {
      console.log(err); // Error: "It broke"
    });

    Promise.then() 接受两个参数,一个用于处理 Promise 执行成功的情况,另一个用于处理 Promise 失败的情况。两个参数都是可选的,所以可以只传函数给第一个参数,用于处理 Promise 成功的情况,也可以只传函数给第二个参数,用于处理 Promise 失败的情况。

    链式调用(chaining)

    Promise.prototype.then() 和 Promise.prototype.catch() 返回的是一个 Promise ,所以可以形成链式调用。链式调用可用于值传递,或者依次地进行后续的异步操作。

    值传递

    var promise = new Promise(function(resolve, reject) {
      resolve(1);
    });
    
    promise.then(function(val) {
      console.log(val); // 1
      return val + 2;
    }).then(function(val) {
      console.log(val); // 3
    });

    当 then() 只是简单地调用另一个函数时,可以只传另一个函数的函数名。例如

    get('story.json').then(function(response) {
      return JSON.parse(response);
    }).then(function(response) {
      console.log("Yey JSON!", response);
    });

    可以简化为

    get('story.json').then(JSON.parse).then(function(response) {
      console.log("Yey JSON!", response);
    });

    实战代码如下

    function getJSON(url) {
      return get(url).then(JSON.parse);
    }

    串联多个异步操作

    如果 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);
    });

    发送一个 story.json 的请求,获得的结果是一个 story 对象,包含着多个 chapter 的 URL。根据第一个 chapter URL ,再次请求 JSON 数据。执行成功后,最后运行 console.log 。

    例子

    已知一个目标资源 story.json,需要实现下面几步

    1. 向 story.json 发送请求,获得各个章节的 URL,

    2. 向各个章节的 URL 发送请求,分别获得各章节内容

    3. 将章节内容添加到 HTML 页面中 

    同步请求数据

    由于是同步请求,所有请求都依次阻塞进行。在请求过程中页面也会被阻塞,无进行其他操作。这种方式可以实现数据加载,但是用户体验差。

    try {
      var story = getJSONSync('story.json');
      addHtmlToPage(story.heading);
    
      story.chapterUrls.forEach(function(chapterUrl) {
        var chapter = getJSONSync(chapterUrl);
        addHtmlToPage(chapter.html);
      });
    
      addTextToPage("All done");
    }
    catch (err) {
      addTextToPage("Argh, broken: " + err.message);
    }
    
    document.querySelector('.spinner').style.display = 'none';

    Promise 异步请求数据

    getJSON('story.json').then(function(story) {
      addHtmlToPage(story.heading);
    
      // TODO: for each url in story.chapterUrls, fetch & display
    }).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';
    });

    上面是一个框架代码,加载每一章节的功能尚未实现。由于数组的 forEach 函数不是异步操作,所以无法简单地使用 forEach 对每一章节进行异步请求。可以将数组转换为 Promise 队列来实现遍历效果。

    // Start off with a promise that always resolves
    var sequence = Promise.resolve();
    
    // Loop through our chapter urls
    story.chapterUrls.forEach(function(chapterUrl) {
      // Add these actions to the end of the sequence
      sequence = sequence.then(function() {
        return getJSON(chapterUrl);
      }).then(function(chapter) {
        addHtmlToPage(chapter.html);
      });
    });

    或者用 array.reduce 来实现

    // Loop through our chapter urls
    story.chapterUrls.reduce(function(sequence, chapterUrl) {
      // Add these actions to the end of the sequence
      return sequence.then(function() {
        return getJSON(chapterUrl);
      }).then(function(chapter) {
        addHtmlToPage(chapter.html);
      });
    }, Promise.resolve());

    将框架代码和加载章节的代码组合起来

    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';
    });

    实现的效果如下:

    现在,能执行异步无阻塞地依次加载数据,并且先加载先显示。不过,这种加载方式还有优化空间。

    Promise 优化版1

    并行加载数据,全部加载后同时显示。效果如下

    Promise 优化版2

    并行加载数据,根据章节顺序,依次显示各个加载完成的章节。这种加载方式的效果最好。使用 Chrome 浏览器时候,感觉数据加载很快,猜想就是用这种方式进行数据加载的。

    效果如下

    最后两个优化版的思路和代码参考 JavaScript Promise, Jake Archibald

    参考资料

    Promise - JavaScript | MDN

    JavaScript Promise, Jake Archibald

  • 相关阅读:
    uniapp微信小程序主包vendor.js过大
    uniapp 微信小程序实现微信分享
    uniapp 解决H5跨域问题
    AT2339[AGC011C]Squared Graph【黑白染色】
    6. Z 字形变换
    PCl1.12.0安装
    C#中两个冒号(::)的作用 转
    只代理中国大陆
    C语言在Linux上的基本开发环境
    在linux上使用命令操作阿里云盘下载上传分享文件
  • 原文地址:https://www.cnblogs.com/TonyYPZhang/p/5813011.html
Copyright © 2020-2023  润新知