תכנות אסינכרוני

תכנות אסינכרוני הוא הלב של Node.js. זה מה שמאפשר לו להיות מהיר ויעיל.


למה צריך אסינכרוניות?

הבעיה עם קוד סינכרוני

// Code סינכרוני - חוסם!
const data = readFileSync('big-file.txt');  // ממתין 2 שניות
console.log('קובץ נקרא');
processOtherRequest();  // מתחיל רק אחרי שהקובץ נקרא

// בזמן ש-readFileSync עובד, השרת לא יכול לעשות כלום אחר!

הפתרון האסינכרוני

// Code אסינכרוני - לא חוסם!
readFile('big-file.txt', (data) => {
    console.log('קובץ נקרא');
});
processOtherRequest();  // מתחיל מיד!

// בזמן שהקובץ נקרא, השרת ממשיך לעבוד על דברים אחרים
סינכרוני:    [קריאת קובץ......] → [עיבוד בקשה] → [סיום]
                                                   זמן: 3 שניות

אסינכרוני:   [התחלת קריאה]
             [עיבוד בקשה] ← קורה במקביל!
             [קריאה הסתיימה]                       זמן: 2 שניות

Callbacks

הדרך הראשונה (והישנה ביותר) לטפל באסינכרוניות.

מהו Callback?

פונקציה שנשלחת כפרמטר ומופעלת כשהפעולה מסתיימת.

const fs = require('fs');

// קריאת קובץ עם callback
fs.readFile('file.txt', 'utf8', (error, data) => {
    // הפונקציה הזו תרוץ כשהקריאה תסתיים
    if (error) {
        console.error('שגיאה:', error.message);
        return;
    }
    console.log('תוכן הקובץ:', data);
});

console.log('זה יודפס ראשון!');

מוסכמה: Error-First Callback

ב-Node.js, ה-callback תמיד מקבל את השגיאה כפרמטר הראשון:

function doSomethingAsync(callback) {
    // אם הצליח
    callback(null, result);

    // אם נכשל
    callback(new Error('משהו השתבש'));
}

// שימוש
doSomethingAsync((error, result) => {
    if (error) {
        // טיפול בשגיאה
        return;
    }
    // שימוש בתוצאה
});

Callback Hell 😱

כשיש הרבה פעולות אסינכרוניות בזו אחר זו:

// ❌ Callback Hell - קשה לקריאה ולתחזוקה
fs.readFile('file1.txt', 'utf8', (err1, data1) => {
    if (err1) throw err1;
    fs.readFile('file2.txt', 'utf8', (err2, data2) => {
        if (err2) throw err2;
        fs.readFile('file3.txt', 'utf8', (err3, data3) => {
            if (err3) throw err3;
            fs.writeFile('result.txt', data1 + data2 + data3, (err4) => {
                if (err4) throw err4;
                console.log('סיימנו!');
            });
        });
    });
});

הפתרון? Promises!


Promises

Promise הוא אובייקט שמייצג פעולה אסינכרונית שעוד לא הסתיימה.

שלושה מצבים של Promise

┌─────────────────────────────────────────────────────┐
│                     Promise                          │
├─────────────────────────────────────────────────────┤
│  Pending (ממתין) → Fulfilled (הצליח) ──→ .then()    │
│                  → Rejected (נכשל) ────→ .catch()   │
└─────────────────────────────────────────────────────┘

יצירת Promise

// Create Promise ידנית
const myPromise = new Promise((resolve, reject) => {
    // פעולה אסינכרונית
    setTimeout(() => {
        const success = true;

        if (success) {
            resolve('הפעולה הצליחה!');  // מעביר לתוצאה
        } else {
            reject(new Error('הפעולה נכשלה'));  // מעביר לשגיאה
        }
    }, 1000);
});

שימוש ב-Promise

myPromise
    .then(result => {
        console.log(result);  // 'הפעולה הצליחה!' אחרי שנייה
    })
    .catch(error => {
        console.error(error.message);
    })
    .finally(() => {
        console.log('סיום (תמיד מתבצע)');
    });

שרשור Promises

const fs = require('fs').promises;  // גרסת Promises של fs

// ✅ שרשור קריא ונקי
fs.readFile('file1.txt', 'utf8')
    .then(data1 => {
        console.log('קובץ 1:', data1);
        return fs.readFile('file2.txt', 'utf8');
    })
    .then(data2 => {
        console.log('קובץ 2:', data2);
        return fs.readFile('file3.txt', 'utf8');
    })
    .then(data3 => {
        console.log('קובץ 3:', data3);
    })
    .catch(error => {
        console.error('שגיאה:', error.message);
    });

async/await

הדרך הכי מודרנית וקריאה לעבוד עם קוד אסינכרוני.

הרעיון הבסיסי

// .then() lots
fetchData()
    .then(data => process(data))
    .then(result => save(result));

// ...אפשר לכתוב כמו קוד סינכרוני!
async function main() {
    const data = await fetchData();
    const result = await process(data);
    await save(result);
}

חוקים חשובים

// 1. await יכול להופיע רק בתוך פונקציית async
async function myFunction() {
    const result = await somePromise();
}

// 2. פונקציית async תמיד מחזירה Promise
async function greet() {
    return 'שלום';
}
greet().then(msg => console.log(msg));  // 'שלום'

// 3. אפשר להשתמש ב-await עם כל Promise
const fs = require('fs').promises;

async function readFiles() {
    const data = await fs.readFile('file.txt', 'utf8');
    console.log(data);
}

טיפול בשגיאות עם try/catch

async function readFile() {
    try {
        const data = await fs.readFile('file.txt', 'utf8');
        console.log(data);
        return data;
    } catch (error) {
        console.error('שגיאה בקריאת הקובץ:', error.message);
        throw error;  // העברת השגיאה הלאה (אופציונלי)
    }
}

דוגמה מלאה

const fs = require('fs').promises;

async function processFiles() {
    try {
        // קריאת שלושה קבצים בזה אחר זה
        const data1 = await fs.readFile('file1.txt', 'utf8');
        const data2 = await fs.readFile('file2.txt', 'utf8');
        const data3 = await fs.readFile('file3.txt', 'utf8');

        // עיבוד
        const result = data1 + data2 + data3;

        // כתיבה
        await fs.writeFile('result.txt', result);
        console.log('הכל הושלם!');

    } catch (error) {
        console.error('שגיאה:', error.message);
    }
}

// הפעלה
processFiles();

Promise.all ו-Promise.race

Promise.all - הרצה במקביל

ממתין שכל ה-Promises יסתיימו.

const fs = require('fs').promises;

async function readAllFiles() {
    // ❌ לא יעיל - קובץ אחרי קובץ
    const data1 = await fs.readFile('file1.txt', 'utf8');  // 1 שנייה
    const data2 = await fs.readFile('file2.txt', 'utf8');  // 1 שנייה
    const data3 = await fs.readFile('file3.txt', 'utf8');  // 1 שנייה
    // סה"כ: 3 שניות

    // ✅ יעיל - במקביל!
    const [data1, data2, data3] = await Promise.all([
        fs.readFile('file1.txt', 'utf8'),
        fs.readFile('file2.txt', 'utf8'),
        fs.readFile('file3.txt', 'utf8')
    ]);
    // סה"כ: ~1 שנייה
}

⚠️ שימו לב: אם Promise אחד נכשל, כל ה-Promise.all נכשל!

Promise.allSettled - גם אם יש כשלונות

const results = await Promise.allSettled([
    fs.readFile('file1.txt', 'utf8'),
    fs.readFile('file2.txt', 'utf8'),
    fs.readFile('nonexistent.txt', 'utf8')  // יכשל
]);

results.forEach(result => {
    if (result.status === 'fulfilled') {
        console.log('הצליח:', result.value);
    } else {
        console.log('נכשל:', result.reason.message);
    }
});

Promise.race - הראשון שמסיים מנצח

// Usefull ל-timeout
async function fetchWithTimeout(url, timeout) {
    return Promise.race([
        fetch(url),
        new Promise((_, reject) => 
            setTimeout(() => reject(new Error('Timeout')), timeout)
        )
    ]);
}

try {
    const response = await fetchWithTimeout('https://api.example.com', 5000);
} catch (error) {
    console.log('הבקשה ארכה יותר מדי זמן');
}

Promise.any - הראשון שמצליח

// Search את השרת הזמין הראשון
const result = await Promise.any([
    fetch('https://server1.com'),
    fetch('https://server2.com'),
    fetch('https://server3.com')
]);
// מחזיר את התגובה מהשרת שענה ראשון

דפוסים נפוצים

IIFE (Immediately Invoked Function Expression)

// async הפעלה מיידית של פונקציית 
(async () => {
    try {
        const data = await fetchData();
        console.log(data);
    } catch (error) {
        console.error(error);
    }
})();

עיבוד מערך אסינכרוני

const urls = ['url1', 'url2', 'url3'];

// ❌ forEach לא מחכה ל-await!
urls.forEach(async (url) => {
    const data = await fetch(url);  // לא מחכה!
});

// ✅ אופציה 1: for...of (בזה אחר זה)
for (const url of urls) {
    const data = await fetch(url);
    console.log(data);
}

// ✅ אופציה 2: Promise.all (במקביל)
const results = await Promise.all(
    urls.map(url => fetch(url))
);

Sequential vs Parallel

// Sequential (סדרתי) - כל אחד ממתין לקודם
async function sequential() {
    const a = await task1();  // 1 שנייה
    const b = await task2();  // 1 שנייה
    const c = await task3();  // 1 שנייה
    // סה"כ: 3 שניות
}

// Parallel (מקבילי) - כולם יחד
async function parallel() {
    const [a, b, c] = await Promise.all([
        task1(),  // 1 שנייה
        task2(),  // 1 שנייה
        task3()   // 1 שנייה
    ]);
    // סה"כ: ~1 שנייה
}

המרת Callbacks ל-Promises

שימוש ב-util.promisify

const util = require('util');
const fs = require('fs');

// המרה ידנית
const readFilePromise = util.promisify(fs.readFile);

async function main() {
    const data = await readFilePromise('file.txt', 'utf8');
    console.log(data);
}

// או פשוט להשתמש ב-fs.promises
const fsPromises = require('fs').promises;

עטיפה ידנית

function readFileAsync(path) {
    return new Promise((resolve, reject) => {
        fs.readFile(path, 'utf8', (error, data) => {
            if (error) {
                reject(error);
            } else {
                resolve(data);
            }
        });
    });
}

סיכום

שיטה יתרונות חסרונות
Callbacks פשוט, עובד בכל מקום Callback Hell
Promises שרשור נקי עדיין מעט מסורבל
async/await קריא כמו קוד סינכרוני דורש try/catch

כללי אצבע

  1. השתמשו ב-async/await - הכי קריא ונוח
  2. תמיד טפלו בשגיאות - try/catch או .catch()
  3. השתמשו ב-Promise.all - כשצריך לעשות דברים במקביל
  4. הימנעו מ-forEach עם async - השתמשו ב-for...of או map

← חזרה למודולים | → חזרה לדף הראשי

בדקו את עצמכם

נסו לענות לבד לפני שאתם פותחים את התשובה.

  1. מה הבעיה בקריאה סינכרונית (למשל readFileSync) בשרת?

    1. היא חוסמת את ה-Event Loop, והשרת לא עונה לאף אחד בזמן הזה
    2. היא לא קוראת קבצים
    3. היא מוחקת את הקובץ
    4. אין בעיה
    הצגת התשובה

    תשובה א. בשרת משתמשים תמיד בגרסאות ה-Promise עם await.

  2. איך מריצים כמה פעולות בלתי תלויות במקביל?

    1. await אחרי await
    2. לולאת for
    3. Promise.all
    4. setTimeout
    הצגת התשובה

    תשובה ג. Promise.all מחכה לכולן יחד, במקום אחת אחרי השנייה.