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

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

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

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

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

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

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

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

  • 55
  • 15
  • 52
  • 10.1

แหล่งที่มา

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

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

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

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

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

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

// 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 "ผู้นำทางแห่งคำสัญญา" อาร์คิบัลด์ ดูว่าฉันเรียก 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 ด้วยค่าสำหรับการวนซ้ำแบบมาตรฐานที่น่าเบื่อและอ่านได้

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

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

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 หากต้องการดำเนินการนี้ คุณต้องมีค่าที่กำหนดล่วงหน้า es2017 ของ Babel และค่าที่กำหนดล่วงหน้า es2015

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

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

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

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