מהפכות קישור נתונים באמצעות Object.observe()

Addy Osmani
Addy Osmani

מבוא

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

בסדר. בסדר. ללא עיכובים נוספים, אני שמח להודיע שהגרסה של Object.observe() הגיעה ל-Chrome 36 יציב. [WOOOO. The CROWD 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()).

גם אם אתם משתמשים הרבה ב-framework או בספריית MV* , ל-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)
  • אובייקטים של קונטיינר (חום אדמדם, עמוד שדרה)

אובייקטים מסוג קונטיינר הם המקום שבו framework יוצרת אובייקטים שבתוך מחזיקי הנתונים. יש להם גישת גישה לנתונים והם יכולים לתעד את מה שהגדרתם או קיבלתם ולשדר באופן פנימי. השיטה הזו עובדת טוב. הוא מניב ביצועים טובים יחסית ויש לו התנהגות אלגוריתמית טובה. בהמשך מוצגת דוגמה לאובייקטים של קונטיינר שמשתמשים ב-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.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()).

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

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

הפתרון לבעיה הזו הוא רשומות שינוי סינתטיות.

רשומות שינויים סינתטיות

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

רשומות שינוי סינתטיות

אפשר לפתור תצפיות על רכיבי גישה ומאפיינים מחושבים באמצעות 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);
  })
}
מסוף לרשומות של שינויים סינתטיים

מאפייני הגישה

הערה מהירה על מאפייני גישה. צייןנו קודם שרק שינויים בערך גלויים במאפייני נתונים. לא לנכסים או לרכיבי גישה שמחושבים. הסיבה לכך היא שב-JavaScript אין ממש מושג של שינויים בערך של פונקציות גישה. רכיב גישה הוא רק אוסף של פונקציות.

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

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

צפייה במספר אובייקטים באמצעות קריאה חוזרת (callback) אחת

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

מעקב אחרי כמה אובייקטים באמצעות קריאה חוזרת אחת

שינויים בקנה מידה גדול

אולי אתם עובדים על אפליקציה גדולה מאוד ואתם צריכים לעבד באופן קבוע שינויים בקנה מידה רחב. אובייקטים יכולים לתאר שינויים סמנטיים גדולים יותר שמשפיעים על הרבה נכסים בצורה קומפקטית יותר (במקום לשדר המון שינויים בנכסים).

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

שינויים בקנה מידה גדול

נבחן את זה כדוגמה לאופן שבו ניתן לתאר שינויים בקנה מידה גדול כאשר אנחנו מגדירים אובייקט Thingy עם מספר כלים מתמטיים (הכפלה, הגדלת, ההגדלה, והכפלה). בכל פעם שנעשה שימוש בכלי עזר, הוא מסמן למערכת שאוסף של עבודות מורכב מסוג מסוים של שינוי.

לדוגמה: 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);
}

עכשיו אפשר להתחיל לשחק עם הקוד הזה. נגדיר עכשיו Thingy חדש:

var thingy = new Thingy(2, 4);

חשוב לעבור על זה ולבצע כמה שינויים. אוי, איזה כיף. כל כך הרבה דברים!

// 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() , הוא כמו של מטמון קריאה. באופן כללי, שמירה במטמון היא בחירה טובה כאשר (לפי סדר החשיבות):

  1. תדירות הקריאות שולטת בתדירות הכתיבה.
  2. אפשר ליצור מטמון שמחליף את כמות העבודה הקבועה במהלך הכתיבה בביצועים אלגוריתמיים טובים יותר במהלך הקריאה.
  3. האצת זמן הכתיבה הקבועה מקובלת.

הפונקציה O.o() מיועדת לתרחישי שימוש כמו 1).

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

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

נבחן כמה מספרים.

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

בדיקת סטטוס 'לא עדכני'

בדיקת ביצועים מלוכלכת

Chrome עם התכונה Object.observe() מופעל

מעקב אחר הביצועים

Polyfilling Object.observe()

מצוין – אפשר להשתמש ב-O.o() ב-Chrome 36, אבל מה לגבי שימוש בו בדפדפנים אחרים? אנחנו כאן כדי לעזור. Observe-JS של Polymer הוא polyfill ל-O.o()‎, שמשתמש בהטמעה המקורית אם היא קיימת, אבל אחרת מבצע polyfill וגם כולל כמה שיפורים שימושיים. הוא מציע תצוגה מצטברת של העולם שמסכמת את השינויים ומספקת דוח על מה שהשתנה. שני הדברים החשובים ביותר שהכלי חושף הם:

  1. אפשר לצפות בנתיבים. כלומר, אפשר לומר שאני רוצה לצפות ב-"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.
});
  1. היא תפיק מידע על נקודות חיבור של מערך. חיבור מחרוזות הוא בעצם הקבוצה המינימלית של פעולות החיבור שצריך לבצע במערך כדי להפוך את הגרסה הישנה של המערך לגרסה החדשה שלו. זהו סוג של טרנספורמציה או תצוגה שונה של המערך. זהו נפח העבודה המינימלי שצריך לבצע כדי לעבור מהמצב הישן למצב החדש.

דוגמה לדיווח על שינויים במערך כקבוצה מינימלית של חלקים:

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.
  });
});

Frameworks ו-Object.observe()

כפי שצוין, הפונקציה O.o() תעניק למסגרות ולספריות הזדמנות נהדרת לשפר את הביצועים של קישור הנתונים שלהן בדפדפנים שתומכים בתכונה.

יהודה כץ ואריק ברין מ-Ember אישרו שהוספת תמיכה ב-O.o() נכללת בתוכנית העבודה של Ember לטווח הקצר. Misko Hervy מ-Angular כתב מסמך עיצוב על זיהוי השינויים המשופר ב-Angular 2.0. הגישה שלהם לטווח הארוך תהיה לנצל את היתרונות של Object.observe() כאשר הוא יגיע ליציבות של Chrome, ולבחור ב-Watchtower.js, גישה משלהם לזיהוי שינויים. זה ממש מרגש.

מסקנות

O.o() הוא תוספת חזקה לפלטפורמת האינטרנט, שאפשר להשתמש בה כבר היום.

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

אז כדאי לכם לפנות לכותבים של מסגרות JavaScript ולשאול אותם על Object.observe() ועל האופן שבו הם מתכננים להשתמש בו כדי לשפר את הביצועים של קישור הנתונים באפליקציות שלכם. בהחלט צפויים לנו זמנים מרגשים!

משאבים

תודה לרפאל ויינשטיין, ג'ייק ארקדיל, אריק בידלמן, פול קילן וויוויאן קרומל על המשוב והביקורות.