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

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

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

ฟังก์ชัน Async ทํางานดังนี้

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

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

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

การรองรับเบราว์เซอร์

  • Chrome: 55
  • Edge: 15.
  • Firefox: 52
  • Safari: 10.1

แหล่งที่มา

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

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

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);
  }
}

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

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

ฟังก์ชัน Async จะแสดงผลพรอมต์เสมอ ไม่ว่าคุณจะใช้ await หรือไม่ก็ตาม Promise นั้นจะยุติด้วยผลลัพธ์ที่ฟังก์ชัน Async แสดง หรือปฏิเสธด้วยสิ่งที่ฟังก์ชัน Async แสดง ดังนั้น เมื่อใช้

// 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);
    });
  });
}

ผมชื่อ Jake "ผู้ทำตามสัญญา" 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 ที่น่าเบื่อแต่เชื่อถือได้ ดีกว่ามาก ในอนาคต คุณจะได้รับตัวดำเนินการแบบแอสซิงค์ ซึ่งจะแทนที่ลูป while ด้วยลูป for-of ทําให้โค้ดเรียบร้อยยิ่งขึ้น

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

เราได้แสดง 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 มิลลิวินาทีจึงจะเสร็จสมบูรณ์ เนื่องจากทั้ง 2 รายการรอพร้อมกัน มาดูตัวอย่างการใช้งานจริงกัน

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

สมมติว่าคุณต้องการดึงข้อมูลชุด 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 เพื่อต่อเชื่อมลำดับของ Promise ฉันฉลาดมาก แต่นี่เป็นการเขียนโค้ดที่ฉลาดมากซึ่งคุณควรหลีกเลี่ยง

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

ไม่แนะนำเนื่องจากมีลำดับมากเกินไป
async function logInOrder(urls) {
  for (const url of urls) {
    const response = await fetch(url);
    console.log(await response.text());
  }
}
ดูเรียบร้อยกว่ามาก แต่การดึงข้อมูลครั้งที่ 2 จะไม่เริ่มต้นจนกว่าการดึงข้อมูลครั้งแรกจะอ่านจนจบ และอื่นๆ ซึ่งช้ากว่าตัวอย่าง Promise ที่ดึงข้อมูลพร้อมกัน แต่โชคดีที่ยังมีวิธีกลางๆ ที่เหมาะเจาะ
แนะนํา - วางขนานกัน
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 มาตรฐานที่อ่านได้และน่าเบื่อ

วิธีแก้ปัญหาการรองรับเบราว์เซอร์: เครื่องมือสร้าง

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

Babel จะดำเนินการนี้ให้คุณ ดูตัวอย่างผ่าน Babel REPL

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

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

…คุณจะต้องใส่ polyfill และเขียนดังนี้

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

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

วิธีแก้ปัญหา: regenerator

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

เอาต์พุตจะไม่สวยมาก ดังนั้นโปรดระวังโค้ดที่ทำงานหนักเกินไป

ทำงานแบบไม่พร้อมกันทั้งหมด

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

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