普通文組 2.5

「從Callback到Promise」相關筆記

總結

本篇筆記內容:

  • JavaScript 的非同步處理如何從呼叫 Callback function 轉換為 Promise 的形式
  • AC 學期 3 的「Promise 包裝作業」思考流程

筆記

Promise

定義:

  • Promises/A+:
    • A promise represents the eventual result of an asynchronous operation.
    • The primary way of interacting with a promise is through its then method, which registers callbacks to receive either a promise’s eventual value or the reason why the promise cannot be fulfilled.
  • MDN:
    • The Promise object represents the eventual completion (or failure) of an asynchronous operation and its resulting value.
    • This lets asynchronous methods return values like synchronous methods: instead of immediately returning the final value, the asynchronous method returns a promise to supply the value at some point in the future.
    • Promise.prototype.then(), Promise.prototype.catch(): As the then and Promise.prototype.catch() methods return promises, they can be chained — an operation called composition.
    • Promise.prototype.finally(): This provides a way for code to be run whether the promise was fulfilled successfully or rejected once the Promise has been dealt with.

小結論:

  • promise 代表一個非同步功能的最終結果
  • async function 最終的結果會是fulfilledrejected二選一
  • then()來接Promise.resolve()的值、用catch()來接Promise.reject(),而finally()內的 function 則是不論結果為 resolve 或 reject 都會執行

callback 使用情境

範例:以下程式碼全部借鑑 YouTube 影片Async JS Crash Course - Callbacks, Promises, Async Await

const posts = [
  { title: "Post 1", content: "Content post 1" },
  { title: "Post 2", content: "Content post 2" },
];

function postToList() {
  setTimeout(() => {
    let content = "";
    posts.forEach((post) => {
      content += `<li>${post.title}</li>`;
    });
    document.body.innerHTML = content;
  }, 1000);
}

postToList();
  • 模擬情境:
    • 瀏覽器向 server 索取 posts 資料,而 server 過了一秒後回覆
    • 瀏覽器收到 posts 後,將所有的 post.title 渲染為清單並放到畫面上
const posts = [
  { title: "Post 1", content: "Content post 1" },
  { title: "Post 2", content: "Content post 2" },
];

function postToList() {
  setTimeout(() => {
    let content = "";
    posts.forEach((post) => {
      content += `<li>${post.title}</li>`;
    });
    document.body.innerHTML = content;
  }, 1000); // 延後1秒執行
}

function addPost(post) {
  setTimeout(() => {
    posts.push(post);
  }, 2000); // 延後2秒執行
}

addPost({ title: "Post 3", content: "Content post 3" });
postToList();
  • 模擬情境:
    • 將第三篇 post 新增到 posts 陣列中
    • 瀏覽器向 server 索取 posts 資料,而 server 過了一秒後回覆
    • 瀏覽器收到 posts 後,將所有的 post.title 渲染為清單並放到畫面上
    • 執行結果:畫面上僅會有 post 1 與 post2,因為addPost()實際執行的時間點(被setTimeout()延後至少 2 秒)比postToList()(被setTimeout()延後至少 1 秒)晚
const posts = [
  { title: "Post 1", content: "Content post 1" },
  { title: "Post 2", content: "Content post 2" },
];

function postToList() {
  setTimeout(() => {
    let content = "";
    posts.forEach((post) => {
      content += `<li>${post.title}</li>`;
    });
    document.body.innerHTML = content;
  }, 1000);
}

function addPost(post, callback) {
  setTimeout(() => {
    posts.push(post);
    callback();
  }, 2000);
}

addPost({ title: "Post 3", content: "Content post 3" }, postToList);
  • callback function 登場,將postToList作為 callback 傳入addPost後,程式碼的行為變成「待addPost結束後,再執行postToList
  • 執行結果:畫面上會印出 post 1、post2 與 post3

將 callback 轉換為 promise 形式

const posts = [
  { title: "Post 1", content: "Content post 1" },
  { title: "Post 2", content: "Content post 2" },
];

function postToList() {
  setTimeout(() => {
    let content = "";
    posts.forEach((post) => {
      content += `<li>${post.title}</li>`;
    });
    document.body.innerHTML = content;
  }, 1000);
}

function addPost(post) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const error = false;
      if (!error) {
        posts.push(post);
        resolve();
      } else {
        reject("something goes wrong");
      }
    });
  });
}

addPost({ title: "Post 3", content: "Content post 3" })
  .then(() => {
    postToList();
  })
  .catch((error) => {
    console.error(error);
  });
  1. 移除addPost的 callback 參數
  2. addPost回傳 Promise 物件,其中resolve()沒有包含任何值,而reject()提供錯誤訊息「something goes wrong
  3. 執行addPost後,透過thencatch來承接addPost順利完成(resolve)或失敗(reject)的情境
  4. addPost成功時,進行postToList()
  5. addPost出現錯誤時,則console.error(error)

AC 作業應用

原始 callback 版本:

const http = require("http");
const https = require("https");
const imgPath = "";

http
  .createServer((req, res) => {
    https
      .get("https://dog.ceo/api/breeds/image/random", (body) => {
        let data = "";

        body.on("data", (chunk) => {
          data += chunk;
        });

        body.on("end", () => {
          console.log(JSON.parse(data));
          imgPath = JSON.parse(data).message;
          res.end(`<h1>DOG PAGE</h1><img src='${imgPath}' >`);
        });
      })
      .on("error", (error) => {
        console.error(error);
      });
  })
  .listen(3000);

Promise 版起手式:

const http = require("http");
const https = require("https");

const requestData = () => {
  // TODO
};

http
  .createServer((req, res) => {
    // TODO
    res.end(`<h1>DOG PAGE</h1><img src='${imgPath}' >`);
  })
  .listen(3000);

思考過程:

  1. resolve()處理JSON.parse(data),用reject()處理.on('error', (error) => { console.error(error) })這一段
  2. 把整個https.get(...)包成 Promise 物件
  3. requestData順利完成後,用then()來接JSON.parse(data)result內容為JSON.parse(data),取result.message即為圖片網址
  4. catch()來處理錯誤狀態,將workingURL替換為變數errorURL後,即可在終端看到error訊息

改裝完成:

const http = require("http");
const https = require("https");

const workingURL = "https://dog.ceo/api/breeds/image/random";
const errorURL = "https://this.will.not/work";

const requestData = () => {
  return new Promise((resolve, reject) => {
    https
      .get(workingURL, (body) => {
        let data = "";

        body.on("data", (chunk) => {
          data += chunk;
        });

        body.on("end", () => {
          console.log(JSON.parse(data));
          resolve(JSON.parse(data));
        });
      })
      .on("error", (error) => {
        reject(error);
      });
  });
};

http
  .createServer((req, res) => {
    requestData()
      .then((result) => {
        const imgPath = result.message;
        res.end(`<h1>DOG PAGE</h1><img src='${imgPath}' >`);
      })
      .catch((error) => {
        console.error(error);
        res.end(`<p>Sorry but something goes wrong.</p>`);
      });
  })
  .listen(3000);

補充:Promise.all()

// MDN範例
const promise1 = Promise.resolve("Answer to the Ultimate Question of Life, ");
const promise2 = "the Universe, and Everything";
const promise3 = new Promise((resolve, reject) => {
  setTimeout(resolve, 1000, "42");
});

Promise.all([promise1, promise2, promise3]).then((values) => {
  console.log(values);
});
// 輸出結果:
// ["Answer to the Ultimate Question of Life, ", "the Universe, and Everything", "42"]

參考MDN

  • It takes an iterable of promises as an input, and returns a single Promise that resolves to an array of the results of the input promises.
  • It rejects immediately upon any of the input promises rejecting or non-promises throwing an error, and will reject with this first rejection message / error.

參考文件