js中Promise的使用例子详细介绍

使用Promise是极好的,它是如此有用以至于我觉得应该好好研究一下Promise,甚至是实现一个简易的版本。实现之前,我们先来看看Promise的用途:

使用Promise
callback hell
Promise的第一个用途是能够很好地解决回调黑洞的问题,假设要实现一个用户展示的任务,这个任务分为三步:

获取用户信息
获取用户图像
弹窗提示
不使用Promise,我们的实现可能是这样子:

getUserInfo(id, function (info) { 
  getUserImage(info.img, function () {
    showTip();
  })
})
这里只是三步,如果有更长串的任务时,我们就会陷入到回调黑洞之中,为了解决这个问题,我们就可以使用Promise来处理这一长串任务,使用Promise的版本是这样子的:

// getUserInfo返回promise
getUserInfo(id) 
  .then(getUserImage)
  .then(showTip)
  .catch(function (e) {
     console.log(e);
  });
原来向右发展的代码,开始向下发展,这样也更适合编程习惯,如果要让我们的代码更加健壮,我们就需要在每一步来处理错误信息,使用promise这后,我们只需要在最后的catch中做善后处理。

并发
假如我们要显示某一个页的10条记录,但是我们只有一个通过id获取记录的接口,这样我们就需要发送10个请求,并且所有请求都完成之后再将记录全部添加到页面之中,Promise在这个场景下使用是特别合适的。

代码可能是这样子:

// ids要获取信息的所有记录id
// getRecordById获取记录的接口,返回promise
Promise.all(ids.map(getRecordById)) 
  .then(showRecords)
  .catch(function (e) {
     console.log(e);
  });
这就是Promise的一些简单的用途,当然令人兴奋的是Promise已经是ES6的标准,而且目前很多浏览器已经原生支持Promise了。对于那些无法使用Promise的浏览器,我们就只能自己去实现了,下面就来看看Promise的简单实现吧。

实现
warm up
先来盗用一张MDN的图,先来热热身,看看Promise的状态迁移: promise Promise有三种状态:

pending:初始状态, 非 fulfilled 或 rejected
fulfilled: 成功的操作
rejected: 失败的操作
我们可以看出新建的Promise是pending状态,fulfill之后就会执行调用then的回调函数了,倘若reject了就会调用catch来进行异常处理了,并且无论是调用then还是catch都会返回新的promise,这就是为什么promise可以链式调用了。

接着,我们来研究一下规范是怎么描述 promise的。这里只抽取核心部分,边界问题不考虑。

构造函数:Promise ( executor )
检查参数:例如executor是不是函数啊
初始化:[[State]]=pending,[[FulfillReactions]]=[],[[RejectReactions]]=[]
创建resolve对象:{[[Resolve]]: resolve, [[Reject]]: reject}
执行executor:executor(resolve, reject)
因此构造函数里面传入的excuter是立即被执行的。FulfillReactions存储着promise执行成功时要做的操作,RejectReactions存储着promise是要执行的操作。

function Promise(resolver) { 
  this._id = counter++;
  this._state = PENDING;
  this._result = undefined;
  this._subscribers = [];

  var promise = this;

  if (noop !== resolver) {
    try {
      resolver(function (value) {
        resolve(promise, value);
      }, function (reason) {
        reject(promise, reason);
      });
    } catch (e) {
      reject(promise, e);
    }
  }
}
FulfillPromise(promise, value)
检查[[state]],必须为pending(不是pending的表示已经解析,不能重复解析)
赋值:[[Result]]=value,[[state]]=fulfilled
触发[[FulfillReactions]]的操作
和FulfillPromise联系最紧密的就是ResolvePromise了,这里我们给出的是ResolvePromise的实现,区别只是多了直接解析Promise。

function resolve(promise, value) { 
  // 要resolve的为promise(then的callback返回的是promise)
  if (typeof value === 'object'
    && promise.constructor === value.constructor) {
    handleOwnThenable(promise, value);
  }
  // 要resolve的是值
  else {
    if (promise._state !== PENDING) { return; }

    promise._result = value;
    promise._state = FULFILLED;

    asap(publish, promise);
  }
}

function handleOwnThenable(promise, thenable) { 
  // 如果返回的promise已经完成
  // 直接用该promise的值resolve父promise
  if (thenable._state === FULFILLED) {
    resolve(promise, thenable._result);
  } else if (thenable._state === REJECTED) {
    reject(promise, thenable._result);
  }
  // 如果返回的promise未完成
  // 要等该promise完成再resolve父promise
  else {
    subscribe(thenable, undefined, function(value) {
      resolve(promise, value);
    }, function(reason) {
      reject(promise, reason);
    });
  }
}
RejectPromise(promise, reason)
检查[[state]],必须为pending(不是pending的表示已经解析,不能重复解析)
赋值:[[Result]]=reason,[[state]]=rejected
触发[[RejectReactions]]的操作
触发[[FulfillReactions]]和触发[[RejectReactions]]实际就是遍历数组,执行所有的回调函数。

function reject(promise, reason) { 
  if (promise._state !== PENDING) { return; }

  promise._state = REJECTED;
  promise._result = reason;

  asap(publish, promise);
}
Promise.prototype.then(onFullfilled, onRejected)
promise=this
新建resultCapability三元组,{[[Promise]], [[Resolve]], [[Reject]]}([[Promise]]新建的)
fulfillReaction={[[Capabilities]]: resultCapability, [[Handler]]: onFulfilled}
rejectReaction={[[Capabilities]]: resultCapability, [[Handler]]: onRejected}
如果[[state]]是pending:fulfillReaction加入[[FulfillReactions]],rejectReaction加入[[RejectReactions]]
如果[[state]]是fulfilled:fulfillReaction加入执行队列
如果[[state]]是rejected:rejectReaction加入执行队列
返回resultCapability.[[Promise]]
这里可以看出构造函数和then的关系是很紧密的,新建的promise如果是异步操作,那么状态就是pending,调用then时会新建子promise,并且将回调操作加入父promise的[[FulfillReactions]]或[[RejectReactions]]的数组里,这实际就是发布订阅模式。

他们是这样的关系: promise构造函数与then的关系

无论是new promise还是调用then或catch,都会得到一个新的promise,这些promise都会订阅父级promise的完成事件,父级promise完成之后就会执行一系列的回调操作,也就是发布。

Promise.prototype.catch(onRejected)
then的语法糖:then(null, onRejected)
下面就是Promise原型:

Promise.prototype = { 
  constructor: Promise,

  then: function (onFulfillment, onRejection) {
    var parent = this;
    var state = parent._state;

    if (state === FULFILLED && !onFulfillment
        || state === REJECTED && !onRejection) {
        return this;
    }

    var child = new Promise(noop);
    var result = parent._result;

    if (state) {
      var callback = arguments[state - 1];
      asap(function () {
        invokeCallback(state, child, callback, result);
      });
    } else {
      subscribe(parent, child, onFulfillment, onRejection);
    }

    return child;
  },

  'catch': function (onRejection) {
    return this.then(null, onRejection);
  }
};
Promise.resolve(value)
新建promise
调用ResolvePromise(promise, value)(未列出,会判断一些情况然后调用FulfillPromise)
返回promise
Promise.resolve = function (arg) { 
  var child = new Promise(noop);
  resolve(child, arg);
  return child;
};
Promise.reject(value)
新建promise
调用RejectPromise(promise, value)
返回promise
Promise.reject = function (reason) { 
  var child = new Promise(noop);
  reject(child, reason);
  return child;
};
Promise.all(iterator)
到这里我们已经能够实现基本的promise了,Promise.all和Promise.race就不继续描述了,有兴趣的可以继续去读规范,这里上图来说明我对这两个函数的理解:

promise.all理解

调用promise.all会新建一个对象来存储所有promise的处理状态,保存执行的结果,当remain为0时,就可以resolve 新建的promise,这样就可以继续往后执行了。

Promise.all = function (promises) { 
  var child = new Promise(noop);
  var record = {
    remain: promises.length,
    values: []
  };
  promises.forEach(function (promise, i) {
    if (promise._state === PENDING) {
      subscribe(promise, undefined, onFulfilled(i), onRejected);
    } else if (promise._state === REJECTED) {
      reject(child, promise._result);
      return false;
    } else {
      --record.remain;
      record.values[i] = promise._result;
      if (record.remain == 0) {
        resolve(child, values);
      }
   }
  });
  return child;

  function onFulfilled(i) {
    return function (val) {
      --record.remain;
      record.values[i] = val;
      if (record.remian === 0) {
        resolve(child, record.values);
      }
    }
  }

  function onRejected(reason) {
    reject(child, reason);
  }
};
Promise.race(iterator)
promise.race与promise.all类似,不过只要有一个promise完成了,我们就可以resolve新建的promise了。

Promise.race = function (promises) { 
  var child = new Promise(noop);

  promises.forEach(function (promise, i) {
    if (promise._state === PENDING) {
      subscribe(promise, undefined, onFulfilled, onRejected);
    } else if (promise._state === REJECTED) {
      reject(child, promise._result);
      return false;
    } else {
      resolve(child, promise._result);
      return false;
    }
  });
  return child;

  function onFulfilled(val) {
    resolve(child, val);
  }

  function onRejected(reason) {
    reject(child, reason);
  }
};
这就是promise的基本内容了,完整代码请戳这里。

其他问题
promises 穿透
如果传入then里面的参数不是函数,实际不会被忽略的,这就是promise穿透的原因,所以永远往then里面传递函数。答案可以从then方法里面调用的一个关键函数invokeCallback中找到答案:

function invokeCallback(settled, promise, callback, detail) { 
    var hasCallback = (typeof callback === 'function'),
      value, error, succeeded, failed;

    if (hasCallback) {

      try {
        value = callback(detail);
      } catch (e) {
        value = {
          error: e
        };
      }

      if (value && !!value.error) {
        failed = true;
        error = value.error;
        value = null;
      } else {
        succeeded = true;
      }

    }
    // then的参数不是函数
    // 会被忽略,也就是promise穿透
    else {
      value = detail;
      succeeded = true;
    }

    if (promise._state === PENDING) {
      if (hasCallback && succeeded
          || settled === FULFILLED) {
        resolve(promise, value);
      } else if (failed || settled === REJECTED) {
        reject(promise, error);
      }
    }
  }
例如如下例子,结果都是输出foo:

Promise.resolve('foo').then(Promise.resolve('bar')).then(function (result) { 
  console.log(result);
});

Promise.resolve('foo').then(null).then(function (result) { 
  console.log(result);
});
拥抱金字塔
promise能够很好的解决金字塔问题,但是有时候我们也是需要适当使用金字塔的,例如我们要同时获取两个promise的结果,但是这两个promise是有关联的,也就是有顺序的,该怎么办?

也许解决方案会是这样,定义一个全局变量,这样在第二个then里面就可以使用两个promise的结果了。

var user; 
getUserByName('nolan').then(function (result) { 
  user = result;
  return getUserAccountById(user.id);
}).then(function (userAccount) {
  // 好了, "user" 和 "userAccount" 都有了
});
但是这不是最好的方案,此时何不抛弃成见,拥抱金字塔:

getUserByName('nolan').then(function (user) { 
  return getUserAccountById(user.id).then(function (userAccount) {
    // 好了, "user" 和 "userAccount" 都有了
  });
});
promise是如此强大而且难以理解,但是抓住实质之后其实并没有想象的那么复杂,这也是为什么我要写下这篇文章。更过关于如何正确使用promise

时间: 2025-01-26 05:03:33

js中Promise的使用例子详细介绍的相关文章

js中substring和substr的详细介绍与用法_javascript技巧

1.substring 方法 用于提取字符串中介于两个指定下标之间的字符 substring(start,end) 开始和结束的位置,从零开始的索引 参数     描述start     必需.一个非负的整数,规定要提取的子串的第一个字符在 stringObject 中的位置.stop     可选.一个非负的整数,比要提取的子串的最后一个字符在 stringObject 中的位置多 1.如果省略该参数,那么返回的子串会一直到字符串的结尾. 返回值 一个新的字符串,该字符串值包含 stringO

JavaScript中的分号插入机制详细介绍

 这篇文章主要介绍了JavaScript中的分号插入机制详细介绍,本文讲解JavaScript中各种情况下的分号插入机制,需要的朋友可以参考下     仅在}之前.一个或多个换行之后和程序输入的结尾被插入 也就是说你只能在一行.一个代码块和一段程序结束的地方省略分号. 也就是说你可以写如下代码 代码如下: function square(x) { var n = +x return n * n } 但是却不可以写的像下面代码一样,这样就报错了哦 代码如下: function area(r) {

Node.js中的流(Stream)介绍

 这篇文章主要介绍了Node.js中的流(Stream)介绍,本文讲解了什么是流.pipe方法.流的分类.Readable流状态的切换等内容,需要的朋友可以参考下     什么是流? 说到流,就涉及到一个*nix的概念:管道--在*nix中,流在Shell中被实现为可以通过 |(管道符) 进行桥接的数据,一个进程的输出(stdout)可被直接作为下一个进程的输入(stdin). 在Node中,流(Stream)的概念与之类似,代表一种数据流可供桥接的能力. pipe 流化的精髓在于 .pipe(

Android中imageview.ScaleType使用方法详细介绍

Android中imageview.ScaleType使用方法详细介绍 ScaleType属性用以表示显示图片的方式,共有8种取值: ScaleType.CENTER:图片大小为原始大小,如果图片大小大于ImageView控件,则截取图片中间部分,若小于,则直接将图片居中显示. ScaleType.CENTER_CROP:将图片等比例缩放,让图像的短边与ImageView的边长度相同,即不能留有空白,缩放后截取中间部分进行显示. ScaleType.CENTER_INSIDE:将图片大小大于Im

JS中数组Array的用法示例介绍

 这篇文章主要介绍了JS中数组Array的用法,需要的朋友可以参考下 new Array()  new Array(len)  new Array([item0,[item1,[item2,...]]]  使用数组对象的方法:  var objArray=new Array();  objArray.concact([item1[,item2[,....]]]-------------------将参数列表连接到objArray的后面形成一个新的数组并返回,原有数组不受影响.如:var arr=

中华通网络电话使用的详细介绍

  中国大陆内电话怎么收费? 1.零月租,全球无漫游.无论在全球哪个国家/地区拨打国内电话0.09元/分钟,充值后资费低至0.05元/分钟,支持拨打国内手机.座机.小灵通.(使用中华通软件拨打电话时,只从中华通账户中扣取费用,不会额外收取网络.电话接通.线路租用等其它费用) 2.只收取主叫方费用,被叫由当地运营商而定:如固话.小灵通接听电话免费的,当地手机如有接听免费业务则接听中华通免费,不会产生其他的费用. 国际长途怎么收费? 1.拨打不同的国家/地区资费不同,各个国家/地区不同号段的号码资费

【IOS-COCOS2D-X 游戏开发之十四】XCODE中C++&OBJECT-C混编,详细介绍如何在COCOS2DX中访问OBJECT函数以及APPLE API

本站文章均为 李华明Himi 原创,转载务必在明显处注明: 转载自[黑米GameDev街区] 原文链接: http://www.himigame.com/iphone-cocos2dx/743.html Cocos2dx系列博文的上一篇详细介绍了如何在Xcode中利用jni调用Android的Java层代码,还没有看过的童鞋,请移步到如下博文: [iOS-cocos2d-X 游戏开发之十三]详细讲解在Xcode中利用预编译并通过Jni调用Android的Java层代码(cocos2dx里访问调用

JS中的this变量的使用介绍_基础知识

JavaScript中this的使用 在JavaScript中this变量是一个令人难以摸清的关键字,this可谓是非常强大,充分了解this的相关知识有助于我们在编写面向对象的JavaScript程序时能够游刃有余. 对于this变量最要的是能够理清this所引用的对象到底是哪一个,也许很多资料上都有自己的解释,但有些概念讲的偏繁杂.而我的理解是:首先分析this所在的函数是当做哪个对象的方法调用的,则该对象就是this所引用的对象. 示例一. 复制代码 代码如下: var obj = {};

JavaScript中的console.profile()函数详细介绍_javascript技巧

编写JavaScript程序时,如果需要知道某段代码的执行时间,可以使用console.time().不过,在分析逻辑较为复杂的JavaScript程序,试图从中找出性能瓶颈的时候,console.time()就不适用了 - 深入分析逻辑较为复杂的JavaScript程序的运行就意味着插入大量的console.time()语句,而这无疑是不可接受的.对于复杂逻辑的JavaScript程序调优,正确的方法是使用console.profile(). 浏览器支持 安装了Firebug插件的Firefo