Promise

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_第1张图片

静态方法

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中打印结果。

Promise_第2张图片

每次调用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
Promise_第3张图片
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)