转载

promise介绍——基础篇

前言

Promise,相信每一个前端工程师都或多或少地在项目中都是用过,毕竟它早已不是一个新名词。ES6中已经原生对它加以支持,在caniuse中搜索一下 Promise ,发现新版的chrome和firefox也已经支持。但是低版本的浏览器我们可以使用 es6-promise 这个 polyfill 库来加以兼容。

暂且不谈 awaitasync ,在Google或百度或360搜索等搜索引擎、或者在segmentfault等社区中,我们可以搜到一大把介绍 promise 的文章,毕竟它已经出现了很长时间,早已有很多大神分析讲解过。

我也看了一些文章,但是感觉都没有达到想要的效果。所以决定自己开一个小系列文章学习讲解一下promise的原理,以及实现,最后再谈一谈与之联系密切的Deferred对象。

本文是该系列的第一篇文章,主要先让大家对Promise有一个基本的认识。

promise简介

Promise的出现,原本是为了解决回调地狱的问题。所有人在讲解 Promise 时,都会以一个ajax请求为例,此处我们也用一个简单的ajax的例子来带大家看一下 Promise 是如何使用的。

ajax请求的传统写法:

getData(method, url, successFun, failFun){
  var xmlHttp = new XMLHttpRequest();
  xmlHttp.open(method, url);
  xmlHttp.send();
  xmlHttp.onload = function () {
    if (this.status == 200 ) {
      successFun(this.response);
    } else {
      failFun(this.statusText);
    }
  };
  xmlHttp.onerror = function () {
    failFun(this.statusText);
  };
}

改为 promise 后的写法:

getData(method, url){
  var promise = new Promise(function(resolve, reject){
    var xmlHttp = new XMLHttpRequest();
    xmlHttp.open(method, url);
    xmlHttp.send();
    xmlHttp.onload = function () {
      if (this.status == 200 ) {
        resolve(this.response);
      } else {
        reject(this.statusText);
      }
    };
    xmlHttp.onerror = function () {
      reject(this.statusText);
    };
  })
  return promise;
}

getData('get','www.xxx.com').then(successFun, failFun)

很显然,我们把异步中使用回调函数的场景改为了 .then().catch() 等函数链式调用的方式。基于 promise 我们可以把复杂的异步回调处理方式进行模块化。

下面,我们就来介绍一下 Promise 到底是个什么东西?它是如何做到的?

Promise 的原理分析

其实 promise 原理说起来并不难,它内部有三个状态,分别是 pendingfulfilledrejected

pending 是对象创建后的初始状态,当对象 fulfill (成功)时变为 fulfilled ,当对象 reject (失败)时变为 rejected 。且只能从 pengding 变为 fulfilledrejected ,而不能逆向或从 fulfilled 变为 rejected 、从 rejected 变为 fulfilled 。如图所示:

<img src=" http://ohjvilcfv.bkt.clouddn.... width="300px" />

Promise 实例方法介绍

Promise 对象拥有两个实例方法 then()catch()

从前面的例子中可以看到,成功和失败的回调函数我们是通过 then() 添加,在 promise 状态改变时分别调用。 promise 构造函数中通常都是异步的,所以 then 方法往往都先于 resolvereject 方法执行。所以 promise 内部需要有一个存储 fulfill 时调用函数的数组和一个存储 reject 时调用函数的数组。

从上面的例子中我们还可以看到 then 方法可以接收两个参数,且通常都是函数(非函数时如何处理下一篇文章中会详细介绍)。第一个参数会添加到 fulfill 时调用的数组中,第二个参数添加到 reject 时调用的数组中。当 promise 状态 fulfill 时,会把 resolve(value) 中的 value 值传给调用的函数中,同理,当 promise 状态 reject 时,会把 reject(reason) 中的 reason 值传给调用的函数。例:

var p = new Promise(function(resolve, reject){
    resolve(5)
}).then(function(value){
    console.log(value) //5
})

var p1 = new Promise(function(resolve, reject){
    reject(new Error('错误'))
}).then(function(value){
    console.log(value)
}, function(reason){
    console.log(reason) //Error: 错误(…)
})

then 方法会返回一个新的 promise ,下面的例子中 p == p1 将返回 false ,说明 p1 是一个全新的对象。

var p = new Promise(function(resolve, reject){
    resolve(5)
})
var p1 = p.then(function(value){
    console.log(value)
})
p == p1 // false

这也是为什么 then 是可以链式调用的,它是在新的对象上添加成功或失败的回调,这与 jQuery 中的链式调用不同。

那么新对象的状态是基于什么改变的呢?是不是说如果 p 的状态 fulfill ,后面的 then 创建的新对象都会成功;或者说如果 p 的状态 reject ,后面的 then 创建的新对象都会失败?

var p = new Promise(function(resolve, reject){
    resolve(5)
})
var p1 = p.then(function(value){
    console.log(value)   // 5
}).then(function(value){
    console.log('fulfill ' + value)   // fulfill undefined
}, function(reason){
    console.log('reject ' + reason)   
})

上面的例子会打印出5和"fulfill undefined"说明它的状态变为成功。那如果我们在 p1then 方法中抛出异常呢?

var p = new Promise(function(resolve, reject){
    resolve(5)
})
var p1 = p.then(function(value){
    console.log(value)   // 5
    throw new Error('test')
}).then(function(value){
    console.log('fulfill ' + value)
}, function(reason){
    console.log('reject ' + reason)   // reject Error: test
})

理所当然,新对象肯定会失败。

反过来如果 p 失败了,会是什么样的呢?

var p = new Promise(function(resolve, reject){
    reject(5)
})
var p1 = p.then(undefined, function(value){
    console.log(value)   // 5
}).then(function(value){
    console.log('fulfill ' + value)   // fulfill undefined
}, function(reason){
    console.log('reject ' + reason)
})

说明新对象状态不会受到前一个对象状态的影响。

再来看如下代码:

var p = new Promise(function(resolve, reject){
    reject(5)
})
var p1 = p.then(function(value){
    console.log(value) 
})
var p2 = p1.then(function(value){
    console.log('fulfill ' + value)
}, function(reason){
    console.log('reject ' + reason)   // reject 5
})

我们发现 p1 的状态变为 rejected ,从而触发了 then 方法第二个参数的函数。这似乎与我们之前提到的有差异啊, p1 的状态受到了 p 的状态的影响。

再来看一个例子:

var p = new Promise(function(resolve, reject){
    resolve(5)
})
var p1 = p.then(undefined, function(value){
    console.log(value) 
})
var p2 = p1.then(function(value){
    console.log('fulfill ' + value)   // fulfill 5
}, function(reason){
    console.log('reject ' + reason)   
})

细心的人可能会发现,该例子中 then 第一个参数是 undefined ,且 value 值5被传到了 p1 成功时的回调函数中。上面那个例子中 then 的第二个参数是 undefined ,同样 reason 值也传到了 p1 失败时的回调函数中。这是因当对应的参数不为函数时,会将前一 promise 的状态和值传递下去。

promise 含有一个实例方法 catch ,从名字上我们就看得出来,它和异常有千丝万缕的关系。其实 catch(onReject) 方法等价于 then(undefined, onReject) ,也就是说如下两种情况是等效的。

new Promise(function(resolve, reject){
    reject(new Error('error'))
}).then(undefined, function(reason){
    console.log(reason) // Error: error(…)
})

new Promise(function(resolve, reject){
    reject(new Error('error'))
}).catch(function(reason){
    console.log(reason) // Error: error(…)
})

我们提到参数不为函数时会把值和状态传递下去。所以我们可以在多个 then 之后添加一个 catch 方法,这样前面只要 reject 或抛出异常,都会被最后的 catch 方法处理。

new Promise(function(resolve, reject){
    resolve(5)
}).then(function(value){
    taskA()
}).then(function(value){
    taskB()
}).then(function(value){
    taskC()
}).catch(function(reason){
    console.log(reason)
})

Promise 的静态方法

Promise 还有四个静态方法,分别是 resolverejectallrace ,下面我们一一介绍。

除了通过 new Promise() 的方式,我们还有两种创建 Promise 对象的方法:

Promise.resolve() 它相当于创建了一个立即 resolve 的对象。如下两段代码作用相同:

Promise.resolve(5)

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

它使得promise对象直接 resolve ,并把5传到后面 then 添加的成功函数中。

Promise.resolve(5).then(function(value){
    console.log(value) // 5
})

Promise.reject() 很明显它相当于创建了一个立即 reject 的对象。如下两段代码作用相同:

Promise.reject(new Error('error'))

new Promise(function(resolve, reject){
    reject(new Error('error'))
})

它使得promise对象直接 reject ,并把error传到后面 catch 添加的函数中。

Promise.reject(new Error('error')).catch(function(reason){
    console.log(reason) // Error: error(…)
})

Promise.all() 它接收一个promise对象组成的数组作为参数,并返回一个新的 promise 对象。

当数组中所有的对象都 resolve 时,新对象状态变为 fulfilled ,所有对象的 resolvevalue 依次添加组成一个新的数组,并以新的数组作为新对象 resolvevalue ,例:

Promise.all([Promise.resolve(5), 
  Promise.resolve(6), 
  Promise.resolve(7)]).then(function(value){
    console.log('fulfill', value)  // fulfill [5, 6, 7]
}, function(reason){
    console.log('reject',reason)
})

当数组中有一个对象 reject 时,新对象状态变为 rejected ,并以当前对象 rejectreason 作为新对象 rejectreason

Promise.all([Promise.resolve(5), 
  Promise.reject(new Error('error')), 
  Promise.resolve(7),
  Promise.reject(new Error('other error'))
  ]).then(function(value){
    console.log('fulfill', value)
}, function(reason){
    console.log('reject', reason)  // reject Error: error(…)
})

那当数组中,传入了非promise对象会如何呢?

Promise.all([Promise.resolve(5), 
  6,
  true,
  'test',
  undefined,
  null,
  {a:1},
  function(){},
  Promise.resolve(7)
  ]).then(function(value){
    console.log('fulfill', value)  // fulfill [5, 6, true, "test", undefined, null, Object, function, 7]
}, function(reason){
    console.log('reject', reason)
})

我们发现,当传入的值为数字、boolean、字符串、undefined、null、{a:1}、function(){}等非promise对象时,会依次把它们添加到新对象 resolve 时传递的数组中。

那数组中的多个对象是同时调用,还是一个接一个的依次调用呢?我们再看个例子

function timeout(time) {
    return new Promise(function (resolve) {
        setTimeout(function () {
            resolve(time);
        }, time);
    });
}
console.time('promise')
Promise.all([
    timeout(10),
    timeout(60),
    timeout(100)
]).then(function (values) {
    console.log(values); [10, 60, 100]
    console.timeEnd('promise');   // 107ms 
});

由此我们可以看出,传入的多个对象几乎是同时执行的,因为总的时间略大于用时最长的一个对象 resolve 的时间。

Promise.race() 它同样接收一个promise对象组成的数组作为参数,并返回一个新的 promise 对象。

Promise.all() 不同,它是在数组中有一个对象 resolvereject 时,就改变自身的状态,并执行响应的回调。

Promise.race([Promise.resolve(5), 
  Promise.reject(new Error('error')), 
  Promise.resolve(7)]).then(function(value){
    console.log('fulfill', value)  // fulfill 5
}, function(reason){
    console.log('reject',reason)
})

Promise.race([Promise.reject(new Error('error')), 
  Promise.resolve(7)]).then(function(value){
    console.log('fulfill', value) 
}, function(reason){
    console.log('reject',reason) //reject Error: error(…)
})

且当第一个参数为数字、boolean、字符串、undefined、null、{a:1}、function(){}时,都会直接以该值 resolve

那么问题又来了,既然数组中第一个元素成功或失败就会改变新对象的状态,那数组中后面的对象是否会执行呢?

function timeout(time) {
    return new Promise(function (resolve) {
        setTimeout(function () {
            console.log(time)
            resolve(time);
        }, time);
    });
}
console.time('promise')
Promise.race([
    timeout(10),
    timeout(60),
    timeout(100)
]).then(function (values) {
    console.log(values); [10, 60, 100]
    console.timeEnd('promise');   // 107ms
});

// 结果依次为
// 10
// 10
// promise: 11.1ms
// 60
// 100

说明即使新对象的状态改变,数组中后面的promise对象还会执行完毕,其实 Promise.all() 中即使前面 reject 了,所有的对象也都会执行完毕。规范中,promise对象执行是不可以中断的。

补充

promise 对象即使立马改变状态,它也是异步执行的。如下所示:

Promise.resolve(5).then(function(value){
  console.log('后打出来', value)
});
console.log('先打出来')

// 结果依次为
// 先打出来
// 后打出来 5

但还有一个有意思的例子,如下:

setTimeout(function(){console.log(4)},0);
new Promise(function(resolve){
    console.log(1)
    for( var i=0 ; i<10000 ; i++ ){
        i==9999 && resolve()
    }
    console.log(2)
}).then(function(){
    console.log(5)
});
console.log(3);

结果是 1 2 3 5 4,命名4是先添加到异步队列中的,为什么结果不是1 2 3 4 5呢?这个涉及到Event loop,后面我会单独讲一下。

原文  https://segmentfault.com/a/1190000007678185
正文到此结束
Loading...