وعده ها محاسبات معوق و ناهمزمان را ساده می کنند. یک وعده نشان دهنده عملیاتی است که هنوز کامل نشده است.
توسعه دهندگان، خود را برای یک لحظه مهم در تاریخ توسعه وب آماده کنید.
[درامول شروع می شود]
وعده ها به جاوا اسکریپت رسیدند!
[آتش بازی منفجر می شود، کاغذ پر زرق و برق از بالا می بارد، جمعیت وحشی می شود]
در این مرحله شما در یکی از این دسته ها قرار می گیرید:
- مردم اطراف شما را تشویق می کنند، اما شما مطمئن نیستید که این همه هیاهو برای چیست. شاید شما حتی مطمئن نیستید که "قول" چیست. شانههایت را بالا میاندازی، اما وزن کاغذ براق روی شانههایت سنگینی میکند. اگر چنین است، نگران نباشید، چند سال طول کشید تا بفهمم چرا باید به این چیزها اهمیت بدهم. احتمالاً می خواهید از ابتدا شروع کنید.
- تو هوا را مشت می کنی! نزدیک به زمان درست است؟ قبلاً از این موارد Promise استفاده کردهاید، اما اینکه همه پیادهسازیها API کمی متفاوت دارند، شما را آزار میدهد. API برای نسخه رسمی جاوا اسکریپت چیست؟ احتمالاً می خواهید با اصطلاحات شروع کنید.
- شما قبلاً در مورد این موضوع می دانستید و کسانی را که انگار برای آنها خبری است بالا و پایین می پرند، مسخره می کنید. چند لحظه وقت بگذارید و از برتری خود لذت ببرید، سپس مستقیماً به مرجع API بروید.
پشتیبانی از مرورگر و polyfill
برای رساندن مرورگرهایی که فاقد اجرای کامل وعدهها هستند، مطابق با مشخصات هستند، یا وعدههایی را به مرورگرهای دیگر و Node.js اضافه کنید، پلیفیل (2kgzipped) را بررسی کنید.
این همه هیاهو برای چیست؟
جاوا اسکریپت تک رشته ای است، به این معنی که دو بیت اسکریپت نمی توانند همزمان اجرا شوند. آنها باید یکی پس از دیگری بدویند. در مرورگرها، جاوا اسکریپت یک رشته را با تعداد زیادی موارد دیگر به اشتراک می گذارد که از مرورگر به مرورگر متفاوت است. اما معمولاً جاوا اسکریپت در همان صف نقاشی، بهروزرسانی سبکها و مدیریت اقدامات کاربر (مانند برجسته کردن متن و تعامل با کنترلهای فرم) قرار دارد. فعالیت در یکی از این موارد باعث به تعویق انداختن موارد دیگر می شود.
به عنوان یک انسان، شما چند رشته ای هستید. میتوانید با چند انگشت تایپ کنید، میتوانید رانندگی کنید و مکالمهای را همزمان انجام دهید. تنها عملکرد مسدود کننده ای که باید با آن مقابله کنیم عطسه است، جایی که تمام فعالیت های فعلی باید در طول مدت عطسه به حالت تعلیق درآید. این بسیار آزاردهنده است، به خصوص زمانی که در حال رانندگی هستید و سعی می کنید مکالمه ای داشته باشید. شما نمی خواهید کدی بنویسید که خنده دار باشد.
احتمالاً از رویدادها و تماسهای تلفنی برای دور زدن این موضوع استفاده کردهاید. در اینجا رویدادها وجود دارد:
var img1 = document.querySelector('.img-1');
img1.addEventListener('load', function() {
// woo yey image loaded
});
img1.addEventListener('error', function() {
// argh everything's broken
});
این اصلا عطسه نیست تصویر را دریافت می کنیم، چند شنونده اضافه می کنیم، سپس جاوا اسکریپت می تواند اجرا را متوقف کند تا زمانی که یکی از آن شنوندگان فراخوانی شود.
متأسفانه، در مثال بالا، این امکان وجود دارد که رویدادها قبل از شروع به گوش دادن به آنها اتفاق افتاده باشند، بنابراین باید با استفاده از ویژگی "کامل" تصاویر، روی آن کار کنیم:
var img1 = document.querySelector('.img-1');
function loaded() {
// woo yey image loaded
}
if (img1.complete) {
loaded();
}
else {
img1.addEventListener('load', loaded);
}
img1.addEventListener('error', function() {
// argh everything's broken
});
این تصاویری را که قبل از اینکه فرصتی برای گوش دادن به آنها داشته باشیم، خطا کردهاند، نمیگیرد. متأسفانه DOM راهی برای انجام این کار به ما نمی دهد. همچنین، این در حال بارگذاری یک تصویر است. اگر بخواهیم بدانیم چه زمانی مجموعه ای از تصاویر بارگذاری شده اند، همه چیز پیچیده تر می شود.
رویدادها همیشه بهترین راه نیستند
رویدادها برای چیزهایی که میتوانند چندین بار روی یک شی اتفاق بیفتند بسیار عالی هستند - keyup
، touchstart
و غیره. اما وقتی نوبت به موفقیت/شکست همگامسازی میشود، در حالت ایدهآل شما چیزی شبیه به این میخواهید:
img1.callThisIfLoadedOrWhenLoaded(function() {
// loaded
}).orIfFailedCallThis(function() {
// failed
});
// and…
whenAllTheseHaveLoaded([img1, img2]).callThis(function() {
// all loaded
}).orIfSomeFailedCallThis(function() {
// one or more failed
});
این همان چیزی است که وعده ها انجام می دهند، اما با نام گذاری بهتر. اگر عناصر تصویر HTML دارای یک متد "آماده" بود که یک وعده را برمی گرداند، می توانیم این کار را انجام دهیم:
img1.ready()
.then(function() {
// loaded
}, function() {
// failed
});
// and…
Promise.all([img1.ready(), img2.ready()])
.then(function() {
// all loaded
}, function() {
// one or more failed
});
در ابتدایی ترین حالت، وعده ها کمی شبیه شنوندگان رویداد هستند به جز:
- یک وعده فقط یک بار می تواند موفق شود یا شکست بخورد. نمی تواند دو بار موفق شود یا شکست بخورد، همچنین نمی تواند از موفقیت به شکست یا برعکس تغییر کند.
- اگر یک وعده موفق یا شکست خورده باشد و بعداً یک تماس برگشتی موفقیت آمیز/شکست اضافه کنید، حتی اگر رویداد زودتر اتفاق افتاده باشد، تماس برگشتی صحیح فراخوانی می شود.
این برای موفقیت/شکست همگامسازی بسیار مفید است، زیرا شما کمتر به زمان دقیقی که چیزی در دسترس قرار میگیرد علاقهمند هستید و بیشتر علاقهمند به واکنش به نتیجه هستید.
اصطلاحات قول
اثبات دومنیک دنیکولا اولین پیش نویس این مقاله را خواند و از نظر اصطلاحات به من درجه "F" داد. او مرا بازداشت کرد، مجبورم کرد 100 بار از ایالت ها و سرنوشت ها کپی کنم و نامه ای نگران به والدینم نوشت. با وجود این، من هنوز بسیاری از اصطلاحات را با هم مخلوط می کنم، اما در اینجا اصول اولیه وجود دارد:
یک قول می تواند این باشد:
- محقق شد - عمل مربوط به قول موفق شد
- رد شد - عمل مربوط به قول شکست خورد
- در انتظار - هنوز انجام نشده یا رد نشده است
- حل و فصل - برآورده یا رد کرده است
این مشخصات همچنین از عبارت thenable برای توصیف شیای استفاده میکند که شبیه به وعده است، از این نظر که دارای یک متد then
است. این اصطلاح من را به یاد تری ونبلز، مدیر سابق فوتبال انگلیس می اندازد، بنابراین تا حد امکان کمتر از آن استفاده خواهم کرد.
وعده ها در جاوا اسکریپت می رسند!
مدتی است که وعده ها در قالب کتابخانه وجود داشته است، مانند:
وعده های فوق و جاوا اسکریپت یک رفتار استاندارد و مشترک به نام Promises/A+ دارند. اگر کاربر jQuery هستید، آنها چیزی شبیه به Deferreds دارند. با این حال، Deferred ها با Promise/A+ سازگار نیستند، که باعث می شود به طور ماهرانه ای متفاوت و کمتر مفید باشند، بنابراین مراقب باشید. jQuery یک نوع Promise نیز دارد، اما این فقط زیرمجموعه Deferred است و همان مشکلات را دارد.
اگرچه پیاده سازی های وعده از یک رفتار استاندارد پیروی می کنند، API های کلی آنها متفاوت است. وعده های جاوا اسکریپت در API مشابه RSVP.js هستند. در اینجا نحوه ایجاد یک قول آمده است:
var promise = new Promise(function(resolve, reject) {
// do a thing, possibly async, then…
if (/* everything turned out fine */) {
resolve("Stuff worked!");
}
else {
reject(Error("It broke"));
}
});
سازنده وعده یک آرگومان می گیرد، یک callback با دو پارامتر، حل و رد. کاری را در پاسخ به تماس انجام دهید، شاید همگامسازی شود، سپس اگر همه چیز جواب داد، حل را فراخوانی کنید، در غیر این صورت رد تماس بگیرید.
مانند throw
در جاوا اسکریپت قدیمی، مرسوم است، اما لازم نیست، با یک شی Error رد شود. مزیت اشیاء خطا این است که یک ردیابی پشته را ضبط می کنند و ابزارهای اشکال زدایی را مفیدتر می کنند.
در اینجا نحوه استفاده از این قول آمده است:
promise.then(function(result) {
console.log(result); // "Stuff worked!"
}, function(err) {
console.log(err); // Error: "It broke"
});
then()
دو آرگومان می گیرد، یک callback برای یک مورد موفقیت و دیگری برای مورد شکست. هر دو اختیاری هستند، بنابراین میتوانید فقط برای مورد موفقیت یا شکست، یک تماس برگشتی اضافه کنید.
وعده های جاوا اسکریپت در DOM به عنوان "آینده" شروع شد، به "Promises" تغییر نام داد و در نهایت به جاوا اسکریپت منتقل شد. وجود آنها در جاوا اسکریپت به جای DOM بسیار عالی است زیرا در زمینه های JS غیر مرورگر مانند Node.js در دسترس خواهند بود (این که آیا آنها از آنها در API های اصلی خود استفاده می کنند یک سوال دیگر است).
اگرچه آنها یک ویژگی جاوا اسکریپت هستند، DOM از استفاده از آنها ترسی ندارد. در واقع، همه APIهای DOM جدید با روشهای موفقیت/شکست همگامسازی، از وعدهها استفاده میکنند. این در حال حاضر با Quota Management ، Font Load Events ، ServiceWorker ، Web MIDI ، Streams و موارد دیگر اتفاق می افتد.
سازگاری با سایر کتابخانه ها
جاوا اسکریپت قول میدهد که API هر چیزی را با متد then()
بهعنوان وعدهای (یا thenable
در آهصدا ) در نظر میگیرد، بنابراین اگر از کتابخانهای استفاده میکنید که یک وعده Q را برمیگرداند، خوب است، با روش جدید خوب بازی میکند. جاوا اسکریپت وعده می دهد.
اگرچه، همانطور که اشاره کردم، Deferred های جی کوئری کمی ... مفید نیستند. خوشبختانه میتوانید آنها را به وعدههای استاندارد بسپارید، که ارزش دارد در اسرع وقت انجام شود:
var jsPromise = Promise.resolve($.ajax('/whatever.json'))
در اینجا، $.ajax
jQuery یک Deferred را برمیگرداند. از آنجایی که دارای متد then()
است، Promise.resolve()
می تواند آن را به یک وعده جاوا اسکریپت تبدیل کند. با این حال، گاهی اوقات معوقها چندین آرگومان را به تماسهای خود ارسال میکنند، برای مثال:
var jqDeferred = $.ajax('/whatever.json');
jqDeferred.then(function(response, statusText, xhrObj) {
// ...
}, function(xhrObj, textStatus, err) {
// ...
})
در حالی که وعده های JS همه چیز را نادیده می گیرد به جز اولین:
jsPromise.then(function(response) {
// ...
}, function(xhrObj) {
// ...
})
خوشبختانه این همان چیزی است که شما می خواهید، یا حداقل به شما امکان می دهد به آنچه می خواهید دسترسی داشته باشید. همچنین، توجه داشته باشید که jQuery از قرارداد انتقال اشیاء خطا به رد کردن پیروی نمی کند.
کد ناهمگام پیچیده آسان تر شده است
درست است، اجازه دهید برخی چیزها را کدگذاری کنیم. بگوییم می خواهیم:
- یک اسپینر را برای نشان دادن بارگیری شروع کنید
- مقداری JSON برای یک داستان، که عنوان و آدرس هر فصل را به ما می دهد، واکشی کنید
- عنوان را به صفحه اضافه کنید
- هر فصل را واکشی کنید
- داستان را به صفحه اضافه کنید
- اسپینر را متوقف کنید
… اما همچنین به کاربر اطلاع دهید که آیا مشکلی در طول مسیر رخ داده است. ما میخواهیم اسپینر را در آن نقطه نیز متوقف کنیم، در غیر این صورت به چرخش ادامه میدهد، سرگیجه میگیرد و با یک رابط کاربری دیگر تصادف میکند.
البته، از جاوا اسکریپت برای ارائه داستان استفاده نمیکنید، زیرا ارائه به عنوان HTML سریعتر است ، اما این الگو در هنگام برخورد با APIها بسیار رایج است: واکشی دادههای متعدد، سپس وقتی همه چیز تمام شد، کاری انجام دهید.
برای شروع، بیایید به واکشی داده ها از شبکه بپردازیم:
نویدبخش XMLHttpRequest
در صورت امکان، APIهای قدیمی برای استفاده از وعدهها بهروزرسانی خواهند شد. XMLHttpRequest
یک کاندید اصلی است، اما در این مدت اجازه دهید یک تابع ساده برای ایجاد یک درخواست GET بنویسیم:
function get(url) {
// Return a new promise.
return new Promise(function(resolve, reject) {
// Do the usual XHR stuff
var req = new XMLHttpRequest();
req.open('GET', url);
req.onload = function() {
// This is called even on 404 etc
// so check the status
if (req.status == 200) {
// Resolve the promise with the response text
resolve(req.response);
}
else {
// Otherwise reject with the status text
// which will hopefully be a meaningful error
reject(Error(req.statusText));
}
};
// Handle network errors
req.onerror = function() {
reject(Error("Network Error"));
};
// Make the request
req.send();
});
}
حالا بیایید از آن استفاده کنیم:
get('story.json').then(function(response) {
console.log("Success!", response);
}, function(error) {
console.error("Failed!", error);
})
اکنون میتوانیم درخواستهای HTTP را بدون تایپ دستی XMLHttpRequest
انجام دهیم، که فوقالعاده است، زیرا هرچه کمتر مجبور باشم پوشش خشمگین XMLHttpRequest
را ببینم، زندگی من شادتر خواهد بود.
زنجیر زدن
then()
پایان داستان نیست، شما می توانید then
s را به هم متصل کنید تا مقادیر را تغییر دهید یا اقدامات async اضافی را یکی پس از دیگری اجرا کنید.
تبدیل ارزش ها
شما می توانید مقادیر را به سادگی با برگرداندن مقدار جدید تبدیل کنید:
var promise = new Promise(function(resolve, reject) {
resolve(1);
});
promise.then(function(val) {
console.log(val); // 1
return val + 2;
}).then(function(val) {
console.log(val); // 3
})
به عنوان یک مثال عملی، اجازه دهید برگردیم به:
get('story.json').then(function(response) {
console.log("Success!", response);
})
پاسخ JSON است، اما در حال حاضر آن را به صورت متن ساده دریافت می کنیم. ما میتوانیم تابع get خود را برای استفاده از JSON responseType
تغییر دهیم، اما همچنین میتوانیم آن را در سرزمین وعدهها حل کنیم:
get('story.json').then(function(response) {
return JSON.parse(response);
}).then(function(response) {
console.log("Yey JSON!", response);
})
از آنجایی که JSON.parse()
یک آرگومان واحد می گیرد و یک مقدار تبدیل شده را برمی گرداند، می توانیم یک میانبر ایجاد کنیم:
get('story.json').then(JSON.parse).then(function(response) {
console.log("Yey JSON!", response);
})
در واقع، ما میتوانیم تابع getJSON()
را به راحتی بسازیم:
function getJSON(url) {
return get(url).then(JSON.parse);
}
getJSON()
همچنان یک وعده را برمیگرداند، قولی که یک URL واکشی میکند و سپس پاسخ را بهعنوان JSON تجزیه میکند.
صف اقدامات ناهمزمان
همچنین میتوانید زنجیرهای then
تا اقدامات همگامسازی را به ترتیب انجام دهید.
وقتی چیزی را از یک callback then()
برمی گردانید، کمی جادو است. اگر مقداری را برگردانید، then()
بعدی با آن مقدار فراخوانی می شود. با این حال، اگر چیزی شبیه به وعده را برگردانید، next then()
روی آن منتظر میماند و تنها زمانی فراخوانی میشود که آن وعده حل شود (موفق/شکست خورد). به عنوان مثال:
getJSON('story.json').then(function(story) {
return getJSON(story.chapterUrls[0]);
}).then(function(chapter1) {
console.log("Got chapter 1!", chapter1);
})
در اینجا ما یک درخواست async به story.json
میکنیم، که مجموعهای از URLها را برای درخواست به ما میدهد، سپس اولین مورد را درخواست میکنیم. این زمانی است که وعدهها واقعاً از الگوهای پاسخ به تماس ساده متمایز میشوند.
حتی می توانید یک روش میانبر برای دریافت فصل ایجاد کنید:
var storyPromise;
function getChapter(i) {
storyPromise = storyPromise || getJSON('story.json');
return storyPromise.then(function(story) {
return getJSON(story.chapterUrls[i]);
})
}
// and using it is simple:
getChapter(0).then(function(chapter) {
console.log(chapter);
return getChapter(1);
}).then(function(chapter) {
console.log(chapter);
})
تا زمانی که getChapter
فراخوانی نشود story.json
دانلود نمیکنیم، اما دفعه بعدی که getChapter
نامیده میشود، از قول داستان دوباره استفاده میکنیم، بنابراین story.json
فقط یک بار واکشی میشود. ای قول!
رسیدگی به خطا
همانطور که قبلاً دیدیم، then()
دو آرگومان میگیرد، یکی برای موفقیت، یکی برای شکست (یا برآورده کردن و رد کردن، در وعدهها-speak):
get('story.json').then(function(response) {
console.log("Success!", response);
}, function(error) {
console.log("Failed!", error);
})
همچنین می توانید از catch()
استفاده کنید:
get('story.json').then(function(response) {
console.log("Success!", response);
}).catch(function(error) {
console.log("Failed!", error);
})
چیز خاصی در مورد catch()
وجود ندارد، فقط شکر برای then(undefined, func)
، اما خواناتر است. توجه داشته باشید که دو مثال کد بالا رفتار یکسانی ندارند، دومی معادل است با:
get('story.json').then(function(response) {
console.log("Success!", response);
}).then(undefined, function(error) {
console.log("Failed!", error);
})
تفاوت ظریف است، اما بسیار مفید است. ردهای Promise با یک callback rejection (یا catch()
به then()
به جلو پرش میشوند، زیرا معادل است. با then(func1, func2)
، func1
یا func2
نامیده می شود، هرگز هر دو. اما با then(func1).catch(func2)
هر دو در صورت رد کردن func1
فراخوانی میشوند، زیرا مراحل جداگانهای در زنجیره هستند. موارد زیر را در نظر بگیرید:
asyncThing1().then(function() {
return asyncThing2();
}).then(function() {
return asyncThing3();
}).catch(function(err) {
return asyncRecovery1();
}).then(function() {
return asyncThing4();
}, function(err) {
return asyncRecovery2();
}).catch(function(err) {
console.log("Don't worry about it");
}).then(function() {
console.log("All done!");
})
جریان بالا بسیار شبیه به try/catch معمولی جاوا اسکریپت است، خطاهایی که در یک "try" رخ می دهند بلافاصله به بلوک catch()
می روند. در اینجا موارد بالا به عنوان فلوچارت آمده است (چون من عاشق فلوچارت هستم):
خطوط آبی را برای وعده هایی که محقق می شوند، یا قرمز را برای وعده هایی که رد می کنند دنبال کنید.
استثناها و وعده های جاوا اسکریپت
ردها زمانی اتفاق میافتند که یک وعده به طور صریح رد شود، اما همچنین به طور ضمنی اگر خطایی در پاسخ سازنده ایجاد شود:
var jsonPromise = new Promise(function(resolve, reject) {
// JSON.parse throws an error if you feed it some
// invalid JSON, so this implicitly rejects:
resolve(JSON.parse("This ain't JSON"));
});
jsonPromise.then(function(data) {
// This never happens:
console.log("It worked!", data);
}).catch(function(err) {
// Instead, this happens:
console.log("It failed!", err);
})
این بدان معنی است که انجام تمام کارهای مربوط به وعده خود در داخل callback سازنده وعده مفید است، بنابراین خطاها به طور خودکار شناسایی می شوند و به رد تبدیل می شوند.
همین امر در مورد خطاهایی که در callbackهای then()
پرتاب می شوند نیز صدق می کند.
get('/').then(JSON.parse).then(function() {
// This never happens, '/' is an HTML page, not JSON
// so JSON.parse throws
console.log("It worked!", data);
}).catch(function(err) {
// Instead, this happens:
console.log("It failed!", err);
})
رسیدگی به خطا در عمل
با داستان و فصلهایمان، میتوانیم از catch برای نمایش یک خطا به کاربر استفاده کنیم:
getJSON('story.json').then(function(story) {
return getJSON(story.chapterUrls[0]);
}).then(function(chapter1) {
addHtmlToPage(chapter1.html);
}).catch(function() {
addTextToPage("Failed to show chapter");
}).then(function() {
document.querySelector('.spinner').style.display = 'none';
})
اگر واکشی story.chapterUrls[0]
ناموفق باشد (مثلاً http 500 یا کاربر آفلاین باشد)، از همه تماسهای موفقیتآمیز بعدی که شامل پاسخی در getJSON()
میشود که سعی میکند پاسخ را بهعنوان JSON تجزیه کند و همچنین از پاسخ تماس که فصل 1.html را به صفحه اضافه می کند. درعوض به Catch Callback حرکت می کند. در نتیجه، در صورت عدم موفقیت هر یک از اقدامات قبلی، «نمایش فصل انجام نشد» به صفحه اضافه میشود.
مانند try/catch جاوا اسکریپت، خطا مشاهده میشود و کد بعدی ادامه مییابد، بنابراین اسپینر همیشه پنهان است، چیزی که ما میخواهیم. نسخه فوق تبدیل به یک نسخه همگام غیر مسدود کننده می شود:
try {
var story = getJSONSync('story.json');
var chapter1 = getJSONSync(story.chapterUrls[0]);
addHtmlToPage(chapter1.html);
}
catch (e) {
addTextToPage("Failed to show chapter");
}
document.querySelector('.spinner').style.display = 'none'
ممکن است بخواهید که catch()
صرفاً برای اهداف ورود به سیستم بدون بازیابی خطا انجام دهید. برای انجام این کار، فقط خطا را دوباره پر کنید. ما می توانیم این کار را در متد getJSON()
خود انجام دهیم:
function getJSON(url) {
return get(url).then(JSON.parse).catch(function(err) {
console.log("getJSON failed for", url, err);
throw err;
});
}
بنابراین ما موفق شده ایم یک فصل را واکشی کنیم، اما همه آنها را می خواهیم. بیایید این اتفاق بیفتد.
موازی سازی و توالی: به دست آوردن بهترین هر دو
ناهمگام فکر کردن آسان نیست. اگر در تلاش برای خارج شدن از علامت هستید، سعی کنید کد را طوری بنویسید که گویی همزمان است. در این مورد:
try {
var story = getJSONSync('story.json');
addHtmlToPage(story.heading);
story.chapterUrls.forEach(function(chapterUrl) {
var chapter = getJSONSync(chapterUrl);
addHtmlToPage(chapter.html);
});
addTextToPage("All done");
}
catch (err) {
addTextToPage("Argh, broken: " + err.message);
}
document.querySelector('.spinner').style.display = 'none'
که کار می کند! اما در حین بارگیری همه چیز، همگامسازی میشود و مرورگر را قفل میکند. برای غیر همگام کردن این کار، then()
استفاده می کنیم تا اتفاقات یکی پس از دیگری رخ دهد.
getJSON('story.json').then(function(story) {
addHtmlToPage(story.heading);
// TODO: for each url in story.chapterUrls, fetch & display
}).then(function() {
// And we're all done!
addTextToPage("All done");
}).catch(function(err) {
// Catch any error that happened along the way
addTextToPage("Argh, broken: " + err.message);
}).then(function() {
// Always hide the spinner
document.querySelector('.spinner').style.display = 'none';
})
اما چگونه میتوانیم آدرسهای اینترنتی فصل را مرور کنیم و آنها را به ترتیب واکشی کنیم؟ این کار نمی کند :
story.chapterUrls.forEach(function(chapterUrl) {
// Fetch chapter
getJSON(chapterUrl).then(function(chapter) {
// and add it to the page
addHtmlToPage(chapter.html);
});
})
forEach
ناهمگامآگاه نیست، بنابراین فصلهای ما به هر ترتیبی که دانلود میشوند ظاهر میشوند، که اساساً به این صورت است که Pulp Fiction نوشته شده است. این پالپ فیکشن نیست، پس بیایید درستش کنیم.
ایجاد یک دنباله
ما می خواهیم آرایه chapterUrls
خود را به دنباله ای از وعده ها تبدیل کنیم. ما می توانیم این کار را با استفاده از then()
انجام دهیم:
// Start off with a promise that always resolves
var sequence = Promise.resolve();
// Loop through our chapter urls
story.chapterUrls.forEach(function(chapterUrl) {
// Add these actions to the end of the sequence
sequence = sequence.then(function() {
return getJSON(chapterUrl);
}).then(function(chapter) {
addHtmlToPage(chapter.html);
});
})
این اولین باری است که Promise.resolve()
میبینیم، که یک وعده ایجاد میکند که به هر مقداری که به آن بدهید، حل میشود. اگر آن را به عنوان نمونه ای از Promise
ارسال کنید، به سادگی آن را برمی گرداند ( توجه داشته باشید: این تغییری در مشخصاتی است که برخی از پیاده سازی ها هنوز از آن پیروی نمی کنند). اگر چیزی شبیه به وعده را به آن بفرستید (یک متد then()
دارد)، یک Promise
واقعی ایجاد میکند که به همان شیوه اجرا/رد میشود. اگر مقدار دیگری را پاس کنید، به عنوان مثال، Promise.resolve('Hello')
, وعده ای ایجاد می کند که با آن مقدار محقق می شود. اگر آن را بدون مقدار صدا کنید، همانطور که در بالا ذکر شد، با "تعریف نشده" تکمیل می شود.
همچنین Promise.reject(val)
وجود دارد که قولی را ایجاد می کند که با مقداری که شما به آن می دهید (یا تعریف نشده) رد می شود.
ما می توانیم کد بالا را با استفاده از array.reduce
مرتب کنیم:
// Loop through our chapter urls
story.chapterUrls.reduce(function(sequence, chapterUrl) {
// Add these actions to the end of the sequence
return sequence.then(function() {
return getJSON(chapterUrl);
}).then(function(chapter) {
addHtmlToPage(chapter.html);
});
}, Promise.resolve())
این کار مانند مثال قبلی انجام می دهد، اما به متغیر "sequence" جداگانه نیاز ندارد. کاهش تماس ما برای هر آیتم در آرایه فراخوانی می شود. "sequence" برای اولین بار Promise.resolve()
است، اما برای بقیه فراخوان ها "sequence" همان چیزی است که از فراخوانی قبلی برگردانده ایم. array.reduce
واقعاً برای جوشاندن یک آرایه به یک مقدار مفید است که در این مورد یک وعده است.
بیایید همه را کنار هم بگذاریم:
getJSON('story.json').then(function(story) {
addHtmlToPage(story.heading);
return story.chapterUrls.reduce(function(sequence, chapterUrl) {
// Once the last chapter's promise is done…
return sequence.then(function() {
// …fetch the next chapter
return getJSON(chapterUrl);
}).then(function(chapter) {
// and add it to the page
addHtmlToPage(chapter.html);
});
}, Promise.resolve());
}).then(function() {
// And we're all done!
addTextToPage("All done");
}).catch(function(err) {
// Catch any error that happened along the way
addTextToPage("Argh, broken: " + err.message);
}).then(function() {
// Always hide the spinner
document.querySelector('.spinner').style.display = 'none';
})
و ما آن را داریم، یک نسخه کاملا ناهمگام از نسخه همگام. اما ما می توانیم بهتر عمل کنیم. در حال حاضر صفحه ما به این صورت در حال دانلود است:
مرورگرها در بارگیری چندین چیز در یک زمان بسیار خوب هستند، بنابراین ما با دانلود فصلها یکی پس از دیگری عملکرد خود را از دست میدهیم. کاری که میخواهیم انجام دهیم این است که همه آنها را همزمان دانلود کنیم، سپس زمانی که همه وارد شدند، آنها را پردازش کنیم. خوشبختانه یک API برای این وجود دارد:
Promise.all(arrayOfPromises).then(function(arrayOfResults) {
//...
})
Promise.all
مجموعهای از وعدهها را میگیرد و وعدهای ایجاد میکند که وقتی همه آنها با موفقیت تکمیل شوند، محقق میشود. شما مجموعهای از نتایج را (هر وعدههایی که به آن عمل کردهاید) به همان ترتیبی که وعدههایی را قبول کردهاید، دریافت میکنید.
getJSON('story.json').then(function(story) {
addHtmlToPage(story.heading);
// Take an array of promises and wait on them all
return Promise.all(
// Map our array of chapter urls to
// an array of chapter json promises
story.chapterUrls.map(getJSON)
);
}).then(function(chapters) {
// Now we have the chapters jsons in order! Loop through…
chapters.forEach(function(chapter) {
// …and add to the page
addHtmlToPage(chapter.html);
});
addTextToPage("All done");
}).catch(function(err) {
// catch any error that happened so far
addTextToPage("Argh, broken: " + err.message);
}).then(function() {
document.querySelector('.spinner').style.display = 'none';
})
بسته به اتصال، این می تواند چند ثانیه سریعتر از بارگیری یک به یک باشد، و کد کمتری نسبت به اولین تلاش ما دارد. فصل ها می توانند به هر ترتیبی دانلود شوند، اما به ترتیب درست روی صفحه نمایش داده می شوند.
با این حال، ما هنوز هم می توانیم عملکرد درک شده را بهبود بخشیم. وقتی فصل اول رسید، باید آن را به صفحه اضافه کنیم. این به کاربر امکان می دهد قبل از رسیدن بقیه فصل ها شروع به خواندن کند. وقتی فصل سوم می رسد، آن را به صفحه اضافه نمی کنیم زیرا کاربر ممکن است متوجه نباشد که فصل دوم وجود ندارد. وقتی فصل دو رسید، می توانیم فصل های دو و سه و غیره را اضافه کنیم.
برای انجام این کار، ما JSON را برای همه فصلهای خود به طور همزمان واکشی میکنیم، سپس یک دنباله ایجاد میکنیم تا آنها را به سند اضافه کنیم:
getJSON('story.json')
.then(function(story) {
addHtmlToPage(story.heading);
// Map our array of chapter urls to
// an array of chapter json promises.
// This makes sure they all download in parallel.
return story.chapterUrls.map(getJSON)
.reduce(function(sequence, chapterPromise) {
// Use reduce to chain the promises together,
// adding content to the page for each chapter
return sequence
.then(function() {
// Wait for everything in the sequence so far,
// then wait for this chapter to arrive.
return chapterPromise;
}).then(function(chapter) {
addHtmlToPage(chapter.html);
});
}, Promise.resolve());
}).then(function() {
addTextToPage("All done");
}).catch(function(err) {
// catch any error that happened along the way
addTextToPage("Argh, broken: " + err.message);
}).then(function() {
document.querySelector('.spinner').style.display = 'none';
})
و ما به آنجا می رویم، بهترین از هر دو! تحویل همه محتوا به همان میزان زمان نیاز دارد، اما کاربر اولین بیت از محتوا را زودتر دریافت می کند.
در این مثال بی اهمیت، همه فصل ها تقریباً در یک زمان می رسند، اما مزیت نمایش یکی در یک زمان با فصل های بیشتر و بزرگتر اغراق آمیز خواهد بود.
انجام موارد بالا با تماسها یا رویدادهای به سبک Node.js تقریباً دو برابر کد است، اما مهمتر از آن به آسانی دنبال نمیشود. با این حال، این پایان داستان برای وعدهها نیست، وقتی با سایر ویژگیهای ES6 ترکیب شوند، حتی سادهتر میشوند.
دور جایزه: قابلیت های گسترش یافته
از زمانی که من در ابتدا این مقاله را نوشتم، توانایی استفاده از Promises بسیار گسترش یافته است. از زمان کروم 55، توابع غیر همگام به کدهای مبتنی بر وعده اجازه میدهند که به صورت همزمان نوشته شوند، اما بدون مسدود کردن رشته اصلی. می توانید در مقاله توابع async من در مورد آن بیشتر بخوانید. پشتیبانی گسترده ای از عملکردهای Promises و async در مرورگرهای اصلی وجود دارد. شما می توانید جزئیات را در مرجع MDN's Promise and async تابع بیابید.
با تشکر فراوان از آن ون کسترن، دومنیک دنیکولا، تام اشورث، رمی شارپ، آدی عثمانی، آرتور ایوانز و یوتاکا هیرانو که این را تصحیح کردند و اصلاحات/توصیه هایی ارائه کردند.
همچنین از Mathias Bynens برای به روز رسانی بخش های مختلف مقاله تشکر می کنم.