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

ฟังก์ชัน 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 และดีใจที่ได้เห็นว่าฟังก์ชันเหล่านี้ใช้งานได้จริงในเบราว์เซอร์ ไชโย