• 作者:老汪软件技巧
  • 发表时间:2024-09-06 15:01
  • 浏览量:

在前端开发的历程中,异步处理一直是一个关键的挑战。在 Promise 出现之前,JavaScript 主要通过回调函数来处理异步任务,但这种方式常常导致复杂的代码结构,也就是传说中的 “回调地狱(callback hell)”。一、回调函数处理异步的困境让我们以一个向女生发短信表白的例子来说明。首先,我们用函数封装了发送短信的动作:

// 向某位女生发送一则表白短信
// name: 女神的姓名
// onFulffiled: 成功后的回调
// onRejected: 失败后的回调
function sendMessage(name, onFulffiled, onRejected) {
  console.log(`帅哥程序员 -> ${name}:最近有谣言说我喜欢你,我要澄清一下,那不是谣言`);
  console.log(`等待${name}回复......`);
  setTimeout(() => {
    if (Math.random() <= 0.1) {
      onFulffiled(`${name} -> 帅哥程序员:我是九,你是三,除了你还是你`);
    } else {
      onRejected(`${name} -> 帅哥程序员:你是个好人`);
    }
  }, 1000);
}

有了这个函数后,我们开始编写程序发送短信。然而,这种层层嵌套的回调函数,形成了回调地狱:

sendMessage(
  'kitty',
  (reply) => {
    console.log(reply);
  },
  (reply) => {
    console.log(reply);
    sendMessage(
      'Cathy',
      (reply) => {
        console.log(reply);
      },
      (reply) => {
        console.log(reply);
        sendMessage(
          'Linda',
          (reply) => {
            console.log(reply);
          },
          (reply) => {
            console.log(reply);
            sendMessage(
              'Marry',
              (reply) => {
                console.log(reply);
              },
              (reply) => {
                console.log(reply);
                console.log('帅哥程序员命犯天煞孤星,注定孤独终老!!');
              }
            );
          }
        );
      }
    );
  }
);

二、Promise 规范的出现Promise 是一套专门处理异步场景的规范,它的出现有效地避免了回调地狱的产生,使异步代码更加清晰、简洁、统一。规范名称为Promise A+,得到了很多开发者的响应。所有的异步场景都表现为一个 Promise 对象,也叫任务对象。

每个任务对象有两个阶段、三个状态。任务总是从未决阶段变到已决阶段,状态从挂起变为完成或失败,且不可逆行,一旦完成或失败,状态就固定下来。

“挂起 -> 完成” 称为resolve,“挂起 -> 失败” 称为reject。任务完成时有相关数据,失败时有失败原因。

可以针对任务进行后续处理,针对完成状态的后续处理称为onFulfilled,针对失败的后续处理称为onRejected。

三、Promise API 的使用ES6 提供了一套 API 实现了 Promise A + 规范。基本使用如下:

const pro = new Promise((resolve, reject) => {
  // 任务的具体执行流程,该函数会立即被执行
  // 调用 resolve(data),可将任务变为 fulfilled 状态, data 为需要传递的相关数据
  // 调用 reject(reason),可将任务变为 rejected 状态,reason 为需要传递的失败原因
});
pro.then(
  (data) => {
    // onFulfilled 函数,当任务完成后,会自动运行该函数,data为任务完成的相关数据
  },
  (reason) => {
    // onRejected 函数,当任务失败后,会自动运行该函数,reason为任务失败的相关原因
  }
);

以百米跑步为例:

前端怎么解决异步回调__js解决回调地狱

const pro = new Promise((resolve, reject) => {
  console.log('开始百米短跑');
  const duration = Math.floor(Math.random() * 5000);
  setTimeout(() => {
    if (Math.random() < 0.5) {
      resolve(duration);
    } else {
      reject('脚扭伤了!');
    }
  }, duration);
});
pro.then(
  (data) => {
    console.log('on yeah! 我跑了', data, '秒');
  },
  (reason) => {
    console.log('不好意思,', reason);
  }
);

四、用 Promise 改造发短信函数学习了 ES6 的 Promise 后,我们对sendMessage函数进行改造:

function sendMessage(name) {
  return new Promise((resolve, reject) => {
    console.log(`帅哥程序员 -> ${name}:最近有谣言说我喜欢你,我要澄清一下,那不是谣言`);
    console.log(`等待${name}回复......`);
    setTimeout(() => {
      if (Math.random() <= 0.1) {
        resolve(`${name} -> 帅哥程序员:我是九,你是三,除了你还是你`);
      } else {
        reject(`${name} -> 帅哥程序员:你是个好人`);
      }
    }, 1000);
  });
}

使用改造后的函数发送消息:

sendMessage('kitty').then(
  (reply) => {
    console.log(reply);
  },
  (reason) => {
    console.log(reason);
  }
);

但此时回调地狱的问题仍未完全解决,还需要进一步学习 Promise 的知识。

五、课后练习

练习一:完成delay函数,该函数返回一个任务,在指定时间后完成。

function delay(duration) {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve();
    }, duration);
  });
}
delay(1000).then(() => {
  console.log('finish');
});

练习二:根据指定的图片路径,创建一个img元素,返回一个 Promise,当图片加载完成后,任务完成,若加载失败,任务失败。任务完成后提供图片 DOM 元素,失败时提供失败原因。

html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Documenttitle>
head>
<body>
  <div class="container">div>
  <p class="label">p>
  <script>
    function createImage(imgUrl) {
      return new Promise((resolve, reject) => {
        const img = document.createElement('img');
        img.src = imgUrl;
        img.onload = () => {
          resolve(img);
        };
        img.onerror = (e) => {
          reject(e);
        };
      });
    }
    const url1 = 'http://t15.baidu.com/it/u=317890015,1802267637&fm=224&app=112&f=JPEG?w=500&h=500';
    createImage(url1).then(
      (img) => {
        const p = document.querySelector('.label');
        p.innerHTML = `${img.width} * ${img.height}`;
      },
      (reason) => {
        console.log(reason);
      }
    );
    createImage(url1).then(
      (img) => {
        const div = document.querySelector('.container');
        div.appendChild(img);
      },
      (reason) => {
        console.log(reason);
      }
    );
  script>
body>
html>

练习三:调用getProvinces函数,远程加载省份数据,返回一个 Promise,成功后得到省份数组,失败时给出失败原因。将省份数据加载到select元素中。

html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Documenttitle>
head>
<body>
  <select id="selProvince">select>
  <script>
    function getProvinces() {
      return fetch('https://study.duyiedu.com/api/citylist')
       .then((resp) => resp.json())
       .then((resp) => resp.data)
       .then((resp) =>
          resp.map((it) => ({ value: it.value, label: it.label }))
        );
    }
    getProvinces().then(
      (ps) => {
        const html = ps
         .map((p) => `">${p.label}`)
         .join('');
        const selProvince = document.getElementById('selProvince');
        selProvince.innerHTML = html;
      },
      (reason) => {
        console.log(reason);
      }
    );
  script>
body>
html>

练习四(面试题):分析下面代码中任务的最终状态、相关数据或失败原因以及最终输出。

const pro1 = new Promise((resolve, reject) => {
  console.log('任务开始');
  resolve(1);
  reject(2); // 无效
  resolve(3); // 无效
  console.log('任务结束');
});
console.log(pro1);
new Promise((resolve, reject) => {
  console.log('任务开始');
  resolve(1);
  resolve(2); // 无效
  console.log('任务结束');
});