מדריך Node.js: קוד אסינכרוני מותנה ומקבילי

כמה דרכים ליצור פונקציות אסינכרוניות ב-Node.js
Node.js Logo

במאמר הקודם דיברנו על קוד אסינכרוני – במאמר הזה נמשיך לדבר על כל מיני סוגים של קוד אסינכרוני. קוד אסינכרוני הוא אחד מהפיצ'רים החשובים של Node.js ולפיכך חובה ללמוד אותו היטב.

כל הדוגמאות שאני משתמש בהן פה הובאו מ: Understanding the node.js loop.

קוד אסינכרוני מותנה

בדוגמה שהראיתי במאמר הקודם, לא היתה חשיבות לסדר הפעולות – ההדפסה של ה-processId היתה יכולה להתקיים לאחר הקריאה למערכת הקבצים. איך אנחנו כותבים קוד אסינכרוני שיש חשיבות לסדר של הפעולות?

על מנת להדגים, אני אראה קוד שמשתמש ב-fs (בדיוק כמו הקוד הקודם) – המודול של Node.js לטיפול בקבצים. ראשית קוד סינכרוני:


var fs = require('fs');

oldFilename = "./processId.txt";
newFilename = "./processIdOld.txt";

fs.chmodSync(oldFilename, 777);
fs.renameSync(oldFilename, newFilename);
isSymLink = fs.lstatSync(newFilename).isSymbolicLink();
console.log("The file is symbolic link? "+isSymLink);

מה הקוד הזה עושה? הוא לוקח קובץ בשם processId.txt:
1. משנה לו את ההרשאות ל-777.
2. משנה את השם שלו.
3. בודק אם השם החדש הוא לינק סימבולי (מן הסתם לא, אבל זה לא משנה).

הפעולות תלויות אחת בשניה. אם אני אנסה לשנות את השם של הקובץ לפני שאני אשנה את ההרשאות שלו ל-777, הפעולה תכשל. אם אני אבדוק את שם הקובץ החדש לפני שעשיתי שינוי שם, גם כן הפעולה תכשל. כלומר יש חשיבות עצומה לסדר הפעולות. איך אני יוצר מזה קוד אסינכרוני? בעצם כל מה שאני צריך לעשות זה ליצור שלוש callbacים. מההתחלה לסוף – קודם כל, שינוי הרשאה. שינוי ההרשאה מתבצע? מעולה! הוא יורה callback שעושה שינוי שם. שינוי השם מתבצע? מעולה. הוא יורה callback שבודק לינק סימבולי.

איך זה נראה? ככה:


var fs = require('fs');

oldFilename = "./processId.txt";
newFilename = "./processIdOld.txt";

fs.chmod(oldFilename, 777, function (err) {   
    fs.rename(oldFilename, newFilename, function (err) {
        fs.lstat(newFilename, function (err, stats) {
            var isSymLink = stats.isSymbolicLink();
            console.log("The file is symbolic link? "+isSymLink);
        });
    });
});

אגב, אם זה נראה לכם גיהנום של callbacks, זה בדיוק מה שזה. זו אחת מנקודות התורפה של Node.js לפי דעתי: שאם לא נזהרים מאוד קל להכנס לתוך חגיגת callbacks מטורפת. אני אסביר בהמשך איך ממזערים את הסיכון לטירוף callbacks.

קוד אסינכרוני שרץ במקביל

אנחנו יכולים להשתמש בקוד אסינכרוני גם למקבל משימות – כלומר להריץ משימות במקביל. לא במובן של concurrent (כלומר שרץ באותו הזמן) אלא במובן של זרימת קוד – רץ באותו קטע קוד. בוא נסתכל על הקוד הסינכרוני הזה למשל:


var fs = require('fs');

function calculateByteSize() {
    var totalBytes = 0,
        i,
        filenames,
        stats;
    filenames = fs.readdirSync(".");
    for (i = 0; i < filenames.length; i ++) {
        stats = fs.statSync("./" + filenames[i]);
        totalBytes += stats.size;
    }
    console.log(totalBytes);
}

calculateByteSize();

הקוד הזה פשוט ביותר, הוא נועד לחשב את מספר הביטים בקבצים בתיקיה. ראשית אני מקבל את רשימת הקבצים ואז אני מריץ אותם בלולאה. בלולאה אני קורא את גודל הקובץ ומכניס אותו למשתנה. את המשתנה אני מדפיס.

זה קוד סינכרוני, בקוד א-סינכרוני אני גם אבצע את כל השלבים – אבל באופן אסינכרוני – כולל בלולאה. איך זה יראה? כ:


var fs = require('fs');

var count = 0,
    totalBytes = 0;

function calculateByteSize() {
    fs.readdir(".", function (err, filenames) {
        var i;
        count = filenames.length;

        for (i = 0; i < filenames.length; i++) {
            fs.stat("./" + filenames[i], function (err, stats) {
                totalBytes += stats.size;
                count--;
                if (count === 0) {
                    console.log(totalBytes);
                }
            });
        }
    });
}

calculateByteSize();

קודם כל קריאת הקבצים מתבצעת באופן אסינכרוני, ה-callback כולל את הלולאה והוא מעביר ללולאה את שמות הקבצים. בתוך הלולאה גם יש callback. בתוך ה-callback יש הוספה של הבייטים למשתנה וכשהלולאה נגמרת ומקבלים את כל התוצאות – הכל מושלם. כך הכל נעשה במקביל. אם יש עוד פונקציות שמופעלות וה-fs עדיין עובד על החישובים, הסקריפט ימשיך לעבוד.

מה שיש בקוד הזה, בנוסף לאסינכרניות מקבילה הוא גם שימוש ב-closure – למי שלא מכיר, כדאי לקרוא על closure במאמר שכתבתי. אבל בגדול זה היכולת של הפונקציות הפנימיות לקרוא למשתנים שזמינים לפונקציות שקראו להם. זו אחת התכונות השימושיות של JavaScript – אם יש משתנה שהוגדר ב-scope של פונקציה א', הוא יהיה זמין גם לפונקציה ב' (או callback) שנקרא ממנה.

קוד אסינכרוני כפונקציה

עד עכשיו ראינו את הקוד האסינכרוני בתור פונקציה אנונימית (שזה ה-callback). למרות ש-callback חייב להיות מוגדר כפונקציה אנונימית ברוב המקרים, אפשר להשתמש בפונקציות בתוך פונקציה אנונימית.

אני אדגים באמצעות פונקציה אסינכרונית מהסוג הזה:


var fs = require('fs');

var path1 = "./",
    path2 = ".././",
    logCount;

function countFiles(path, callback_function) {
    fs.readdir(path, function (err, filenames) {
        callback_function(err, path, filenames.length);
    });
}

logCount = function (err, path, count) {
    console.log(count + " files in " + path);
};

countFiles(path1, logCount);
countFiles(path2, logCount);

אני מגדיר את countFiles עם ארגומנט שנקרא callback_function. שימו לב ש-countFiles היא פונקציה רגילה לחלוטין. בתוך הפונקציה יש קריאה ל-fs.readdir שמקבלת את ארגומנט path וכיאה וכיאות לפונקציה אסינכרונית היא גם כוללת פונקציה אנונימית כ-callback. כשה-callback האנונימי נורה, מיד אחרי ש-fs.readdir מסתיימת – אז אני יכול להתמש ב-callback_function שהיא בעצם שם הפונקציה. איך callback_function זמין ל-callback? רק בגלל ה-closure.
הכל מופעל עם countFiles. אני מעביר לה שני ארגומנטים – אחד מהם הוא שם הפונקציה, או ה-callback_function, שאני רוצה להעביר – שמו בישראל logCount והוא עושה את המסלול שתיארתי ממש עכשיו. עובר ל-countFiles כארגומנט ונקרא בתוך ה-callback של fs.readdir.

לסיכום: קוד אסינכרוני הוא קוד שקשה להבין אותו בהתחלה – לוקח זמן להבין למה צריך את ה-callback, איך ה-scoping עובד ואיך להעביר משתנים מה-scope הכללי אל ה-scope של ה-callback. אבל התוצאה היא, לפחות ב-Node.js, קוד שרץ מאוד מאוד מהר – כיוון שכל הפעולות של ה-Input Output (למערכת הקבצים, למסד הנתונים וכו') רצות והקוד שלי לא מחכה להן.

במאמר הבא נדבר על require.

⚠️ תזכורת – המדריכים האלו הם רק טעימה, בספר שלי "ללמוד Node.js בעברית" יש הסברים מלאים ומקיפים על השפה המיועדים ללימוד עצמי. עם תרגילים והסברים. הספר יצא לאור בשיתוף הקריה האקדמית אונו ובתמיכת החברות אלמנטור, ו-Iron source ונערך טכנית על ידי בנג'י גרינבאום (מפתח ליבה של Node.js), גיל פינק ומתכנתים מעולים נוספים. 

פוסטים נוספים שכדאי לקרוא

גלילה לראש העמוד