Promise

作者: 小小的白菜 | 来源:发表于2018-09-28 21:28 被阅读0次

    Promise 迷你书

    所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。

    Promise对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。

    pending变为fulfilled和从pending变为rejected。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果,这时就称为resolved(已定型)。如果改变已经发生了,你再对Promise对象添加回调函数,也会立即得到这个结果。这与事件(Event)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。

    function asyncFunction() {
        (1)
        return new Promise(function (resolve, reject) {
            setTimeout(function () {
                resolve('Async Hello world');
            }, 16);
        });
    }
    (2)
    asyncFunction().then(function (value) {
        console.log(value);    // => 'Async Hello world'
    }).catch(function (error) {
        console.log(error);
    });
    
    • (1)new Promise构造器之后,会返回一个promise对象;
    • (2)为promise对象用设置.then 调用返回值时的回调函数。
    创建promise对象
    • new Promise(fn)返回一个promise对象
    • fn中指定异步等处理
      • 处理结果正常的话,调用resolve(处理结果值)
      • 处理结果错误的话,调用reject(Error对象)
    function getURL(URL) {
        return new Promise(function (resolve, reject) {
          var req = new XMLHttpRequest()
          req.open('GET', URL, true)
          req.onload = function () {
            if (req.status === 200) {
              resolve(req.responseText)
            } else {
              reject(new Error(req.responseText))
            }
          }
    
          req.onerror = function () {
            reject(new Error(req.statusText))
          }
        })
      }
    
      var URL = "http://httpbin.org/get"
      getURL(URL).then(function onFulfilled(value){
        console.log(value)
      }).catch(function onRejected(error){
        console.log(error)
      })
    

    getURL 只有在通过XHR取得结果状态为200时才会调用 resolve - 也就是只有数据取得成功时,而其他情况(取得失败)时则会调用reject方法。

    resolve(req.responseText)response的内容中加入了参数。 resolve方法的参数并没有特别的规则,基本上把要传给回调函数参数放进去就可以了。 (then方法可以接收到这个参数值)

    • promise对象添加处理方法主要有以下两种
      • promise对象被resolve 时的处理(onFulfilled)
      • promise对象被 reject时的处理(onRejected)

    静态方法

    Promise.resolve

    静态方法Promise.resolve(value) 可以认为是 new Promise() 方法的快捷方式。

    比如Promise.resolve(42); 可以认为是以下代码的语法糖。

    new Promise(function(resolve) {
        resolve(42)
    })
    

    在这段代码中的resolve(42); 会让这个promise对象立即进入确定(即resolved)状态,并将42传递给后面then里所指定的onFulfilled函数。

    方法Promise.resolve(value); 的返回值也是一个promise对象,所以我们可以像下面那样接着对其返回值进行.then调用。

    Promise.resolve(42).then(function(value) {
        console.log(value)
    })
    
    Thenable

    Promise.resolve 方法另一个作用就是将 thenable 对象转换为promise对象。

    最简单的例子就是 jQuery.ajax(),它的返回值就是thenable的。因为jQuery.ajax() 的返回值是 jqXHR Object 对象,这个对象具有 .then 方法。

    $.ajax('/json/comment.json');// => 拥有 `.then` 方法的对象
    

    将thenable对象转换promise对象

    var promise = Promise.resolve($.ajax('/json/comment.json'));  // => promise对象
    promise.then(function(value){
       console.log(value);
    });
    
    Promise.reject

    Promise.reject(error)是和 Promise.resolve(value) 类似的静态方法,是 new Promise() 方法的快捷方式。

    比如 Promise.reject(new Error("出错了")) 就是下面代码的语法糖形式。

    new Promise(function(resolve,reject){
        reject(new Error("出错了"));
    });
    

    这段代码的功能是调用该promise对象通过then指定的 onRejected函数,并将错误(Error)对象传递给这个 onRejected函数。

    Promise.reject(new Error("BOOM!")).catch(function(error){
        console.error(error);
    });
    
    同步调用和异步调用同时存在导致的混乱

    mixed-onready.js

    function onReady(fn) {
        var readyState = document.readyState;
        if (readyState === 'interactive' || readyState === 'complete') {
            fn();
        } else {
            window.addEventListener('DOMContentLoaded', fn);
        }
    }
    onReady(function () {
        console.log('DOM fully loaded and parsed');
    });
    console.log('==Starting==');
    

    mixed-onready.js会根据执行时DOM是否已经装载完毕来决定是对回调函数进行同步调用还是异步调用。

    如果在调用onReady之前DOM已经载入的话,对回调函数进行同步调用;如果在调用onReady之前DOM还没有载入的话,通过注册 DOMContentLoaded 事件监听器来对回调函数进行异步调用。因此,如果这段代码在源文件中出现的位置不同,在控制台上打印的log消息顺序也会不同。

    async-onready.js

    function onReady(fn) {
        var readyState = document.readyState;
        if (readyState === 'interactive' || readyState === 'complete') {
            setTimeout(fn, 0);
        } else {
            window.addEventListener('DOMContentLoaded', fn);
        }
    }
    onReady(function () {
        console.log('DOM fully loaded and parsed');
    });
    console.log('==Starting==');
    

    Effective JavaScript

    • 绝对不能对异步回调函数(即使在数据已经就绪)进行同步调用。
    • 如果对异步回调函数进行同步调用的话,处理顺序可能会与预期不符,可能带来意料之外的后果。
    • 对异步回调函数进行同步调用,还可能导致栈溢出或异常处理错乱等问题。
    • 如果想在将来某时刻调用异步回调函数的话,可以使用 setTimeout等异步API
      — David Herman
    function onReadyPromise() {
        return new Promise(function (resolve, reject) {
            var readyState = document.readyState;
            if (readyState === 'interactive' || readyState === 'complete') {
                resolve();
            } else {
                window.addEventListener('DOMContentLoaded', resolve);
            }
        });
    }
    onReadyPromise().then(function () {
        console.log('DOM fully loaded and parsed');
    });
    console.log('==Starting==');
    
    catch
    var promise = Promise.reject(new Error("message"));
    promise.catch(function (error) {
        console.error(error);
    });
    

    实际上这和 catch 是ECMAScript的 保留字 (Reserved Word)有关。

    ECMAScript 3中保留字是不能作为对象的属性名使用的。 而IE8及以下版本都是基于ECMAScript 3实现的,因此不能将 catch 作为属性来使用,也就不能编写类似 promise.catch() 的代码,因此就出现了 identifier not found 这种语法错误了。

    使用Promise#then代替Promise#catch

    var promise = Promise.reject(new Error("message"));
    promise.then(undefined, function (error) {
        console.error(error);
    });
    
    Promise.all
    function getURL(URL) {
        return new Promise(function(resolve, reject) {
          var req = new XMLHttpRequest()
          req.open('GET', URL, true)
          req.onload = function() {
            if(req.status === 200) {
              resolve(req.responseText)
            }
          }
          req.onerror = function() {
            reject(new Error(req.statusText))
          }
    
          req.send()
        })
      }
    
      var request = {
        comment: function getComment() {
          return getURL('http://azu.github.io/promises-book/json/comment.json').then(JSON.parse);
        },
        people: function getPeople() {
          return getURL('http://azu.github.io/promises-book/json/people.json').then(JSON.parse);
        }
      };
    
      function main() {
        return Promise.all([request.comment(), request.people()]);
      }
    
      // 运行示例
      main().then(function (value) {
        console.log(value);
      }).catch(function(error){
        console.log(error);
      });
    
    • main中的处理流程显得非常清晰
    • Promise.all 接收 promise对象组成的数组作为参数
    Promise.all([request.comment(), request.people()]);
    

    在上面的代码中,request.comment()request.people() 会同时开始执行,而且每个promise的结果(resolvereject时传递的参数值),和传递给 Promise.allpromise数组的顺序是一致的。

    也就是说,这时候 .then 得到的promise数组的执行结果的顺序是固定的,即[comment, people]

    main().then(function (results) {
        console.log(results); // 按照[comment, people]的顺序
    });
    

    如果像下面那样使用一个计时器来计算一下程序执行时间的话,那么就可以非常清楚的知道传递给 Promise.all(http://liubin.org/promises-book/#Promise.all) 的promise数组是同时开始执行的。

    promise-all-timer.js

    // `delay`毫秒后执行resolve
    function timerPromisefy(delay) {
        return new Promise(function (resolve) {
            setTimeout(function () {
                resolve(delay);
            }, delay);
        });
    }
    var startDate = Date.now();
    // 所有promise变为resolve后程序退出
    Promise.all([
        timerPromisefy(1),
        timerPromisefy(32),
        timerPromisefy(64),
        timerPromisefy(128)
    ]).then(function (values) {
        console.log(Date.now() - startDate + 'ms');
        // 約128ms
        console.log(values);    // [1,32,64,128]
    });
    

    timerPromisefy 会每隔一定时间(通过参数指定)之后,返回一个promise对象,状态为FulFilled,其状态值为传给 timerPromisefy 的参数。

    而传给 Promise.all 的则是由上述promise组成的数组。

    var promises = [
        timerPromisefy(1),
        timerPromisefy(32),
        timerPromisefy(64),
        timerPromisefy(128)
    ];
    

    这时候,每隔1, 32, 64, 128 ms都会有一个promise发生 resolve 行为。

    也就是说,这个promise对象数组中所有promise都变为resolve状态的话,至少需要128ms。实际我们计算一下Promise.all 的执行时间的话,它确实是消耗了128ms的时间。

    从上述结果可以看出,传递给 Promise.allpromise并不是一个个的顺序执行的,而是同时开始、并行执行的。

    Promise.race

    接着我们来看看和 Promise.all 类似的对多个promise对象进行处理的 Promise.race 方法。

    它的使用方法和Promise.all一样,接收一个promise对象数组为参数。

    Promise.all 在接收到的所有的对象promise都变为 FulFilled或者 Rejected状态之后才会继续进行后面的处理, 与之相对的是 Promise.race 只要有一个promise对象进入FulFilled 或者Rejected 状态的话,就会继续进行后面的处理。

    Promise.all时的例子一样,我们来看一个带计时器的 Promise.race 的使用例子。

    promise-race-timer.js

    // `delay`毫秒后执行resolve
    function timerPromisefy(delay) {
        return new Promise(function (resolve) {
            setTimeout(function () {
                resolve(delay);
            }, delay);
        });
    }
    // 任何一个promise变为resolve或reject 的话程序就停止运行
    Promise.race([
        timerPromisefy(1),
        timerPromisefy(32),
        timerPromisefy(64),
        timerPromisefy(128)
    ]).then(function (value) {
        console.log(value);    // => 1
    });
    

    上面的代码创建了4个promise对象,这些promise对象会分别在1ms,32ms,64ms和128ms后变为确定状态,即FulFilled,并且在第一个变为确定状态的1ms后, .then 注册的回调函数就会被调用,这时候确定状态的promise对象会调用 resolve(1) 因此传递给 value 的值也是1,控制台上会打印出1来。

    下面我们再来看看在第一个promise对象变为确定(FulFilled)状态后,它之后的promise对象是否还在继续运行。

    promise-race-other.js

    var winnerPromise = new Promise(function (resolve) {
            setTimeout(function () {
                console.log('this is winner');
                resolve('this is winner');
            }, 4);
        });
    var loserPromise = new Promise(function (resolve) {
            setTimeout(function () {
                console.log('this is loser');
                resolve('this is loser');
            }, 1000);
        });
    // 第一个promise变为resolve后程序停止
    Promise.race([winnerPromise, loserPromise]).then(function (value) {
        console.log(value);    // => 'this is winner'
    });
    

    Promise chain

    function doubleUp(value) {
        return value * 2;
    }
    function increment(value) {
        return value + 1;
    }
    function output(value) {
        console.log(value);// => (1 + 1) * 2
    }
    
    var promise = Promise.resolve(1);
    promise
        .then(increment)
        .then(doubleUp)
        .then(output)
        .catch(function(error){
            // promise chain中出现异常的时候会被调用
            console.error(error);
        });
    

    这段代码的入口函数是 Promise.resolve(1); ,整体的promise chain执行流程如下所示。

    Promise.resolve(1); 传递 1 给increment 函数,函数increment 对接收的参数进行 +1 操作并返回(通过return)。这时参数变为2,并再次传给doubleUp 函数,最后在函数output中打印结果。

    每次调用then都会返回一个新创建的promise对象

    从代码上乍一看,aPromise.then(...).catch(...)像是针对最初的 aPromise对象进行了一连串的方法链调用。

    然而实际上不管是then还是catch方法调用,都返回了一个新的promise对象。

    下面我们就来看看如何确认这两个方法返回的到底是不是新的promise对象。

    var aPromise = new Promise(function (resolve) {
        resolve(100);
    });
    var thenPromise = aPromise.then(function (value) {
        console.log(value);
    });
    var catchPromise = thenPromise.catch(function (error) {
        console.error(error);
    });
    console.log(aPromise !== thenPromise); // => true
    console.log(thenPromise !== catchPromise);// => true
    
    then 的错误使用方法
    function badAsyncCall() {
        var promise = Promise.resolve();
        promise.then(function() {
            // 任意处理
            return newVar;
        });
        return promise;
    }
    

    这种写法有很多问题,首先在 promise.then中产生的异常不会被外部捕获,此外,也不能得到then的返回值,即使其有返回值。

    由于每次promise.then调用都会返回一个新创建的promise对象,因此需要像上述方式2那样,采用promise chain的方式将调用进行链式化,修改后的代码如下所示。

    function anAsyncCall() {
        var promise = Promise.resolve();
        return promise.then(function() {
            // 任意处理
            return newVar;
        });
    }
    

    相关文章

      网友评论

          本文标题:Promise

          本文链接:https://www.haomeiwen.com/subject/xjixoftx.html