Promise 对象

Promise 建立

Promise 对象用来传递异步操作消息,代表一个未来才会知道结果的事件,并且对不同事件提供统一的 API 以便进一步处理。Promise 具有以下特点:

  • 由异步操作结果决定改状态,其他操作绝不影响该状态;
  • 对象状态不受外界影响:Promise 代表的异步操作有三个状态:
  1. Pending: 进行中
  2. Resolved: 已完成(Fulfilled)
  3. Rejected: 已失败
  • 一旦状态改变,就不会再变:Promise 的状态只有2种可能:
  1. 从 Pending 到 Resolved
  2. 从 Pending 到 Rejected

对于同一个 promise, 当以上状态发生一个(只能发生其一),就不会再改变了。之后任何时间你都能得到这个状态,且永不改变。
有了 Promise 就可以将层层的回调写为同步的样子,表示起来更清晰。不过需要注意以下几点:

  • Promise 一旦建立就立即执行,并且无法中断或取消
  • 如果没有设置回调函数,那么 Promise 中的产生的错误不会抛到外部
  • Pending 状态时,我们无法知道其具体进度

Promise 的基本结构如下:

var promise = new Promise(function(resolve, reject){
  if(/*异步操作成功*/){
    resolve(value);
  } else {
    reject(error);
  }
});

构造函数接受一个回调函数为参数,回调函数具有2个参数,也都是函数,resolve 在 Promise 状态变为 resolved 时调用,reject 在 Promise 状态变为 rejected 时调用。resolve 的接受一个参数——值或另一个 promise 对象; rejectj接受一个参数——错误。需要说明的是,这里的 resole 和 reject 函数已经由系统部署好了,我们可以不写。

promise 构建好以后我们就可以调用它的then()方法,then(resolve(value){},reject(value){})方法接受2个函数参数,resolve 在 Promise 状态变为 resolved 时调用,reject 在 Promise 状态变为 rejected 时调用。其中 reject 参数是可选的。和构造函数不同的是,then 方法的 reject 和 resolve 都使用 promise 传出的值作为其唯一的参数。

这里写一个简单的例子,理解一下:

function timeout(ms){
  return new Promise((resolve, reject) => {
    console.log("promise");            //"promise"
    setTimeout(resolve, ms, 'done');
  });
}
timeout(2000).then((value) => {
  console.log(value);                  //2秒后得到 "done"
});

利用 Promise 异步加载图片:

function loadImageAsync(url){
  return new Promise(function(resole, reject){
    var image = new Image();
    image.onload = function(){
      resolve(image);
    };
    image.onerror = function(){
      reject(new Error(`Could not load image at ${url}`));
    };
    image.src = url;
  });
}

利用 Promise 实现 Ajax:

  var id = document.getElementById("primary");
  var getJSON = function(url){
    var promise = new Promise(function(resolve, reject){
      var client = new XMLHttpRequest();
      client.open("GET", url);
      client.onreadystatechange = handler;
      client.response = "json";
      client.setRequestHeader("Accept", "application/json");
      client.send();

      function handler(){
        if(client.readyState !== 4) return;
        if(this.status === 200){
          resolve(client.response);
        } else {
          reject(new Error(this.statusText));
        }
      }
    });
    return promise;
  }
  getJSON('info.json').then(
    json => id.innerHTML = "<pre>" + json + "</pre>",
    err => id.innerHTML = err
  );

如果 resolve 的参数是一个promise:

var p1 = new Promise(function(resolve, reject){
  //...
});
var p2 = new Promise(function(resolve, reject){
  //...
  resolve(p1);
});

上面代码中 p1 的状态传给了 p2,也就是p1运行完成(状态为 resolve 或 reject)后 p2 的回调函数会立刻开始执行:

var p1 = new Promise(function(resolve, reject){
  setTimeout(() => reject(new Error('failed')), 3000);
});
var p2 = new Promise(function(resolve, reject){
  setTimeout(() => resolve(p1), 1000);
});
p2.then(result => console.log(result));
p2.catch(error => console.log(error));

p1 建立,进入 setTimeout 异步计时器。之后 p2 建立,进入 setTimeout 异步计时器。1s 后 p2 准备执行 resolve, 但是 resolve 的参数是 p1, 此时 p1 还是 Pending 状态,所以 p2 开始等待。又过了 2s, p1 的 reject 执行,变为 rejected 状态,随即 p2 也跟着变成 rejected 状态。

Promise 对象方法

  • then() 方法
    then(resolve(value){},reject(value){})方法接受2个函数参数,resolve 在 Promise 状态变为 resolved 时调用,reject 在 Promise 状态变为 rejected 时调用。其中 reject 参数是可选的。和构造函数不同的是,then 方法的 reject 和 resolve 都使用 promise 传出的值作为其唯一的参数。
    then() 方法返回一个新的 Promise 实例,注意,不是之前那个。因此可以用链式调用,不断添加"回调"函数。 then 的返回值成了下一个 then 中回调函数的参数:
var p = new Promise(function(resolve, reject){
  resolve("from new Promise");
}).then(function (value){
  console.log(value);     //from new Promise    其次输出这个
  return "from the first 'then'";
}).then(function(value){
  console.log(value);     //from the first 'then'    最后输出这个
  return "from the second 'then'";
});
console.log(p);           //Promise{...}    先输出这个

注意,如果 promise 的状态是 resolved 则执行 then参数中的第一个回调函数;如果 promise 的状态是 rejected 则执行 then参数中的第二个回调函数。这个状态是不断传递下来的,这一点和之前的例子类似。

  • catch() 方法:
    catch(reject) 方法是 then(null, reject) 的别名,在发生错误的时候执行其参数函数:
new Promise(function(resolve, reject){
  resolve("resolved");
}).then(function(val){
  console.log(val);           //resolved
  throw new Error("man-made Error");
}).catch(function(err){
  console.log(err.message);   //man-made Error
});

错误会从最初的请求沿着回调函数,一直被传递下来。这一点和传统的错误冒泡类似,无论哪里有错误都可以被捕获到:

new Promise(function(resolve, reject){
  reject(new Error("original Error"));
}).then(function(val){
  console.log(val);           //不执行
  throw new Error("man-made Error");
}).catch(function(err){
  console.log(err.message);   //original Error
});

当然也可以在半路截住错误:

new Promise(function(resolve, reject){
  reject(new Error("original Error"));
}).then(function(val){
  console.log(val);           //不执行
  throw new Error("man-made Error");
}, function(err){
  console.log(`Uncaught Error: ${err.message}`);  //Uncaught Error: original Error
}).catch(function(err){
  console.log(err.message);   //不执行
});

这里需要注意以下几点:

  1. reject 和 throw 一样可以抛出错误。
  2. 在 Promise 状态变为 resolved 或 rejected 之后抛出的错误会被忽略。
  3. 建议总是使用 catch() 方法,而不要在 then() 方法中定义 reject 函数。
  4. 如果一个 promise 既没有 catch方法,也没有可以捕获到错误的 then 方法,那么这个错误就消失了。它不会到 promise 外面来。
  5. try...catch... 只能捕获同步代码的错误,不能捕获异步代码的错误(这个是 ES5 就有的)。
  6. catch() 方法可以继续抛出错误,就像 try...catch 中的 catch 一样可以抛出错误。
    这里需要说明的是第4条:错误不会到 Promise 外面是 ES6 规范的说法。具体理解(浏览器环境):控制台依旧会报错,但是不影响 promise 语句之后续代码执行。此外,promise 语句内的异步语句(如事件,定时器等等)抛出的错误,不属于 promise 内部,发生错误会传播出去:
var p = new Promise(function(resolve, reject){
  resolve("ok");
  setTimeout(function(){throw new Error("setTimeout error")},0);
});
p.then(function(val){console.log(val);});     //ok
//Uncaught Error: setTimeout error

其次,就以上前两个注意事项举一例说明:

new Promise(function(resolve, reject){
  resolve("resolved");
  throw "original Error";     //被忽略
}).then(function(val){
  console.log(val);           //resolved
  throw (new Error("man-made Error"));
}).catch(function(err){
  console.log(err.message);   //man-made Error
});

catch 方法的返回值还是一个新的 promise 对象,可以继续调用 then 等其他方法:

new Promise(function(resolve, reject){
  reject(new Error("reject"));
}).catch(function(err){
  console.log("1st catch");   //被跳过
  return "continue";
}).then(function(val){
  console.log(val);           //continue
});

如果 catch之前没有错误,该 catch 会被跳过。这意味着,catch 不能捕获在其后面的语句中出现的错误:

new Promise(function(resolve, reject){
  resolve("resolved");
}).catch(function(err){
  console.log("1st catch");   //被跳过
}).then(function(val){
  console.log(val);           //resolved
  throw (new Error());
}).catch(function(err){
  console.log("2nd catch");   //2nd catch
});
  • finally() 方法

finally() 接受一个回调函数(无参数)为参数,和 try...catch...finally 中的 finally 类似,不论 promise 是什么状态,该回调函数都一定会运行。可以用它关闭文件,或者关闭服务器等:

server.listen(0).then(function(){
  //do sth.
}).finally(server.stop);

finally() 内部实现如下:

Promise.prototype.finally = function(callback){
  return this.then(
    value => {Promise.resolve(callback()).then(() => value)},
    error => {Promise.resolve(callback()).then(() => {throw error})}
  );
};
  • done() 方法

done() 方法用在 promise 处理语句的末端,用来处理可能未捕获的错误,并抛向全局。如果其带有参数,可以等效为 done() 之前多了一个 then():

p.done(fun1, fun2);
//相当于
p.then(fun1,fun2).done();

done() 内部实现如下:

Promise.prototype.done = function(onResolve, onRejected){
  this.then(onResolve, onRejected).catch(function(err){
    setTimeout(() => {throw err}, 0);
  });
};

Promise 静态方法

  • Promise.all()
    将多个 promise 对象合并成一个新的 promise 实例。其接受一个装仅有 promise 对象的可遍历结构为参数,如果不是 promise 对象,系统会调用 Promise.resolve() 进行类型转换。
    promise.all() 方法得到的新的 promise 对象状态由构成它的所有 promise 对象决定,具体分为2种情况:
  1. 当所有构成它的 promise 对象的状态都变成 resolved,这个新的对象状态才变为 resolved。此时构成它所有的 Promise 的返回值构成一个数组作为新的 promise 对象的回调函数参数;
  2. 当所有构成它的 promise 对象的状态有一个变成 rejected,这个新的对象状态就变为 rejected。此时第一个被 reject 的 Promise 的返回值作为新的 promise 对象的回调函数参数;
//伪代码, 由于没有正确的 url
var getJSON = function(url){
  var promise = new Promise(function(resolve, reject){
    var client = new XMLHttpRequest();
    client.open("GET", url);
    client.onreadystatechange = handler;
    client.response = "json";
    client.setRequestHeader("Accept", "application/json");
    client.send();
    function handler(){
      if(client.readyState !== 4) return;
      if(this.status === 200){
        resolve(client.response);
      } else {
        reject(new Error(this.statusText));
      }
    }
  });
  return promise;
}
var pros = ['url1', 'url2', 'url3'].map(url => getJSON(url));
Promise.all(pros).then(function(){
  console.log("all successful");
}, function(){
  console.log("one rejected");       //one rejected, 由于没有正确的 url
});
  • Promise.race()
    将多个 promise 对象合并成一个新的 promise 实例。其接受一个装仅有 promise 对象的可遍历结构为参数,如果不是 promise 对象,系统会调用 Promise.resolve() 进行类型转换。
    和 promise.all() 不同的是 Promise.race() 方法得到的新的 promise 对象状态由构成它的 promise 对象中最先改变状态的那一个决定。
//伪代码, 由于没有正确的 url
var getJSON = function(url){
  var promise = new Promise(function(resolve, reject){
    var client = new XMLHttpRequest();
    client.open("GET", url);
    client.onreadystatechange = handler;
    client.response = "json";
    client.setRequestHeader("Accept", "application/json");
    client.send();
    function handler(){
      if(client.readyState !== 4) return;
      if(this.status === 200){
        resolve(client.response);
      } else {
        reject(new Error(this.statusText));
      }
    }
  });
  return promise;
}
//如果5s不能获得数据就报错
var p = Promise.race([
  getJSON("url"),
  new Promise(function(resolve, reject){
    setTimeout(() => reject(new Error("Timeout")), 5000);
  })
]).then(res => console.log(res))
.catch(err => console.log(err));    //Error, 由于没有正确的 url
  • Promise.resolve()
    将现有对象转化为 promise 对象:
var p = Promise.resolve($.ajax('url'));  //jQuery的 $.ajax 方法
//等同于:
var p = new Promise(function(resolve){
  resolve($.ajax('url'));
});

如果传入 Promise.resolve() 的对象不具有 then 方法(ie. unthenable), 则返回一个状态为 resolved 的新 promise 对象。

Promise.resolve("hello").then(function(val){
  console.log(val);                             //hello
});

如果你仅仅想得到一个 promise 对象,那利用 resolve() 方法是最简单的:

var promise = Promise.resolve();
  • Promise.reject()
    Promise.reject(reason), 返回一个状态为 rejected 的 promise 实例。参数 reason 会被传递被实例的回调函数。
Promise.reject(new Error("error occured")).catch(err => console.log(err.message));  //error occured

应用举例

  1. 加载图片:
var preloadImage = function(url){
  return new Promise(function(resolve, reject){
    var image = new Image();
    image.onload = resolve;
    image.onerror = reject;
    image.src = url;
  });
};
  1. 使用 Generator 管理流程,用 promise 进行异步操作
function getFoo(){
  return new Promise(function(resolve){
    resolve("foo");
  });
}
function* gen(){
  try{
    var foo = yield getFoo();
    console.log(foo);
  } catch(e) {
    console.log(e);
  }
}

var it = gen();
(function go(result){
  if(result.done) return result.value;
  return result.value.then(function(value){
    return go(it.next(value));
  }, function(err){
    return go(it.throw(error));
  });
})(it.next());      //foo
  • 异步中模拟 sleep 函数
const sleep = (time) => new Promise(function(resolve){
  setTimeout(resolve, time);
});
(async () => {
  for(var i = 0; i < 5; i++){
    await sleep(1000);
    console.log(new Date, i);
  }
  await sleep(1000);
  console.log(new Date, i);
})();
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,393评论 5 467
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,790评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,391评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,703评论 1 270
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,613评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,003评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,507评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,158评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,300评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,256评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,274评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,984评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,569评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,662评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,899评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,268评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,840评论 2 339

推荐阅读更多精彩内容

  • 00、前言Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。它由社区...
    夜幕小草阅读 2,124评论 0 12
  • Promise的含义:   Promise是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和...
    呼呼哥阅读 2,158评论 0 16
  • Promise由浅入深 前言 我们都知道javaScript是单线程的,代码执行的顺序是从上往下。但是在实际开发中...
    一蓑烟雨任平生_cui阅读 362评论 0 1
  • 今天饭饭开学 上幼儿园亲子班 早上起床一睁开眼就开始哭 情绪不是很好 我匆匆的吃过早饭便去了学校 开学第一天学校一...
    AI大叔阅读 324评论 0 0
  • —— 游天祝小三峡 去年的国庆节前夕,我们几个同事相约去天祝三峡游玩。照说游天祝三峡最好的季节是夏季,...
    无诗雨阅读 263评论 2 9