Promise,es6 前端必修课

说两句废话

Promise,一个颠覆习惯的东西,虽然用起来各种别扭,但它能使代码结构更加清楚规范,各主流框架也纷纷用其规范架构,所以,必须下功夫消化之。

说到Promise,() => 箭头函数then,自然也属于这篇笔记的内容。

建议git clone一下笔记底部的github工程,里面有我用到的所有例子代码,对照着做能有助于理解。

概念

不多说了,给个传送门

概念和基本用法看完了,接下来来看我的实战

从一个简单的例子讲起

先看代码:

example1.js

var sayyes = function(){
  return new Promise(function(resolve){
    $.getJSON('/testjson.json',function(data){
      resolve(data.word);
    });
  });
}
var sayyes2 = function(w){
  return new Promise(function(resolve, reject){
    $.getJSON('/testjson.json',function(data){
      resolve(w + data.word);
    });
  });
}
var sayyes3 = function(w){
  return new Promise(function(resolve){
    $.getJSON('/testjson.json',function(data){
      resolve(w + data.word);
    });
  });
}

sayyes()
.then(function(w){
  return sayyes2(w);
})
.then(function(w){
  return sayyes3(w);
})
.then(function(text){
  log(text);
})
.catch(function(e){
  //统一进行错误处理
  log(e.message);
});

以上例子中

  1. sayyes()函数是new一个新的Promise,该Promise是内容是通过jQuery的getJSON()方法异步访问一个json接口testjson.json,在得到异步返回后,通过resolve()返回结果
  2. sayyes2()sayyes3()目前是一样的函数,也是new一个新的Promise,进行getJSON()获取接口数据,组合到参数中并返回。sayyes2()中函数参数多了一个reject,后面会用到
  3. 函数定义完后,用管道方式调用函数,实现的情况是sayyes()的异步操作resolve()时返回其结果,then中的函数才接着接行,就像管道一样,then中的参数w将被resolve()中的值注入,然后w作为参数进行下一个函数sayyes2(),待其resolve()被调用后,结果再注入下一个管道,同理,最后执行log(text)时并不是Promise,结果即时输出

说到这里,如果能理解,请往下看,否则,再看看消化一下

代码最后还有个catch()函数,其作用是处理管道中抛出的异常和reject()

动动手,在sayyes2中throw个Error

var sayyes2 = function(w){
  return new Promise(function(resolve, reject){
    throw new Error('an error in sayyes2');
    $.getJSON('/testjson.json',function(data){
      resolve(w + data.word);
    });
  });
}

好了,现在再刷新页面,错误将被成功捕获。记得gulp是监听状态(npm start后会gulp会进入监听),否则发布不到dist中

还原一下,再来试试reject()

var sayyes2 = function(w){
  return new Promise(function(resolve, reject){
    $.getJSON('/testjson.json',function(data){
      //resolve(w + data.word);
      reject(new Error('an error after ajax in sayyes2'));
    });
  });
}

最后一个catch()就能掌管所有异常,在结构上处理起来很方便。

管道思想

受传统的ajax写法影响,我们经常需要针对每一次异步请求做出响应,遇到分顺序的异步操作时,多层的嵌套可读性差,维护成本高。

我们来实现一个需求:

现有文章ID,一个可根据文章ID获取文章信息的接口getArticleById.json可获取所属栏目ID,但栏目详细信息需要通过另一个接口getCategoryById.json获取

  1. 接口内容:

    getArticleById.json

     {
       "Result": 200,
       "Data": {
         "id": 10001,
         "title": "来自文章对象的标题",
         "content": "来自文章对象的内容",
         "categoryid": 3
       }
     }
    

    getCategoryById.json

     {
       "Result": 200,
       "Data": {
         "id": 3,
         "title": "分类甲",
         "summary": "分类甲对象的描述",
         "ownerid": 10004
       }
     }
    
  2. 传统的实现方法:

    example2.js

     var articleid = 10001;
     $.getJSON('/getArticleById.json',{id:articleid},function(data){
       if(data.Result == 200){
         //成功获取文章对象,去取栏目信息
         var article = data.Data;
         $.getJSON('/getCategoryById.json',{id: article.categoryid},function(data){
           if(data.Result == 200){
             //成功获取栏目对象
             var category = data.Data;
             log('简介:'+category.summary);
             log('栏目拥有者ID:'+category.ownerid);
             log('下面再根据拥有者ID去接口找栏目拥有者的信息');
           }
           else{
             //错误处理
             log('在读取栏目信息时发生错误!');
           }
         });
       }
       else{
         //错误处理
         log('在读取文章信息时发生错误!');
       }
     });
    
  3. 使用es6新特性的实现方法

    example3.js

     class Article {
       constructor(a){
         this.id = a.id;
         this.title = a.title;
         this.content = a.content;
         this.categoryid = a.categoryid;
       }
     
       static findById (articleid) {
         return new Promise((resolve, reject) => {
           $.getJSON('/getArticleById.json',{id: articleid},function(data){
             if(data.Result == 200){
               //成功获取文章对象
               resolve(new Article(data.Data));
             }
             else{
               //错误处理
               reject(new Error('在读取文章信息时发生错误!'));
             }
           });
         });
       }
     }
     
     class Category {
       constructor(a){
         this.id = a.id;
         this.title = a.title;
         this.summary = a.summary;
         this.ownerid = a.ownerid;
       }
     
       static findById (categoryid) {
         return new Promise((resolve, reject) => {
           $.getJSON('/getCategoryById.json',{id: categoryid},function(data){
             if(data.Result == 200){
               //成功获取栏目对象
               resolve(new Category(data.Data));
             }
             else{
               //错误处理
               reject(new Error('在读取栏目信息时发生错误!'));
             }
           });
         });
       }
     }
     
     //主进程
     Article.findById(10001)
     .then((article) => {
       return Category.findById(article.categoryid);
     })
     .then((category) => {
       log('简介:'+category.summary);
       log('栏目拥有者ID:'+category.ownerid);
       log('下面再根据拥有者ID去接口找栏目拥有者的信息');
     })
     .catch((e) => {
       log(e.message);
     });
    

    思路:

    • 定义文章类和栏目类,其中分别定义静态方法findById来返回一个Promise对象,该对象实现通过$.getJSON()方法访问接口取得一个Article或Category的对象
    • 调用Article.findById()方法来访问接口获取文章详情
    • 调用Promise对象的then()方法来组建管道,通过上一管道得到的文章对象里的categoryid去获取栏目对象
    • 调用Promise对象的then()方法继续组建管道,输出上一管道得到的栏目对象的内容
    • 管道最后调用catch()方法统一处理管道中捕获的所有异常

多个Promise实例间的协调

Promise提供了all()race()两个方法来解决多个对象的协同问题

  1. Promise.all()

    该方法的参数是一个由多个Promise对象组成的数组,它把它们封装成一个新的Promise对象,当所有的实例都resolve后才执行后面的管道

    以下例子已知文章ID和栏目ID,同时调用两个接口,当两个接口都resolve返回后分别输出两个对象的内容

    example4.js

     Promise.all([
       Article.findById(10001),
       Category.findById(3)
     ]).then(([article, category]) => {
       log('文章标题:'+article.title);
       log('文章内容:'+article.content);
       log('栏目简介:'+category.summary);
       log('栏目拥有者ID:'+category.ownerid);
     })
     .catch((e) => {
       log(e.message);
     });
    
  2. Promise.race()

    该方法与Promise.all()的区别在于,race()只需得到一个resolve返回就执行后面的管道,即对象们具有竞争关系

    在吕大豹的博客看到比较好诠释其用法的例子,就是用超时方法来竞速,即超时跑赢时抛出异常

    example5.js

     let timeout = function(t){
       return new Promise((resolve, reject) => {
         setTimeout(() => {
           reject(new Error('超时错误'));
         }, t);
       });
     }
     
     Promise.race([
       Article.findById(10001)
       .then((article) => {
         return Category.findById(article.categoryid);
       })
       .then((category) => {
         log('依然执行到结束');
         return category;
       }),
       timeout(1)
       //timeout(200)
     ])
     .then((category) => {
       log('简介:'+category.summary);
       log('栏目拥有者ID:'+category.ownerid);
       log('下面再根据拥有者ID去接口找栏目拥有者的信息');
     })
     .catch((e) => {
       log(e.message);
     });
    

    需要注意,Promise一旦启动异步是不能停止的,虽然超时方法reject()后管道不往后执行,但封装在异步方法中的代码仍会执行,如例中的log('依然执行到结束');

代码已放上github

本文所有代码我已打包成项目,方便后来者学习

https://github.com/coolhihi/promise-demo-es6

参考:

http://liubin.org/promises-book/

http://web.jobbole.com/82601/

http://www.cnblogs.com/lvdabao/p/es6-promise-1.html

若您觉得我的博文对您有帮助,欢迎点击下方按钮对我打赏
打赏