دورات ربط البيانات باستخدام 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)، يمكنك تنفيذ ربط البيانات الثنائي بدون الحاجة إلى إطار عمل.

هذا لا يعني عدم استخدام كلمة المرور. بالنسبة للمشروعات الكبيرة ذات منطق الأعمال المعقد، تعد الأطر ذات الآراء التي لا تقدر بثمن ولا تقدر بثمن ويجب عليك الاستمرار في استخدامها. كما أنّها تبسّط توجّهات المطوّرين الجدد وتتطلّب صيانة أقل للرموز البرمجية وتفرض أنماطًا حول كيفية إنجاز المهام الشائعة. وعندما لا تحتاج إلى مكتبة، يمكنك استخدام مكتبات أصغر حجمًا وأكثر تركيزًا مثل Polymer (التي تستفيد حاليًا من O.o()).

حتى إذا كنت تستخدم إطار عمل أو مكتبة الفيديوهات الموسيقية* بكثرة، بإمكان O.o() تزويدها ببعض التحسينات المناسبة على الأداء، وذلك من خلال تنفيذ أسرع وأسهل مع الحفاظ على واجهة برمجة التطبيقات نفسها. على سبيل المثال، وجد Angular العام الماضي أنّه في مقياس أداء تم فيه إجراء تغييرات على أحد النماذج، استغرق الفحص غير الصحيح 40 ملي ثانية لكل تحديث، واستغرق O.o() من 1 إلى 2 ملي ثانية لكل تحديث (تحسّن السرعة بمقدار 20 إلى 40 مرة).

ربط البيانات دون الحاجة إلى الكثير من التعليمات البرمجية المعقدة يعني أيضًا أنه لم يعد عليك البحث عن التغييرات، بحيث يكون عمر البطارية أطول!

إذا كنت تبيع منتجاتك بالفعل على O.o()، فتخطَّ إلى مقدمة الميزة، أو اقرأ المزيد عن المشكلات التي يحلها.

ما الذي نريد ملاحظته؟

عندما نتحدث عن ملاحظة البيانات، فإننا نشير عادةً إلى مراقبة بعض الأنواع المحددة من التغييرات:

  • التغييرات على كائنات JavaScript الأولية
  • عند إضافة المواقع أو تغييرها أو حذفها
  • عندما تحتوي الصفائف على عناصر مقسمة داخلها وخارجها
  • التغييرات على النموذج الأولي للكائن

أهمية ربط البيانات

يبدأ ربط البيانات يصبح مهمًا عندما تهتم بفصل عناصر التحكم في عرض النموذج. وتشكّل لغة HTML آلية بيانية رائعة، ولكنها ثابتة تمامًا. بشكل مثالي، يكفي أن تعلن عن العلاقة بين بياناتك ونموذج العناصر في المستند (DOM) وإبقاء DOM محدّثًا. وهذا يوفر لك الاستفادة من الوقت ويوفر لك الكثير من الوقت في كتابة التعليمات البرمجية المتكررة التي ترسل البيانات من وإلى DOM بين الحالة الداخلية لتطبيقك أو الخادم.

يكون ربط البيانات مفيدًا بشكل خاص عندما تكون لديك واجهة مستخدم معقدة حيث تحتاج إلى ربط العلاقات بين مواقع متعددة في نماذج البيانات الخاصة بك مع عناصر متعددة في طرق العرض الخاصة بك. هذا أمر شائع جدًا في تطبيقات الصفحة الواحدة التي ننشئها اليوم.

ومن خلال إعداد طريقة لمراقبة البيانات في المتصفح بشكل طبيعي، نتيح لأُطر عمل 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، العمود الأساسي)

كائنات الحاوية هي المكان الذي ينشئ فيه إطار العمل عناصر تحتوي داخلها على البيانات. فبإمكان هذه الأجهزة الوصول إلى البيانات، كما يمكنها تسجيل ما تضبطه أو تحصل عليه وما يتم بثه داخليًا. هذا يعمل بشكل جيد. وتتميز بأداء جيد نسبيًا ولها سلوك خوارزمي جيد. في ما يلي مثال على كائنات الحاوية باستخدام 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 بسيطًا يمثل vanilla

// A model can be a simple vanilla object
var todoModel = {
  label: 'Default',
  completed: false
};

يمكننا بعد ذلك تحديد استدعاء كلما تم إجراء طفرات (تغييرات) على الكائن:

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

لِنبدأ في إجراء بعض التغييرات على عنصر نموذج المهام:

todoModel.label = 'Buy some more milk';

بعد الاطّلاع على وحدة التحكّم، سنقدّم لك بعض المعلومات المفيدة. نحن نعرف قيمة الخاصية التي تغيرت، وكيف تم تغييرها، والقيمة الجديدة.

تقرير Console

لا يوجد المزيد. وداعًا أيها التدقيق السيء! يجب أن يكون شاهد ضريحك منحوتًا في 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() أيضًا مع مفهوم الإشعارات. وهي لا تشبه أبدًا تلك الأشياء المزعجة التي تحصل عليها على الهاتف، ولكنها مفيدة إلى حد ما. تتشابه الإشعارات مع ميزة مراقبو التبديل. تحدث في نهاية المهمة الدقيقة. وفي سياق المتصفِّح، تظهر هذه القائمة دائمًا في نهاية معالج الأحداث الحالي.

التوقيت جيد لأنه بشكل عام تم الانتهاء من وحدة عمل واحدة والآن يتمكن المراقبون من القيام بعملهم. إنه نموذج معالجة جيد يعتمد على تبادل الأدوار.

يتشابه سير عمل استخدام الإشعار على النحو التالي:

الإشعارات

لنلقِ نظرة على مثال عن كيفية استخدام الإشعارات عمليًا لتحديد إشعارات مخصَّصة عند الحصول على خصائص أحد العناصر أو ضبطها. اطّلِع على التعليقات بانتظام هنا:

// 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 عند تغيُّر الحالة الداخلية (خاصة).

كذلك، من المفترض أن تتوقّع أدوات webdev من المكتبات المساعدة في تسهيل عملية الإعلام والأساليب المختلفة للخصائص المحسوبة (وتقليل النصوص النموذجية).

لنقم بإعداد المثال التالي، وهو فئة الدائرة. الفكرة هنا هي أن لدينا هذه الدائرة وهناك خاصية نصف قطر. في هذه الحالة، يكون نصف القطر موصّلًا وعندما تتغير قيمته، سيعلم نفسه في الواقع بتغيير القيمة. وسيتم تسليم ذلك مع كل التغييرات الأخرى التي يتم إجراؤها على هذا العنصر أو على أي عنصر آخر. وبشكلٍ أساسي، إذا كنت تنفّذ كائنًا، يجب أن يكون لديك خصائص اصطناعية أو محسوبة أو كان عليك اختيار استراتيجية للطريقة التي سيتم بها تنفيذ ذلك. بمجرد القيام بذلك، سيتناسب ذلك مع نظامك ككل.

يمكنك تخطّي الرمز البرمجي لرؤية هذا العمل من خلال "أدوات مطوري البرامج".

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 بالنسبة إليها. يجب أن نكون قادرين على معرفة ما حدث هنا. هذه في الواقع مشكلة لا يمكن حلها. يوضح المثال السبب. لا توجد طريقة حقًا لأي نظام ليعرف ما المقصود بذلك لأنه قد يكون رمزًا عشوائيًا. ويمكنه فعل ما يريد في هذه الحالة. يتم تحديث القيمة في كل مرة يتم فيها الوصول إليها، لذا فإن السؤال عما إذا كانت تغيرت ليس منطقيًا.

مراقبة عناصر متعددة من خلال معاودة اتصال واحدة

هناك نمط آخر ممكن باستخدام O.o() وهو مفهوم مراقب معاودة الاتصال الفردي. يسمح هذا باستخدام معاودة اتصال واحدة بصفته "مراقب" للعديد من العناصر المختلفة. سيتم تسليم المجموعة الكاملة من التغييرات إلى جميع الكائنات التي يلاحظها في "نهاية المهمة الصغيرة" (لاحظ التشابه مع مراقبي التغييرات).

مراقبة عناصر متعددة من خلال معاودة اتصال واحدة

التغييرات على نطاق واسع

ربما تعمل على تطبيق كبير حقًا وتضطر إلى العمل بانتظام على تغييرات واسعة النطاق. قد ترغب الكائنات في وصف التغييرات الدلالية الأكبر التي تؤثر في الكثير من الخصائص بطريقة أكثر إحكامًا (بدلاً من بث العديد من التغييرات على الخصائص).

ويمكن استخدام 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 }
التغييرات على نطاق واسع

يُعتبر كل شيء داخل "دالة الأداء" من عمل "التغيير الكبير". وبالتالي، سيتلقى المراقبون الذين يقبلون "التغيير الكبير" سجل "التغيير الكبير" فقط. المراقبون الذين لا يتلقون التغييرات الأساسية الناتجة عن العمل الذي "ينفذ الدالة".

مراقبة الصفائف

تحدثنا لفترة عن ملاحظة التغييرات التي تطرأ على الكائنات، ولكن ماذا عن الصفائف؟ سؤال رائع. عندما يقول لي أحدهم، "سؤال رائع". لا أسمع إجابتهم مطلقًا لأنني مشغولة جدًا بتهنئة نفسي على طرح هذا السؤال الرائع، ولكنني أتكيف. لدينا طرق جديدة للعمل مع الصفائف أيضًا!

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() ، وهي مهيكلة على شكل رسوم بيانية لمجموعات العناصر المرصودة مقابل عدد التغييرات. والنتيجة العامة هي أن أداء الفحص غير الصحيح يتناسب من الناحية الخوارزمية مع عدد الكائنات المرصودة بينما يتناسب أداء O.o() مع عدد الطفرات التي تم إجراؤها.

التدقيق

تقييم غير مكتمل للأداء

متصفِّح Chrome مع تفعيل Object.observe()

مراقبة الأداء

Polyfill Object.observe()

رائع - إذًا يمكن استخدام O.o() في Chrome 36، ولكن ماذا عن استخدامه في المتصفحات الأخرى؟ لقد تولينا أمرك. خوارزمية Monitore-JS من Polymer هي عبارة عن polyfill لـ 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. وسوف يخبرك عن تقطيعات الصفائف. وصلات الصفيف هي في الأساس مجموعة عمليات الوصل التي يجب عليك إجراؤها على صفيف من أجل تحويل الإصدار القديم من الصفيفة إلى الإصدار الجديد من الصفيفة. هذا نوع من التحويل أو عرض مختلف للصفيفة. هذا هو الحد الأدنى لمقدار العمل الذي تحتاج إلى القيام به للانتقال من الحالة القديمة إلى الحالة الجديدة.

مثال على الإبلاغ عن تغييرات في مصفوفة كأدنى مجموعة من الفواصل:

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() أطر العمل والمكتبات فرصة كبيرة لتحسين أداء ربط البيانات الخاص بها في المتصفحات التي تدعم الميزة.

أكد يهودا كاتز وإريك برين من إمبر أن إضافة دعم لـ O.o() هو في خارطة طريق Ember على المدى القريب. كتبت ميسكو هيرفي من Angular مستند تصميم حول اكتشاف التغيير المحسن في Angular 2.0. وسيتمثّل المنهج الطويل الأمد في الاستفادة من Object.observe() عند الوصول إلى ثبات Chrome، مع اختيار Watchtower.js، وهو أسلوبه الخاص برصد التغيير حتى ذلك الوقت. سوبر مثير.

الاستنتاجات

O.o() هو إضافة قوية إلى منصة الويب والتي يمكنك طرحها واستخدامها اليوم.

نأمل أن تتوفّر هذه الميزة مع الوقت في المزيد من المتصفّحات، ما يسمح لإطارات عمل JavaScript بتحسين الأداء بفضل إمكانية الوصول إلى إمكانات رصد العناصر الأصلية. من المفترض أن يتمكن هؤلاء الذين يستهدفون Chrome من استخدام O.o() في Chrome 36 (والإصدارات الأحدث منه)، كما يجب أن تكون هذه الميزة متاحة في إصدار Opera المستقبلي.

لذلك، يمكنك التعرّف على مؤلفي أُطر عمل JavaScript حول Object.observe() والخطط التي يخطّطون لها لاستخدامها لتحسين أداء عملية ربط البيانات في التطبيقات. لا شكّ في أنّ هناك أوقاتًا رائعة في المستقبل.

المراجِع

نشكر "رافاييل وينشتاين" و"جيك أرشيبالد" و"إريك بيدلمان" و"بول كينلان" و"فيفيان كرومويل" على ملاحظاتهم وآرائهم.