ES6 Promise对象之实例方法(2)

ES6 Promise对象之实例方法(2)

上一篇关于Promise的文章介绍了Promise的基本用法,这一篇继续上一篇,介绍Promise的各种方法:

(1)Promise.prototype.then()

then方法是定义在原型对象Promise.prototype上的。它的作用是为 Promise 实例添加状态改变时的回调函数。前一篇关于Promise对象的文章中提到过then方法的两个参数,在此不再叙述。

then方法返回的是一个新的Promise实例(不是原来的Promise实例)。由此可以采用链式写法,即then方法后面再调用另一个then方法:

    let promise = new Promise(function (resolve, reject) {
        //some code
    });
    promise.then(function (value) {
        //some code
    }).then(function (value) {
        //some code
    });

使用then方法依次指定了两个回调函数,第一个回调函数完成以后,会将返回结果作为参数传入第二个回调函数。如果前一个then回调函数返回的还是一个Promise对象(即有异步操作),这时后一个then回调函数,就会等待该Promise对象的状态发生变化,才会被调用。

(2)Promise.prototype.catch()

1.catch方法用于指定发生错误时的回调函数。
    let promise = new Promise(function (resolve, reject) {
        //some code
    });
    promise.then(function (value) {
        //some code
    }).catch(function (value) {
        //some code
    });

如果Promise实例对象状态变为Resolved,则调用then方法指定的回调函数;
如果异步操作抛出错误,Promise实例对象状态就会变为Rejected,就会调用catch方法指定的回调函数处理这个错误。
另外,then方法指定的回调函数如果在运行中抛出错误,也会被catch方法捕获。

2.catch方法的作用相当于then方法的第二个回调函数:
promise.then((value) => console.log(value))
       .catch((error) => console.log(error));

// 等同于
promise.then((value) => console.log(value))
       .then(null, (error) => console.log(error));
3.reject方法的作用等同于抛出错误

Promise抛出一个错误被catch方法指定的函数捕获,共有三种写法(三种写法是等价的):

    let promise = new Promise((resolve, reject) => {
       throw new Error("Error!")
    });
    promise.catch(function (error) {
        console.log(error)
    });
    //Error: Error!
    let promise = new Promise((resolve, reject) => {
        try {
            throw new Error("Error!")
        }
        catch (error) {
            reject(error)
        }

    });
    promise.catch(function (error) {
        console.log(error)
    });
    //Error: Error!
    let promise = new Promise((resolve, reject) => {
        reject(new Error("Error!"))
    });
    promise.catch(function (error) {
        console.log(error)
    });
    //Error: Error!
4.在resolve函数后面再抛出错误,并不会被捕获
    let promise = new Promise((resolve, reject) => {
        reject("Rejected!");
        throw new Error("Error!")
    });
    promise.catch(function (error) {
        console.log(error)
    });
    //Rejected!
5.Promise对象的错误会一直向后传递,直到被捕获为止

也就是说,错误总会被下一个catch语句捕获。

    let promise = new Promise(function (resolve, reject) {
        //some code
    });
    promise.then(function (value) {
        //some code
    }).then(function (value) {
        //some code
    }).catch(function (value) {
        //some code
    });

这里的catch方法会处理前面Promise实例对象以及两个then方法执行中抛出的错误。

一般来说,不要在then方法中定义Rejected状态的回调函数(then的第二个参数),最好是用catch方法:
因为使用catch方法可以捕获前面所有then方法中产生的错误,而采用then方法的第二个回调函数无法对同一个then方法中的第一个回调函数中的错误进行处理(对于前一个then方法中的错误可以进行处理)。

    promise
        .then(function(data){
           //some code 
        }
        .then(function(data){
           //some code 
        },function(error){
            //some code
        });


    promise
       .then(function(data){
           //some code 
        }
        .then(function(data){
            //some code 
        })
        .catch(function(error){
            //some code
        });

第二种写法要好于第一种写法,因为第二种写法catch方法可以捕获前面所有then方法执行中的错误。

6.如果没有用catch方法,抛出的错误不会传递到外层代码
    const someAsyncThing = function() {
        return new Promise(function(resolve, reject) {
            // 下面一行会报错,因为x没有声明
            resolve(x + 2);
        });
    };

    someAsyncThing().then(function() {
        console.log('everything is great');
    });

    setTimeout(() => { console.log(123) }, 2000);

在chrome中会打印如下:

    // Uncaught (in promise) ReferenceError: x is not defined
    // 123

而在火狐中打印如下(没有打印错误提示):

   //123

上面代码中,someAsyncThing函数产生的 Promise 对象,内部有语法错误。浏览器运行到这一行,不会退出进程、终止脚本执行,2 秒之后还是会输出123。这就是说,Promise 内部的错误不会影响到 Promise 外部的代码,通俗的说法就是“Promise 会吃掉错误”。
再来看下面的例子:

    const promise = new Promise(function (resolve, reject) {
        resolve('ok');
        setTimeout(function () { throw new Error('test') }, 0)
    });
    promise.then(function (value) { console.log(value) });

    setTimeout(() => { console.log(123) }, 2000);
    // ok
    // Uncaught Error: test
    // 123

上面代码中,Promise 指定在下一轮“事件循环”再抛出错误。到了那个时候,Promise 的运行已经结束了,所以这个错误是在 Promise 函数体外抛出的,会冒泡到最外层,成了未捕获的错误。

一般总是建议,Promise 对象后面要跟catch方法,这样可以处理 Promise 内部发生的错误。

7.catch方法之后还可以调用then方法

catch运行完后返回的还是一个Promise对象,因此后面还可以调用then方法。

    Promise.resolve()
        .catch(function (error) {
            console.log("oh no!" + error);
        })
        .then(function (data) {
            console.log("carry on!")
        });
    //carry on!

代码运行完catch方法的回调函数之后会接着运行后面那个then方法。
如果没有报错,就会跳过catch方法。
以上代码如果then中的回调函数执行中出现错误,就与前面的catch无关了。

8.catch方法还可以再抛出错误
const someAsyncThing = function() {
  return new Promise(function(resolve, reject) {
    // 下面一行会报错,因为x没有声明
    resolve(x + 2);
  });
};

someAsyncThing().then(function() {
  return someOtherAsyncThing();
}).catch(function(error) {
  console.log('oh no', error);
  // 下面一行会报错,因为 y 没有声明
  y + 2;
}).then(function() {
  console.log('carry on');
});
// oh no [ReferenceError: x is not defined]

上面代码中,catch方法抛出一个错误,因为后面没有别的catch方法了,导致这个错误不会被捕获,也不会传递到外层。

someAsyncThing().then(function() {
  return someOtherAsyncThing();
}).catch(function(error) {
  console.log('oh no', error);
  // 下面一行会报错,因为y没有声明
  y + 2;
}).catch(function(error) {
  console.log('carry on', error);
});
// oh no [ReferenceError: x is not defined]
// carry on [ReferenceError: y is not defined]

上面代码中,第二个catch方法用来捕获前一个catch方法抛出的错误。

(3)Promise.resolve()

Promise.resolve用于将现有对象转为Promise对象。下面两种写法等价:

Promise.resolve('foo')
// 等价于
new Promise(resolve => resolve('foo'))
1.参数是一个Promise实例

如果参数时Promise实例,那么Promise.resolve将不做任何修改返回这个实例。

2.参数是一个thenable实例

thenable对象指的是具有then方法的对象,例如:

    let thenable = {
        then: function (resolve,reject) {
            resolve(42);
        }
    }

将这个对象转为Promise对象:

    let thenable = {
        then: function (resolve,reject) {
            resolve(42);
        }
    };
    let p1=Promise.resolve(thenable);
    p1.then(function(value){
        console.log(value);
    })
    //42
3.参数不具有then方法的对象或根本不是对象

如果参数是一个原始值,或者是一个不具有then方法的对象,Promise.resolve方法会返回一个新的Promise对象,状态为Resolved。

下面我们先来看一下什么是原始值:
在ECMAScript中,变量可以存放两种类型的值:
a、原始值:固定而简单的值,是存放在栈(stack)中的简单数据段,它们的值直接存储在变量访问的位置。原始类型有以下五种类型:Undefined,Null,Boolean,Number,String;
b、引用值:存放在堆(heap)中的对象,存储在变量处的是一个指针,指向存储对象的内存地址。所有引用类型都集成自Object。

    //字符串
    let p1=Promise.resolve("hello");
    p1.then(function(value){
        console.log(value);
    })
    //hello

    //数组
    let p2=Promise.resolve([1,2,3]);
    p2.then(function(value){
        console.log(value);
    })
    //[1,2,3]

    //不具有then方法的对象
    let p3=Promise.resolve({"1":1});
    p3.then(function(value){
        console.log(value);
    })
    //{"1":1}

    //null
    let p4=Promise.resolve(null);
    p4.then(function(value){
        console.log(value);
    })
    //null

    //undefined
    let p5 = Promise.resolve(undefined);
    p5.then(function (value) {
        console.log(value);
    })
    //undefined
    
   //数字
    let p6 = Promise.resolve(123);
    p6.then(function (value) {
        console.log(value);
    })
    //123

    //布尔值
    let p7 = Promise.resolve(true);
    p7.then(function (value) {
        console.log(value);
    })
    //true

以上Promise实例返回的状态都是Resolved,所以会调用then方法的第一个回调函数。

4.不带有任何参数

直接返回一个Resolved状态的Promise对象。因此,如果希望得到一个Promise对象,比较方便的方法就是直接调用Promise.resolve方法。

Promise.resolve()得到一个Promise对象变成Resolved状态是在本轮“事件循环”结束时,而不是在下一轮“事件循环”开始时。

    setTimeout(function(){
        console.log("three");
    },0);

    Promise.resolve().then(function(){
        console.log("two");
    });
    console.log("one")
    //one
    //two
    //three

上面代码中,setTimeout()是在下一轮“事件循环”开始的时候执行的,Promise.resolve()在本轮“事件循环”结束的时候执行,console.log(“one”)则立即执行。

(4)Promise.reject()

Promise.reject方法返回一个新的Promise实例,状态为Rejected:
下面两种写法等价:

const p = Promise.reject('出错了');
// 等同于
const p = new Promise((resolve, reject) => reject('出错了'))

Promise的实例对象p生成后状态为Rejected,回调函数会立即执行:

const p = Promise.reject('出错了');
p.then(null, function (s) {
  console.log(s)
});
// 出错了
1.参数是一个Promise实例

将Promise实例的状态变为Rejected,并返回一个新的Promise对象。reject函数的参数是Promise.reject()方法中传入的参数(在这里是Promises实例a):

    let a = Promise.resolve();
    const p = Promise.reject(a);
    p.then(null, function (s) {
        console.log(s);// Promise对象
        console.log(s===a);//true
    });
      console.log(p===a);//false  说明返回新的Promise对象
2.参数是一个thenable实例

将thenable对象转为Promise对象,新生成的Promise实例的状态为Rejected。reject函数的参数是Promise.reject()方法中传入的参数(在这里是对象thenable1):

    let thenable1 = {
        then: function (resolve,reject) {
            reject(42);
        }
    };
    let p1=Promise.reject(thenable1);
    p1.then(null,function(error){
        console.log(error);//Object
        console.log(error===thenable1);//true
    })

注意这里与Promise.resolve方法的不同:

    //Promise.resolve方法
    let thenable = {
        then: function (resolve,reject) {
            resolve(42);
        }
    };
    let p1=Promise.resolve(thenable);
    p1.then(function(value){
        console.log(value);
    });
    //42


    //Promise.reject方法
    let thenable1 = {
        then: function (resolve,reject) {
            reject(42);
        }
    };
    let p2=Promise.reject(thenable1);
    p2.then(null,function(error){
        console.log(error);//Object
        console.log(error===thenable1);//true
    })

reject函数的参数是Promise.reject()方法中传入的参数(这里是对象thenable1)。

3.参数不具有then方法的对象或根本不是对象

如果参数是一个原始值,或者是一个不具有then方法的对象,Promise.reject方法会返回一个新的Promise对象,状态为Rejected。

      //字符串
    let p1=Promise.reject("hello");
    p1.then(null,function(value){
        console.log(value);
    });
    //hello

    //数组
    let p2=Promise.reject([1,2,3]);
    p2.then(null,function(value){
        console.log(value);
    })
    //[1,2,3]

    //不具有then方法的对象
    let p3=Promise.reject({"1":1});
    p3.then(null,function(value){
        console.log(value);
    })
    //{"1":1}

    //null
    let p4=Promise.reject(null);
    p4.then(null,function(value){
        console.log(value);
    })
    //null

    //undefined
    let p5 = Promise.reject(undefined);
    p5.then(null,function (value) {
        console.log(value);
    })
    //undefined

    //数字
    let p6 = Promise.reject(123);
    p6.then(null,function (value) {
        console.log(value);
    })
    //123

    //布尔值
    let p7 = Promise.reject(true);
    p7.then(null,function (value) {
        console.log(value);
    })
    //true

以上Promise实例返回的状态都是Rejected,所以会调用then方法的第二个回调函数。

4.不带有任何参数

直接返回一个Resjected状态的Promise对象:

    let p1=Promise.reject();
    p1.then(null,function(error){
        console.log(error);
    })
    //undefined

Promise.reject()得到一个Promise对象变成Rejected状态是在本轮“事件循环”结束时,而不是在下一轮“事件循环”开始时:

      setTimeout(function(){
        console.log("two");
    },0);

    let p1=Promise.reject();
    p1.then(null,function(error){
        console.log(error);
    });
    
    console.log("one");
    //one 
    //undefined 
    //two

上面代码中,setTimeout()是在下一轮“事件循环”开始的时候执行的,Promise.reject()在本轮“事件循环”结束的时候执行,console.log(“one”)则立即执行。

(5)Promise.all()

Promise.all方法用于将多个Promise实例包装成一个新的Promise实例。
promise.all方法接受的参数要具有Iterator接口,并且返回的每个成员都是Promise实例,以数组为例:

let p=Promise.all([p1,p2,p3])

p1,p2,p3都是Promise实例;如果不是,就会先调用Promise.resolve方法将参数转为Promise实例,再进一步处理。

p的状态由p1、p2、p3决定,分成两种情况:
1、只有p1、p2、p3的状态都变成Fulfilled,p的状态才会变成Fulfilled,此时p1、p2、p3的返回值组成一个数组,传递给p的回调函数:

    let p1=Promise.resolve("hello");
    p1.then(function(value){
        console.log(value);
    });
    //hello

    let p2=Promise.resolve([1,2,3]);
    p2.then(function(value){
        console.log(value);
    });
    //[1,2,3]

    let p6 = Promise.resolve(123);
    p6.then(function (value) {
        console.log(value);
    });
    //123

    Promise.all([p1, p2, p6]).then(function (posts) {
        console.log(posts);
    }).catch(function (reason) {
        console.log(reason);
    });
    //["hello",[1,2,3],123]

2、只要p1、p2、p3之中有一个状态变成Rejected,p的状态就变成Rejected,此时第一个状态为Rejected的实例的返回值,会传递给p的回调函数:

    let p1 = Promise.reject("hello");
    p1.then(null, function (value) {
        console.log(value);
    });
    //hello

    let p2 = Promise.reject([1, 2, 3]);
    p2.then(null, function (value) {
        console.log(value);
    });
    //[1,2,3]
    
    let p6 = Promise.reject(123);
    p6.then(null, function (value) {
        console.log(value);
    });
    //123
    
    Promise.all([p1, p2, p6]).then(function (posts) {
        console.log(posts);
    }).catch(function (reason) {
        console.log(reason);
    });
    //hello

如果作为参数的Promise实例自身定义了catch方法,那么它状态变为Rejected时并不会触发Promise.all()的catch方法;

    let promise1 = new Promise((resolve, reject) => {
        resolve();
    }).then(result => console.log("promise1_then"))
        .catch(e => console.log("promise1_catch"));


    let promise2 = new Promise((resolve, reject) => {
        throw new Error()
    }).then(result => console.log("promise2_then"))
        .catch(e => console.log("promise2_catch"));

    
    Promise.all([promise1, promise2])
        .then(result => console.log("promise_then"))
        .catch(e => console.log("promise_catch"));
    //promise1_then
    //promise2_catch
    //promise_then

在上面的例子中,promise1状态变为Resolved,promise2首先状态变为Rejected,之后执行catch中的回调函数,执行完成后,promise2指向一个新的Promise实例,这个实例的状态为Resolved,所以Promise.all()里的两个实例的状态都是Resolved,因此调用then方法中的回调函数。
修改一下:

    let promise1 = new Promise((resolve, reject) => {
        resolve();
    }).then(result => console.log("promise1_then"))
        .catch(e => console.log("promise1_catch"));


    let promise2 = new Promise((resolve, reject) => {
        throw new Error()
    }).then(result => console.log("promise2_then"));
        // .catch(e => console.log("promise2_catch"));


    Promise.all([promise1, promise2])
        .then(result => console.log("promise_then"))
        .catch(e => console.log("promise_catch"))
    //promise1_then
    //promise_catch

(6)Promise.race()

Promise.race方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例:

const p = Promise.race([p1, p2, p3]);

上面代码中,只要p1、p2、p3之中有一个实例率先改变状态,p的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给p的回调函数。

Promise.race方法的参数与Promise.all方法一样,如果不是 Promise 实例,就会先调用Promise.resolve方法,将参数转为 Promise 实例,再进一步处理。

下面是一个例子,如果指定时间内没有获得结果,就将 Promise 的状态变为Rejected,否则变为Resolved:

const p = Promise.race([
  fetch('/resource-that-may-take-a-while'),
  new Promise(function (resolve, reject) {
    setTimeout(() => reject(new Error('request timeout')), 5000)
  })
]);

p
.then(console.log)
.catch(console.error);

上面代码中,如果 5 秒之内fetch方法无法返回结果,变量p的状态就会变为Rejected,从而触发catch方法指定的回调函数。

(7)Promise.prototype.finally()

1.finally方法用于指定不管 Promise 对象最后状态如何,都会执行的操作

该方法是 ES2018 引入标准的。
不管Promise最后的状态,在执行完then或catch指定的回调函数以后,都会执行finally方法指定的回调函数:

  new Promise(function (resolve, reject) {
        resolve();
    })
        .then(result => {
            console.log(1)
        })
        .catch(error => {
            console.log(2)
        })
        .finally(() => {
            console.log(3)
        });
    //1
    //3
 new Promise(function (resolve, reject) {
        reject();
    })
        .then(result => {
            console.log(1)
        })
        .catch(error => {
            console.log(2)
        })
        .finally(() => {
            console.log(3)
        });
    //2
    //3

finally方法的实现:
下面代码中,不管前面的 Promise 是Fulfilled还是Rejected,都会执行回调函数callback。

Promise.prototype.finally = function (callback) {
  let P = this.constructor;
  return this.then(
    value  => P.resolve(callback()).then(() => value),
    reason => P.resolve(callback()).then(() => { throw reason })
  );
};

下面是一个例子,服务器使用 Promise 处理请求,然后使用finally方法关掉服务器:

  server.listen(port)
  .then(function () {
    // ...
  })
  .finally(server.stop);
2.finally方法与then方法类似,但二者不相同:
promise
.finally(() => {
  // 语句
});

// 等同于
promise
.then(
  result => {
    // 语句
    return result;
  },
  error => {
    // 语句
    throw error;
  }
);

上面代码中,如果不使用finally方法,同样的语句需要为成功和失败两种情况各写一次。有了finally方法,则只需要写一次。

a、finally方法的回调函数不接受任何参数(而then方法的回调函数是接受参数的)

这意味着没有办法知道,前面的 Promise 状态到底是Fulfilled还是Rejected。这表明,finally方法里面的操作,应该是与状态无关的,不依赖于 Promise 的执行结果。

b、finally方法是不改变Promise的状态的(而then方法会改变Promise的状态)
     let p1=Promise.resolve(1).then(()=>2,()=>3);
     console.log(p1);
     // Promise {
     // [[PromiseStatus]]: "resolved"
     // [[PromiseValue]]: 2
     // }
     // then返回了一个Resolved为2的Promise


     let p2=Promise.resolve(1).finally(()=>2);
     console.log(p2);
     // Promise {
     // [[PromiseStatus]]: "resolved"
     // [[PromiseValue]]: 1
     // }
     // finally不改变原来Resolved的状态


     let p3=Promise.reject(1).then(()=>2,()=>3);
     console.log(p3);
     // Promise {
     // [[PromiseStatus]]: "resolved"
     // [[PromiseValue]]: 3
     // }
     // then返回了一个状态为Resolved为3的Promise


     let p4=Promise.reject(1).finally(()=>2);
     console.log(p4);
     // Promise {
     // [[PromiseStatus]]: "rejected"
     // [[PromiseValue]]: 1
     // }
     // finally不改变原来Rejected的状态

(8)应用

1.加载图片

我们可以将图片的加载写成一个Promise,一旦加载完成,Promise的状态就发生变化。

  const preloadImage = function (path) {
  return new Promise(function (resolve, reject) {
    const image = new Image();
    image.onload  = resolve;
    image.onerror = reject;
    image.src = path;
  });
};
2.Generator函数与Promise的结合

使用 Generator 函数管理流程,遇到异步操作的时候,通常返回一个Promise对象。

unction getFoo () {
  return new Promise(function (resolve, reject){
    resolve('foo');
  });
}

const g = function* () {
  try {
    const foo = yield getFoo();
    console.log(foo);
  } catch (e) {
    console.log(e);
  }
};

function run (generator) {
  const it = generator();

  function go(result) {
    if (result.done) return result.value;

    return result.value.then(function (value) {
      return go(it.next(value));
    }, function (error) {
      return go(it.throw(error));
    });
  }

  go(it.next());
}

run(g);

上面代码的 Generator 函数g之中,有一个异步操作getFoo,它返回的就是一个Promise对象。函数run用来处理这个Promise对象,并调用下一个next方法。

(9)Promise.try()

不知道或者不想区分函数f是同步函数还是异步函数,但是希望有统一的写法,也就是说有一种统一的API,让同步函数同步执行,异步函数异步执行。
有两种写法:

//写法一
const f = () => console.log('now');
(async () => f())();
console.log('next');
// now
// next


//写法二
const f = () => console.log('now');
(
  () => new Promise(
    resolve => resolve(f())
  )
)();
console.log('next');
// now
// next

1.写法一:第二行是一个立即执行的匿名函数,会立即执行里面的async函数,因此如果f是同步的,就会得到同步的结果;如果f是异步的,就可以用then指定下一步:

(async () => f())()
.then(...)

需要注意的是,async() => f()会吃掉f()抛出的错误。所以,如果想捕获错误,要使用Promise.catch方法:

(async () => f())()
.then(...)
.catch(...)

2.写法二:也是使用立即执行的匿名函数,执行new Promise()。这种情况下,同步函数也是同步执行的。

提案:提供Promise.try方法替代上面的写法

鉴于这是一个很常见的需求,所以现在有一个提案,提供Promise.try方法替代上面的写法。

const f = () => console.log('now');
Promise.try(f);
console.log('next');
// now
// next

还可以统一用Promise.catch()捕获所有同步和异步的错误:

Promise.try(...)
       .then(...)
       .catch(...)

事实上,Promise.try就是模拟try代码块,就像promise.catch模拟的是catch代码块。

(10)Promise存在的一些问题

1.无法取消Promise,一旦新建它就会立即执行,无法中途取消。
2.如果不设置回调函数,Promise内部抛出的错误不会反应到外部。
3.当处于Pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。

参考文献:《ECMAScript 6 入门》阮一峰

猜你喜欢

转载自blog.csdn.net/weixin_42695446/article/details/84527577