دورات ربط البيانات باستخدام Object.observe()

Addy Osmani
Addy Osmani

مقدمة

ثورة قادمة هناك إضافة جديدة إلى 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 بين الحالة الداخلية للتطبيق أو الخادم.

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

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

الوضع الحالي في العالم

الفحص غير النظيفة

أين رأيت ربط البيانات من قبل؟ إذا كنت تستخدم مكتبة فيديوهات موسيقية حديثة* لإنشاء تطبيقاتك على الويب (مثل 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.observe() وObject.unobserve()

لنتخيل أن لدينا كائن JavaScript بسيط يمثل نموذجًا:

// 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';

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

تقرير وحدة التحكم

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

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

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

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

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

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

تساعد الدالة 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);

يُرجى ملاحظتها ثم إجراء بعض التغييرات. يا له من أمر ممتع. هناك الكثير من العناصر.

// 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) إجراء مقارنة بين "التحقّق غير النظيفة" و"O.o() "، مع العِلم بأنّه يتم تنظيمها كرسوم بيانية لـ "Note-Object-Set-Size" مقابل "Number-Of-Mutations". النتيجة العامة هي أن أداء الفحص غير النظيفة يتناسب خوارزميًا مع عدد الكائنات المرصودة بينما يتناسب أداء O.o() مع عدد الطفرات التي حدثت.

التحقّق من صحة البيانات

أداء التحقّق من صحة البيانات

Chrome مع تفعيل Object.observe()

ملاحظة الأداء

Polyfilling Object.observe()

رائع، يمكن استخدام O.o() في Chrome 36، ولكن ماذا عن استخدامه في المتصفحات الأخرى؟ سنقدّم لك المساعدة المطلوبة. Observe-JS في البوليمر عبارة عن رمز 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() أطر العمل والمكتبات فرصة كبيرة لتحسين أداء ربط البيانات الخاص بها في المتصفحات التي تدعم هذه الميزة.

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

الاستنتاجات

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

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

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

الموارد

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