قبل از این که توابع Generator رو توضیح بدم، می خوام یکم مقدمه بگم که ممکنه برای بعضی ها خسته کننده باشه، اما از اونجا که بعضی از برنامه نویس ها این ها رو نمی دونن، می خوام توضیح بدم. در صورتی که می خواید مستقیم وارد مبحث بشید می تونید از مقدمه صرف نظر کنید.
توی جاواسکریپت، در حالت عادی توابع خط به خط اجرا می شن تا این که یکی از حالت های زیر اتفاق بیافته:
1- رسیدن به کلمه کلیدی return: توی این حالت، مقداری که return شده به کسی که تابع رو صدا زده برگشت داده می شه. مثلا کد زیر رو ببینید:
function x() { return 1; } function y() { return x(); } y();
بذارید مراحل اجرا رو یه دور با هم مرور کنیم. می دونیم که روی تعریف تابع، تا وقتی که فراخونی نشه هیچ اجرایی صورت نمی گیره، برای همین اولی خطی که اجرا می شه، فراخونی تابع y خواهد بود:
y();
وقتی مرورگر به این خط می رسه، می دونه که برای اجرای این خط باید بره جایی که تابع y تعریف شده رو پیدا کنه و تابع y رو از خط اول اجرا کنه. ولی قبل از اون باید محلی که الان توش قرار داره رو یه جا برای خودش نگه داره تا بعد از اجرای تابع y، دوباره به همینجا برگرده. بهترین روش برای نگهداری محل آخرین اجرا، stack هست. پس اگه نخوایم وارد پیچیدگی های دنیای واقعی بشیم، می تونیم بگیم که مرورگر عدد 12 (شماره خطی که الان داره اجرا می کنه) رو توی stack ذخیره می کنه. پس الان stack به این صورته:
stack: [12]
و کدهای زیر اجرا خواهد شد:
function y() { return x(); }
توی بدنه تابع y تنها یه خط وجود داره، فراخونی تابع x. پس باید دوباره محل تعریف تابع x پیدا بشه و بدنه اون اجرا بشه. اما قبل از این که مرورگر تابع x رو اجرا کنه، باز هم محلی که الان توش قرار داره رو برای خودش توی stack ذخیره می کنه. با این حساب، stack به صورت زیر در میاد:
stack: [12, 8]
و کدهای زیر اجرا می شه:
function x() { return 1; }
توی تابع x فقط یک کار برای انجام هست، برگشت دادن عدد 1. پس مرورگر عدد 1 رو به جایی که تابع x رو صدا زده بر می گردونه. برای این کار به stack مراجعه می کنه و آخرین مقدار رو ازش برداشت می کنه. آخرین مقدار توی stack عدد 8 هست، پس stack به این صورت در میاد:
stack: [12]
و مرورگر به خط 8 می ره. خط 8 مربوط به فراخونی تابع x و return کردن مقدار برگشت داده شده از تابع x بود. الان مرورگر مقدار برگشت داده شده از تابع x رو می دونه. برای همین هم در واقع کد زیر اجرا می شه:
return 1; // return x()
چون باز هم به کلمه کلیدی return رسیدیم، مرورگر به stack مراجعه می کنه و عدد 12 رو ازش بر می داره. حالا دیگه stack خالی شده:
stack: []
وقتی stack خالیه یعنی دیگه به بالاترین سطح رسیدیم. چون عدد 12 از stack برداشته شده، مرورگر به سراغ اجرای خط 12 می ره. خط 12 چیزی جز فراخونی تابع y نبود. از اونجا که تابع y مقدار 1 رو برگردونده، خط به این صورت اجرا می شه:
1; // y()
2- به کلمه کلیدی throw برسه: توی این حالت، مقداری که throw شده، به کسی که تابع رو صدا زده برگشت داده می شه، وقتی صدا زننده تابع متوجه بروز خطا می شه، در صورتی که فراخونی تابع داخل try...catch باشه، از ادامه اجرای try منصرف می شه و کدهای داخل catch رو اجرا می کنه. اما اگه توی try...catch نباشه، یه مرحله به عقبتر بر می گرده و می ره سراغ کسی که اون تابع رو صدا زده. اینقدر این کار انجام می شه که یا به try...catch برسه، یا این که به بالاترین سطح برسه (stack خالی بشه). اگه به بالاترین سطح برسه و try..catch دیده نشه، یه خطا توی console نمایش داده می شه و برنامه از کار می افته.
3- به آخر تابع برسه: آخرین راه پایان یک تابع، رسیدن به انتهای تابعه. در این صورت باز هم کنترل برنامه به کسی که تابع رو صدا زده برگشت داده می شه، با این تفاوتی که هیچ مقداری برگشت داده نشده (یا مقدار برگشت داده شده undefined هست).
تابع زیر رو ببینید:
function search(needle, haystack) { let results = []; for(let i=0; i<haystack.length; i++) { if(needle === haystack[i]) { results.push(i); } } return results; }
ورودی اول تابع search مقداریه که دنبالش می گردیم و ورودی دوم یک آرایست. تابع search دونه دونه مقادیر داخل haystack رو با needle مقایسه می کنه و در صورتی که برابر باشن، به آرایه result اضافه می کنه. در آخر تابع result برگشت داده می شه. خیلی هم عالی!
اما تابع search توی مقیاس بزرگ دو تا اشکال داره:
1- باید تابع به طور کامل اجرا بشه تا به نتیجه جستجو دسترسی داشته باشیم.
2- مجبور شدیم آرایه ای به نام results تعریف کنیم و نتیجه جستجو رو توی اون بریزیم. اگه تعداد جوابهای جستجو زیاد باشه، آرایه results خیلی حجیم می شه و مموری زیادی مصرف می شه.
خدا رو شکر توی ES6 توابع Generator معرفی شدن. این توابع به ما کمک می کنن که اگر تابعی مثل تابع search تعریف شده در بالا رو داریم، دچار مشکلات گفته شده نشیم. تابع search رو اگه بخوایم با استفاده از Generator بنویسیم به این شکل می شه:
function* search(needle, haystack) { for(let i=0; i<haystack.length; i++) { if(needle === haystack[i]) { yield i; } } }
تغییراتی که توی کد داده شده ایناست:
1- بعد از کلمه کلیدی function کاراکتر * اضافه شده. این کار برای اینه که مرورگر بدونه این یه تابع معمولی نیست!
2- آرایه results به کلی حذف شده و در عوض هر جا که نتیجه جدیدی حاصل می شه، از کلمه کلیدی yield استفاده می کنیم. کلمه کلیدی yield خیلی شبیه return عمل می کنه، یعنی باعث بازگشت مقدار به کسی که تابع رو فراخونی کرده می شه اما بر خلاف return باعث خاتمه کار تابع نمی شه.
اما نکته ای که وجود داره توی نحوه استفاده از توابع Generator هست. اگه تابع search فوق رو به صورت زیر اجرا کنیم نتیجه دلخواه رو نمی گیریم:
console.log(search("x", ["x", "y", "z", "x"])); // Generator{ }
در واقع اصلا تابع اجرا نمی شه که بخواید نتیجه ای بگیرید. تنها اتفاقی که با فراخونی تابع search می افته اینه که آبجکتی از نوع Generator برگشت داده می شه. برای اجرای تابع search باید تابع next آبجکت Generator رو فراخونی کنید:
console.log(search("x", ["x", "y", "z", "x"]).next()); // Object { value: 0, done: false }
شاید خروجی بالا هم خیلی مطلوبتون نباشه، ولی از خروجی قبلی خیلی بهتره! بذارید اول معنی مقدار برگشت داده شده رو بگیم. آبجکتی که برگشت داده شد، دو تا کلید داره:
کد بالا باعث می شه تابع search تا رسیدن به یکی از چهار حالت زیر ادامه پیدا کنه:
به عبارت دیگه وقتی تابع به کلمه کلیدی yield می رسه، به طور موقت متوقف می شه و دفعه بعد که شما دوباره تابع next رو صدا بزنید، تابع از همونجا که متوقف شده به کارش ادامه می ده.
حالا با توضیحاتی که داده شد می شه کد بالا رو تکمیل کرد:
const SEARCH_GEN = search("x", ["x", "y", "z", "x"]); console.log(SEARCH_GEN.next()); // Object { value: 0, done: false } console.log(SEARCH_GEN.next()); // Object { value: 3, done: false } console.log(SEARCH_GEN.next()); // Object { value: undefined, done: true }
ظاهرا به چیزی که می خوایم رسیدیم. اما هنوز یکم تمیزکاری مونده. آبجکت Generator قابل استفاده توی حلقه for..of هست. یعنی می شه کد بالا رو به این صورت اصلاح کرد:
const SEARCH_GEN = search("x", ["x", "y", "z", "x"]); for(let result of SEARCH_GEN) { console.log(result); } // 0 // 3 // undefined
برای راحت شدن از دست undefined آخر (که جزء جواب نیست) دو تا راه وجود داره:
for(let result of SEARCH_GEN) { if(type of result === "undefined") { break; } console.log(result); } // 0 // 3
متاسفانه توی توابع Generator به راحتی نمی تونید از توابع آسنکرون استفاده کنید. در واقع مشکل از توابع Generator نیست، مشکل از Callback هاییه که موقع استفاده از توایع آسنکرون تعریف می کنیم. اما برای حل این مشکل هم راه حل هایی وجود داره. یکی از راه حل ها، استفاده از async...await است. مثلا کد زیر درست کار نمی کنه:
function generator() { yield(1); setTimeout(() => { yield(2); }, 2000); } for(let result of generator()) { console.log(result); } // 1
همونطور که می بینید فقط مقدار 1 لاگ می شه. اما کد بالا می تونه به این صورت اصلاح بشه که کار کنه:
function sleep(time) { return new Promise( (resolve, reject) => { setTimeout(() => resolve(), time); } ); } async function* generator() { yield 1; await sleep(2000); yield 2; } async function consumer() { for await(let result of generator()) { console.log(result); } } consumer(); // 1 // 2
اگه کد بالا رو اجرا کنید می بینید که اول 1 توی کنسول نمایش داده می شه و 2 ثانیه بعد، 2 نمایش داده می شه.
توی کد بالا اول از همه اومدیم تابع setTimeout رو به یه Promise تبدیل کردیم که بتونیم از await استفاده کنیم. بعد توی حلقه for...of از قابلیت جدید ES استفاده کردیم و await رو بعد از کلمه کلیدی for قرار دادیم. با این کار می تونیم تابع generator که async هست رو توی حلقه استفاده کنیم. در نهایت از اونجا که نمی شه از await خارج از تابع async استفاده کرد، کل حلقه رو داخل تابع consumer که async هست قرار دادیم و در عوض تابع consumer رو فراخونی کردیم.