اندازهی قلم متن
تخمین مدت زمان مطالعهی مطلب:
هفت دقیقه
جاوا اسکریپت به صورت single-thread عمل میکند. به این معنا که دو اسکریپت نمیتوانند به صورت همزمان اجرا شوند و باید یکی پس از دیگری اجرا شوند. سادهترین شکل برنامهنویسی غیرهمزمان در جاوا اسکریپت استفاده از callback میباشد. به عنوان مثال در سناریوی زیر Caller یکسری عملیات غیرهمزمان را مانند یک فراخوانی XHR و یا یک تایمر، انجام میدهد. زمانیکه Caller عملیات غیرهمزمانی را آغاز کرد، یک callback را به آن ارسال خواهد کرد و بعد از مطمئن شدن از موفق بودن عملیات، callback را فراخوانی میکند. بعد از پایان عملیات، callback درون call stack قرار خواهد گرفت و هر وقت که بقیهی عملیات به اتمام رسید، اجرا خواهد شد.
این روش چندین مشکل دارد:
- تنها caller از پایان یافتن عملیات غیرهمزمان مطلع خواهد شد.
- هندل کردن خطا و همچنین مدیریت چندین عملیات asynchronous به صورت همزمان خیلی سخت خواهد بود.
در اینحالت callback باید به چندین کار رسیدگی کند:
- پردازش نتایج فراخوانیهای async
- اجرای دیگر عملیات براساس پاسخ
کد زیر را در نظر بگیرید:
function getCompanyFromOrderId(orderId) { getOrder(orderId, function(order) { getUser(order.userId, function(user) { getCompany(user.companyId, function(company) { // do something with company }); }); }); }
function getCompanyFromOrderId(orderId) { try { getOrder(orderId, function (order) { try { getUser(order.userId, function (user) { try { getCompany(user.companyId, function (company) { try { // do something with company } catch (ex) { // handle exception } }); } catch (ex) { // handle exception } }); } catch (ex) { // handle exception } }); } catch (ex) { // handle exception } }
راهحل: استفاده از Promises
Promises همیشه به عنوان یک راهحل برای callback hell شناخته شده هستند. Promises در واقع اشیایی هستند که این اطمینان را به شما خواهند داد تا بعد از پایان یک عملیات غیرهمزمان، پاسخ را صرفنظر از اینکه عملیات fail و یا success شده باشد، در اختیارتان قرار خواهند داد. یک Promise از دو قسمت تشکیل شده است:
- Control
- Promise
قسمت اول یا Control در بیشتر کتابخانهها با نام Deferred نیز از آن نامبرده میشود و در واقع یک شیء مستقل است. در بعضی از پیادهسازیها این شیء در واقع خودش یک callback است. قسمت دوم نیز خود Promise است. این شیء میتواند دیگر قسمتهای کد را از پایان یافتن عملیات غیرهمزمان مطلع سازد.
یک Promise میتواند یکی از حالتهای زیر را داشته باشد:
- pending: یعنی وضعیت اولیه، هنوز به پایان نرسیده است.
- fulfilled: یعنی عملیات با موفقیت پایان پذیرفته است.
- rejected: یعنی عملیات با شکست مواجه شده است.
با ایجاد یک Promise، وضعیت آن در اولین مرحله و pending خواهد بود. سپس تبدیل به یکی از وضعیتهای fulfilled و یا rejected خواهد شد.
اکنون اگر بخواهیم کد قبلی را با استفاده از Promises پیادهسازی کنیم به این چنین نتیجهایی خواهیم رسید:
function getCompanyFromOrderId(orderId) { getOrder(orderId).then(function(order) { return getUser(order.orderId); }).then(function(user) { return getCompany(user.companyId); }).then(function(company) { // do something with company }).then(undefined, function(error) { // handle error }) }
promises خیلی وقت است که در قالب کتابخانههای third-party مانند Q, when, WinJS, RSVP.js در اختیار برنامهنویسان جاوا اسکریپت قرار دارد. در نتیجه کد فوق به صورت جنریک است؛ به این معنا که با هر کدام از کتابخانههای عنوان شده سازگاری دارد.
نحوهی ایجاد Promise
ساختار اولیه برای ایجاد یک promise به اینصورت است:
var promise = new Promise(function(resolve, reject) { // انجام یکسری عملیات به عنوان مثال دریافت اطلاعات از سرور و... if (/* اگر کدهای فوق با موفقیت انجام شدند */) { resolve("عملیات با موفقیت انجام پذیرفت"); } else { reject(Error("خطایی رخ داده است")); } });
در ادامه نحوهی استفاده از promise فوق را مشاهده میکنید:
promise.then(function(result) { console.log(result); // "عملیات با موفقیت انجام پذیرفت " }, function(err) { console.log(err); // Error: "خطایی رخ داده است" });
به عنوان یک مثال عملی میتوانیم متد get جیکوئری را به این صورت درون یک Promise قرار دهیم:
function get(url){ return new Promise(function(resolve, reject) { $.get(url, function(data) { resolve(data); }) .fail(function(){ reject(); }); }); }
get('users.all').then(function(users){ myController.users = users; }, function(){ delete myController.users; });
get('users.all').then(function(users){ myController.users = users; }) .catch(function(){ delete myController.users; });
در شرایطی ممکن است بخواهیم بعد از اینکه تمامی Promise هایمان کارشان به اتمام رسید، یکسری عملیات دیگر را انجام دهیم:
var usersPromise = get('users.all'); var postsPromise = get('posts.everyone'); Promise.all([usersPromise, postsPromise]) .then(function(result){ myController.users = result[0]; myController.posts = result[1]; }, function(){ delete myController.users; delete myController.posts; });
اگر خروجی then به صورت رشتهایی باشد چه اتفاقی خواهد افتاد؟
در حالت کلی خروجی هر then. به then بعدی پاس داده خواهد شد. به عنوان مثال در کد زیر نتایج به صورت رشتهایی برگردانده خواهند شد و میتوانیم آنها را به سادگی توسط JSON.parse به then بعدی ارسال کنیم:
get('users.all').then(function(usersString){ return JSON.parse(usersString); }).then(function(users){ myController.users = users; });
get('users.all').then(JSON.parse).then(function(users){ myController.users = users; });