מבוא
המהפכה בדרך. יש תוספת חדשה ל-JavaScript שתשנה הכול שאתם חושבים שאתם יודעים על קישור נתונים. הוא ישנה גם את האופן שבו רבות מספריות ה-MVC שלכם יתבוננו במודלים לצורך עריכה ועדכון. רוצים לקבל שיפורים משמעותיים בביצועים של אפליקציות שמתעניינות במעקב אחר נכסים?
בסדר. בסדר. בלי עיכובים נוספים, אשמח להודיע ש-Object.observe()
הגיע לגרסה היציבה Chrome 36. [WOOOO. THE CROWD GOES WILD].
Object.observe()
, שהוא חלק מתקן ECMAScript עתידי, הוא שיטה למעקב אסינכרוני אחרי שינויים באובייקטים של JavaScript… בלי צורך בספרייה נפרדת. הוא מאפשר למשתמש לצפות ברצף של רשומות שינוי לפי סדר זמן, שמתארות את קבוצת השינויים שהתרחשו בקבוצת אובייקטים שנצפו.
// Let's say we have a model with data
var model = {};
// Which we then observe
Object.observe(model, function(changes){
// This asynchronous callback runs
changes.forEach(function(change) {
// Letting us know what changed
console.log(change.type, change.name, change.oldValue);
});
});
בכל פעם שמתבצע שינוי, הוא מדווח:

בעזרת Object.observe()
(אני אוהב לקרוא לו O.o() או Oooooooo), אפשר להטמיע קישור נתונים דו-כיווני בלי צורך ב-framework.
עם זאת, אין משמעות לכך שאסור להשתמש באחד מהם. בפרויקטים גדולים עם לוגיקה עסקית מורכבת, מסגרות עם דעה מוגדרת מראש הן חסרות ערך וצריך להמשיך להשתמש בהן. הם מפשטים את ההדרכה למפתחים חדשים, דורשים פחות תחזוקת קוד ומכתיבים דפוסים לביצוע משימות נפוצות. אם אתם לא צריכים ספרייה כזו, תוכלו להשתמש בספריות קטנות יותר שמתמקדות בנושאים ספציפיים, כמו Polymer (שכבר מנצלת את O.o()).
גם אם אתם משתמשים הרבה בספרייה של MV* או ב-framework, O.o() יכול לשפר את הביצועים שלהם, עם הטמעה מהירה ופשוטה יותר תוך שמירה על אותו ממשק API. לדוגמה, בשנה שעברה התגלה ב-Angular שבבדיקת ביצועים שבה בוצעו שינויים במודל, בדיקת העדכונים נמשכה 40 אלפיות שנייה לכל עדכון, והפעלת O.o() נמשכה 1-2 אלפיות שנייה לכל עדכון (שיפור של פי 20 עד פי 40).
כשמשתמשים בקישור נתונים בלי צורך בלא מעט קוד מסובך, לא צריך יותר לבצע סקרים כדי לבדוק אם יש שינויים, ולכן חיי הסוללה ארוכים יותר.
אם כבר התאהבתם ב-O.o(), תוכלו לדלג אל ההסבר על התכונה או לקרוא בהמשך מידע נוסף על הבעיות שהיא פותרת.
מה אנחנו רוצים לצפות בו?
כשאנחנו מדברים על מעקב אחרי נתונים, בדרך כלל אנחנו מתכוונים למעקב אחרי סוגים ספציפיים של שינויים:
- שינויים באובייקטים גולמיים של JavaScript
- כשנכסים מתווספים, משתנים או נמחקים
- כשיש במערכים רכיבים שמצורפים אליהם או מופרדים מהם
- שינויים באב טיפוס של האובייקט
החשיבות של קישור נתונים
קישור נתונים מתחיל להיות חשוב כשחשוב לכם להפריד בין אמצעי הבקרה של המודל לתצוגה. HTML הוא מנגנון דקלרטיבי מצוין, אבל הוא סטטי לחלוטין. באופן אידיאלי, פשוט רוצים להצהיר על הקשר בין הנתונים לבין ה-DOM ולשמור על ה-DOM מעודכן. כך אפשר לחסוך זמן רב בכתיבה של קוד חוזר, שפשוט שולח נתונים אל DOM וממנו, בין המצב הפנימי של האפליקציה לבין השרת.
קישור נתונים שימושי במיוחד כשיש ממשק משתמש מורכב שבו צריך לחבר קשרים בין כמה נכסים במודלים של הנתונים לבין כמה אלמנטים בתצוגות. המצב הזה נפוץ למדי באפליקציות בדף יחיד שאנחנו מפתחים היום.
אנחנו נותנים ל-JavaScript frameworks (ולספריות קטנות של כלי עזר שאתם כותבים) דרך לצפות בשינויים בנתוני המודלים, בלי להסתמך על חלק מהפריצות האיטיות שבהן העולם משתמש היום, על ידי הוספת דרך לצפות בנתונים בדפדפן באופן מקורי.
איך נראה העולם היום
בדיקת סטטוס 'לא עדכני'
איפה נתקלתם בעבר בקישור נתונים? אם אתם משתמשים בספריית MV* מודרנית ליצירת אפליקציות אינטרנט (למשל Angular, Knockout), סביר להניח שאתם רגילים לקישור נתוני מודל ל-DOM. כהתזכורת, הנה דוגמה לאפליקציה של רשימת מספרי טלפון שבה אנחנו מקשרים את הערך של כל טלפון במערך phones
(שהוגדר ב-JavaScript) לפריט ברשימה, כדי שהנתונים וממשק המשתמש תמיד יהיו מסונכרנים:
<html ng-app>
<head>
...
<script src='angular.js'></script>
<script src='controller.js'></script>
</head>
<body ng-controller='PhoneListCtrl'>
<ul>
<li ng-repeat='phone in phones'>
<p></p>
</li>
</ul>
</body>
</html>
וקוד ה-JavaScript של הבקר:
var phonecatApp = angular.module('phonecatApp', []);
phonecatApp.controller('PhoneListCtrl', function($scope) {
$scope.phones = [
{'name': 'Nexus S',
'snippet': 'Fast just got faster with Nexus S.'},
{'name': 'Motorola XOOM with Wi-Fi',
'snippet': 'The Next, Next Generation tablet.'},
{'name': 'MOTOROLA XOOM',
'snippet': 'The Next, Next Generation tablet.'}
];
});
בכל פעם שנתוני המודל הבסיסיים משתנים, הרשימה שלנו ב-DOM מתעדכנת. איך Angular עושה זאת? מאחורי הקלעים מתבצעת בדיקה שנקראת בדיקת 'מצב מלוכלך'.

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

העלות של הפעולה הזו היא פרופורציונלית למספר הכולל של העצמים שנצפו. יכול להיות שאצטרך לבצע בדיקות רבות. יכול להיות שתצטרכו גם דרך להפעיל בדיקה של נתונים שעשויים להשתנות. יש הרבה טריקים חכמים שספריות משתמשות בהם כדי לעשות את זה. לא ברור אם זה תמיד יהיה מושלם.
לסביבת האינטרנט צריכה להיות יותר יכולת לחדש ולפתח מנגנונים מצהירים משלה, למשל:
- מערכות מודלים מבוססות-אילוצים
- מערכות שמשמרות נתונים באופן אוטומטי (למשל, שמירת שינויים ב-IndexedDB או ב-localStorage)
- אובייקטים של קונטיינרים (Ember, Backbone)
אובייקטים מסוג קונטיינר הם אובייקטים שנוצרים על ידי מסגרת, שמכילים בתוכם את הנתונים. יש להם גישות לנתונים, והם יכולים לתעד את מה שהגדרתם או קיבלתם ולשדר אותו באופן פנימי. השיטה הזו עובדת טוב. הביצועים שלו טובים יחסית, והוא מציג התנהגות אלגוריתמית טובה. בהמשך מופיעה דוגמה לאובייקטים של מאגרים באמצעות Ember:
// Container objects
MyApp.president = Ember.Object.create({
name: "Barack Obama"
});
MyApp.country = Ember.Object.create({
// ending a property with "Binding" tells Ember to
// create a binding to the presidentName property
presidentNameBinding: "MyApp.president.name"
});
// Later, after Ember has resolved bindings
MyApp.country.get("presidentName");
// "Barack Obama"
// Data from the server needs to be converted
// Composes poorly with existing code
העלות של גילוי מה השתנה כאן היא פרופורציונלית למספר הדברים שהשתנו. בעיה נוספת היא שאתם משתמשים עכשיו באובייקט מסוג אחר. באופן כללי, צריך להמיר את הנתונים שמקבלים מהשרת לאובייקטים האלה כדי שאפשר יהיה לצפות בהם.
הקוד הזה לא מתאים במיוחד לקוד JS קיים, כי רוב הקוד מניח שהוא יכול לפעול על נתונים גולמיים. לא עבור סוגי האובייקטים המיוחדים האלה.
Introducing Object.observe()
באופן אידיאלי, אנחנו רוצים ליהנות משני העולמות – דרך לצפות בנתונים עם תמיכה באובייקטים של נתונים גולמיים (אובייקטים רגילים של JavaScript), אם נבחר לעשות זאת, וגם בלי צורך לבצע בדיקה של נתונים לא נקיים כל הזמן. משהו עם התנהגות אלגוריתמית טובה. משהו שמתאים ליצירת קומפוזיציות ומוטמע בפלטפורמה. זהו היופי של Object.observe()
.
הוא מאפשר לנו לצפות באובייקט, לשנות מאפיינים ולראות את דוח השינויים של מה שהשתנה. אבל די לתיאוריה, נסתכל על קוד.

Object.observe() ו-Object.unobserve()
נניח שיש לנו אובייקט JavaScript פשוט שמייצג מודל:
// A model can be a simple vanilla object
var todoModel = {
label: 'Default',
completed: false
};
לאחר מכן אפשר לציין פונקציית קריאה חוזרת (callback) שתופעל בכל פעם שמתבצעות טרנספורמציות (שינויים) באובייקט:
function observer(changes){
changes.forEach(function(change, i){
console.log('what property changed? ' + change.name);
console.log('how did it change? ' + change.type);
console.log('whats the current value? ' + change.object[change.name]);
console.log(change); // all changes
});
}
לאחר מכן נוכל לראות את השינויים האלה באמצעות O.o(), ולהעביר את האובייקט כארגומנטים הראשון ואת הפונקציה הלא סטטית להפעלה חוזרת כארגומנטים השני:
Object.observe(todoModel, observer);
נתחיל לבצע כמה שינויים באובייקט המודל של Todos:
todoModel.label = 'Buy some more milk';
כשבודקים את המסוף, מקבלים מידע שימושי. אנחנו יודעים איזה מאפיין השתנה, איך הוא השתנה ומה הערך החדש.

יש! שלום ולא להתראות, בדיקה לא נקייה! על האבן צריך לחרוט ב-Comic Sans. נשנה מאפיין אחר. הפעם completeBy
:
todoModel.completeBy = '01/01/2014';
שוב, אנחנו רואים שקיבלנו דוח שינויים:

מצוין. מה יקרה אם נחליט למחוק את המאפיין 'completed' מהאובייקט שלנו:
delete todoModel.completed;

כפי שאפשר לראות, דוח השינויים שהוחזר כולל מידע על המחיקה. כצפוי, הערך החדש של המאפיין לא מוגדר עכשיו. עכשיו ברור לכם שאפשר לבדוק מתי נכסים נוספו. כשהן נמחקות. בעיקרון, הקבוצה של המאפיינים באובייקט ('חדש', 'נמחק', 'הוגדר מחדש') והשינוי של אב הטיפוס (proto).
כמו בכל מערכת תצפית, יש גם שיטה להפסיק את ההאזנה לשינויים. במקרה הזה, זהו Object.unobserve()
, שיש לו אותה חתימה כמו O.o(), אבל אפשר לקרוא לו באופן הבא:
Object.unobserve(todoModel, observer);
כפי שאפשר לראות בהמשך, כל המוטציות שבוצעו באובייקט אחרי ההרצה הזו לא מובילה יותר להחזרת רשימה של רשומות שינוי.

ציון שינויים בתחומי העניין
עכשיו הבנו את העקרונות הבסיסיים של קבלת רשימה של שינויים באובייקט שנצפה. מה קורה אם אתם מתעניינים רק בקבוצת משנה של השינויים שבוצעו באובייקט, ולא בכל השינויים? כולם צריכים מסנן ספאם. משתמשים צופים יכולים לציין רק את סוגי השינויים שהם רוצים לקבל הודעות עליהם באמצעות רשימת אישורים. אפשר לציין את זה באמצעות הארגומנט השלישי של O.o() באופן הבא:
Object.observe(obj, callback, optAcceptList)
נסביר בעזרת דוגמה איך אפשר להשתמש בכך:
// Like earlier, a model can be a simple vanilla object
var todoModel = {
label: 'Default',
completed: false
};
// We then specify a callback for whenever mutations
// are made to the object
function observer(changes){
changes.forEach(function(change, i){
console.log(change);
})
};
// Which we then observe, specifying an array of change
// types we're interested in
Object.observe(todoModel, observer, ['delete']);
// without this third option, the change types provided
// default to intrinsic types
todoModel.label = 'Buy some milk';
// note that no changes were reported
עם זאת, אם נמחק את התווית עכשיו, שימו לב שסוג השינוי הזה כן ידווח:
delete todoModel.label;
אם לא מציינים רשימת סוגי קבלה ל-O.o(), ברירת המחדל היא סוגי השינויים 'הפנימיים' של אובייקטים (add
, update
, delete
, reconfigure
, preventExtensions
(למקרים שבהם לא ניתן לצפות בשינוי של אובייקט שמפסיק להיות ניתן להרחבה)).
התראות
O.o() כולל גם את המושג 'התראות'. הם לא דומים לדברים המרגיזים שמקבלים בטלפון, אלא מועילים. התראות דומות למשגיחי מוטציות. הם מתרחשים בסוף המיקרו-משימה. בהקשר של הדפדפן, זה כמעט תמיד יקרה בסוף פונקציית הטיפול באירוע הנוכחית.
זהו זמן נוח כי בדרך כלל יחידת עבודה אחת מסתיימת, והצופים יכולים עכשיו לבצע את העבודה שלהם. זהו מודל עיבוד נחמד מבוסס-תור.
תהליך העבודה בשימוש בשירות התראות נראה כך:

נבחן דוגמה לשימוש בפועל במודעות כדי להגדיר התראות בהתאמה אישית למקרה שבו מאפיינים באובייקט מתקבלים או מוגדרים. כדאי לעקוב אחרי התגובות כאן:
// Define a simple model
var model = {
a: {}
};
// And a separate variable we'll be using for our model's
// getter in just a moment
var _b = 2;
// Define a new property 'b' under 'a' with a custom
// getter and setter
Object.defineProperty(model.a, 'b', {
get: function () {
return _b;
},
set: function (b) {
// Whenever 'b' is set on the model
// notify the world about a specific type
// of change being made. This gives you a huge
// amount of control over notifications
Object.getNotifier(this).notify({
type: 'update',
name: 'b',
oldValue: _b
});
// Let's also log out the value anytime it gets
// set for kicks
console.log('set', b);
_b = b;
}
});
// Set up our observer
function observer(changes) {
changes.forEach(function (change, i) {
console.log(change);
})
}
// Begin observing model.a for changes
Object.observe(model.a, observer);

כאן אנחנו מדווחים על שינויים בערך של מאפייני הנתונים ('עדכון'). כל דבר אחר שהטמעת האובייקט בוחרת לדווח עליו (notifier.notifyChange()
).
שנים של ניסיון בפלטפורמת האינטרנט לימדו אותנו שגישה סינכרונית היא הדבר הראשון שצריך לנסות, כי היא הפשוטה ביותר להבנה. הבעיה היא שהשיטה הזו יוצרת מודל עיבוד מסוכן באופן מהותי. אם כותבים קוד ומעדכנים את המאפיין של אובייקט, לא רוצים שעדכון המאפיין של אותו אובייקט יגרום לקוד שרירותי כלשהו לבצע כל פעולה שהוא רוצה. לא מומלץ שההנחות שלכם יבוטלו באמצע הפעלת פונקציה.
אם אתם משתתפים צופים, עדיף שלא תתקבלו קריאה אם מישהו נמצא באמצע משהו. אתם לא רוצים לבצע עבודה על מצב לא עקבי של העולם. עליכם לבצע הרבה יותר בדיקות שגיאות. הוא מנסה לסבול הרבה יותר מצבים גרועים, ובאופן כללי קשה לעבוד איתו. קשה יותר להתמודד עם קריאות אסינכרניות, אבל בסוף היום הן מודל טוב יותר.
הפתרון לבעיה הזו הוא רשומות שינוי סינתטיות.
רשומות של שינויים סינתטיים
בעיקרון, אם אתם רוצים להשתמש ב-accessors או במאפיינים מחושבים, אתם אחראים להודיע כשהערכים האלה משתנים. זהו קצת עבודה נוספת, אבל היא תוכננה כסוג של תכונה ברמה ראשונה של המנגנון הזה, וההתראות האלה יועברו עם שאר ההתראות מאובייקטי הנתונים הבסיסיים. מנכסי נתונים.

אפשר לצפות ב-accessors ובמאפיינים מחושבים באמצעות notifier.notify – חלק נוסף של O.o(). רוב מערכות התצפית רוצות לצפות בערכים נגזרים בצורה כלשהי. יש הרבה דרכים לעשות את זה. O.o לא פוסק לגבי הדרך 'הנכונה'. מאפיינים מחושבים צריכים להיות פונקציות גישה ששולחות הודעה כשהמצב הפנימי (הפרטי) משתנה.
שוב, מפתחי אתרים צריכים לצפות שהספריות יעזרו להם לשלוח התראות וליישם גישות שונות לנכסים מחושבים בקלות (ולהפחית את הקוד הסטנדרטי).
נגדיר את הדוגמה הבאה, שהיא כיתה של עיגול. הרעיון הוא שיש לנו עיגול ויש נכס רדיוס. במקרה הזה, הרדיוס הוא רכיב גישה, וכשהערך שלו ישתנה הוא יעדכן את עצמו שהערך השתנה. הוא יישלח עם כל השינויים האחרים באובייקט הזה או בכל אובייקט אחר. בעיקרון, אם מטמיעים אובייקט שרוצים שיהיה לו מאפיינים סינתטיים או מחושבים, צריך לבחור אסטרטגיה לדרך שבה זה יתבצע. לאחר מכן, המערכת תתאים את עצמה לכך.
אפשר לדלג על הקוד כדי לראות איך זה עובד בכלי הפיתוח.
function Circle(r) {
var radius = r;
var notifier = Object.getNotifier(this);
function notifyAreaAndRadius(radius) {
notifier.notify({
type: 'update',
name: 'radius',
oldValue: radius
})
notifier.notify({
type: 'update',
name: 'area',
oldValue: Math.pow(radius * Math.PI, 2)
});
}
Object.defineProperty(this, 'radius', {
get: function() {
return radius;
},
set: function(r) {
if (radius === r)
return;
notifyAreaAndRadius(radius);
radius = r;
}
});
Object.defineProperty(this, 'area', {
get: function() {
return Math.pow(radius, 2) * Math.PI;
},
set: function(a) {
r = Math.sqrt(a/Math.PI);
notifyAreaAndRadius(radius);
radius = r;
}
});
}
function observer(changes){
changes.forEach(function(change, i){
console.log(change);
})
}

מאפייני Accessor
הערה מהירה על מאפייני גישה. צייןנו קודם שרק שינויים בערך גלויים במאפייני נתונים. לא למאפיינים מחושבים או למשתמשי גישה. הסיבה לכך היא שב-JavaScript אין ממש מושג של שינויים בערך של פונקציות גישה. פונקציית גישה היא רק אוסף של פונקציות.
אם מקצים ל-JavaScript פונקציית גישה, היא פשוט מפעילה את הפונקציה שם, ומבחינתה לא השתנה דבר. הוא רק נתן לקוד מסוים הזדמנות לפעול.
הבעיה היא שאנחנו יכולים להתייחס ל-5 כערך שהוקצה למשתנה. אנחנו אמורים לדעת מה קרה כאן. זו בעצם בעיה שלא ניתן לפתור. הדוגמה הבאה ממחישה את הסיבה לכך. אין שום דרך למערכת לדעת מה המשמעות של זה, כי זה יכול להיות קוד שרירותי. במקרה כזה, הוא יכול לעשות מה שהוא רוצה. הערך מתעדכן בכל פעם שמתבצעת גישה אליו, ולכן אין הרבה טעם לשאול אם הוא השתנה.
מעקב אחרי כמה אובייקטים באמצעות קריאה חוזרת אחת
דפוס אפשרי נוסף עם O.o() הוא הרעיון של משתמש אחד למעקב אחרי קריאה חוזרת (callback). כך אפשר להשתמש בקריאה חוזרת אחת כ'משתמש צופה' עבור הרבה אובייקטים שונים. פונקציית ה-callback תקבל את קבוצת השינויים המלאה בכל האובייקטים שהיא עוקבת אחריהם "בסוף המיקרו-משימה" (שימו לב לדמיון ל-Mutation Observers).

שינויים בקנה מידה גדול
אולי אתם עובדים על אפליקציה גדולה מאוד ואתם צריכים לעבד שינויים בקנה מידה רחב באופן קבוע. אובייקטים יכולים לתאר שינויים סמנטיים גדולים יותר שמשפיעים על הרבה נכסים בצורה קומפקטית יותר (במקום לשדר המון שינויים בנכסים).
כדי לעזור בכך, O.o() כולל שתי תוכנות שירות ספציפיות: notifier.performChange()
ו-notifier.notify()
, שכבר הצגנו.

נבחן את הנושא באמצעות דוגמה שמראה איך אפשר לתאר שינויים בקנה מידה רחב, שבה אנחנו מגדירים אובייקט Thingy עם כמה כלי שירות מתמטיים (multiply, increment, incrementAndMultiply). בכל פעם שמשתמשים בכלי, הוא מודיע למערכת שאוסף עבודות מורכב מסוג מסוים של שינוי.
לדוגמה: notifier.performChange('foo', performFooChangeFn);
function Thingy(a, b, c) {
this.a = a;
this.b = b;
}
Thingy.MULTIPLY = 'multiply';
Thingy.INCREMENT = 'increment';
Thingy.INCREMENT_AND_MULTIPLY = 'incrementAndMultiply';
Thingy.prototype = {
increment: function(amount) {
var notifier = Object.getNotifier(this);
// Tell the system that a collection of work comprises
// a given changeType. e.g
// notifier.performChange('foo', performFooChangeFn);
// notifier.notify('foo', 'fooChangeRecord');
notifier.performChange(Thingy.INCREMENT, function() {
this.a += amount;
this.b += amount;
}, this);
notifier.notify({
object: this,
type: Thingy.INCREMENT,
incremented: amount
});
},
multiply: function(amount) {
var notifier = Object.getNotifier(this);
notifier.performChange(Thingy.MULTIPLY, function() {
this.a *= amount;
this.b *= amount;
}, this);
notifier.notify({
object: this,
type: Thingy.MULTIPLY,
multiplied: amount
});
},
incrementAndMultiply: function(incAmount, multAmount) {
var notifier = Object.getNotifier(this);
notifier.performChange(Thingy.INCREMENT_AND_MULTIPLY, function() {
this.increment(incAmount);
this.multiply(multAmount);
}, this);
notifier.notify({
object: this,
type: Thingy.INCREMENT_AND_MULTIPLY,
incremented: incAmount,
multiplied: multAmount
});
}
}
לאחר מכן מגדירים שני משגיחים לאובייקט: אחד שמקבל את כל השינויים, והשני שמדווח רק על סוגי אישור ספציפיים שהגדרתם (Thingy.INCREMENT, Thingy.MULTIPLY, Thingy.INCREMENT_AND_MULTIPLY).
var observer, observer2 = {
records: undefined,
callbackCount: 0,
reset: function() {
this.records = undefined;
this.callbackCount = 0;
},
};
observer.callback = function(r) {
console.log(r);
observer.records = r;
observer.callbackCount++;
};
observer2.callback = function(r){
console.log('Observer 2', r);
}
Thingy.observe = function(thingy, callback) {
// Object.observe(obj, callback, optAcceptList)
Object.observe(thingy, callback, [Thingy.INCREMENT,
Thingy.MULTIPLY,
Thingy.INCREMENT_AND_MULTIPLY,
'update']);
}
Thingy.unobserve = function(thingy, callback) {
Object.unobserve(thingy);
}
עכשיו אפשר להתחיל לשחק עם הקוד הזה. נגדיר מכשיר חדש:
var thingy = new Thingy(2, 4);
בודקים את הנתונים ומבצעים שינויים. OMG, כיף כזה. יש כל כך הרבה דברים!
// Observe thingy
Object.observe(thingy, observer.callback);
Thingy.observe(thingy, observer2.callback);
// Play with the methods thingy exposes
thingy.increment(3); // { a: 5, b: 7 }
thingy.b++; // { a: 5, b: 8 }
thingy.multiply(2); // { a: 10, b: 16 }
thingy.a++; // { a: 11, b: 16 }
thingy.incrementAndMultiply(2, 2); // { a: 26, b: 36 }

כל מה שנמצא בתוך 'פונקציית הביצוע' נחשב לעבודה של 'big-change'. משקיפים שמקבלים את 'big-change' יקבלו רק את הרשומה 'big-change'. משתמשים שלא מציינים את האפשרות הזו יקבלו את השינויים הבסיסיים שנובעים מהעבודה שבוצעה על ידי הפונקציה 'ביצוע פונקציה'.
תצפית במערכים
דיברנו לא מעט על מעקב אחרי שינויים באובייקטים, אבל מה לגבי מעקב אחרי שינויים במערכים? שאלה מצוינת. כשמישהו אומר לי, "שאלה מעולה". אף פעם לא שומעים את התשובה שלהם כי אני עסוק בהתפעלות מעצמי על ששאלתי שאלה כל כך טובה, אבל זה לא הנושא. יש לנו גם שיטות חדשות לעבודה עם מערכי נתונים.
Array.observe()
היא שיטה שמתייחסת לשינויים גדולים בעצמה – לדוגמה, חיבור, העברה או כל דבר שמשנה את האורך שלה באופן משתמע – כרשומת שינוי מסוג 'חיבור'. בתוך הקוד נעשה שימוש ב-notifier.performChange("splice",...)
.
בדוגמה הבאה אנחנו בודקים 'מערך' של מודל, ובאופן דומה מקבלים בחזרה רשימה של שינויים כשיש שינויים בנתונים הבסיסיים:
var model = ['Buy some milk', 'Learn to code', 'Wear some plaid'];
var count = 0;
Array.observe(model, function(changeRecords) {
count++;
console.log('Array observe', changeRecords, count);
});
model[0] = 'Teach Paul Lewis to code';
model[1] = 'Channel your inner Paul Irish';

ביצועים
כדי להבין את ההשפעה של O.o() על ביצועי החישוב, אפשר לחשוב עליה כמטמון קריאה. באופן כללי, מטמון הוא בחירה מצוינת במקרים הבאים (בסדר החשיבות):
- תדירות הקריאות שולטת בתדירות הכתיבה.
- אפשר ליצור מטמון שמחליף את כמות העבודה הקבועה במהלך הכתיבה בביצועים אלגוריתמיים טובים יותר במהלך הקריאה.
- הזמן הארוך יותר לכתיבה הוא קבוע ומתקבל.
הפונקציה O.o() מיועדת לתרחישי שימוש כמו 1).
כדי לבצע בדיקה של נתונים שגויים, צריך לשמור עותק של כל הנתונים שאתם צופים בהם. כלומר, יש עלות מבנית של זיכרון לבדיקה של נתונים ששונו, שלא קיימת ב-O.o(). בדיקה של נתונים ששונו היא פתרון יעיל לטווח קצר, אבל היא גם אבסוקציה עם דליפות שעלולה ליצור מורכבות מיותרת באפליקציות.
למה? בדיקת הסטטוס 'לא עדכני' צריכה לפעול בכל פעם שיכול להיות שהנתונים השתנו. פשוט אין דרך חזקה מאוד לעשות זאת, וכל גישה לכך כוללת חסרונות משמעותיים (למשל, בדיקה במרווח זמן של סקרים עלולה לגרום לבעיות חזותיות ולתנאי מרוץ בין בעיות בקוד). בדיקת סטטוס 'שינויים' דורשת גם רישום גלובלי של משקיפים, מה שיוצר סיכונים של דליפת זיכרון ועלויות של פירוק, ש-O.o() מונעת.
נבחן כמה מספרים.
בדיקות נקודת השוואה הבאות (שזמינות ב-GitHub) מאפשרות לנו להשוות בין בדיקת סטטוס 'שינויים' לבין O.o(). הן מובנות כגרפים של 'גודל קבוצת האובייקטים שנצפו' לעומת 'מספר המוטציות'. התוצאה הכללית היא שהביצועים של בדיקת הסטטוס 'לא עדכני' פרופורציוניים מבחינה אלגוריתמית למספר העצמים שנצפו, בעוד שהביצועים של O.o() פרופורציוניים למספר המוטציות שבוצעו.
בדיקת סטטוס 'לא עדכני'

Chrome עם Object.observe() מופעל

הוספת polyfill ל-Object.observe()
מצוין – אפשר להשתמש ב-O.o() ב-Chrome 36, אבל מה לגבי שימוש בו בדפדפנים אחרים? אנחנו נדאג לכם. Observe-JS של Polymer הוא polyfill ל-O.o(), שמשתמש בהטמעה המקורית אם היא קיימת, אבל אחרת מבצע polyfill וגם כולל כמה שיפורים שימושיים. הוא מציע תצוגה מצטברת של העולם שמסכמת את השינויים ומספקת דוח על מה שהשתנה. יש בו שני דברים חזקים מאוד:
- אפשר לצפות בנתיבים. כלומר, אפשר לומר "אני רוצה לעקוב אחרי foo.bar.baz" מאובייקט נתון, והם יגידו לכם מתי הערך בנתיב הזה השתנה. אם לא ניתן להגיע לנתיב, הערך נחשב ללא מוגדר.
דוגמה למעקב אחרי ערך בנתיב מאובייקט נתון:
var obj = { foo: { bar: 'baz' } };
var observer = new PathObserver(obj, 'foo.bar');
observer.open(function(newValue, oldValue) {
// respond to obj.foo.bar having changed value.
});
- שם יפורטו פרטים על חיבור מחרוזות. בעצם, חיבור מחרוזות הוא הקבוצה המינימלית של פעולות החיבור שצריך לבצע במערך כדי להפוך את הגרסה הישנה של המערך לגרסה החדשה שלו. זהו סוג של טרנספורמציה או תצוגה שונה של המערך. זהו נפח העבודה המינימלי שצריך לבצע כדי לעבור מהמצב הישן למצב החדש.
דוגמה לדיווח על שינויים במערך כקבוצה מינימלית של צירופי קטעים:
var arr = [0, 1, 2, 4];
var observer = new ArrayObserver(arr);
observer.open(function(splices) {
// respond to changes to the elements of arr.
splices.forEach(function(splice) {
splice.index; // index position that the change occurred.
splice.removed; // an array of values representing the sequence of elements which were removed
splice.addedCount; // the number of elements which were inserted.
});
});
מסגרות ו-Object.observe()
כפי שצוין, הפונקציה O.o() תעניק למסגרות ולספריות הזדמנות נהדרת לשפר את הביצועים של קישור הנתונים שלהן בדפדפנים שתומכים בתכונה.
יהודה כץ ואריק ברין מ-Ember אישרו שהוספת תמיכה ב-O.o() נכללת בתוכנית העבודה של Ember לטווח הקצר. Misko Hervy מ-Angular כתב מסמך עיצוב על זיהוי השינויים המשופרת ב-Angular 2.0. הגישה לטווח הארוך שלהם היא לנצל את Object.observe() כשהיא תגיע לגרסת Chrome היציבה, ולהשתמש עד אז ב-Watchtower.js, הגישה שלהם לזיהוי שינויים. זה ממש מרגש.
מסקנות
O.o() הוא תוספת חזקה לפלטפורמת האינטרנט, שאפשר להשתמש בה כבר היום.
אנחנו מקווים שהתכונה הזו תגיע עם הזמן לדפדפנים נוספים, ותאפשר ל-JavaScript frameworks לשפר את הביצועים באמצעות גישה ליכולות של תצפית על אובייקטים מקומיים. משתמשים שמטרגטים את Chrome אמורים להיות מסוגלים להשתמש ב-O.o() בגרסה 36 של Chrome (ומעלה), והתכונה אמורה להיות זמינה גם בגרסה עתידית של Opera.
אז כדאי לכם לפנות אל מחברי מסגרות JavaScript ולשאול אותם על Object.observe()
ועל האופן שבו הם מתכננים להשתמש בו כדי לשפר את הביצועים של קישור הנתונים באפליקציות שלכם. בהחלט צפויים רגעים מרגשים!
משאבים
- Object.observe() בוויקי של Harmony>
- קישור נתונים באמצעות Object.observe() מאת Rick Waldron
- כל מה שתמיד רציתם לדעת על Object.observe() – JSConf
- למה Object.observe() היא התכונה הטובה ביותר ב-ES7
תודה לרפאל ויינשטיין, ג'ייק ארקדיל, אריק בידלמן, פול קילן וויוויאן קרומל על המשוב והביקורות.