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

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

ฟังก์ชัน 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);
  }
}

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

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

ฟังก์ชัน 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 ด้วยลูปสำหรับวนรอบ ทำให้ดูดียิ่งกว่าเดิม

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

เราได้แสดง 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 พร้อมกัน แต่จะใช้การวนซ้ำ for แบบมาตรฐานที่อ่านง่ายและน่าเบื่อแทนการดําเนินการแบบ "อัจฉริยะ" ของ reduce

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

หากกำหนดเป้าหมายเป็นเบราว์เซอร์ที่รองรับโปรแกรมสร้าง (ซึ่งรวมถึงเบราว์เซอร์หลักเวอร์ชันล่าสุดทั้งหมด) คุณสามารถจัดเรียงฟังก์ชันอะซิงโครนัส 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 นอกจากนี้ ยังทำงานเหมือนเดิม

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

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

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

ไม่ซิงค์ทุกเรื่องเลย!

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

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