4.7 לולאת האירועים ותכנות פונקציונלי הרצאה
ג'אווהסקריפט היא שפה חד-תהליכונית - Single Threaded¶
ג'אווהסקריפט רצה על thread יחיד. זה אומר שהיא יכולה לבצע רק פעולה אחת בכל רגע נתון.
- אין ריבוי threads כמו בג'אווה או C++
- אם פעולה כבדה חוסמת את ה-thread, כל הדף קופא - כולל ממשק המשתמש
- אז איך fetch, setTimeout ו-event listeners עובדים בלי לחסום? התשובה: לולאת האירועים
מחסנית הקריאות - Call Stack¶
ה-Call Stack הוא מבנה נתונים שעוקב אחרי הפונקציות שרצות כרגע.
- כשקוראים לפונקציה, היא נדחפת למחסנית
- כשפונקציה מסתיימת, היא יוצאת מהמחסנית
- JS מבצעת את מה שנמצא בראש המחסנית
function multiply(a, b) {
return a * b;
}
function square(n) {
return multiply(n, n);
}
function printSquare(n) {
const result = square(n);
console.log(result);
}
printSquare(4);
מה קורה במחסנית:
printSquare(4)נכנסת למחסניתsquare(4)נכנסת למחסניתmultiply(4, 4)נכנסת למחסניתmultiplyמחזירה 16 ויוצאתsquareמחזירה 16 ויוצאתconsole.log(16)נכנסת ויוצאתprintSquareמסתיימת ויוצאת- המחסנית ריקה
ממשקי הדפדפן - Web APIs¶
הדפדפן מספק APIs שרצים מחוץ ל-thread הראשי של JS:
setTimeout/setInterval- טיימריםfetch- בקשות רשתaddEventListener- האזנה לאירועיםrequestAnimationFrame- אנימציותgeolocation,IntersectionObserverועוד
כשקוראים ל-setTimeout(fn, 1000), הדפדפן מתחיל טיימר ברקע. ה-JS ממשיכה הלאה. כשהטיימר מסתיים, הדפדפן מכניס את fn לתור.
תור המשימות - Macrotask Queue¶
כשה-Web API מסיים את העבודה שלו, הקולבק לא רץ מיד. הוא נכנס לתור המשימות (macrotask queue).
דוגמאות למשימות שנכנסות לתור:
- קולבקים של setTimeout / setInterval
- קולבקים של אירועי DOM (click, input...)
הפלט:
למרות שה-timeout הוא 0 מילישניות, הקולבק לא רץ מיד. הוא נכנס לתור ומחכה שהמחסנית תתרוקן.
תור המיקרו-משימות - Microtask Queue¶
יש תור נוסף עם עדיפות גבוהה יותר - תור המיקרו-משימות.
מה נכנס לתור המיקרו-משימות:
- קולבקים של Promises (then, catch, finally)
- queueMicrotask()
- MutationObserver
הכלל: לולאת האירועים מרוקנת את כל המיקרו-משימות לפני שהיא עוברת למשימה הבאה בתור הרגיל.
console.log("Start");
setTimeout(() => {
console.log("Timeout");
}, 0);
Promise.resolve().then(() => {
console.log("Promise");
});
console.log("End");
הפלט:
"Start"ו-"End"רצים מיד (קוד סינכרוני)"Promise"רץ לפני"Timeout"כי מיקרו-משימות מתבצעות לפני מאקרו-משימות
מחזור לולאת האירועים - Event Loop Cycle¶
לולאת האירועים עובדת בלולאה אינסופית לפי הסדר הזה:
- הרצת כל הקוד הסינכרוני (עד שהמחסנית ריקה)
- ריקון כל תור המיקרו-משימות
- ביצוע משימה אחת מתור המאקרו-משימות
- חזרה לשלב 2
חשוב: אם מיקרו-משימה מוסיפה מיקרו-משימה נוספת, גם היא תרוץ לפני המאקרו-משימה הבאה.
console.log("1");
setTimeout(() => {
console.log("2");
Promise.resolve().then(() => console.log("3"));
}, 0);
setTimeout(() => {
console.log("4");
}, 0);
Promise.resolve().then(() => {
console.log("5");
Promise.resolve().then(() => console.log("6"));
});
console.log("7");
הפלט:
הסבר:
- 1 ו-7 - קוד סינכרוני
- 5 - מיקרו-משימה מה-Promise הראשון
- 6 - מיקרו-משימה שנוצרה מתוך מיקרו-משימה (עדיין מתרוקנת לפני המאקרו)
- 2 - מאקרו-משימה ראשונה (setTimeout הראשון)
- 3 - מיקרו-משימה שנוצרה מתוך המאקרו-משימה
- 4 - מאקרו-משימה שנייה (setTimeout השני)
נחשו את סדר הפלט - תרגול¶
דוגמה 1¶
console.log("A");
setTimeout(() => console.log("B"), 0);
Promise.resolve()
.then(() => console.log("C"))
.then(() => console.log("D"));
setTimeout(() => console.log("E"), 0);
console.log("F");
תשובה:
דוגמה 2¶
async function foo() {
console.log("foo start");
await Promise.resolve();
console.log("foo end");
}
console.log("start");
foo();
console.log("end");
תשובה:
awaitגורם לשארית הפונקציה לרוץ כמיקרו-משימה"foo start"רץ סינכרונית (לפני ה-await)"foo end"רץ אחרי"end"כי הוא ממתין לסיום ה-await
דוגמה 3¶
setTimeout(() => console.log("1"), 0);
Promise.resolve().then(() => {
console.log("2");
setTimeout(() => console.log("3"), 0);
});
Promise.resolve().then(() => console.log("4"));
setTimeout(() => console.log("5"), 0);
תשובה:
setTimeout(fn, 0) - למה זה שימושי¶
setTimeout(fn, 0) לא אומר "הרץ עכשיו". זה אומר "הרץ ברגע שהמחסנית תתרוקן ותור המיקרו-משימות יתרוקן".
שימושים:
- לתת לדפדפן לעדכן את ה-DOM לפני שממשיכים
- לפצל עבודה כבדה לחלקים קטנים
- לדחות פעולה לאחרי כל ה-event handlers הנוכחיים
button.addEventListener("click", () => {
// update the DOM
output.textContent = "Processing...";
// defer heavy work so the DOM update renders first
setTimeout(() => {
const result = heavyCalculation();
output.textContent = result;
}, 0);
});
בלי ה-setTimeout, הדפדפן לא יספיק לצייר את "Processing..." לפני שהחישוב הכבד מתחיל.
requestAnimationFrame¶
requestAnimationFrame (rAF) מבקש מהדפדפן להריץ פונקציה לפני הציור הבא (בדרך כלל 60 פעמים בשנייה).
function animate() {
// update position, size, color, etc.
element.style.left = position + "px";
position += 2;
if (position < 500) {
requestAnimationFrame(animate); // schedule next frame
}
}
requestAnimationFrame(animate); // start animation
- עדיף על
setIntervalלאנימציות כי הוא מסונכרן עם קצב הרענון של המסך - אם הטאב לא גלוי, rAF מושהה (חוסך משאבים)
- rAF רץ לפני שהדפדפן מצייר, אחרי המיקרו-משימות
סדר ביצוע מלא¶
- קוד סינכרוני (Call Stack)
- מיקרו-משימות (Promises)
- requestAnimationFrame
- ציור מסך (render/paint)
- מאקרו-משימות (setTimeout, events)
חסימה - Blocking¶
אם קוד סינכרוני רץ יותר מדי זמן, הוא חוסם את הכל - ה-UI, אירועים, אנימציות.
// BAD - blocks the main thread
button.addEventListener("click", () => {
// simulate heavy computation
const start = Date.now();
while (Date.now() - start < 3000) {
// busy loop for 3 seconds
}
console.log("Done");
// during these 3 seconds, nothing works
});
פתרונות לחסימה:
- לפצל עבודה כבדה עם setTimeout או requestAnimationFrame
- להשתמש ב-Web Workers לחישובים כבדים (thread נפרד)
- להשתמש בפעולות אסינכרוניות (fetch, IndexedDB)
// GOOD - break heavy work into chunks
function processChunk(items, index, callback) {
const chunkSize = 100;
const end = Math.min(index + chunkSize, items.length);
for (let i = index; i < end; i++) {
// process item
}
if (end < items.length) {
setTimeout(() => processChunk(items, end, callback), 0);
} else {
callback();
}
}
תכנות פונקציונלי - Functional Programming¶
תכנות פונקציונלי (FP) הוא פרדיגמה שבה בונים תוכנה מפונקציות טהורות, נמנעים משינוי מצב, ומשתמשים בהרכבה של פונקציות.
JS לא שפה פונקציונלית טהורה, אבל היא תומכת בהרבה מהעקרונות - ואנחנו כבר משתמשים בחלק מהם (map, filter, reduce).
פונקציות טהורות - Pure Functions¶
פונקציה טהורה:
- מחזירה תמיד את אותה תוצאה עבור אותם ארגומנטים
- אין לה תופעות לוואי (side effects) - לא משנה משתנים חיצוניים, לא כותבת לקובץ, לא משנה DOM
// PURE - same input always gives same output
function add(a, b) {
return a + b;
}
// PURE
function formatName(first, last) {
return `${first} ${last}`;
}
// NOT PURE - depends on external variable
let taxRate = 0.17;
function calculateTax(price) {
return price * taxRate; // result changes if taxRate changes
}
// NOT PURE - side effect (modifies external state)
let total = 0;
function addToTotal(amount) {
total += amount; // modifies external variable
return total;
}
// NOT PURE - side effect (console.log)
function greet(name) {
console.log(`Hello ${name}`); // side effect
}
למה פונקציות טהורות טובות:
- קל לבדוק אותן (לא צריך להכין סביבה)
- קל להבין אותן (לא צריך לעקוב אחרי מצב חיצוני)
- קל להריץ אותן במקביל (אין שינוי של מצב משותף)
- קל לשמור תוצאות ב-cache (מוכרנות - memoization)
אי-שינוי - Immutability¶
באי-שינוי, במקום לשנות אובייקט קיים, יוצרים אובייקט חדש:
// MUTABLE approach - BAD
const user = { name: "Alice", age: 25 };
user.age = 26; // mutating the original object
// IMMUTABLE approach - GOOD
const user2 = { name: "Alice", age: 25 };
const updatedUser = { ...user2, age: 26 }; // new object
// MUTABLE array - BAD
const numbers = [1, 2, 3];
numbers.push(4); // mutating the original array
// IMMUTABLE array - GOOD
const numbers2 = [1, 2, 3];
const newNumbers = [...numbers2, 4]; // new array
פעולות אי-שינוי נפוצות על מערכים¶
const items = [1, 2, 3, 4, 5];
// add item (instead of push)
const added = [...items, 6];
// remove item (instead of splice)
const removed = items.filter(item => item !== 3);
// update item (instead of direct assignment)
const updated = items.map(item => item === 3 ? 30 : item);
// sort without mutation (sort mutates!)
const sorted = [...items].sort((a, b) => b - a);
פעולות אי-שינוי על אובייקטים¶
const user = { name: "Alice", age: 25, address: { city: "Tel Aviv" } };
// update property
const updated = { ...user, age: 26 };
// remove property
const { age, ...withoutAge } = user;
// nested update (need to spread each level)
const movedUser = {
...user,
address: { ...user.address, city: "Jerusalem" }
};
הרכבה - Composition¶
הרכבה היא בניית פונקציות מורכבות משילוב של פונקציות פשוטות:
// small, focused functions
function trim(str) {
return str.trim();
}
function toLowerCase(str) {
return str.toLowerCase();
}
function replaceSpaces(str) {
return str.replace(/\s+/g, "-");
}
// compose them manually
function createSlug(title) {
return replaceSpaces(toLowerCase(trim(title)));
}
createSlug(" Hello World "); // "hello-world"
פונקציית הרכבה כללית¶
function compose(...fns) {
return function (value) {
return fns.reduceRight((acc, fn) => fn(acc), value);
};
}
function pipe(...fns) {
return function (value) {
return fns.reduce((acc, fn) => fn(acc), value);
};
}
// compose reads right to left (like math: f(g(x)))
const createSlug = compose(replaceSpaces, toLowerCase, trim);
// pipe reads left to right (more intuitive)
const createSlug2 = pipe(trim, toLowerCase, replaceSpaces);
createSlug(" Hello World "); // "hello-world"
createSlug2(" Hello World "); // "hello-world"
compose- מפעיל מימין לשמאל (כמו במתמטיקה)pipe- מפעיל משמאל לימין (יותר אינטואיטיבי)
ייצור חלקי - Currying¶
קרינג (currying) הוא הפיכת פונקציה שמקבלת כמה ארגומנטים לסדרה של פונקציות שכל אחת מקבלת ארגומנט אחד:
// regular function
function add(a, b) {
return a + b;
}
add(2, 3); // 5
// curried version
function curriedAdd(a) {
return function (b) {
return a + b;
};
}
curriedAdd(2)(3); // 5
// with arrow functions - more concise
const curriedAdd2 = a => b => a + b;
curriedAdd2(2)(3); // 5
למה currying שימושי - יצירת פונקציות מותאמות:
// create specialized functions
const add5 = curriedAdd(5);
const add10 = curriedAdd(10);
add5(3); // 8
add10(3); // 13
// practical example: filtering
const filterBy = property => value => array =>
array.filter(item => item[property] === value);
const filterByStatus = filterBy("status");
const getActive = filterByStatus("active");
const getInactive = filterByStatus("inactive");
const users = [
{ name: "Alice", status: "active" },
{ name: "Bob", status: "inactive" },
{ name: "Charlie", status: "active" }
];
getActive(users); // [Alice, Charlie]
getInactive(users); // [Bob]
קרינג עם pipe¶
const multiply = a => b => a * b;
const add = a => b => a + b;
const subtract = a => b => b - a;
const double = multiply(2);
const addTen = add(10);
const subtractThree = subtract(3);
const transform = pipe(double, addTen, subtractThree);
transform(5); // ((5 * 2) + 10) - 3 = 17
transform(10); // ((10 * 2) + 10) - 3 = 27
דקלרטיבי מול אימפרטיבי - Declarative vs Imperative¶
קוד אימפרטיבי אומר איך לעשות משהו (צעד אחרי צעד). קוד דקלרטיבי אומר מה רוצים לקבל.
const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// IMPERATIVE - how to do it
const evenDoubled = [];
for (let i = 0; i < numbers.length; i++) {
if (numbers[i] % 2 === 0) {
evenDoubled.push(numbers[i] * 2);
}
}
// DECLARATIVE - what we want
const evenDoubled2 = numbers
.filter(n => n % 2 === 0)
.map(n => n * 2);
דוגמה נוספת - חיפוש והמרה¶
const users = [
{ name: "Alice", age: 25, active: true },
{ name: "Bob", age: 30, active: false },
{ name: "Charlie", age: 35, active: true },
{ name: "Dana", age: 28, active: true }
];
// IMPERATIVE
let result = "";
for (let i = 0; i < users.length; i++) {
if (users[i].active && users[i].age >= 28) {
if (result !== "") {
result += ", ";
}
result += users[i].name;
}
}
// DECLARATIVE
const result2 = users
.filter(user => user.active && user.age >= 28)
.map(user => user.name)
.join(", ");
הגישה הדקלרטיבית:
- קלה יותר לקריאה
- פחות מקום לבאגים (אין ניהול אינדקסים)
- כל שלב עושה דבר אחד
- זה בדיוק מה ש-React עושה - מתארים את ה-UI הרצוי במקום לתפעל DOM ידנית
סיכום¶
לולאת האירועים:
- JS היא חד-תהליכונית, אבל הדפדפן מספק Web APIs שרצים ברקע
- יש שני תורים: מיקרו-משימות (Promises) ומאקרו-משימות (setTimeout, events)
- מיקרו-משימות תמיד רצות לפני מאקרו-משימות
- setTimeout(fn, 0) לא מיידי - מחכה שהמחסנית ותור המיקרו-משימות יתרוקנו
- requestAnimationFrame מסונכרן עם ציור המסך
- קוד סינכרוני כבד חוסם את הכל
תכנות פונקציונלי:
- פונקציות טהורות - אין תופעות לוואי, תוצאה צפויה
- אי-שינוי - יוצרים אובייקטים חדשים במקום לשנות קיימים
- הרכבה - pipe ו-compose לבניית פונקציות מורכבות מפשוטות
- קרינג - currying - יצירת פונקציות מותאמות מפונקציות כלליות
- גישה דקלרטיבית - מתארים מה רוצים, לא איך לעשות