Promise 深度学习

108 阅读6分钟

Promise 由来

我们处理异步函数最普通的方法是这样的,等待上一次请求结束再执行下一步操作:

// 一般以定时器来模拟一次请求
setTimeout(() => {
  console.log("first");
  // 处理的内容 或者 下一次请求
  // TODO
}, 1000);

这是两次的请求,看着是比较简单,这么写好像也没什么影响,但如果TODO后面还有多个请求时:

// 用多个定时器来模拟多个请求
setTimeout(() => {
  console.log("第一次处理");
  setTimeout(() => {
    console.log("第二次处理");
    setTimeout(() => {
      console.log("第三次处理");
        setTimeout(() => {
          console.log("第四次处理");
          // TODO
        }, 4000)
    }, 3000)
  }, 2000)
}, 1000);

回调函数中嵌套回调函数,就是回调地狱,这种写法仔细看我们肯定是能看得出来的,但是无限嵌套之后,代码的可读性非常差,日常维护也是很繁琐的事情。为了更好的处理嵌套格式的代码,我们就可以使用Promise。这当然也是ES6(ES2015)中最重要的特性之一,开发中经常使用,面试的时候也经常问。

Promise的用法

我们可以先打印看看,Promise长什么样:

console.dir(Promise);

image.png 显而易见的Promise是一个构造函数,本身带着 all、race、reject、resolve等方法,prototype原型上也带着 catch、then等方法。new Promise:

// 一般Promise new的时候都喜欢用一个函数包一下,这里还是使用定时器代替请求
const p = new Promise((resolve, reject) => {
  // 异步操作
  setTimeout(() => {
    console.log("时间到了");
    resolve("成功操作")
  }, 1000)
});
p.then(res => {
  console.log(res);
});
​
// 执行后的结果为: 
// 时间到了
// 成功操作

Promise的构造函数接收了一个参数,是一个函数,并且函数中传入两个参数 resolve、reject,分别是异步操作执行成功后的回调函数和异步操作执行失败后的回调函数(这么描述并不是正确的,实际上是 resolve将Promise的状态置为fullfiled,reject是将Promise的状态置为rejected,状态操作是不可逆的,Promise一开始的状态是pending初始态,状态改变方式也就两种)。

上面代码,定时器一秒之后输出“时间到了”,并且调用resolve方法。在异步任务执行完之后,再打印“成功操作”,这就是Promise的作用:将原来的回调写法分离出来,在异步操作执行完之后,用链式调用的写法执行回调函数

这里只是最简单的Promise,如果要改造前面写的多个定时器请求,我们可以这样写:

// Promise优势在于,可以在then方法中继续写Promise对象并返回,然后继续调用then来进行回调操作
const p1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    console.log("第一次请求");
    resolve();
  }, 1000)
})
​
// 链式操作的用法
p1
  .then(() => {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        console.log("第二次请求");
        resolve()
      }, 2000)
    })
  })
  .then(() => {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        console.log("第三次请求");
        resolve();
      }, 3000)
    })
  })
  .then(() => {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        console.log("第四次请求");
      })
    }, 4000)
  })
​
// 换一种写法,执行结果一样 将函数定义一下
function p1() {
  const p = new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log("第一次请求");
      resolve(1);
    }, 1000)
  })
  return p;
}
​
function p2() {
  const p = new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log("第二次请求");
      resolve(2);
    }, 1000)
  })
  return p;
}
​
function p3() {
  const p = new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log("第三次请求");
      resolve(3);
    })
  })
  return p;
}
​
// p1只是返回函数,没有调用,需要在p1后面添加一个 ()
p1()
  .then(res => {
    console.log(res);
    return p2();
  })
  .then(res => {
    console.log(res);
    return p3();
  })
  .then(res => {
    console.log(res);
  })
// 这样就没有回调地狱了

reject的用法

上面的代码都只用了resolve,我们还没用过reject。事实上前面的代码假设都是成功的,还没有失败的情况:

// 一个在定时器中的随机生成[0,10)随机数的函数
function getNumFn() {
 const p = new Promise((resolve, reject) => {
    setTimeout(() => {
      let num = Math.ceil(Math.random() * 10); // 随机生成1-10(不包含)的随机数
      if (num <= 5) {
        // 模拟成功时的操作
        resolve("数字小于5", num);
      } else {
        // 模拟失败时的操作
        reject("数字大于5", num);
      }
    }, 1000)
 });
 return p;
};
​
getNumFn()
  .then(res => {
    console.log("resolve", res);
  })
  .catch(err => {
    console.log("reject", err);
  })

我们可以看出数字小于5的时候会执行then里的内容,数字大于5时会执行catch中的操作,一般resolve对应then,reject对应catch;但其中then是一个卷王,它可以接收两个参数,一个对应resolve的回调,第二个对应的reject的回调(曾经有个面试官问过我then能接收几个参数,啥也不会),上面的调用也可以写成这样:

getNumFn().then(
  res => {
    console.log(res);
  },
  err => {
    console.log(err);
  }
)

这时就简单达到跟catch一样的效果了。但实际上,在执行resolve的回调时,如果抛出异常了/代码报错了,那么就会卡死,这时catch就显得不可缺少了:

// 这里是没有catch,代码会报错,控制台会出现红色的提示 后面又操作也不会继续执行
getNumFn().then(
  res => {
    console.log(num); // 未定义的num,会报错
    console.log(res);
  },
  err => {
    console.log(num); // 未定义的num,会报错
    console.log(err);
  }
)
​
// 写上catch之后
getNumFn().then(
  res => {
    console.log(num);
    console.log(res);
  },
  err => {
    console.log(num);
    console.log(err);
  }
).catch(err => {
  console.log("有错误", err);
  // 后面有操作可继续执行
  console.log(123); // 会执行
})
// 控制台的错误提示不会显示为红色

catch方法会把错误原因传到err这个参数中,即便有代码也不会报错,与try/catch语句有相同的功能。

finally

finally方法,这个方法不接受任何参数,但是可以在回调函数中访问之前Promise的解决值或拒绝原因,无论 Promise 的状态如何,最终操作的消息都将打印到控制台。

// 简单拿上面获取随机数函数调一下 (很少用到)
getNumFn().finally(() => {
  console.log("我都会执行");
})

all的用法

all方法,是在所有异步操作都执行完之后才执行回调:

function p1() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log("第一次请求");
      resolve("操作1");
    }, 1000)
  })
}

function p2() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log("第二次请求");
      resolve("操作2");
    })
  }, 500)
}

function p3() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log("第三次请求");
      resolve("操作3");
    }, 100)
  })
}

Promise.all([ p1(), p2(), p3() ]).then(res => {
  console.log(res);
})

// 执行结果为:
// 第二次请求
// 第三次请求
// 第一次请求
// ["操作1", "操作2", "操作3"]

all方法接收一个数组参数,里面的值最终都算返回Promise对象。这样就算异步操作是并行执行的,all也会等他们全部执行完,才会进入then里面,三个异步操作的数据都被all放到一个数组中了。

race的用法

all方法和race方法区别在于:all全部执行完再执行回调,race一个执行完就执行回调

// 函数还是用上面的 p1、p2、p3
Promise.race([ p1(), p2(), p3() ]).then(res => {
  console.log(res);
})
// 执行结果:
// 第二次请求
// 操作2 // 这是 Promise.race的执行结果
// 第三次请求
// 第一次请求

总结

Promise不止这些,还有async await语法糖。

看一遍,实际用一遍,你就会了;学吧,学到了都是你自己的。