مقدمة
ثورة قادمة هناك إضافة جديدة إلى 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()).
حتى إذا كنت تستخدِم إطار عمل أو مكتبة MV* بشكل كبير، يمكن أن يوفّر لك الإجراء 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 وBackbone)
كائنات الحاويات هي المكان الذي ينشئ فيه إطار العمل كائنات تحتوي على البيانات. لديهم أدوات وصول إلى البيانات ويمكنهم تسجيل ما تحدّده أو تحصل عليه ونشره داخليًا. هذا الإجراء مناسب. يحقّق أداءً جيدًا نسبيًا ويتميز بسلوك خوارزمي جيد. يمكنك الاطّلاع أدناه على مثال على عناصر الحاوية التي تستخدم Ember:
// Container objects
MyApp.president = Ember.Object.create({
name: "Barack Obama"
});
MyApp.country = Ember.Object.create({
// ending a property with "Binding" tells Ember to
// create a binding to the presidentName property
presidentNameBinding: "MyApp.president.name"
});
// Later, after Ember has resolved bindings
MyApp.country.get("presidentName");
// "Barack Obama"
// Data from the server needs to be converted
// Composes poorly with existing code
وتتناسب تكلفة اكتشاف ما تغيّر هنا مع عدد التغييرات التي تم إجراؤها. هناك مشكلة أخرى وهي أنّك تستخدم الآن نوعًا مختلفًا من العناصر. بشكل عام، عليك تحويل البيانات التي تحصل عليها من الخادم إلى هذه العناصر حتى تصبح قابلة للتتبّع.
لا يتوافق هذا الإجراء بشكلٍ جيد مع رمز JS الحالي لأنّ معظم الرموز البرمجية تفترض أنّه يمكنها العمل على البيانات الأولية. لا يُستخدم هذا الإجراء مع هذه الأنواع من العناصر المخصّصة.
Introducing Object.observe()
نريد الحصول على أفضل ما في الحالتَين، أي طريقة لمراقبة البيانات مع إتاحة استخدام عناصر البيانات الأولية (عناصر JavaScript العادية) إذا أردنا ذلك، وبدون الحاجة إلى التحقّق من كل شيء في كلّ وقت. محتوى يتمتع بسلوك خوارزمي جيد محتوى يُنسج بشكل جيد ويُدمج في المنصة وهذا هو جمال ما تقدّمه Object.observe()
.
ويسمح لنا ذلك بملاحظة عنصر وتغيير السمات والاطّلاع على تقرير التغييرات التي تم إجراؤها. ولكن يكفي من النظريات، لنلقِ نظرة على بعض الرموز البرمجية.

Object.observe() وObject.unobserve()
لنفترض أنّ لدينا كائن JavaScript بسيطًا يمثّل نموذجًا:
// A model can be a simple vanilla object
var todoModel = {
label: 'Default',
completed: false
};
يمكننا بعد ذلك تحديد دالة استدعاء عند إجراء أيّ تغييرات على الكائن:
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() أيضًا مفهوم الإشعارات. وهي ليست مثل تلك الإعلانات المزعجة التي تظهر على الهاتف، بل مفيدة. تشبه الإشعارات مراقبي الطفرات. تحدث هذه المكافآت في نهاية المهمة الصغيرة. في سياق المتصفّح، سيكون ذلك دائمًا تقريبًا في نهاية معالِج الحدث الحالي.
إنّ هذا التوقيت مناسب لأنّه عادةً ما يتم الانتهاء من وحدة عمل واحدة، ويصبح بإمكان المراقبين الآن تنفيذ عملهم. وهو نموذج معالجة تفاعلي جيد.
في ما يلي الخطوات التي يجب اتّباعها لاستخدام مُرسِل إشعارات:

لنلقِ نظرة على مثال على كيفية استخدام مُرسِلي الإشعارات في الممارسة لتحديد إشعارات مخصّصة عند الحصول على سمات في عنصر أو ضبطها. يمكنك مراقبة التعليقات هنا:
// 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 لا تتضمّن مفهوم التغييرات في القيمة لمستخدِمي Accessors. إنّ دالة الوصول هي مجرد مجموعة من الدوال.
في حال تحديد 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() هي التفكير فيه كذاكرة تخزين مؤقت للقراءة. بشكل عام، تكون ذاكرة التخزين المؤقت خيارًا رائعًا في الحالات التالية (بترتيب الأهمية):
- يُرجى العِلم أنّ معدّل تكرار عمليات القراءة يفوق معدّل تكرار عمليات الكتابة.
- يمكنك إنشاء ذاكرة تخزين مؤقت تتبادل بين كمية العمل الثابتة المُدرَجة أثناء عمليات الكتابة والأداء الأفضل من حيث الخوارزميات أثناء عمليات القراءة.
- إنّ التباطؤ المستمر في وقت عمليات الكتابة مقبول.
تم تصميم O.o() لحالات الاستخدام مثل 1).
تتطلّب ميزة "التحقّق من التغييرات" الاحتفاظ بنسخة من جميع البيانات التي تراقبها. وهذا يعني أنّك تتحمل تكلفة ذاكرة هيكلية للتحقّق من التغييرات التي لا تحصل عليها باستخدام O.o(). على الرغم من أنّ التحقّق من التغييرات هو حلّ مؤقت جيد، إلا أنّه أيضًا عملية تجريدية غير دقيقة يمكن أن تؤدي إلى تعقيدات غير ضرورية للتطبيقات.
لماذا؟ يجب تنفيذ عملية التحقّق من التغييرات في أي وقت قد تتغيّر فيه البيانات. ببساطة، لا تتوفّر طريقة فعّالة جدًا لإجراء ذلك، وأيّ نهج اتّباعه له عيوب كبيرة (على سبيل المثال، التحقّق من فاصل الاستطلاع قد يؤدي إلى ظهور عناصر مرئية وحالات تداخل بين مشاكل الرموز البرمجية). تتطلّب ميزة "التحقّق من التغييرات" أيضًا إنشاء سجلّ عالمي للمراقبين، ما يتسبب في حدوث مخاطر تسرُّب الذاكرة وتكاليف إزالة العناصر التي يتجنّبها الإجراء O.o().
لنلقِ نظرة على بعض الأرقام.
تسمح لنا اختبارات الأداء أدناه (المتوفّرة على GitHub) بمقارنة فحص التغييرات في الذاكرة مع O.o(). وهي مصمّمة كرسوم بيانية لحجم مجموعة العناصر المرصودة مقارنةً بعدد الطفرات. والنتيجة العامة هي أنّ أداء التحقّق من التغييرات يتناسب بشكلٍ آلي مع عدد العناصر المرصودة، في حين يتناسب أداء O.o() مع عدد عمليات التحويل التي تم إجراؤها.
التحقّق من صحة البيانات

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

إضافة polyfill إلى دالة Object.observe()
رائع، إذًا يمكن استخدام O.o() في الإصدار 36 من Chrome، ولكن ماذا عن استخدامه في المتصفّحات الأخرى؟ سنقدّم لك المساعدة المطلوبة. Observe-JS من Polymer هو عنصر polyfill لواجهة برمجة التطبيقات O.o()، وسيستخدم التنفيذ الأصلي إذا كان متوفّرًا، ولكن في حال عدم توفّره، سيستخدم polyfill ويضيف بعض التحسينات المفيدة. ويقدّم هذا التقرير نظرة إجمالية على مستوى العالم تلخّص التغييرات وتوفّر تقريرًا عن التغييرات التي طرأت. هناك شيئان فعّالان للغاية يعرضهما هذا التقرير:
- يمكنك مراقبة المسارات. وهذا يعني أنّه يمكنك القول، أريد مراقبة "foo.bar.baz" من كائن معيّن، وسيخبرك عندما تتغيّر القيمة في هذا المسار. إذا تعذّر الوصول إلى المسار، يتم اعتبار القيمة غير محدّدة.
مثال على رصد قيمة في مسار من عنصر معيّن:
var obj = { foo: { bar: 'baz' } };
var observer = new PathObserver(obj, 'foo.bar');
observer.open(function(newValue, oldValue) {
// respond to obj.foo.bar having changed value.
});
- سيخبرك هذا القسم عن عمليات تقسيم الصفيف. إنّ عمليات تقسيم الصفيف هي في الأساس الحد الأدنى من عمليات تقسيم الصفيف التي عليك إجراؤها على صفيف من أجل تحويل الإصدار القديم من الصفيف إلى الإصدار الجديد منه. هذا نوع من التحويل أو عرض مختلف للصفيف. وهو الحد الأدنى من العمل الذي عليك تنفيذه للانتقال من الحالة القديمة إلى الحالة الجديدة.
مثال على الإبلاغ عن تغييرات في صفيف كمجموعة دنيا من عمليات الربط:
var arr = [0, 1, 2, 4];
var observer = new ArrayObserver(arr);
observer.open(function(splices) {
// respond to changes to the elements of arr.
splices.forEach(function(splice) {
splice.index; // index position that the change occurred.
splice.removed; // an array of values representing the sequence of elements which were removed
splice.addedCount; // the number of elements which were inserted.
});
});
الإطارات الأساسية وObject.observe()
كما ذكرنا، ستوفّر دالة O.o() إطارات العمل والمكتبات فرصة كبيرة لتحسين أداء ربط البيانات في المتصفّحات التي تتيح هذه الميزة.
أكّد "يهودا كاتس" و"إريك براين" من Ember أنّ إضافة وظائف O.o() قيد المراجعة في خطة Ember على المدى القريب. كتب "ميسوك هرفي" من Angular مستند تصميم حول ميزة رصد التغيير المحسّنة في Angular 2.0. وسيعتمد الفريق على Object.observe() على المدى الطويل عندما يصبح متاحًا في الإصدار الثابت من Chrome، وسيستخدم Watchtower.js، وهو نهج رصد التغييرات الخاص به، إلى ذلك الحين. هذا أمر رائع.
الاستنتاجات
O.o() هي إضافة فعّالة لمنصّة الويب التي يمكنك استخدامها اليوم.
نأمل أن تصبح الميزة متاحة في المزيد من المتصفحات بمرور الوقت، ما يسمح لإطارات عمل JavaScript بتحسين الأداء من خلال الوصول إلى إمكانات مراقبة الأجسام الأصلية. من المفترض أن يتمكّن مطوّرو التطبيقات التي تستهدف Chrome من استخدام O.o() في الإصدار 36 من Chrome (والإصدارات الأحدث)، ومن المفترض أن تتوفّر الميزة أيضًا في إصدار مستقبلي من Opera.
لذا، يمكنك التواصل مع مؤلفي إطارات عمل JavaScript بشأن Object.observe()
وكيفية تخطيطهم لاستخدامها لتحسين أداء ربط البيانات في تطبيقاتك. نتطلّع إلى تقديم ميزات جديدة ومثيرة في المستقبل.
الموارد
- Object.observe() على wiki الخاص بـ Harmony>
- ربط البيانات باستخدام Object.observe() من تأليف "ريك والدورون"
- كل ما تريد معرفته عن دالة Object.observe() - JSConf
- سبب كون دالة Object.observe() هي أفضل ميزة في ES7
نشكر "رافائيل وينشتاين" و"جاك أرشيبالد" و"إريك بيدلمان" و"بول كينلان" و"فيفيان كرومويل" على مساهماتهم ومراجعاتهم.