深入理解JavaScript ES6 Promise基本使用和进阶讲解

Source

一项新技术的出现一定是为了解决某个痛点问题的。ES6的Promise就是为了解决ES5在处理异步任务时所存在的问题。所以Promise是一个异步处理框架。

Promise前夕——ES5对于异步任务的处理

我们先来看看ES5是怎么处理异步任务的。主要的手段就是通过回调函数。回调函数是JavaScript本身就支持的机制。可以通过回调函数实现类似下面这个的网络请求过程。虽然这个函数的实现看起来代码有点不好看,但是在使用的时候逻辑还是比较清晰的。

//1.实现主要逻辑
function getCities(path, onSuccess, onErr) {
    
      
  if (path === "") {
    
      
    //或者path格式有问题
    onErr("请求失败,path不能为空");
  } else {
    
      
    //发生请求
    console.log("开始发生请求");
    console.log("数据请求中......");
    setTimeout(()=>{
    
      
        console.log("请求到数据了");
        //假装获取到数据了
        const data = ["广州", "上海", "温州"];
        onSuccess(data);
    },2000)
  }
}

调用的时候,逻辑还是非常清晰的,成功和失败分别在两个回调函数得到处理。

//2.测试成功状况
console.log("..............测试成功请求...............");
getCities(
  "/home/getCityData",
  res => {
    
      
    console.log(res);
  },
  err=> {
    
      
    console.log(err);
  }
);
..............测试成功请求...............
开始发生请求
数据请求中......
请求到数据了
[ '广州', '上海', '温州' ]

path为空,模拟失败的情况。

//3.测试失败状况
console.log("..............测试失败请求...............");
getCities("",
    res => {
    
      
      console.log(res);
    },
    err=> {
    
      
      console.log(err);
    }
  );
..............测试失败请求...............
请求失败,path不能为空

总的来说,使用回调函数的方式实现异步任务的方式存在下面的问题:
1.函数的实现非常的难看。而且实现比较麻烦。
2.函数在调用的时候参数先后顺序是没有约定的,完全取决于设计者自身。

使用Promise对异步任务进行处理

既然ES5的回调函数方式存在一些问题,那么Promise是怎么解决的呢?我们来看一下promise的用法。把上面的代码逻辑改成用promise的实现。

Promise是一个类,里面有两个回调函数resolve和reject,实际上,这就相当于前面的onSuccess和onErr回调函数。
而且resolve和reject顺序是固定的,你不能随便改。除了这些之外,整个实现逻辑和ES5实际上是一样的。

function getCities(path) {
    
      
    //1.使用promise处理
    const promise = new Promise((resolve, reject) => {
    
      
    if (path === "") {
    
      
      //或者path格式有问题
      reject("path不能为空");
    } else {
    
      
      //发生请求
      console.log("开始发生请求");
      console.log("数据请求中......");
      setTimeout(() => {
    
      
        console.log("请求到数据了");
        //假装获取到数据了
        const data = ["广州", "上海", "温州"];
        resolve(data);
      }, 2000);
    }
  });

  return promise;
}

结果返回一个promise,通过then和catch方法分别处理成功和失败的情况。

const promose2=getCities("/home/getCityData");
promose2.then(value=>{
    
      
    console.log("数据为:"+value);
})

promose2.catch(err=>{
    
      
    console.log("失败的原因是:"+err);
})

简写方式:

promose2.then(value=>{
    
      
    console.log("数据为:"+value);
}).catch(err=>{
    
      
    console.log("失败的原因是:"+err);
})

光从代码量上来说,和ES5相比是差不多的。使用难度也是差不多的。只是Promise给出了限定,回调函数都是固定的,这样就解决了回调函数命名和顺序的问题。虽然实现的代码还是有点难看,但至少定义了标准,不会出现随意设计的情况。
如果去除业务逻辑,实际上主干逻辑就是下面这样,非常的简单清晰。

function getCities(path) {
    
      
    const promise = new Promise((resolve, reject) => {
    
      
      //成功的情况
      resolve(data);
      //失败的情况
      reject("path不能为空");
    });

  return promise;
}

const promose2=getCities("/home/getCityData");

promose2.then(value=>{
    
      
    console.log("数据为:"+value);
}).catch(err=>{
    
      
    console.log("失败的原因是:"+err);
})

promise的特性和细节

执行和输出细节

promise的用法也不难,接下来看一下promise的特性,以及使用的时候都有哪些细节。
我们精简一下代码,写成下面这个样子。

function getCities(path) {
    
      
  const promise = new Promise((resolve, reject) => {
    
      
    resolve("xxx");
    resolve("xxx");
    console.log("........."); 
    reject("path不能为空");
  });
  return promise;
}

getCities("/home/getCityData")
.then(value=>{
    
      
    console.log("数据为:"+value);
}).catch(err=>{
    
      
    console.log("失败的原因是:"+err);
})

输出:
可以看到resolve方法只会执行一次,并且 console.log(“…”); 这行的输出还是在前面输出的。可见resolve函数是异步操作。reject方法也是一样的,可以自行测试。

.........
数据为:xxx

promise的三个状态

这里设计到promise的三个状态:
1.pending 等待状态
在new Promise后,resolve和reject函数执行前都是等待状态。
2.funfilled 兑现状态
resolve执行后。
3.rejected 拒绝状态
reject执行后。
一旦状态确定,就不可以再更改了。也不能再次执行回调函数。

executor函数

promise里面的这个函数会被立即执行,这个函数有个专门的名字叫executor。

  const promise = new Promise((resolve, reject) => {
    
      

  });
}

resolve细节

resolve函数的参数类型

resolve函数是可以传递容易数据类型的。下面的传值都是可以的,可以直接试下。

function getCityData(path){
    
      
    return new Promise((resolve,reject)=>{
    
      
        //  resolve("Tom")
        //  resolve(10)
        //  resolve([1,2,3])
         resolve({
    
      name:"Tom",age:18})
    })
}

getCityData("/home/city").then(res=>{
    
      
    console.log("数据是"+res);
})

比较特殊的情况是传的值是一个Promise。这时候then并不会立即接收到值,而是会接收来自传入的那个Promis的resolve传过来的值,并且可以多层嵌套。

function getCityData(path){
    
      
    return new Promise((resolve,reject)=>{
    
      
         console.log("..第二个Promise..");
         resolve(new Promise((resolve,reject)=>{
    
      
            console.log("..第二个Promise..");
            resolve("在第二个Promise的resolve被处理")
         }))
    })
}

getCityData("/home/city").then(res=>{
    
      
    console.log("数据是"+res);
})
..第二个Promise..
..第二个Promise..
数据是在第二个Promise的resolve被处理

这种写法在框架里面比较常见。一般使用的时候不会用到这种写法。
还有一种更加少用的写法,就是thenable的写法。

thenable写法(了解)

在给resolve传对象类型的时候,如果里面有个属性是then,那么就可以写一个函数,并且把前面的resolve作为then的参数传进去。实际上就是实现了类似两个Promise嵌套的情况。这种用法是非常少的,了解就行。

        resolve({
    
      
            name:"Tom",
            then:function(resolve){
    
      
                //异步,不保证顺序
                console.log("........");
                resolve("333")
                console.log(">>>>>>>>");
            }
        })
getCityData("/home/city").then(res=>{
    
      
    console.log("数据是"+res);
})
........
>>>>>>>>
数据是333

then方法的细节

then的另一种写法

then的另一种写法,这个和ES5的回调函数写法是一样的。还是推荐then,catch的写法,更加的清晰。

promise.then(
  (res) => {
    
      
    console.log("成功:", res);
  },
  (err) => {
    
      
    console.log("失败:", err);
  }
);

then的多次监听

虽然resolve不会执行多次,但是then是可以在多个地方监听的。

promise
  .then((res) => {
    
      
    console.log("成功:", res);
  })
  .catch((err) => {
    
      
    console.log("失败:", err);
  });

promise
  .then((res) => {
    
      
    console.log("成功:", res);
  })
  .catch((err) => {
    
      
    console.log("失败:", err);
  });

catch的细节

catch是可以单独监听的,并且可以多出监听,但是如果如果只有then,没有catch的话,会报没有监听catch的错误。

const promise = new Promise((resolve, reject) => {
    
      
  // resolve("success")
  reject("failure");
});

promise
  .then((res) => {
    
      
    console.log("成功:", res);
  })

promise.catch((err) => {
    
      
    console.log("失败:", err);
});

promise.catch((err) => {
    
      
  console.log("失败:", err);
});

then的链式调用

then是可以链式调用的,因为then返回一个新的promise。

const promise = new Promise((resolve, reject) => {
    
      
  resolve("success");
  //reject("failure");
});

promise
  .then((res) => {
    
      
    console.log("第一次调用:", res);
  })
  .then((res) => {
    
      
    console.log("第二次调用:", res);
  })
  .then((res) => {
    
      
    console.log("第三次调用:", res);
  });

promise.then((res) => {
    
      
  console.log("独立的then:", res);
});

从第二次调用开始,then的res就没有值了,输出undefined。

第一次调用: success
独立的then: success
第二次调用: undefined
第三次调用: undefined

从第二个then开始,res的值实际上是来自上一个then的返回值。

promise
  .then((res) => {
    
      
    console.log("第一次调用:", res);
    return "来自第一次调用的结果"
  })
  .then((res) => {
    
      
    console.log("第二次调用:", res);
  })
  .then((res) => {
    
      
    console.log("第三次调用:", res);
  });

可以看到,第二个then的res的值来自第一个then的返回值。

第一次调用: success
独立的then: success
第二次调用: 来自第一次调用的结果
第三次调用: undefined

这里要注意,res的值并不是直接放进来的,中间还是通过promise来操作resolve函数来实现的。

then链式调用返回新的promise

这种情况并不复杂。还是会逐层传递,通过输出就可以很好的观察到。

const promise = new Promise((resolve, reject) => {
    
      
  resolve("success");
  //reject("failure");
});

const p=new Promise((resolve,reject)=>{
    
      
  resolve("来自新的promise")
})

promise
  .then((res) => {
    
      
    console.log("第一次调用:", res);
    return p
  })
  .then((res) => {
    
      
    console.log("第二次调用:", res);
  })
  .then((res) => {
    
      
    console.log("第三次调用:", res);
  });

promise.then((res) => {
    
      
  console.log("独立的then:", res);
});
 
第一次调用: success
独立的then: success
第二次调用: 来自新的promise
第三次调用: undefined

then链式调用返回thenable

这种情况也并不复杂,只是一般不会这样写。还是会逐层传递,通过输出就可以很好的观察到。

const promise = new Promise((resolve, reject) => {
    
      
  resolve("success");
  //reject("failure");
});

promise
  .then((res) => {
    
      
    console.log("第一次调用:", res);
    return {
    
      
      then:function(resolve){
    
      
           resolve("来自thenable的数据")
      }
    }
  })
  .then((res) => {
    
      
    console.log("第二次调用:", res);
  })
  .then((res) => {
    
      
    console.log("第三次调用:", res);
  });

promise.then((res) => {
    
      
  console.log("独立的then:", res);
});
 
第一次调用: success
独立的then: success
第二次调用: 来自thenable的数据
第三次调用: undefined