JavaScript之Promise的使用【一】

360 阅读7分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

Promise简介

Promise也称期约, 是ES6推出的一个异步解决方案, 可以有效的解决异步函数嵌套太深("回调地狱")的问题

什么是回调地狱?

假设有个需求需要获取用户的指定数据用户数据3这个数据依赖用户数据2而用户数据2又依赖于用户数据1, 所以正确的数据获取数据顺序为: 用户数据1-->用户数据2-->用户数据3, 模拟一下使用回调函数的写法如下:

Node接口:
const router = require('express').Router();

const data1 = { data: "用户数据1" };
router.get('/testData1', (req, res) => {
    res.json(data1);
})

const data2 = { data: "用户数据1,用户数据2" };
router.get('/testData2', (req, res) => {
    if (req.query.data === data1.data) {
        res.json(data2);
    } else {
        res.status(401).json("参数错误");
    }
})

router.get('/testData3', (req, res) => {
    if (req.query.data === data2.data) {
        res.json({ data: "用户数据1,用户数据2,用户数据3" });
    } else {
        res.status(401).json("参数错误");
    }
})

module.exports = router;

前端请求代码:
// 简单封装的 XMLHttpRequest 请求函数
const baseUrl = "http://localhost:8888/test";
const request = (url, cb) => {
  const xhr = new XMLHttpRequest();
  xhr.open('GET', baseUrl + url);
  xhr.send();
  xhr.onreadystatechange = () => {
    const { readyState, status, response } = xhr;
    if (readyState === 4) {
      if (status >= 200 && status <= 299) {
        cb(JSON.parse(response));
      } else {
        throw new Error(response);
      }
    }
  }
}

// 因为下一个请求需要上一个请求的数据所以需要一个回调函数嵌套一个回调函数
request("/testData1", res1 => {
  console.log(res1); // => {data: '用户数据1'}
  request(`/testData2?data=${res1.data}`, res2 => {
    console.log(res2); // => {data: '用户数据1,用户数据2'}
    request(`/testData3?data=${res2.data}`, res3 => {
      console.log("需求需要的数据", res3); // => 需求需要的数据 {data: '用户数据1,用户数据2,用户数据3'}
      // ....
    })
  })
})

这个代码看着就头大, 如果需求复杂的话, 我只能用一张图表示

在这里插入图片描述

这种一个回调嵌套一个回调的代码可读性和可维护性都很差, 被称为"回调地狱", 而Promise的出现就可以很好的解决这个问题

Promise的特点

Promise对象有一个状态, 这个状态不受外界影响, 状态一共分为3种:

  • Pending状态 (进行中(又称待定)) 初始状态
  • Fulfilled状态 (成功(又称兑现))
  • Rejected状态(失败(又称拒绝))

关于Promise的状态叫法下文统一使用成功,失败

一旦Promise对象的状态改变(成功或失败)就不会再变, 是单向的:

  • Pending(进行中) -> Fulfilled(成功)
  • Pending(进行中) -> Rejected(失败)

创建Promise实例

创建Promise实例需要new Promise构造函数, 该构造函数接受一个函数(处理器函数)作为参数, 该函数会收到两个参数, 这两个参数分别是resolve和reject(叫什么都行一般还是要语义化名称)它们是两个函数, 不用自己实现就可以使用, 如下:

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

})
console.log(p);

在这里插入图片描述

可以看到这个状态默认是Pending(进行中)

resolvereject这两个参数(函数)可以在处理器函数里面调用(可以传递参数), 这样将会改变 Promise 对象的状态:

const p = new Promise((resolve, reject) => {
  resolve();
})
console.log(p);

在这里插入图片描述

当调用resolve函数时会将Priomise对象的状态修改为Fulfilled(成功), 调用reject函数则会将状态修改为Rejected(失败)

then方法

Promise实例的then方法, 可以接受两个参数(都是函数)分别指定Primise实例里面状态(成功或失败)改变时调用的回调函数(并且Promise的then方法是异步的微任务):

const p = new Promise((resolve, reject) => {
  // 将p的状态修改为成功 
  resolve();
})
    
console.log("同步代码");
p.then(
  () => {
    console.log("成功的回调");
  },
  () => {
    console.log("失败的回调");
  }
)

结果如下:

// 同步代码
// 成功的回调

反之调用reject函数就会触发then方法的第二个回调函数, 如果将resolve函数和reject函数都调用只会生效最先调用的(因为状态时单向的嘛)

resolve 和 reject 的参数传递

经过上面的测试我们知道resolvereject这两个函数是可以修改状态并且触发Promise的then方法的回调函数的, 那么是函数就可以传递参数, 这个参数会被传递给对应的then方法的回调函数接收到:

const p = new Promise((resolve, reject) => {
  // 将p的状态修改为成功, 并且传递一个参数
  resolve("ok");
})

console.log("同步代码");
p.then(
  // 这里接收 resolve 函数传递的参数
  res => {
    console.log("成功的回调", res);
  },
  // 这里接收 reject 函数传递的参数
  err => {
    console.log("失败的回调");
  }
)

结果如下:

// 同步代码
// 成功的回调 ok

then()链式调用

了解完then方法以后我们就可以稍微修改一下一开始最上面的需求:

// 请求函数使用 Promise 封装
const baseUrl = "http://localhost:8888/test";
const request = (url) => {
  // 返回一个Promise实例
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();
    xhr.open('GET', baseUrl + url);
    xhr.send();
    xhr.onreadystatechange = () => {
      const { readyState, status, response } = xhr;
      if (readyState === 4) {
        if (status >= 200 && status <= 299) {
          const res = JSON.parse(response);
          // 修改状态为成功并且把响应传递过去
          resolve(res);
        } else {
          // 修改状态为失败也把响应传递过去
          reject(response);
        }
      }
    }
  })
}

// 使用then方法
request("/testData1").then(
  res1 => {
    console.log(res1); // {data: '用户数据1'}
    request(`/testData2?data=${res1.data}`).then(
      res2 => {
        console.log(res2); // {data: '用户数据1,用户数据2'}
        request(`/testData3?data=${res2.data}`).then(
          res3 => {
            console.log("需求需要的数据", res3); // => 需求需要的数据 {data: '用户数据1,用户数据2,用户数据3'}
          }
        )
      }
    )
  }
)

写完以后发现好像还不如使用回调函数的方式写, 看到这里好像Promise还是不能很好的解决回调嵌套的问题; 换个思路如果我们在then方法中再返回一个Promise实例, 那么不就又可以调用then方法了吗? 代码中测试一下:

const p1 = new Promise((resolve, reject) => {
  resolve("p1数据");
})

// 这里的p2就是p1.then方法成功回调里面返回的p2
const p2 = p1.then(
    // 这里的res1参数就是p1的处理器函数中调用 resolve("p1数据") 传递的参数
    res1 => {
      console.log(res1); // p1数据

      // 这里新建一个新的Promise实例, 记作p2
      const p2 = new Promise((resolve, reject) => {
        // 0.5s后修改状态
        setTimeout(() => {
          resolve(res1 + ",p2数据");
        }, 500);

      })
      // 将p2返回
      return p2;
    }
  )

const p3 = p2.then(
  res2 => {
    console.log(res2); // p1数据,p2数据
    // 这里和上面的同理
    const p3 = new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve(res2 + ",p3数据");
      }, 500);

    })
    return p3;
  }
)

p3.then(
  res3 => {
    console.log(res3); // p1数据,p2数据,p3数据
  }
)

发现可行后, 把需求代码再修改一下:

// 这里的p1就是 request("/testData1") 返回的Promise实例
const p1 = request("/testData1");

// 这里的p2就是p1.then方法成功回调返回的p2
const p2 = p1.then(
    res1 => {
      console.log(res1); // {data: '用户数据1'}

      // 再调用request方法将其返回的Promise实例记作p2
      const p2 = request(`/testData2?data=${res1.data}`);
      // 将p2返回
      return p2;
    }
  )

const p3 = p2.then(
  res2 => {
    console.log(res2); // {data: '用户数据1,用户数据2'}
    // 这里和上面的同理
    const p3 = request(`/testData3?data=${res2.data}`);
    return p3;
  }
)

p3.then(
  res3 => {
    console.log("需求需要的数据", res3); // 需求需要的数据 {data: '用户数据1,用户数据2,用户数据3'}
  }
)

需求实现是实现了, 就是代码有点冗余, 可以精简一下, 如下:

request("/testData1").then(
  res1 => {
    console.log(res1); // {data: '用户数据1'}
    // 这里直接返回 request方法的返回值Promise实例
    return request(`/testData2?data=${res1.data}`);
  }
).then( 
  // 这个成功回调时上一个then方法返回的Promise实例的成功回调
  res2 => {
    console.log(res2); // {data: '用户数据1,用户数据2'}
    // 同理
    return request(`/testData3?data=${res2.data}`);
  }
).then(
  // 同理
  res3 => {
    console.log("需求需要的数据", res3); // 需求需要的数据 {data: '用户数据1,用户数据2,用户数据3'}
  }
)

上面的代码格式就像链条一样所以又被称为"链式调用"

then()的返回值

经过上面的代码测试, then方法除了返回Promise对象外还可以返回其他任意的值, 返回会被转换为Promise对象, 内部的状态视返回值而定:

const p1 = new Promise(resolve => resolve());
p1.then(() => {
  // 这里相当于是 return undefined
}).then(
  res1 => {
    console.log(res1); // undefined
    return "hello";
  }
).then(
  res2 => {
    console.log(res2); // hello
    return { name: "张三" };
  }
).then(
  res3 => {
    console.log(res3); // { name: "张三" }

    // 返回错误对象该Promise对象的状态也是成功
    return new Error("error object");
  }
).then(
  res4 => {
    console.log(res4 instanceof Error); // true
    console.log(res4.message); // error object

    // 抛出一个错误则该Promise对象的状态是失败
    throw "thorw error";
  }
).then(
  () => { },
  err => {
    console.log(err); // thorw error
  }
)

catch方法

上面使用then方法的链式调用可以解决回调嵌套太深的问题, 但是还没处理请求之间的失败回调处理, then方法的第二个回调就是指定失败的回调, 但是一般都不使用这个回调来处理错误, 而是使用catch方法来失败, 使用格式如下:

p1.then(
  // ...
).then(
  // ...

).catch(err => { // 这个catch可以捕获这一条调用链上的错误
    // 处理错误
})

finally方法

除了catch方法那自然就有finally方法, finally方法和try...catch...finally中的finally是一样的作用, 使用格式如下:

p.then(
  // ...
).then(
  // ...

).catch(err => { // 这个catch方法可以捕获这一条调用链上的错误
  // 处理错误
    
}).finally(() => { // 无论成功还是失败这个finally中指定的回调都会执行
  // 清除loading, 重置状态...
})