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

אדי אוסמאני
אדי אוסמאני

מבוא

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

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

גם אם אתם משתמשים הרבה ב-framework או בספריית MV* , ל-O.o() יש פוטנציאל לספק להם כמה שיפורי ביצועים בריאים, עם הטמעה מהירה ופשוטה יותר תוך שמירה על אותו API. לדוגמה, בשנה שעברה Angular מצאה שבנקודת השוואה שבה בוצעו שינויים במודל, הבדיקה המלוכלכת נמשכה 40 אלפיות השנייה בכל עדכון ו-O.o() נמשכה 1-2 אלפיות שנייה לכל עדכון (שיפור של פי 20-40 מהר יותר).

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

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

על מה אנחנו רוצים לשמור?

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

  • שינויים באובייקטים גולמיים של JavaScript
  • כשנכסים נוספים, משתנים או נמחקים
  • כאשר יש במערכים רכיבים המתחברים אליהם ויוצאים מהם
  • שינויים באב-טיפוס של האובייקט

החשיבות של קישור נתונים

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

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

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

איך העולם נראה היום

בדיקות מלוכלכות

איפה ראית בעבר קישור נתונים? אם אתם משתמשים בספריית 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)

אובייקטים של קונטיינר הם המקום שבו 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';

כמו שאפשר לראות, אנחנו שוב מקבלים בהצלחה דוח שינויים:

שינוי הדוח.

מצוין. מה יקרה אם עכשיו נחליט למחוק את המאפיין 'הושלם' מהאובייקט שלנו:

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

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

תהליך העבודה לשימוש בשירות ההודעות נראה בערך כך:

התראות

הדוגמה הבאה ממחישה איך אפשר להשתמש במיידעי הודעות בפועל כדי להגדיר התראות מותאמות אישית על המקרים שבהם מאפיינים של אובייקט מקבלים או מוגדרים. מומלץ לעקוב אחרי התגובות כאן:

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

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

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

אפשר לדלג על הקוד כדי לראות אותו פועל בכלי הפיתוח.

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 של רכיב גישה, JavaScript רק מפעיל את הפונקציה שם ומנקודת המבט שלו, שום דבר לא השתנה. הוא פשוט העניק לחלק קוד הזדמנות להריץ.

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

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

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

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

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

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

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

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

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

לדוגמה: 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 }
שינויים בקנה מידה גדול

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

תצפית על מערכים

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

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) מאפשרות לנו להשוות בין בדיקות מלוכלכות (dirty-checking) לבין O.o(). הן בנויות כתרשימים של Exploreed-Object-Set-Size לעומת Number-Of-Mutations. התוצאה הכללית היא שביצועי הבדיקה המלוכלכת פרופורציונלית באופן אלגוריתמי למספר האובייקטים המתועדים, וביצועי O.o() פרופורציונליים למספר המוטציות שבוצעו.

בדיקות מלוכלכות

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

Chrome שבו מופעל Object.observe()

צפייה בנתוני הביצועים

Polyfilling Object.observe()

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

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

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

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() היא חלק ממפת הדרכים של אמבר לטווח קרוב. מיסקו הרבי מ-Angular, כתב מסמך עיצוב על זיהוי השינויים המשופר של Angular 2.0. בטווח הארוך, החברה תהיה משתמשת ב-Object.observe() כשהממשק היציב של Chrome יתמוך ב-Watchtower.js, גישת החברה לזיהוי שינויים עד אז. מרגש ב-Suuuuper.

מסקנות

O.o() הוא תוספת עוצמתית לפלטפורמת האינטרנט שדרכה אפשר לצאת ולהשתמש כבר היום.

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

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

משאבים

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