ასინქრონული ჯავასკრიპტი
ცოტა უცნაურად დავიწყოთ..
წარმოვიდგინოთ რესტორნის სამზარეულო, რომელიც შემდეგი პრინციპით მუშაობს : დროის კონკრეტულ მომენტში მზარეული ამზადებს მხოლოდ ერთ კერძს, ანუ თითოეული კერძის მზადება იწყება მას შემდეგ, რაც სრულდება წინა კერძი. ასეთი მიდგომა შეიძლება არაეფექტური იყოს და მომხმარებლებს დიდხანს მოუხდეთ ლოდინი.
უმჯობესია თუ მზარეული კერძებს პარალელურ რეჟიმში დაამზადებს, ანუ რამდენიმე კერძის ერთდროულად მზადებას შეუდგება, პერიოდულად გადაამოწმებს კონკრეტული კერძების მდგომარეობას და საჭიროებისამებრ დაუთმობს დროს თითოეულ მათგანს. ბუნებრივია ამ შემთხვევაში შეკვეთები გაცილებით სწრაფად გამზადდება და მომხმარებლებსაც ნაკლები ლოდინი მოუწევთ.
უფრო კონკრეტულად : დავუშვათ მზარეული ამზადებს ჩაქაფულს (🌿🍀🥩😀), გაამზადა ყველა საჭირო ინგრედიენტი და შემოდგა ხორცი ცეცხლზე 🔥 ამ დროს შემოვიდა ყველის ასორტის შეკვეთა 🧀, იმის ნაცვლად, რომ დაელოდოს ჩაქაფულის დასრულებას, მზარეული იწყებს ყველის დაჭრას, შიგადაშიგ კი ურევს ცეცხლზე შემოდგმულ ხორცს, ასორტის შეკვეთაც მზადდება და არც ხორცი იწვება 😍
სინქრონული პროგრამირება
სინქრონული პროგრამირება არის პროგრამირების მოდელი, რომელშიც ოპერაციები სრულდება თანმიმდევრულად, ერთმანეთის მიყოლებით - სრულდება ერთი, იწყება მეორე და ა.შ. ეს წრფიული მიდგომა გულისხმობს იმას, რომ ისეთი ოპერაციები, რომელთა შესრულებასაც შედარებით დიდი დრო სჭირდება, წარმოადგენენ ერთგვარ მბლოკავებს შესრულების ნაკადში მათ შემდეგ მდგომი ოპერაციებისათვის. სინქრონული პროგრამირება დაფუძნებულია ერთნაკადიან, მბლოკავ არქიტექტურაზე.
სწორედ ამ მიდგომით მუშაობდა მზარეული ზემოთ მოყვანილ პირველ მაგალითში.
ასინქრონული პროგრამირება
ასინქრონული პროგრამირება არის პროგრამირების მოდელი, რომელშიც ინსტრუქციები სრულდება ერთმანეთის პარალელურად. ამ მოდელით შექმნილ სისტემებში ოპერაციები არ ბლოკავენ ერთმანეთს. ერთი ოპერაცია შეიძლება შესრულდეს ისე, რომ არ დაელოდოს პარალელურად მიმდინარე სხვა ოპერაციის ან ოპერაციების დასრულებას. ასინქრონული მოდელი იყენებს არამბლოკავ მიდგომას (non-blocking).
სწორედ ამგვარად მუშაობდა მზარეული ზემოთ მოყვანილ მეორე მაგალითში.
ასინქრონული პროგრამირება VS სინქრონული პროგრამირება
წარმადობა და ეფექტურობა
როდესაც საქმე ეხება შეტანა/გამოტანა (I/O - Input/Output) ტიპის ოპერაციებს, როგორც წესი, ხშირადაა საჭირო მონაცემთა ბაზებთან მიმართვა, http მოთხოვნების გაგზავნა, სხვადასხვა ფაილებში ინფორმაციების ჩაწერა ან პირიქით - ფაილების შიგთავსების წაკითხვა... ეს ყველაფერი შედარებით მეტ დროით რესურსს მოითხოვს. ასეთ დროს ასინქრონული მიდგომა უპირობო ლიდერია, რადგან იგი ამ ოპერაციებს საშუალებას არ აძლევს დაბლოკონ სხვა ოპერაციები.
მაგალითისათვის განვიხილოთ მომხმარებლის რეგისტრაციის პროცესი ასინქრონული მიდგომის შემთხვევაში :
-
ინფორმაციის გაგზავნის სწრაფდამოწმება:
- როდესაც მომხმარებელი აგზავნის ინფორმაციას, სისტემას შეუძლია რეგისტრაციის პროცესის დასრულებამდე დაუდასტუროს მას, რომ ინფორმაცია წარმატებით გაიგზავნა - ეკრანზე გამოუტანოს შესაბამისი შეტყობინება.
-
პროგრესის მაჩვენებლები:
- მანამ სერვერი რეგისტრაციის პროცესს ამუშავებს, ეკრანზე შეიძლება გამოვიდეს პროგრესის მაჩვენებელი, ე.წ პროგრეს-ბარი, რომლის მეშვეობითაც მომხმარებელი შეიტყობს თუ რა ეტაპზეა და როგორ მიმდინარეობს რეგისტრაციის პროცესი. ეს ყველაფერი იწვევს შეგრძნებას, რომ სისტემა გაყინული არ არის და რაღაც ხდება :))
-
ინტერაქტიულობა:
- იმის ნაცვლად, რომ მომხმარებელს ვაიძულოთ უყუროს სტატიკურ, გაყინულ, ჩატვირთვის გვერდს (page loading), შეგვიძლია სამომხმარებლო ინტერფეისი გაცილებით უფრო ინტერაქტიულად დავაგეგმაროთ: მომხმარებელმა შეძლოს აპლიკაციის სხვა ნაწილებზე გადასვლა, სხვა სექციების ნახვა, სხვა ოპერაციების შესრულება და ა.შ.
-
დაუყოვნებლივი შეტყობინებები შეცდომების შესახებ:
- თუ რეგისტრაციისას დაფიქსირდება რაიმე შეცდომები, მაგალითად ელ_ფოსტა, რომელიც მომხმარებელმა აკრიფა, უკვე დაკავებული იქნება, სერვერს შეუძლია დაუყოვნებლივ დაამყაროს უკუკავშირი მომხმარებელთან და შეატყობინოს ამ შეცდომის შესახებ, ნაცვლად იმისა, რომ ხელახლა ჩატვირთოს მთლიანი გვერდი. ასინქრონული პროგრამირებით შესაძლებელია ინფორმაციის პირდაპირ რეჟიმში (real-time) ვალიდაცია და შეცდომების დამუშავება (დიდი ალბათობით ერთხელ მაინც გექნებათ გაკეთებული რეგისტრაციის სისტემა ან რაიმე მსგავსი მაგალითად AJAX-ის მეშვეობით, სწორედ ასე მუშაობს იგი).
-
მრავალსაფეხურიანი რეგისტრაცია:
- თუ რეგისტრაციის პროცესი რამდენიმე საფეხურისაგან შედგება, შეგვიძლია მომხმარებელი ისე გადავიყვანოთ შემდეგ საფეხურზე, რომ სერვერს ჯერ კიდევ არ ჰქონდეს წინა საფეხურის დამუშავება დასრულებული. ეს კი საკმაოდ მოქნილი მიდგომაა სამომხმარებლო ინტერფეისის კომფორტულობის თვალსაზრისით.
-
ოპერაციები პარალელურ რეჟიმში:
- თუ ჩასატარებელი გვაქვს რეგისტრაციასთან დაკავშირებული დამატებითი ოპერაციები (მაგალითად შეტყობინების გაგზავნა ელ_ფოსტაზე), ეს შეგვიძლია გავაკეთოთ პარალელურ რეჟიმში, რაც იმას ნიშნავს, რომ მომხმარებელი დაყოვნების გარეშე გააგრძელებს მუშაობას აპლიკაციაში.
სინქრონული მიდგომის შემთხვევაში, როდესაც მომხმარებელი დააჭერს რეგისტრაციის ღილაკს, მოხდება გვერდის განახლება (refresh) რომელიც გაგრძელდება მანამ, სანამ სერვერი არ დაასრულებს რეგისტრაციის პროცესს, განახლების დასრულებამდე შეუძლებელი იქნება რაიმეს გაკეთება.
სინქრონული მიდგომით დაწერილი რეგისტრაციის ნიმუში იხილეთ აქ, ასინქრონული მიდგომით დაწერილი რეგისტრაციის ნიმუში კი აქ.
მეორეს მხრივ, არის სიტუაციები როდესაც უმჯობესია სინქრონული მიდგომის გამოყენება. მაგალითად : მათემატიკური გამოთვლებისას, დიდი ზომის მონაცემთა ანალიზისას, მაღალი ხარისხის 3D გრაფიკული გამოსახულებების გენერირებასას და ზოგადად - ისეთი ოპერაციებისას, რომლებიც ცენტრალური პროცესორის ინტენსიურ ჩართულობას მოითხოვენ და შეტანა/გამოტანა ტიპის ამოცანებთან ნაკლებად აქვთ შეხება. ასეთ დროს ფოკუსირება გამოთვლების სისწრაფესა და სიზუსტეზე ხდება, გამოთვლებისას კი სინქრონული მიდგომის წრფიულობას, პროგნოზირებადობასა და თანმიმდევრულობას შეიძლება გადამწყვეტი მნიშვნელობა ჰქონდეს, მით უფრო თუ ოპერაციათა ჯაჭვის ყოველი რგოლი დამოკიდებული იქნება მისი წინა რგოლის შედეგზე.
კოდის სიმარტივე
გამომდინარე იქიდან, რომ სინქრონული პროგრამირება წრფიულ მიდგომას იყენებს, მასში დაწერილი კოდი უფრო ადვილად აღქმადია, ამიტომ ხარვეზების ძიება და შეცდომების აღმოფხვრაც შედარებით მარტივად ხდება. მოკლედ - სინქროლი მიდგომისას უფრო ლამაზი კოდი იწერება :))
პარალელიზმი
პარალელიზმის თვალსაზრისით ასინქრონული პროგრამირებაა ლიდერი, რადგან, როგორც ზემოთ აღვნიშნეთ, მისი ერთ-ერთი მთავარი მახასიათებელი სწორედ ისაა, რომ შეგვიძლია ერთდროულად შევასრულოთ სხვადასხვა ოპერაციები, რასაც ვერ ვიტყვით სინქრონულ პროგრამირებაზე.
როდის გამოვიყენოთ სინქრონული პროგრამირება ?
- საშუალო და მცირე ზომის პროექტებში - სადაც პარალელიზმის საჭიროება ნაკლებია და შესაბამისად სხვადასხვა ოპერაციების ერთდროულად შესრულებაც არ გვიწევს.
- CPU-ინტენსიურ ამოცანებში - სადაც ცენტრალური პროცესორის ინტენსიური ჩართულობაა საჭირო და შეტანა/გამოტანა ტიპის ამოცანებთან ნაკლებად გვაქვს შეხება.
- პროექტებში რომლებიც მოითხოვენ კოდის შესრულების თანმიმდევრულობას - სადაც ინსტრუქციების შესრულების თანმიმდევრულობასა და წრფიულობას გადამწყვეტი მნიშვნელობა აქვს, რადგან ოპერაციათა ჯაჭვის ყოველი რგოლი დამოკიდებულია მის წინა რგოლზე.
როდის გამოვიყენოთ ასინქრონული პროგრამირება ?
- აპი პროგრამირებისას - პარალელიზმი საკმაოდ ეფექტური მიდგომაა აპლიკაციების პროგრამირების ინტერფეისების (API) წერისას.
- პირდაპირ რეჟიმში მიმდინარე ოპერაციებში - ჩათ-აპლიკაციები, სათამაშო სერვერები, პირდაპირ რეჟიმში სამაუწყებლო პლატფორმები (live streaming).
- შეტანა/გამოტანა ტიპის ოპერაციებში - სადაც პროცესები ინტერაქტიულ რეჟიმში მიმდინარეობს (მაგ: მონხმარებელი გზავნის ინფორმაციას სერვერზე და ეს ინფორმაცია ინახება მონაცემთა ბაზაში, იგზავნება http მოთხოვნები, ვკითხულობთ სხვადასხვა ფაილების შიგთავსებს...).
საბოლოოდ თუ ყველაფერს შევაჯამებთ შეიძლება ითქვას, რომ როდესაც საქმე ეხება სინქრონულ და ასინქრონულ მიდგომებს შორის არჩევანს, გადამწყვეტი მნიშვნელობა მაინც პროექტის შინაარსსა და მოთხოვნებს ენიჭება - არის სიტუაციები, სადაც სინქრონული მიდგომის გამოყენება სჯობს და არის შემთხვევები, როდესაც ასინქრონული მიდგომაა კარგი გადაწყვეტა ('სოლუშენი' - ძალიან რომ უყვართ ეს სიტყვა 😕)
ერთნაკადიანი მბლოკავი ჯავასკრიპტი
განვიხილოთ ასეთი კოდი :
function funcOne() {
console.log('პირველი');
}
function funcTwo() {
console.log('მეორე');
}
funcOne();
funcTwo();
შედეგი იქნება :
პირველი
მეორე
ანუ მანამ funcOne ფუნქციაში აღწერილი ინსტრუქციები არ შესრულდება, funcTwo ფუნქცია დაიბლოკება და ეს ბლოკირება მით უფრო დიდხანს გაგრძელდება, რაც უფრო რთულ და კომპლექსურ ოპერაციებს შევასრულებთ პირველ ფუნქციაში.
ზოგადად, კოდის ნებისმიერი ფრაგმენტი, რომლის შესრულებასაც დიდი დროითი რესურსი სჭირდება, შეიძლება განხილულ იქნას, როგორც მის შემდეგ არსებული კოდის მბლოკავი. მბლოკავში შეიძლება ხდებოდეს გარე რესურსთან მიმართვა, დიდი ზომის ფოტოს ჩატვირთვა, სრულდებოდეს 1000000 იტერაციიანი ციკლური ოპერაცია, ან გვქონდეს უბრალოდ alert ფუნქცია, რომელიც ყველაფერს ბლოკავს მანამ, სანამ მომხმარებელი არ ირეაგირებს მასზე:
alert('გამარჯობა');
console.log('სხვა შეტყობინება');
მბლოკავის კიდევ ერთი მაგალითია უსასრულო ციკლი (ეს კოდი თუ გინდათ არ გაუშვათ 😀) :
while (true) {
console.log('უსასრულო ციკლი');
}
console.log('ეს შეტყობინება არასდროს გამოჩნდება');
ასე მუშაობს სტანდარტული ანუ ერთნაკადიანი (single-thread), მბლოკავი (blocking) ჯავასკრიპტი თავისი სპეციფიკაციიდან (ECMAScript) გაგმოდინარე.
ასინქრონული ჯავასკრიპტისადმი მიძღვნილ სტატიაში ვამბობთ, რომ :
ყველაფერს მალე გავარკვევთ 😊 ცოტა შორიდან დავიწყოთ.
რა არის ჯავასკრიპტის შესრულების გარემო ?
ჯავასკრიპტის შესრულების გარემო (runtime environment) არის პროგრამული გარემო, ერთგვარი ინფრასტრუქტურა, რომელიც უზრუნველყოფს ჯავასკრიპტის კოდის შესრულებას. იგი მოიცავს ჯავასკრიპტის ძრავს, ასევე სხვადასხვა აპი-ებს (API) და სერვისებს, რომლებიც ამრავალფეროვნებენ სტარნდარტული ჯავასკრიპტის შესაძლებლობებს.
ადრეულ წლებში ერთადერთ ასეთ გარემოდ ვებ-ბრაუზერი მოიაზრებოდა, რადგან, როგორც ცნობილია, ეს ენა იმისთვის შეიქმნა, რომ "გაეცოცხლებინა", უფრო დინამიური გაეხადა HTML გვერდები, მაშინ მას მეტი არაფერი მოეთხოვებოდა, დღესდღეობით კი სიტუაცია შეიცვალა და ჯავასკრიპტი სერვერულ მხარესაც წარმატებით გამოიყენება, ასე რომ შეგვიძლია გამოვყოთ ყველაზე პოპულარული ორი გარემო : ბრაუზერი და სერვერი.
რა არის ჯავასკრიპტის ძრავი ?
მარტივი სიტყვებით თუ ვიტყვით ჯავასკრიპრის ძრავი (javascript engine) არის შესრულების გარემოში არსებული პროგრამა, რომელიც ახდენს ჩვენს მიერ დაწერილი კოდის ანალიზს და შემდეგ თარგმნის მას ცენტრალური პროცესორისათვის გასაგებ ენაზე.
ბრაუზერ Chrome-ს შემთხვევაში ჯავასკრიპტის ძრავი არის Google-ს მიერ შექმნილი V8, ხოლო სერვერის შემთხვევაში ყველაზე პოპულარულია ამავე V8-ზე დაფუძნებული პროგრამული პლატფორმა Node.js, რომელიც ჯავასკრიპტს, როგორც სპეციალიზირებულ ენას, გარდაქმნის პროგრამირების სრულფასოვან და საერთო დანიშნულების ენად. ამ პლატფორმაზე დაფუძნებით, ჯავასკრიპტი შეგვიძლია გამოვიყენოთ სერვერული მხარის სამუშაოებში.
განვიხილოთ ძრავის მუშაობის ერთ-ერთი ყველაზე გავრცელებული სქემა :
ახლა გავარკვიოთ რა ხდება ამ სურათზე :))
სინტაქსური ანალიზატორი
ჯავასკრიპტის კოდი გადაეცემა სინტაქსურ ანალიზატორს (parser), რომელიც კითხულობს მას და ამოწმებს გამართულია თუ არა ყველაფერი სინტაქსური თვალსაზრისით. თუ კოდში რაიმე შეცდომაა - გვატყობინებს, თუ არა და ქმნის აბსტრაქტულ სინტაქსურ ხეს (AST - Abstract syntax tree).
აბსტრაქტული სინტაქსური ხე
აბსტრაქტული სინტაქსური ხე წარმოადგენს კოდში არსებული ცვლადების, ოპერაციების, გამოსახულებების და ა.შ საფუძველზე შექმნილი კვანძების იერარქიულ ხეს. ასეთი სახით წარმოდგენილი კოდის დამუშავება შედარებით უფრო მარტივია ინტერპრეტირების/კომპილირების დროს, რაც ბუნებრივია აისახება შესრულების სისწრაფეზე.
ინტერპრეტატორი
აბსტრაქტული სინტაქსური ხე გადაეცემა ინტერპრეტატორს, რომელიც მისგან აგენერირებს კოდის შუალედურ წარმოდგენას (IR - Intermediate Representation). არსებობს შუალედური წარმოდგენის სხვადასხვა ფორმები, ჯავასკრიპტის შემთხვევაში ეს ფორმა ხშირადაა Bytecode.
შუალედური წარმოდგენა
შუალედური წარმოდგენა არის ერთგვარი ხიდი ჯავასკრიპტის კოდსა და სამანქანო ენას შორის. მისი ძირითადი დანიშნულებაა AST-საგან შექმნას ისეთი სტრუქტურა და კოდის აბსტრაქცია, რომლის ანალიზი, ოპტიმიზაცია და სამანქანო ენაში გადაყვანაც უფრო მარტივი იქნება. ეს აბსტრაქცია უზრუნველყოფს შესრულების პროცესის მოქნილობასაც - ერთი და იგივე შუალედური წარმოდგენა წარმატებით იმუშავებს სხვადასხვა პლატფორმებში.
ინტერპრეტაცია
საბოლოოდ, Bytecode უნდა გადაითარგმონს ცენტრალური პროცესორისათვის გასაგებ, ანუ ორობით ენაზე. ინტერპრეტაციის შემთხვევაში ხდება Bytecode-ს სათითაო ხაზების გადათარგმნა და შესრულება : ითარგმნება ერთი ხაზი - ცენტრალური პროცესორი ასრულებს ორობით კოდს, ითარგმნება შემდეგი ხაზი - პროცესორი ასრულებს კოდს და ა.შ.
ასეთი მიდგომის უპირატესობა ისაა, რომ შედარებით სწრაფად იწყება შესრულების პროცესი, რადგან არავითარი წინასწარი სამუშაოს ჩატარება არ არის საჭირო, უარყოფითი მხარე კი შემდეგია : გამომდინარე იქიდან, რომ სათითაო ხაზები ითარგმნება, სრული პროცესი შედარებით დიდხანს გრძელდება საერთო ჯამში.
კომპილაცია
კომპილაციის შემთხვევაში ჯერ მთლიანი Bytecode ითარგმნება სამანქანო ენაზე, წინასწარ იქმნება ერთი გამშვები ფაილი ორობითი კოდით და ამის შემდეგ ხდება შესრულება. ეს არის სტანდარტული ანუ AOT კომპილაცია (Ahead Of Time - წინასწარ). ამ მიდგომის დადებითი მხარე ის არის, რომ შესრულების საერთო დრო შედარებით ნაკლებია, რადგან ყველაფერი მზადაა და ცენტრალურ პროცესორს ისღა დარჩენია უბრალოდ წაიკითხოს და შეასრულოს თავის მშობლიურ ენაზე დაწერილი კოდი :)) უარყოფითი მხარე კი ისაა, რომ პროცესის დაწყებას ესაჭიროება შედარებით მეტი დრო, ვინაიდან ჯერ გამშვები ფაილი იქმნება.
JIT კომპილაცია
JIT კომპილაცია (ინგ: Just In Time - დროული), არის სტანდარტული კომპილაციისა და ინტერპრეტაციის ერთგავრი ნაზავი. კოდის შესრულება იწყება ინტერპრეტირების ან ნაწილობრივი კომპილირების გზით. JIT კომპილატორი ახდენს კოდის საკვანძო, ხშირად განმეორებადი ფრაგმენტების იდენტიფიცირებას და მთლიანი კოდის ნაცვლად მხოლოდ ამ ფრაგმენტებს (პირობითად დავარქვათ კოდის ცხელი წერტილები 🔥) თარგმნის, ასევე ახდენს გადათარგმნილი 'ცხელი წერტილების' ქეშირებას, რაც ზრდის შესრულების სისწრაფეს.
შეჯამება: JIT კომპილაციისას შესრულების პროცესი იწყება სწრაფად, რადგან არ ხდება გამშვები ფაილის გენერირება. უშუალოდ შესრულებისას კი კოდის საკვანძო წერტილები მთლიანად ითარგმნება სამნქანო ენაში და აღარ ხდება სათითაო ხაზების გადათარგმნა-შესრულება. შეიძლება ითქვას, რომ ამ მიდგომაში გაერთიანებულია სტანდარტული კომპილაციისა და ინტერპრეტაციის დადებითი მხარეები.
გამოძახების დასტა
ჯავასკრიპტში შესრულების კონტექსტი (EC - execution context) ეწოდება იმ სივრცეს, გარემოს, რომელშიც ხდება კოდში აღწერილი ფუნქციების შესრულება (უცხო სიტყვ. ლექსიკონი : კონტექსტი - სპეციფიკური გარემო, რომელშიც ხდება მოქმედება ან მოვლენა).
გამოძახების დასტა (call stack) არის მექანიზმი, რომლის მეშვეობითაც ჯავასკრიპტის ძრავი ახდენს ძირითადი შესრულების კონტექსტის (General EC) მეთვალყურეობას, მონიტორინგს, აკონტროლებს როდის, რომელი ფუნქციის გამოძახება ანუ ჩამატება მოხდეს კონტექსტში, როდის რომელი ფუნქცია ამოიშალოს კონტექსტიდან და ა.შ.
განვიხილოთ ასეთი კოდი :
function firstFunction() {
console.log(1);
secondFunction();
}
function secondFunction() {
console.log(2);
thirdFunction();
}
function thirdFunction() {
console.log(3);
}
firstFunction();
შედეგი იქნება :
1
2
3
აღვწეროთ სრული პროცესი :
-
საწყის ეტაპზე გამოძახების დასტა ცარიელია :
|--------------------------------------| | გამოძახების დასტა ცარიელია | |--------------------------------------|
-
firstFunction დაემატა გამოძახების დასტაში :
|--------------------------------------| | firstFunction | |--------------------------------------| | გამოძახების დასტა | |--------------------------------------|
-
firstFunction ფუნქციამ ეკრანზე გამოიტანა '1' და გამოიძახა secondFunction, რომელიც ასევე დაემატა გამოძახების დასტაში :
|--------------------------------------| | secondFunction | |--------------------------------------| | firstFunction | |--------------------------------------| | გამოძახების დასტა | |--------------------------------------|
-
secondFunction ფუნქციამ ეკრანზე გამოიტანა '2' და გამოიძახა thirdFunction, რომელიც ასევე დაემატა გამოძახების დასტაში :
|--------------------------------------| | thirdFunction | |--------------------------------------| | secondFunction | |--------------------------------------| | firstFunction | |--------------------------------------| | გამოძახების დასტა | |--------------------------------------|
-
thirdFunction ფუნქციამ ეკრანზე გამოიტანა '3', დაასრულა მუშაობა და წაიშალა გამოძახების დასტიდან :
|--------------------------------------| | secondFunction | |--------------------------------------| | firstFunction | |--------------------------------------| | გამოძახების დასტა | |--------------------------------------|
-
secondFunction ფუნქციამ დაასრულა მუშაობა და წაიშალა გამოძახების დასტიდან :
|--------------------------------------| | firstFunction | |--------------------------------------| | გამოძახების დასტა | |--------------------------------------|
-
firstFunction ფუნქციამ დაასრულა მუშაობა და წაიშალა გამოძახების დასტიდან :
|--------------------------------------| | გამოძახების დასტა ცარიელია | |--------------------------------------|
თუ დავაკვირდებით აღმოვაჩენთ, რომ დასტას პირველი ტოვებს ის ფუნქცია, რომელიც დასტაში ყველაზე ზემოთაა, ანუ ყველაზე ბოლოს დაემატა. სწორედ ამიტომ ამ პროცესს LIFO - ს უწოდებენ ხოლმე (Last In First Out) - ბოლოს შესული პირველი გამოდის :))
აღსანიშნავია, რომ თუ კონკრეტული ფუნქციის გამოძახებების რაოდენობა უსასრულოდ დიდი იქნება, მაგალითად :
function myFunc() {
myFunc();
}
myFunc()
დაფიქსირდება შემდეგი შეცდომა :
Uncaught RangeError: Maximum call stack size exceeded
ანუ მოხდება დასტის გადატვირთვა - stack overflow (სახელი ხომ არ გეცნობათ ? 😍)
მეხსიერების მენეჯმენტი
როდესაც პროგრამირების ამა თუ იმ ენაში ვმუშაობთ, ბუნებრივია გვჭირდება გარკვეული ცვლადების, ობიექტების, ფუნქციების და ა.შ აღწერა. ეს ყველაფერი ინახება კომპიუტერის მეხსიერებაში. არსებობს ენები რომლებშიც ჩვენვე გვიწევს იმის განსაზღვრა თუ როდის გამოიყოს სივრცე მეხსიერებაში კონკრეტული მიზნისათვის, შემდეგ როდის გასუფთავდეს ეს სივრცე და ა.შ (C, C++). ჯავასკრიპტში ეს ავტომატურად ხდება 'დასუფთავების სამსახურის' (Garbage Collection) დამსახურებით 😀 :
function createObject() {
// ობიექტს გამოეყო ადგილი მეხსიერებაში
let myObject = {
name: "სატესტო ობიექტი",
value: 42
};
// ქმედებები ობიექტზე
console.log(myObject.name); // შედეგი: სატესტო ობიექტი
// როდესაც ფუნქცია დაასრულებს მუშაობას, `myObject` აღარ იქნება ხელმისაწვდომი
}
// ფუნქციის გამოძახება
createObject();
/*
აქ უკვე შეუძლებელია `myObject` ობიექტთან წვდომა, მან თავის საქმე გააკეთა
და აღარ არის საჭირო, ამიტომ დასუფთავების სამსახური წაშლის მეხსიერებიდან
*/
თუ createObject ფუნქციას ხელახლა გამოვიძახებთ, მეხსიერებაში ხელახლა გამოიყოფა საჭირო სივრცეები, შემდეგ ისევ გასუფთავდება და ა.შ. უნდა აღინიშნოს, რომ ეს საკმაოდ მოსახერხებელი და ეფექტური მიდგომაა.
მონაცემთა ტიპები მეხსიერების თვალსაზრისით
მეხსიერების თვალსაზრისით მონაცემები იყოფა ორ ნაწილად :
- პრიმიტიული ტიპის მონაცემები - number, string, boolean, null, undefined და BigInt ტიპის მონაცემები, რომელთა სტრუქტურაც შედარებით მარტივია და მათი ზომაც წინასწარაა ცნობილი. ასეთი მონაცემები ინახება მეხსიერების კონკრეტულ რეგიონში, არეში, რომელსაც უწოდებენ მეხსიერების დასტას (memory stack).
- კომპლექსური ტიპის მონაცემები - მასივი, ფუნქცია, ობიექტი. ამ ტიპის მონაცემების სტრუქტურა შედარებით რთულია და იმის წინასწარ განსაზღვრაც შეუძლებელია თუ რამდენი ადგილი შეიძლება დაიკავონ მეხსიერებაში. ასეთი მონაცემები ინახება მეხსიერების სხვა რეგიონში, რომელსაც მეხსიერების ნაკრები (memory heap) ეწოდება.
განვიხილოთ ასეთი კოდი :
let name = 'გიორგი';
let age = 30;
let person = {
name: 'დავითი',
age: 35
}
let newName = name;
newName = 'ლევანი';
let newPerson = person;
newPerson.name = 'კოტე';
console.log(person.name); // კოტე
მეხსიერების თვალსაზრისით ხდება შემდეგი :
გავყვეთ სურათს :
-
პრიმიტიული ცვლადები:
-
let name = 'გიორგი';
- აღიწერა პრიმიტიული ცვლადი name და მიენიჭა მნიშვნელობა 'გიორგი'
- მოხდა მისი შენახვა დასტაში (stack)
-
let age = 30;
- აღიწერა ასევე პრიმიტიული ცვლადი age და მიენიჭა მნიშვნელობა 30
- ისიც ასევე შეინახა დასტაში (stack)
-
let name = 'გიორგი';
-
ობიექტი:
-
აღიწერა ობიექტი let person = { name: 'დავითი', age: 35 };
- ყურადღება : უშუალოდ ობიექტი შეინახა ნაკრებში (heap), ხოლო person ცვლადი, რომლეც ამ ობიექტს მოიცავს შეინახა დასტაში (stack), ანუ დასტაში შენახულ person ცვლადს მიება ნაკრებში შენახული ობიექტის შიგთავსი
-
აღიწერა ობიექტი let person = { name: 'დავითი', age: 35 };
-
პრიმიტიული ცვლადის დაკოპირება :
-
let newName = name;
- გამოცხადდა ახალი ცვლადი newName, მნიშვნელობად მიენიჭა უკვე არსებული name ცვლადი, ანუ newName-ის მნიშვნელობა გახდა 'გიორგი' და იგი დამოუკიდებლად შეინახა დასტაში (stack)
-
newName = 'ლევანი';
- განახლდა newName ცვლადის მნიშვნელობა - მიენიჭა 'ლევანი'
- ამას არ მოუხდენია ზემოქმედება უკვე არსებულ name ცვლადზე, და მისი მნიშვნელობა არის ისევ 'გიორგი'
-
let newName = name;
-
კომპლექსური ცვლადის დაკოპირება:
-
let newPerson = person;
- გამოცხადდა ახალი ცვლადი newPerson და მნიშვნელობად მიენიჭა უკვე არსებული person ცვლადი
- გამომდინარე იქიდან, რომ person ცვლადი არის კომპლექსური ტიპის, newPerson ცვლადის მიბმაც უკვე არსებულ ობიექტზე მოხდა, ახალი ობიექტი არ შექმინლა
-
newPerson.name = 'კოტე';
- newPerson ობიექტის name თვისება გახდა 'კოტე'
- ვინაიდან newPerson და person ცვლადები ორივენი მიბმულნი არიან ერთი და იმავე ობიექტზე, მოხდა ამ ობიექტის განახლება
-
let newPerson = person;
-
შედეგი:
-
console.log(person.name);
- შედეგი იქნება 'კოტე' რადგან person ცვლადთან ასოცირებული ობიექტის name თვისება განახლდა newPerson ცვლადის მეშვეობით
-
console.log(person.name);
ამ სქემით მუშაობს მეხსიერების მენეჯმენტი ჯავასკრიპტში.
შეიძლება ითქვას, რომ უკვე დავასრულეთ სტანდარტული ჯავასკრიპტის მუშაობის ძირითადი პრინციპების მიმოხილვა.
***
განვიხილოთ ასეთი კოდი :
setTimeout(() => {
console.log('შეტყობინება');
}, 2000);
console.log('ტექსტი');
როგორც ვიცით setTimeout ფუნქცია გამოიყენება კონკრეტული ინსტრუქციების გადასავადებლად.
აქამდე განხილულ მაგალითებში ინსტრუქციები სრულდებოდა ერთმანეთის მიყოლებით. ამავე ლოგიკით თუ ვიხელმძღვანელებთ, ჯერ უნდა შესრულდეს setTimeout ჩანაწერი, შემდეგ კი console.log('ტექსტი'), კოდის შედეგი კი უნდა იყოს :
შეტყობინება
ტექსტი
მაგრამ სინამდვილეში შედეგი იქნება :
ტექსტი
შეტყობინება
რა მოხდა ? რატომ დაირღვა აქამდე არაერთხელ ნახსენები მბლოკავობის პრინციპი ? რატომ არ დაბლოკა setTimeout ფუნქციამ console.log('ტექსტი'); ჩანაწერი ? აშკარაა, რომ სტანდატული ჯავასკრიპტი რაღაცამ აიძულა გადაეხვია თავისი ბუნებისათვის და ეს რაღაც არის ვებ-აპი (Web API).
რა არის ვებ-აპი ?
ვებ-აპი (Web API) არის ბრაუზერში ჩაშენებული ინტერფეისების, ფუნქციების, მეთოდების ნაკრები, რომლის მეშვეობითაც ჯავასკრიპტი ხდება გაცილებით უფრო მაგიური და ძლიერი ვიდრე ის თავისი სპეციფიკაციიდან (ECMAScript) გამომდინარეა. ვებ-აპი არის ბრაუზერის ხელსაწყო და არა უშუალოდ ჯავასკრიპტის.
ზოგადად, თუ ვამბობთ, რომ ოპერაციათა შესრულების ნაკადში არსებული ისეთი ფუნქციები, რომელთა შესრულებასაც შედარებით მეტი დრო სჭირდებათ, არ უნდა ბლოკავდნენ მის შემდეგ მდგარ ფუნქციებს, ეს იმას ნიშნავს, რომ ასეთი ფუნქცია 'რაღაცამ' სადმე 'პარალელურ სამყაროში' უნდა 'გადაიტანოს' დროებით 😀. ხოლო მას შემდეგ რაც ფუნქცია მუშაობას დაასრულებს 'იქ, სადღაც', რაიმე მექანიზზმა ისევ დააბრუნოს ნაკადში.
რა არის დავალებათა რიგი ?
როდესაც გვსურს რაიმე ფუნქციის კონკრეტული დროით გადავადება setTimeout ფუნქციის მეშვეობით, ეს იმას ნიშნავს, რომ სადღაც უნდა მოხდეს ამ დროის ათვლა, ანუ გვჭირდება დროის მთვლელი. ასევე გვჭირდება მექანიზმი, რომელიც დროებით სადმე 'შეინახავს' გადავადებულ ფუნქციას, დროის ამოწურვის შემდეგ კი დააბრუნებს გამოძახების დასტაში. განვიხილოთ სურათი :
როგორც ვხედავთ გამოძახების დასტაში პირველი მოხვდა setTimeout ფუნქცია, რომელმაც აშკარად არ დაბლოკა შემდეგ არსებული ინსტრუქციები და აი რატომ : setTimeout არის ბრაუზერში ჩაშენებული ვებ-აპი, რომელიც ინსტრუქციების ასინქრონულად შესრულების საშუალებას იძლევა და არა ჯავასკრიპტის სტანდარტული ფუნქცია.
ვებ-აპი-ში არსებულმა დროის მთვლელმა გადათვალა 2000 მილიწამი, ანუ 2 წამი და setTimeout-ში აღწერილი ანონიმური უკუფუნქცია ჩააგდო დავალებათა რიგში (task queue):
() => {
console.log('შეტყობინება');
};
დავალებათა რიგი, ან შეიძლება ეწოდოს ამოცანათა რიგიც (task queue), არის ადგილი, სადაც ხვდება ნებისმიერი ასინქრონული ფუნქცია, რომელმაც მუშაობა დაასრულა, კონკრეტული შედეგი დააბრუნა და მზადაა ძირითად ნაკადში დასაბრუნებლად. ჩვენს შემთხვევაში : გავიდა ორი წამი და setTimeout-ის უკუფუნქცია ჩადგა ამ რიგში.
დავალებათა რიგი მუშაობს ე.წ FIFO (First-In-First-Out) პრინციპით, ანუ რიგს პირველი ტოვებს ის ფუნქცია, რომელიც პირველი ჩადგება რიგში.
რა არის მოვლენათა ციკლი ?
მოვლენათა ციკლი (event loop) არის ჯავასკრიპტის შესრულების გარემოში არსებული უმნიშვნელოვანესი მექანიზმი, რომელიც საშუალებას გვაძლევს დავარღვიოთ სტანდარტული ჯავასკრიპტის ერთნაკადიანი და მბლოკავი სტრუქტურა და ვიმუშაოთ ასინქრონულ, არამბლოკავ ამოცანებთან. კერძოდ : მოვლენათა ციკლი გამუდმებით ახდენს გამოძახების დასტისა და დავალებათა რიგის მონიტორინგს შემდეგი პრინციპით : პირველ რიგში ამოწმებს გამოძახების დასტას, თუ დასტა ცარიელია, ამოწმებს დავალებათა რიგს და თუ იგი ცარიელი არ არის, რიგში მდგარი პირველივე დავალება გადააქვს დასტაში. შემდეგ ისევ იგივე და ა.შ. ანუ დასტაში არსებულ ინსტრუქციებს ენიჭებათ უპირატესობა. სწორედ ამიტომ, შეიძლება რომ, მაგალითად 2 წამიანი setTimeout ფუნქცია არ შესრულდეს 2 წამის შემდეგ, თუ დასტაში ისეთი ოპერაციები გვექნება რომელთა შესრულებასაც 2 წამზე მეტი ესაჭიროება.
ჩვენს მაგალითს თუ დავუბრუნდებით : მოვლენათა ციკლმა გადაამოწმა დავალებათა რიგი, ნახა რომ რიგში დგას setTimeout-ის უკუფუნქცია, შემდეგ გადაამოწმა გამოძახების დასტა და მას შემდეგ, რაც შესრულდა console.log('ტექსტი') ჩანაწერი, ანუ გამოძახების დასტა გასუფთავდა, უკუფუნქცია გადააგდო გამოძახების დასტაში.
შეიკრა წრე, რომელშიც მიმოვიხილეთ თუ რა პრინციპებით და საშუალებებით ხდება ასინქრონული ოპერაციების ჩატარება ჯავასკრიპტის შესრულების გარემოში. ახლა შეგვიძლია გავეცნოთ უშუალოდ იმ ხელსაწყოებს, რომლებიც ამ ოპერაციების ჩატარებას გვიმარტივებენ.
რა არის უკუფუნქცია ?
უკუფუნქცია (callback) ეწოდება ფუნქციას, რომელიც არგუმენტად გადაეცემა სხვა ფუნქციას და რომლის გამოძახებაც ხდება ოდნავ მოგვიანებით - მშობელი ფუნქციის შესრულების შემდეგ.
მოვიყვანოთ მარტივი მაგალითი :
function greet(name, myCallback) {
console.log('გამარჯობა ' + name);
myCallback();
}
function myCallback() {
console.log('ნახვამდის!');
}
greet('ვასო', myCallback);
მშობელ ფუნქცია greet-ს მეორე პარამეტრად გადაეცა უკუფუნქციის პირობითი დასახელება - myCallback, შემდეგ ამავე მშობელ ფუნქციაში, საჭირო ადგილას მოხდა უკუფუნქციის გამოძახება.
შემდეგ აღიწერა უშუალოდ უკუფუნქცია - myCallback().
ბოლოს კი მოხდა მშობელი ფუნქციის გამოძახება.
უკუფუნქციებს საკმაოდ დიდი როლი ენიჭებათ ასინქრონული ოპერაციებისას, როდესაც ერთ ფუნქციას უწევს მეორე ფუნქციის დალოდება. ამ ყველაფერს სულ მალე ვნახავთ პრაქტიკაში.
დავუშვათ გვაქვს ასეთი ამოცანა : უნდა მივაკითხოთ შემდეგ ბმულს : https://jsonplaceholder.typicode.com/users/1, თუ მოთხოვნა (request) წარმატებით დასრულდება, პასუხი (response) იქნება რაღაც ამდაგვარი :
{
"id": 1,
"name": "Leanne Graham",
"username": "Bret",
"email": "Sincere@april.biz",
"address": {
"street": "Kulas Light",
"suite": "Apt. 556",
"city": "Gwenborough",
"zipcode": "92998-3874",
"geo": {
"lat": "-37.3159",
"lng": "81.1496"
}
},
"phone": "1-770-736-8031 x56442",
"website": "hildegard.org",
"company": {
"name": "Romaguera-Crona",
"catchPhrase": "Multi-layered client-server neural-net",
"bs": "harness real-time e-markets"
}
}
ინფორმაციის წამოღების შემდეგ მომხმარებლის ელ_ფოსტაზე უნდა გავაგზანოთ რაიმე შეტყობინება.
გადავჭრათ ეს ამოცანა უკუფუნქციის გამოყენებით :
function getUserData(callback) {
const xhr = new XMLHttpRequest();
xhr.open('GET', 'https://jsonplaceholder.typicode.com/users/1', true);
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) { // მოთხოვნა გაკეთდა
if (xhr.status === 200) { // წარმატებული პასუხი
const user = JSON.parse(xhr.responseText);
const email = user.email;
callback(email, null);
} else {
const error = 'დაფიქსირდა შეცდომა';
callback(null, error);
}
}
};
xhr.send();
}
getUserData(function(email, error) {
if(email) {
console.log(`შეტყობინება იგზავნება მისამართზე: ${email}`);
}
else if (error) {
console.log(error);
}
});
ორიოდ სიტყვით XMLHttpRequest-ის შესახებ : იგი არის ბრაუზერში ჩაშენებული ერთ-ერთი ვებ-აპი, რომელიც გამოიყენება სერვერზე HTTP მოთხოვნების ასინქრონულად გასაგზავნად, მომხმარებლის მხარის (client side) კოდიდან.
ვფიქრობ ამ კოდში ყველაფერი გასაგებია : უბრალოდ მოვახდინეთ ასინქრონული ოპერაციის დემონსტრირება უკუფუნქციის მონაწილეობით. აქვე უნდა აღინიშნოს, რომ ამოცანა საკმაოდ მარტივი იყო. უფრო რთულ და კომპლექსურ სიტუაციებში კოდი გაცილებით ჩახლართული და რთულად წაკითხვადი იქნება, მითუმეტეს თუ რამდენიმე უკუფუნქციის გამოყენება დაგვჭირდება (ასეთ შემთხვევებს 'უკუფუნქციების ჯოჯოხეთს' უწოდებენ ხოლმე - callback hell 😈).
შესაძლებელია გაჩნდეს კითხვები : 'რა საჭიროა უკუფუნქციის გამოყენება? იგივე ამოცანა ხომ მის გარეშეც შეიძლება გადაწყდეს ?' :
function getUserData() {
const xhr = new XMLHttpRequest();
xhr.open('GET', 'https://jsonplaceholder.typicode.com/users/1', true);
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) { // მოთხოვნა გაკეთდა
if (xhr.status === 200) { // წარმატებული პასუხი
const user = JSON.parse(xhr.responseText);
const email = user.email;
console.log(`შეტყობინება იგზავნება მისამართზე: ${email}`);
} else {
const error = 'დაფიქსირდა შეცდომა';
console.log(error);
}
}
};
xhr.send();
}
თანაც, როგოც ვხედავთ, ამ შემთხვევაში კოდი შედარებით მოკლე და ლაკონურია.
უკუფუნქციების გამოყენებას აქვს რამოდენიმე უპირატესობა. ძირითადი კი კოდის მოქნილობა და მრავალჯერადად გამოყენების შესაძლებლობაა.
-
პასუხისმგებლობების განცალკევება:
- უკუფუნქციით: getUserData ფუნქცია პასუხისმგებელია, მხოლოდ და მხოლოდ ინფორმაციის წამოღებაზე. იგი არ ახდენს მიღებული ინფორმაციის დამუშავებას. ნაცვლად ამისა - უკუფუნქციას გადასცემს ამ ინფორმაციას. ეს ყველაფერი უფრო მოდულარულსა და ერთ კონკრეტულ საკითხზე ფოკუსირებულს ხდის getUserData ფუნქციას (შედარებით ვრცელი ინფორმაცია მოდულარული მიდგომის შესახებ შეგიძლიათ იხილოთ აქ).
- უკუფუნქციის გარეშე: getUserData ფუნქცია ახდენს ინფორმაციის წამოღებასაც და ამ ინფორმაციის დამუშავებასაც. ამ შემთხვევაში ფუნქციის მრავალჯერადად გამოყენებადობის შესაძლებლობა მცირდება და აგრეთვე რთულდება მისი გატესტვაც.
-
მრავალჯერადად გამოყენებადობა:
- უკუფუნქციით: getUserData ფუნქცია შეგვიძლია საჭიროებისამებრ გამოვიყენოთ აპლიკაციის სხვადასხვა ადგილებში სხვადასხვა უკუფუნქციების მიმაგრებით. მაგალითად: შეიძლება მიღებული ინფორმაცია ერთ შემთხვევაში ელ_ფოსტაზე გაიგზავნოს, მეორე შემთხვევაში მონაცემთა ბაზაში შევინახოთ და ა.შ. ანუ ინფორმაციის მიღება ექცევა ერთ წერტილში და შემდეგ გვეძლევა სრული თავისუფლება - შეგვიძლია სადაც რა გვინდა ის ვუქნათ ამ ინფორმაციას 😀
- უკუფუნქციის გარეშე: მიღებული ინფორმაციის დამუშავების პროცესი სტატიკურია, მრავალჯერადად გამოყენებადობის შესაძლებლობა ნაკლები : თუ მიღებულ ინფორმაციას, აპლიკაციის ერთ კონკრეტულ ადგილას ელ_ფოსტაზე ვგზავნით სხვა ადგილას კი მისი ბაზაში შენახვა გვჭირდება, ინფორმაციის წამოღების ფუნქციონალიც ხელახლა უნდა აღიწეროს, ეს კი დუბლირებულ კოდს ნიშნავს, რაც არც ისე კარგი პრაქტიკაა.
-
ასინქრონული დამუშავება:
- უკუფუნქციით: ჯავასკრიპტში უკუფუნქცია არის ასინქრონული ოპერაციების დამუშავების სტანდარტი, რომელიც საშუალებას გვაძლევს ვაკონტროლოთ ქმედებები, რომლებიც ასინქრონული ოპერაციის დასრულების შემდეგ უნდა შესრულდეს.
- უკუფუნქციის გარეშე: შედარებით რთულია ასინქრონული ოპერაციის დასრულების შემდეგ შესასრულებელი ქმედებების კონტროლი, რაც კოდს მოუქნელს ხდის.
დაპირებები
საბედნიეროდ, ჯავასკრიპტში (ES6 - ECMAScript 2015) არსებობს ამ ყველაფრის შედარებით მარტივად და გასაგებად ჩაწერის გზები - დაპირებები.
ტერმინი 'დაპირება (Promise)' მომდინარეობს ინფორმატიკაში არსებული დაპირების კონცეფციიდან, რომელიც დაკავშირებულია ასინქრონული ოპერაციის მოსალოდნელ წარმატება/წარუმატებლობასთან. ანუ 'გვპირდება', მაგრამ გარანტიას ვერ 'გვაძლევს' როდის იქნება საპასუხო ინფორმაცია ხელმისაწვდომი. დაპირება გვეუბნება : 'შენ არ გაჩერდე, გააგრძელე მუშაობა და გააკეთე რისი გაკეთებაც შეგიძლია, მე კი გპირდები რომ, როგორც კი შევძლებ, შემოგაშველებ მოთხოვნილ ინფორმაციას (ან შეგისრულებ მოთხოვნილ ოპერაციას)'.
დაპირებებთან სამუშაოდ გამოიყენება ჯავასკრიპტში არსებული Promise კლასი, რომლის ძირითადი დანიშნულებაც, ასინქრონული ოპერაციების შედარებით სუფთად და ლამაზად დაწერაა. მოვიყვანოთ დაპირების შექმნისა და გამოყენების მაგალითი:
let myPromise = new Promise(function(myResolve, myReject) {
// როგორც წესი, აქ ხდება ხოლმე ასინქრონული ოპერაციის ჩატარება, რომელიც შეიძლება დასრულდეს წარმატებით ან წარუმატებლად
let success = true; // წარმატების სიმულაცია
// რეალურ გარემოში დაგვიბრუნდება კონკრეტული შედეგი, მაგალითად მონაცემთა ბაზიდან ამოღებული ჩანაწერები
// შედეგი შეიძლება შევინახოთ მაგალითად result ცვლადში
// შემდეგ ცვლადი გადავცეთ myResolve ფუნქციას
if (success) {
let result = 'დასრულდა წარმატებით' // ამ ეტაპზე myResolve ფუნქციას გადავცეთ სტატიკური ტექსტი
myResolve(result);
} else {
let error = 'დაფიქსირდა ხარვეზი' // გადავცეთ შეცდომის სტატიკური შეტყობინება
myReject(error); //
}
});
პირველ რიგში შეიქმნა Promise კლასის ობიექტი 'myPromise'.
Promise კლასს არგუმენტად გადაეცა ფუნქცია, რომელსაც, თავის მხრივ, გადაეცა ორი არგუმენტი - myResolve და myReject. ორივე მათგანი თავის თავში მოიცავს ასევე ფუნქციას. myResolve ასოცირდება სცენარის წარმატებით განვითარებასთან, myReject კი პირიქით - წარუმატებლობასთან.
შემდეგ გვაქვს პირობითი ოპერატორი, რომელიც ამოწმებს თუ რა შედეგით დასრულდა ასინქრონული ოპერაცია.
შედეგების დამუშავება
ნებისმიერ დაპირებას, დროის კონკრეტულ მომენტში შეიძლება გააჩნდეს ამ სამი სტატუსიდან ერთ-ერთი :
- Pending - მიმდინარე, დაპირება ახლახანს შეიქმნა და ჯერ მისი შედეგი უცნობია
- Fulfilled - დასრულებული, დაპირება წარმატებით შესრულდა და შედეგად მივიღეთ კონკრეტული მნიშვნელობა
- Rejected - უარყოფილი, რაღაც მიზეზების გამო დაპირება ვერ გაეშვა შესრულებაზე, დაგვიბრუნდა კონკრეტული შეცდომის ობიექტი
დაპირების შედეგების დასამუშავებლად გამოიყენება then და catch მეთოდები. პირველი წარმატების დასამუშავებლად, მეორე - წარუმატებლობის.
myPromise.then(
// ამ ფუნქციის ფსევდონიმია myResolve
function(result) {
console.log(result) // result ცვლადში მოთავსდება myResolve ფუნქციისათვის გადაცემული არგუმენტი ანუ "დასრულდა წარმატებით"
},
).catch(
// ამ ფუნქციის ფსევდონიმია myReject
function(error) {
console.log(error); // error ცვლადში მოთავსდება myReject ფუნქციისათვის გადაცემული არგუმენტი ანუ "დაფიქსირდა ხარვეზი"
}
);
ეს ყველაფერი საბოლოოდ მიიღებს ასეთ სახეს :
let myPromise = new Promise(function(myResolve, myReject) {
let success = true;
if (success) {
let result = 'დასრულდა წარმატებით'
myResolve(result);
} else {
let error = 'დაფიქსირდა ხარვეზი'
myReject(error); //
}
});
myPromise.then(
function(result) {
console.log(result)
},
).catch(
function(error) {
console.log(error);
}
);
ეს გახლავთ დაპირებების გამოყენების ზოგადი სინტაქსი. ახლა კი ზემოთ მოყვანილი ამოცანა გადავჭრათ დაპირებების მეშვეობით :
let myPromise = new Promise(function(myResolve, myReject) {
const xhr = new XMLHttpRequest();
xhr.open('GET', 'https://jsonplaceholder.typicode.com/users/1', true);
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) { // მოთხოვნა გაკეთდა
if (xhr.status === 200) { // წარმატებული პასუხი
let user = JSON.parse(xhr.responseText);
myResolve(user);
} else {
let error = 'დაფიქსირდა ხარვეზი';
myReject(error);
}
}
};
xhr.send();
});
myPromise.then(
function(user) {
console.log('შეტყობინება იგზავნება მისამართზე: ' + user.email);
}
).catch(
function(error) {
console.error(error);
}
);
უნდა აღიშნოს რომ, მოყვანილ შემთხვევაში სპეციალურად არ ვიყენებთ ე.წ ისარ-ფუნქციებს, რადგან ეს შეიძლება დამაბნეველი იყოს დამწყებისთვის, რომელსაც არც დაპირებებთან მუშაობის გამოცდილება აქვს და არც ფუნქციების ასეთი სახით წერის :
let myPromise = new Promise((myResolve, myReject) => {
const xhr = new XMLHttpRequest();
xhr.open('GET', 'https://jsonplaceholder.typicode.com/users/1', true);
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) { // მოთხოვნა გაკეთდა
if (xhr.status === 200) { // წარმატებული პასუხი
let user = JSON.parse(xhr.responseText);
myResolve(user);
} else {
let error = 'დაფიქსირდა ხარვეზი';
myReject(error);
}
}
};
xhr.send();
});
myPromise.then(
(user) => {
console.log('შეტყობინება იგზავნება მისამართზე: ' + user.email);
}
).catch(
(error) => {
console.error(error);
}
);
თუმცა ესეც უნდა აღინიშნოს - ეს კოდი უფრო წაკითხვადი და მარტივია.
async/await
async/await მიდგომის მეშვეობით კიდევ უფრო მარტივდება დაპირებებთან მუშაობა.
სიტყვაგასაღებები async და await წარმოადგენენ ასინქრონული ოპერაციების დამუშავების განუყოფელ ნაწილს თანამედროვე ჯავასკრიპტში. მათი დანიშნულების შესახებ მათივე დასახელებები გვამცნობენ :
- async: ეს სიტყვაგასაღები გამოიყენება ასინქრონული ფუნქციის აღსაწერად, რომელიც ყოველთვის დააბრუნებს დაპირებას. თუ დაბრუნდება კონკრეტული შედეგი, მაშინ ეს შედეგი ავტომატურად მოექცევა წარმატებულ (resolved) დაპირებაში, ხოლო თუ დაფიქსირდება შეცდომა, მაშინ ეს შეცდომა, ავტომატურად მოექცევა უარყოფილ (rejected) დაპირებაში.
- await: ეს სიტყვაგასაღები გამოიყენება მხოლოდ async ფუნქციაში. იგი წყვეტს ფუნქციის მუშაობას, მანამ სანამ დაპირება არ გადაწყდება და კონკრეტული პასუხი არ მიიღება. უნდა აღინიშნოს, რომ წყდება მხოლოდ async ფუნქციის მუშაობა, დაპირების მოგვარების შემდეგ კი ფუნცია ისევ აგრძელებს მუშაობას. ეს ყოველივე საშუალებას გვაძლევს ასინქრონული კოდი ჩაიწეროს სინქრონული კოდის სტილში, გავარჩიოთ რის მერე რა სრულდება.
მოვიყვანოთ დაპირების მაგალითი :
function fetchData() {
return new Promise(resolve => {
setTimeout(() => {
resolve('ინფორმაციის ამოღება დასრულდა');
}, 2000);
});
}
fetchData()
.then(data => {
console.log(data);
return fetchData();
})
.then(data => {
console.log(data);
})
.catch(error => {
console.error(error);
});
console.log('ეს ხაზი შესრულდება fetchData() ფუყნქციის გამოძახების შემდეგ');
ახლა იგივე ჩავწეროთ async/await-ის მეშვეობით :
function fetchData() {
return new Promise(resolve => {
setTimeout(() => {
resolve('ინფორმაციის ამოღება დასრულდა');
}, 2000);
});
}
async function fetchAndLogData() {
try {
const data1 = await fetchData();
console.log(data1);
const data2 = await fetchData();
console.log(data2);
} catch (error) {
console.error(error);
}
}
fetchAndLogData();
console.log('ეს ხაზი შესრულდება fetchData() ფუყნქციის გამოძახების შემდეგ');
ამ მაგალითში :
- await სიტყვაგასაღები აპაუზებს fetchAndLogData ფუნქციას, მანამ, სანამ fetchData() დაპირება არ მოგვარდება (ინგ: await - ცდა, იცდის, უცდის, ლოდინი).
- fetchAndLogData ფუნქციაში კოდი დაწერილია თანმიმდევრული, წრფიული სტილით და მისი გარჩევა და წაკითხვა შედარებით მარტივია. ადრე აღვნიშნეთ, რომ სინქრონული მიდგომისას უფრო ლამაზი კოდი იწერებოდა, async/await-მა კი საშუალება მოგვცა ასინქრონული კოდი დაგვეწერა სინქრონულის სტილში 💖.
- მიუხედავად იმისა, რომ კოდი სინქრონულის მაგვარად გამოიყურება, პროცესი მაინც არამბლოკავი პრინციპით გრძელდება : fetchAndLogData ფუნქციის გარეთ არსებული კოდი ჩვეულებრივად გააგრძელებს მუშაობას.
- გაცილებით მარტვივად და ლამაზად ხდება შეცდომების დამუშავება try/catch ბლოკების მეშვეობით. async/await-ის კონტექსტში 'try' არის დაპირების 'then' მეთოდის ანალოგი, 'catch' კი იგივე ფუნქციას ასრულებს რასაც დაპირებაში ასრულდებდა.
დავუბრუნდეთ ჩვენს მიერ ადრე მოყვანილ მაგალითს :
let myPromise = new Promise((myResolve, myReject) => {
const xhr = new XMLHttpRequest();
xhr.open('GET', 'https://jsonplaceholder.typicode.com/users/1', true);
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) { // მოთხოვნა გაკეთდა
if (xhr.status === 200) { // წარმატებული პასუხი
let user = JSON.parse(xhr.responseText);
myResolve(user);
} else {
let error = 'დაფიქსირდა ხარვეზი';
myReject(error);
}
}
};
xhr.send();
});
myPromise.then(
(user) => {
console.log('შეტყობინება იგზავნება მისამართზე: ' + user.email);
}
).catch(
(error) => {
console.error(error);
}
);
ახლა ეს ყველაფერი ჩავწეროთ async/await-ის მეშვეობით :
async function getUserData() {
try {
// ინფორმაციის წამოღება API-დან
const response = await fetch('https://jsonplaceholder.typicode.com/users/1');
// წარმატებულია თუ არა პასუხი ?
if (!response.ok) {
throw new Error('დაფიქსირდა შეცდომა');
}
// დავაფორმატოთ პასუხი
const user = await response.json();
// შევასრულოთ ელ_ფოსტასთან დაკავშირებული ოპერაცია
console.log('შეტყობინება იგზავნება მისამართზე: ' + user.email);
} catch (error) {
// გამოვიტანოთ შეტყობინება მისი არსებობის შემთხვევაში
console.error(error);
}
}
// გამოვიძახოთ async ფუნქცია
getUserData();
***
სულ ეს იყო რისი თქმაც მსურდა და შევძელი ასინქრონული ჯავასკრიპტის და ზოგადად - ჯავასკრიპტის შესახებ 💖
ჯავასკრიპტის სრული კურსი შეგიძლიათ იხილოთ აქ.
კითხვებისა და გაუგებრობების შემთხვევაში გთხოვთ დატოვოთ კომენტარი 💖
გისურვებთ წარმატებას ! 💖