ฟังก์ชันไม่พร้อมกัน: ทำตามสัญญาง่าย

ฟังก์ชันแบบไม่พร้อมกันช่วยให้คุณเขียนโค้ดที่อิงตามสัญญาได้เสมือนว่าเป็นแบบซิงโครนัส

เจค อาร์ชิบาลด์
เจค อาร์ชิบาลด์

ฟังก์ชันอะซิงโครนัสจะเปิดใช้โดยค่าเริ่มต้นใน Chrome, Edge, Firefox และ Safari ซึ่งก็น่าทึ่งมาก ซึ่งช่วยให้คุณเขียนโค้ดตามคำสัญญาเสมือนได้ว่าเป็นแบบซิงโครนัส แต่ไม่บล็อกเทรดหลัก ทำให้โค้ดอะซิงโครนัสของคุณ "อ่านง่าย" และอ่านง่ายขึ้น

ฟังก์ชันอะซิงโครนัสทำงานดังต่อไปนี้

async function myFirstAsyncFunction() {
  try {
    const fulfilledValue = await promise;
  } catch (rejectedValue) {
    // …
  }
}

หากใช้คีย์เวิร์ด async ก่อนคำจำกัดความของฟังก์ชัน คุณจะใช้ await ภายในฟังก์ชันดังกล่าวได้ เมื่อคุณ await สัญญา ฟังก์ชันจะหยุดชั่วคราวแบบไม่บล็อกจนกว่าคำสัญญาจะเสร็จสิ้น หากคำสัญญานั้นได้ผล คุณจะได้รับมูลค่ากลับมา หากคำสัญญาปฏิเสธ ระบบจะส่งค่าที่ถูกปฏิเสธ

การสนับสนุนเบราว์เซอร์

การสนับสนุนเบราว์เซอร์

  • 55
  • 15
  • 52
  • 10.1

แหล่งที่มา

ตัวอย่าง: การบันทึกการดึงข้อมูล

สมมติว่าคุณต้องการดึงข้อมูล URL และบันทึกคำตอบเป็นข้อความ นี่คือหน้าตาของ Google ที่ใช้คำสัญญา

function logFetch(url) {
  return fetch(url)
    .then((response) => response.text())
    .then((text) => {
      console.log(text);
    })
    .catch((err) => {
      console.error('fetch failed', err);
    });
}

และนี่คือสิ่งเดียวกันในการใช้ฟังก์ชันอะซิงโครนัส:

async function logFetch(url) {
  try {
    const response = await fetch(url);
    console.log(await response.text());
  } catch (err) {
    console.log('fetch failed', err);
  }
}

มีจำนวนบรรทัดเท่ากัน แต่โค้ดเรียกกลับทั้งหมดจะหายไป วิธีนี้ช่วยให้อ่านง่ายขึ้น โดยเฉพาะสำหรับผู้ที่ไม่ค่อยคุ้นเคยกับคำสัญญา

ค่าการแสดงผลแบบไม่พร้อมกัน

ฟังก์ชันที่ไม่พร้อมกันจะแสดงคำสัญญาเสมอ ไม่ว่าคุณจะใช้ await หรือไม่ก็ตาม ซึ่งจะแก้ไขได้ด้วยฟังก์ชันการอะซิงโครนัสใดๆ ก็ตามที่แสดงผล หรือปฏิเสธพร้อมด้วยฟังก์ชันการอะซิงโครนัสใดๆ ที่ส่งกลับมา ดังนั้นด้วย:

// wait ms milliseconds
function wait(ms) {
  return new Promise((r) => setTimeout(r, ms));
}

async function hello() {
  await wait(500);
  return 'world';
}

...การเรียก hello() จะส่งคืนคำสัญญาว่า ตอบสนอง ด้วย "world"

async function foo() {
  await wait(500);
  throw Error('bar');
}

...การเรียก foo() จะส่งคืนสัญญาว่า ปฏิเสธ ด้วย Error('bar')

ตัวอย่าง: การสตรีมคำตอบ

ประโยชน์ของฟังก์ชันแบบไม่พร้อมกันจะมีตัวอย่างที่ซับซ้อนมากขึ้น สมมติว่าคุณต้องการสตรีมคำตอบขณะที่นำข้อความส่วนออก แล้วแสดงผลขนาดสุดท้าย

ให้คำมั่นสัญญาไว้ว่า

function getResponseSize(url) {
  return fetch(url).then((response) => {
    const reader = response.body.getReader();
    let total = 0;

    return reader.read().then(function processResult(result) {
      if (result.done) return total;

      const value = result.value;
      total += value.length;
      console.log('Received chunk', value);

      return reader.read().then(processResult);
    });
  });
}

ดูสิ เจค "เจ้าแห่งคำมั่นสัญญา" Archibald ดูสิว่าฉันเรียก processResult() ภายในตัวเองเพื่อตั้งค่าลูปแบบไม่พร้อมกันไหม การเขียนที่ทำให้ฉัน รู้สึกว่าฉลาดมาก แต่เช่นเดียวกับโค้ดที่ "ฉลาด" ส่วนใหญ่ คุณต้องจ้องมองดู ให้อายุก่อนอยากรู้ว่าโค้ดนี้ใช้ทำอะไร เช่น รูปภาพตาวิเศษชิ้นหนึ่งจากยุค 90

ลองดูอีกครั้งด้วยฟังก์ชันอะซิงโครนัส:

async function getResponseSize(url) {
  const response = await fetch(url);
  const reader = response.body.getReader();
  let result = await reader.read();
  let total = 0;

  while (!result.done) {
    const value = result.value;
    total += value.length;
    console.log('Received chunk', value);
    // get the next result
    result = await reader.read();
  }

  return total;
}

คำว่า "ฉลาด" หายไปแล้ว ลูปที่ไม่พร้อมกันทำให้ฉันรู้สึกอบอุ่นใจ ถูกแทนที่ด้วยความน่าเบื่อ การวนรอบ ยิ่งไปกว่านั้น ในอนาคตคุณจะได้รับตัววนซ้ำแบบไม่พร้อมกัน ซึ่งจะแทนที่ลูป while ด้วยลูปแบบวนรอบ ซึ่งทำให้ใช้งานได้อย่างลื่นไหลอีกด้วย

ไวยากรณ์ฟังก์ชันไม่พร้อมกันอื่นๆ

ฉันเคยแสดง async function() {} แล้ว แต่คุณใช้คีย์เวิร์ด async กับไวยากรณ์ของฟังก์ชันอื่นๆ ได้ ดังนี้

ฟังก์ชันลูกศร

// map some URLs to json-promises
const jsonPromises = urls.map(async (url) => {
  const response = await fetch(url);
  return response.json();
});

เมธอดวัตถุ

const storage = {
  async getAvatar(name) {
    const cache = await caches.open('avatars');
    return cache.match(`/avatars/${name}.jpg`);
  }
};

storage.getAvatar('jaffathecake').then(…);

วิธีการของชั้นเรียน

class Storage {
  constructor() {
    this.cachePromise = caches.open('avatars');
  }

  async getAvatar(name) {
    const cache = await this.cachePromise;
    return cache.match(`/avatars/${name}.jpg`);
  }
}

const storage = new Storage();
storage.getAvatar('jaffathecake').then(…);

ระวังด้วยนะ หลีกเลี่ยงการเรียงลำดับมากเกินไป

แม้ว่าคุณจะเขียนโค้ดที่ดูพร้อมกัน แต่ต้องไม่พลาดโอกาสในการทำงานต่างๆ ควบคู่กันไป

async function series() {
  await wait(500); // Wait 500ms…
  await wait(500); // …then wait another 500ms.
  return 'done!';
}

การดำเนินการข้างต้นใช้เวลา 1, 000 มิลลิวินาที จึงเสร็จสมบูรณ์

async function parallel() {
  const wait1 = wait(500); // Start a 500ms timer asynchronously…
  const wait2 = wait(500); // …meaning this timer happens in parallel.
  await Promise.all([wait1, wait2]); // Wait for both timers in parallel.
  return 'done!';
}

การดำเนินการข้างต้นใช้เวลา 500 มิลลิวินาที เนื่องจากต้องรอพร้อมกัน ลองมาดูตัวอย่างที่ใช้งานได้จริงกัน

ตัวอย่าง: การแสดงการดึงข้อมูลตามลำดับ

สมมติว่าคุณต้องการดึงข้อมูลชุด URL และบันทึกไว้โดยเร็วที่สุดตามลำดับที่ถูกต้อง

ลมหายใจลึก - นี่คือหน้าตาของสัญญา

function markHandled(promise) {
  promise.catch(() => {});
  return promise;
}

function logInOrder(urls) {
  // fetch all the URLs
  const textPromises = urls.map((url) => {
    return markHandled(fetch(url).then((response) => response.text()));
  });

  // log them in order
  return textPromises.reduce((chain, textPromise) => {
    return chain.then(() => textPromise).then((text) => console.log(text));
  }, Promise.resolve());
}

ใช่แล้ว ฉันใช้ reduce เพื่อเชื่อมโยงสัญญาต่างๆ ตามลำดับ ฉันฉลาดมาก แต่นี่เป็นเพียงการเขียนโค้ดที่ชาญฉลาดยิ่งขึ้น คุณจะดีกว่าถ้าไม่ได้ใช้

อย่างไรก็ตาม เมื่อแปลงข้อมูลข้างต้นเป็นฟังก์ชันอะซิงโครนัส คุณอาจอยากทำตามลำดับที่มากเกินไป เช่น

ไม่แนะนำ - ตามลำดับมากเกินไป
async function logInOrder(urls) {
  for (const url of urls) {
    const response = await fetch(url);
    console.log(await response.text());
  }
}
ดูดีมาก แต่การดึงข้อมูลครั้งที่ 2 จะไม่เริ่มต้นจนกระทั่งการดึงข้อมูลครั้งแรกจะอ่านจบแล้ว และอื่นๆ ซึ่งช้ากว่าตัวอย่างที่ให้สัญญาไว้ ซึ่งทำการดึงข้อมูลไปพร้อมกัน โชคดีที่มีจุดกึ่งกลางที่เหมาะสม
แนะนำ - ดูดีและเข้ากันได้ดี
function markHandled(...promises) {
  Promise.allSettled(promises);
}

async function logInOrder(urls) {
  // fetch all the URLs in parallel
  const textPromises = urls.map(async (url) => {
    const response = await fetch(url);
    return response.text();
  });

  markHandled(...textPromises);

  // log them in sequence
  for (const textPromise of textPromises) {
    console.log(await textPromise);
  }
}
ในตัวอย่างนี้ ระบบจะดึงข้อมูลและอ่าน URL พร้อมกัน แต่บิต "อัจฉริยะ" reduce กลับถูกแทนที่ด้วยฟีเจอร์ for-loop มาตรฐาน น่าเบื่อ และสามารถอ่านได้

วิธีแก้ปัญหาเบื้องต้นสำหรับการสนับสนุนเบราว์เซอร์: โปรแกรมสร้าง

หากกำหนดเป้าหมายเบราว์เซอร์ที่รองรับโปรแกรมสร้าง (ซึ่งรวมถึงเบราว์เซอร์หลักเวอร์ชันล่าสุดทั้งหมด) คุณจะจัดเรียงฟังก์ชัน polyfill async ได้

Babel จะทำวิธีนี้ให้คุณ ต่อไปนี้เป็นตัวอย่างผ่านการตอบกลับ Babel

เราขอแนะนำให้ใช้วิธีการเปลี่ยนรูปแบบ เนื่องจากคุณจะปิดใช้งานได้เมื่อเบราว์เซอร์เป้าหมายรองรับฟังก์ชันอะซิงโครนัส แต่หากจริงๆ แล้วไม่ต้องการใช้ตัวเปลี่ยนรูปแบบ ก็ใช้โพลีฟิลของ Babel และใช้งานด้วยตัวเองได้เลย แทนที่จะเป็น:

async function slowEcho(val) {
  await wait(1000);
  return val;
}

...ให้คุณใส่ polyfill และเขียนว่า

const slowEcho = createAsyncFunction(function* (val) {
  yield wait(1000);
  return val;
});

โปรดทราบว่าคุณต้องส่งโปรแกรมสร้าง (function*) ไปยัง createAsyncFunction และใช้ yield แทน await อื่นๆ นอกเหนือจากนั้นทำงานเหมือนเดิม

วิธีแก้ปัญหาเบื้องต้น: สร้างอีกครั้ง

หากคุณกำหนดเป้าหมายเบราว์เซอร์รุ่นเก่า Babel ยังสามารถเปลี่ยนรูปแบบเครื่องกำเนิดไฟฟ้า ซึ่งทำให้คุณสามารถใช้ฟังก์ชันอะซิงโครนัสทั้งหมดไปจนถึง IE8 โดยต้องใช้ค่าที่กำหนดล่วงหน้าของ Babel es2017 และค่าที่กำหนดล่วงหน้า es2015

เอาต์พุตอาจดูไม่สวยพอ ดังนั้นระวังอย่าปล่อยให้โค้ดขยายตัว

ซิงค์ทุกอย่างพร้อมกัน

เมื่อลงฟังก์ชันอะซิงโครนัสในทุกเบราว์เซอร์แล้ว ให้ใช้ฟังก์ชันเหล่านั้นใน ทุกฟังก์ชันที่สัญญาว่าจะส่งคืน วิธีนี้ไม่เพียงแต่ทำให้โค้ดเป็นระเบียบมากขึ้นเท่านั้น แต่ยังช่วยให้มั่นใจว่าฟังก์ชันจะให้ผลลัพธ์เสมอเสมอด้วย

ผมตื่นเต้นมากกับฟังก์ชันแบบอะซิงโครนัสเมื่อปี 2014 และผมดีใจมากที่ได้เห็นฟังก์ชันไม่พร้อมกันในเบราว์เซอร์ อ๊ะ!