درباره نویسنده
برت کامرون توسعه دهنده و نویسنده مستقر در لندن است. او یک مهندس تمام پشته در استارتاپ insurtech YuLife است ، و علاقه زیادی به همه چیز دارد
بیشتر در مورد
برت
…
then/catch
(ES6) و async/await
(ES7) این نحوها عملکرد اساسی یکسانی را به ما ارائه می دهند ، اما به روش های مختلف بر خوانایی و دامنه تأثیر می گذارند. در این مقاله ، خواهیم دید که چگونه یک نحو به کد قابل نگهداری وام می دهد ، در حالی که دیگری ما را در مسیر جهنم پاسخگویی قرار می دهد!
JavaScript کد را خط به خط اجرا می کند و فقط پس از اجرای کد قبلی به خط بعدی کد می رود. اما اجرای کدی مانند این فقط می تواند ما را تا اینجا ببرد. گاهی اوقات ، ما باید کارهایی را انجام دهیم که مدت زمان طولانی یا غیر قابل پیش بینی آنها طول می کشد: به عنوان مثال واکشی داده ها یا ایجاد عوارض جانبی از طریق API.
به جای اینکه به این وظایف اجازه دهید تا موضوع اصلی JavaScript را مسدود کنند ، این زبان به ما امکان می دهد برخی کارها را به طور موازی اجرا کنیم. ES6 معرفی شی Promise و همچنین روشهای جدید برای مدیریت این وعده ها را مشاهده کرد: then
، catch
، و finally
. اما یک سال بعد ، در ES7 ، این زبان رویکرد دیگری و دو کلمه کلیدی جدید اضافه کرد: async
و await
.
این مقاله توضیح دهنده JavaScript ناهمگام نیست. منابع خوبی برای آن وجود دارد. درعوض ، موضوع کمتری را پوشش می دهد: کدام نحو – then/catch
یا async/await
– بهتر است؟ به نظر من ، مگر اینکه یک کتابخانه یا پایگاه کد قدیمی شما را مجبور به استفاده کند then/catch
، گزینه بهتر برای خوانایی و قابلیت نگهداری است async/await
. برای نشان دادن این ، ما از هر دو نحو برای حل یک مسئله استفاده خواهیم کرد. با اندکی تغییر در الزامات ، باید مشخص شود که اصلاح و نگهداری کدام روش آسان تر است.
ما قبل از انتقال به سناریوی مثال خود ، با بازخوانی ویژگیهای اصلی هر نحو شروع خواهیم کرد.
then
، catch
و finally
then
و catch
و finally
روشهای شی Promise هستند و آنها یکی پس از دیگری زنجیر می شوند. هر یک از آنها عملکرد بازگشتی را به عنوان آرگومان خود در نظر می گیرند و یک قول را برمی گردانند.
به عنوان مثال ، بیایید یک قول ساده را مثال بزنیم:
const greeting = new Promise((resolve, reject) => {
resolve("Hello!");
});
استفاده كردن then
، catch
و finally
، ما می توانیم یک سری اقدامات را بر اساس اینکه آیا وعده حل شده است انجام دهیم (then
) یا رد شد (catch
) – در حالی که finally
به ما امکان می دهد پس از تسویه حساب وعده ، بدون در نظر گرفتن حل یا رد ، کد را اجرا کنیم:
greeting
.then((value) => {
console.log("The Promise is resolved!", value);
})
.catch((error) => {
console.error("The Promise is rejected!", error);
})
.finally(() => {
console.log(
"The Promise is settled, meaning it has been resolved or rejected."
);
});
برای اهداف این مقاله ، ما فقط نیاز به استفاده داریم then
. زنجیر زدن چندتایی then
روش ها به ما امکان می دهد عملیات پی در پی را با یک وعده حل شده انجام دهیم. به عنوان مثال ، یک الگوی معمول برای واکشی داده ها با then
ممکن است چیزی شبیه به این باشد:
fetch(url)
.then((response) => response.json())
.then((data) => {
return {
data: data,
status: response.status,
};
})
.then((res) => {
console.log(res.data, res.status);
});
async
و await
در مقابل ، async
و await
کلمات کلیدی هستند که کد همگام را ناهمزمان می کنند. ما استفاده می کنیم async
هنگام تعریف یک تابع برای نشان دادن اینکه یک قول را برمی گرداند. توجه کنید که چگونه محل قرارگیری async
کلمه کلیدی بستگی به این دارد که ما از توابع منظم استفاده می کنیم یا از توابع پیکان:
async function doSomethingAsynchronous() {
// logic
}
const doSomethingAsynchronous = async () => {
// logic
};
await
در همین حال ، قبل از یک وعده استفاده می شود. اجرای تابع ناهمزمان را تا زمان حل وعده متوقف می کند. به عنوان مثال ، در انتظار ما greeting
در بالا ، می توانیم بنویسیم:
async function doSomethingAsynchronous() {
const value = await greeting;
}
سپس می توانیم از خود استفاده کنیم value
متغیری که گویی بخشی از کد همگام عادی است.
در مورد مدیریت خطا ، ما می توانیم هر کد ناهمزمان را درون a قرار دهیم try...catch...finally
بیانیه ، مانند این:
async function doSomethingAsynchronous() {
try {
const value = await greeting;
console.log("The Promise is resolved!", value);
} catch (e) {
console.error("The Promise is rejected!", error);
} finally {
console.log(
"The Promise is settled, meaning it has been resolved or rejected."
);
}
}
سرانجام ، هنگام بازگشت یک وعده در داخل async
عملکرد ، نیازی به استفاده ندارید await
. بنابراین دستور زیر قابل قبول است.
async function getGreeting() {
return greeting;
}
با این حال ، یک استثنا در این قانون وجود دارد: شما نیاز به نوشتن دارید return await
اگر به دنبال این هستید که وعده را رد کنید در a try...catch
مسدود کردن.
async function getGreeting() {
try {
return await greeting;
} catch (e) {
console.error(e);
}
}
استفاده از مثالهای انتزاعی ممکن است به ما در درک هر یک از نحوها کمک کند ، اما درک اینکه چرا یکی ممکن است به دیگری ارجح باشد تا وقتی که به یک مثال بپردازیم دشوار است.
مشکل
بیایید تصور کنیم که برای یک کتابفروشی باید عملیاتی را روی یک مجموعه داده بزرگ انجام دهیم. وظیفه ما این است که تمام نویسندگانی را که بیش از 10 کتاب در مجموعه داده های ما نوشته اند پیدا کنیم و بیوگرافی خود را بازگردانیم. ما با سه روش ناهمزمان به کتابخانه دسترسی داریم:
// getAuthors - returns all the authors in the database
// getBooks - returns all the books in the database
// getBio - returns the bio of a specific author
اشیا Our ما به این شکل هستند:
// Author: { id: "3b4ab205", name: "Frank Herbert Jr.", bioId: "1138089a" }
// Book: { id: "e31f7b5e", title: "Dune", authorId: "3b4ab205" }
// Bio: { id: "1138089a", description: "Franklin Herbert Jr. was an American science-fiction author..." }
در آخر ، ما به یک تابع کمکی نیاز خواهیم داشت ، filterProlificAuthors
، که همه پست ها و همه کتاب ها را به عنوان آرگومان می گیرد و شناسه آن نویسندگان را با بیش از 10 کتاب برمی گرداند:
function filterProlificAuthors() {
return authors.filter(
({ id }) => books.filter(({ authorId }) => authorId === id).length > 10
);
}
راه حل
قسمت 1
برای حل این مشکل ، ما باید تمام نویسندگان و همه کتابها را بیاوریم ، نتایج خود را بر اساس معیارهای داده شده فیلتر کنیم و سپس شرح حال نویسندگان متناسب با این معیارها را دریافت کنیم. در کد شبه ، راه حل ما ممکن است شبیه به این باشد:
FETCH all authors
FETCH all books
FILTER authors with more than 10 books
FOR each filtered author
FETCH the author’s bio
هر وقت می بینیم FETCH
در بالا ، ما باید یک کار ناهمزمان را انجام دهیم. بنابراین چگونه می توانیم این را به JavaScript تبدیل کنیم؟ اول ، بیایید ببینیم که چگونه می توانیم با استفاده از این مراحل کدگذاری کنیم then
:
getAuthors().then((authors) =>
getBooks()
.then((books) => {
const prolificAuthorIds = filterProlificAuthors(authors, books);
return Promise.all(prolificAuthorIds.map((id) => getBio(id)));
})
.then((bios) => {
// Do something with the bios
})
);
این کد کار را انجام می دهد ، اما لانه سازی در حال انجام است که درک آن در یک نگاه دشوار است. دومین then
درون اولین لانه شده است then
، در حالی که سوم then
موازی با دوم است.
در صورت استفاده کد ما ممکن است کمی خواناتر شود then
حتی کد همزمان را برگردانید؟ می توانستیم بدهیم filterProlificAuthors
خودش then
روش ، مانند زیر:
getAuthors().then((authors) =>
getBooks()
.then((books) => filterProlificAuthors(authors, books))
.then((ids) => Promise.all(ids.map((id) => getBio(id))))
.then((bios) => {
// Do something with the bios
})
);
این نسخه به نفع هر یک است then
متناسب با یک خط است ، اما ما را از چندین سطح لانه سازی نجات نمی دهد.
در مورد استفاده چطور؟ async
و await
؟ اولین پاس ما در یک راه حل ممکن است چیزی شبیه به این باشد:
async function getBios() {
const authors = await getAuthors();
const books = await getBooks();
const prolificAuthorIds = filterProlificAuthors(authors, books);
const bios = await Promise.all(prolificAuthorIds.map((id) => getBio(id)));
// Do something with the bios
}
به نظر من ، این راه حل در حال حاضر ساده تر به نظر می رسد. شامل هیچ لانه سازی نیست و می تواند به راحتی فقط در چهار خط بیان شود – همه در همان سطح تورفتگی است. با این حال ، مزایای آن async/await
با تغییر الزامات ما بیشتر آشکار می شود.
قسمت 2
بیایید یک نیاز جدید را معرفی کنیم. این بار ، یک بار ما خودمان را داریم bios
آرایه ، ما می خواهیم یک شی object حاوی ایجاد کنیم bios
، تعداد کل نویسندگان و تعداد کل کتاب ها.
این بار ، ما با شروع می کنیم async/await
:
async function getBios() {
const authors = await getAuthors();
const books = await getBooks();
const prolificAuthorIds = filterProlificAuthors(authors, books);
const bios = await Promise.all(prolificAuthorIds.map((id) => getBio(id)));
const result = {
bios,
totalAuthors: authors.length,
totalBooks: books.length,
};
}
آسان! ما مجبور نیستیم با کد موجود خود کاری انجام دهیم ، زیرا همه متغیرهای مورد نیاز ما از قبل در دامنه هستند. ما فقط می توانیم تعریف کنیم result
در پایان شی.
با then
، خیلی ساده نیست. در ما then
راه حل از قسمت 1 ، books
و bios
متغیرها هرگز در یک دامنه نیستند. در حالی که ما میتوانست جهانی معرفی کنید books
متغیر ، که باعث می شود فضای نام جهانی با چیزی که فقط در کد ناهمزمان به آن نیاز داریم آلوده شود. بهتر است کد ما را دوباره قالب بندی کنید. پس چگونه توانستیم این کار را انجام دهیم؟
یک گزینه معرفی سطح سوم تودرتو است:
getAuthors().then((authors) =>
getBooks().then((books) => {
const prolificAuthorIds = filterProlificAuthors(authors, books);
return Promise.all(prolificAuthorIds.map((id) => getBio(id))).then(
(bios) => {
const result = {
bios,
totalAuthors: authors.length,
totalBooks: books.length,
};
}
);
})
);
متناوباً ، ما می توانیم از نحو تخریب آرایه برای کمک به تصویب استفاده کنیم books
در هر مرحله از طریق زنجیره پایین بروید:
getAuthors().then((authors) =>
getBooks()
.then((books) => [books, filterProlificAuthors(authors, books)])
.then(([books, ids]) =>
Promise.all([books, ...ids.map((id) => getBio(id))])
)
.then(([books, bios]) => {
const result = {
bios,
totalAuthors: authors.length,
totalBooks: books.length,
};
})
);
برای من ، هیچ یک از این راه حل ها به ویژه قابل خواندن نیست. کار کردن در یک نگاه دشوار است – کدام یک از متغیرها در کجا قابل دسترسی هستند.
قسمت 3
به عنوان یک بهینه سازی نهایی ، می توانیم عملکرد محلول خود را بهبود بخشیم و با استفاده از آن کمی تمیز کنیم Promise.all
به طور همزمان نویسندگان و کتاب ها را بیاورید. این به پاکسازی ما کمک می کند then
کمی راه حل:
Promise.all([getAuthors(), getBooks()]).then(([authors, books]) => {
const prolificAuthorIds = filterProlificAuthors(authors, books);
return Promise.all(prolificAuthorIds.map((id) => getBio(id))).then((bios) => {
const result = {
bios,
totalAuthors: authors.length,
totalBooks: books.length,
};
});
});
این ممکن است بهترین باشد then
راه حل دسته. نیاز به چندین سطح لانه سازی را برطرف می کند و کد با سرعت بیشتری اجرا می شود.
با این اوصاف، async/await
ساده تر می ماند:
async function getBios() {
const [authors, books] = await Promise.all([getAuthors(), getBooks()]);
const prolificAuthorIds = filterProlificAuthors(authors, books);
const bios = await Promise.all(prolificAuthorIds.map((id) => getBio(id)));
const result = {
bios,
totalAuthors: authors.length,
totalBooks: books.length,
};
}
هیچ لانه سازی وجود ندارد ، فقط یک سطح تورفتگی وجود دارد و احتمال سردرگمی مبتنی بر براکت بسیار کمتر است!
نتیجه
اغلب ، با استفاده از زنجیر then
روش ها می توانند نیاز به تغییرات جزئی داشته باشند ، به ویژه هنگامی که بخواهیم اطمینان حاصل کنیم که متغیرهای خاص از دامنه استفاده می کنند. حتی برای یک سناریوی ساده مانند سناریوی که ما در مورد آن بحث کردیم ، بهترین راه حل واضح وجود ندارد: هر یک از پنج راه حل با استفاده از then
برای خوانایی معاملات متفاوتی داشت. در مقابل ، async/await
خود را به یک راه حل قابل خواندن پیشنهاد کرد که نیاز به تغییر بسیار کمی داشت ، زیرا نیازهای مشکل ما اصلاح شد.
در برنامه های واقعی ، الزامات کد ناهمزمان ما اغلب پیچیده تر از سناریوی ارائه شده در اینجا است. در حالی که async/await
فهم اساسی برای نوشتن منطق پیچیده تر ، و افزودن بسیاری از موارد ، برای ما فراهم می کند then
این روش ها می توانند ما را به سادگی در مسیر جهنم پاسخگویی مجبور کنند – با بسیاری از پرانتزها و سطح تورفتگی ، مشخص نیست که یک بلوک به کجا پایان می یابد و بلوک بعدی شروع می شود.
به همین دلیل – اگر اختیار دارید – انتخاب کنید async/await
بر فراز then/catch
.
