การปฏิวัติการเชื่อมโยงข้อมูลด้วย 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) ช่วยให้คุณใช้การเชื่อมโยงข้อมูลแบบ 2 ทางได้โดยไม่ต้องใช้เฟรมเวิร์ก

แต่นั่นไม่ได้หมายความว่าคุณไม่ควรใช้ สำหรับโปรเจ็กต์ขนาดใหญ่ที่มีตรรกะทางธุรกิจที่ซับซ้อน เฟรมเวิร์กที่มีแนวคิดเป็นของตัวเองจะมีประโยชน์อย่างยิ่งและคุณควรใช้ต่อไป ซึ่งช่วยให้การเริ่มต้นใช้งานของนักพัฒนาแอปรายใหม่ง่ายขึ้น ต้องการการบำรุงรักษาโค้ดน้อยลง และกำหนดรูปแบบเกี่ยวกับวิธีทำงานทั่วไป เมื่อไม่จําเป็นต้องใช้ คุณสามารถเลือกใช้ไลบรารีขนาดเล็กที่มุ่งเน้นเฉพาะด้านมากขึ้น เช่น Polymer (ซึ่งใช้ประโยชน์จาก O.o() อยู่แล้ว)

แม้ว่าคุณจะใช้เฟรมเวิร์กหรือไลบรารี MV* อย่างมาก แต่ O.o() ก็สามารถช่วยปรับปรุงประสิทธิภาพให้ดีขึ้นได้ด้วยการนำไปใช้งานที่ง่ายและรวดเร็วขึ้นโดยยังคงใช้ API เดิม ตัวอย่างเช่น เมื่อปีที่แล้ว Angular พบว่าในการทดสอบประสิทธิภาพที่มีการทําการเปลี่ยนแปลงโมเดล การตรวจสอบข้อมูลใหม่ใช้เวลา 40 มิลลิวินาทีต่อการอัปเดต 1 ครั้ง และ O.o() ใช้เวลา 1-2 มิลลิวินาทีต่อการอัปเดต 1 ครั้ง (เร็วขึ้น 20-40 เท่า)

การเชื่อมโยงข้อมูลโดยไม่ต้องใช้โค้ดที่ซับซ้อนจํานวนมากยังหมายความว่าคุณไม่จําเป็นต้องตรวจสอบการเปลี่ยนแปลงอีกต่อไป แบตเตอรี่จึงใช้งานได้นานขึ้น

หากสนใจ O.o() อยู่แล้ว ให้ข้ามไปดูการแนะนำฟีเจอร์ หรืออ่านต่อเพื่อดูข้อมูลเพิ่มเติมเกี่ยวกับปัญหาที่ฟีเจอร์นี้ช่วยแก้ปัญหาได้

เราต้องการสังเกตอะไร

เมื่อพูดถึงการสังเกตข้อมูล โดยทั่วไปเราหมายถึงการคอยสังเกตการเปลี่ยนแปลงบางประเภทต่อไปนี้

  • การเปลี่ยนแปลงออบเจ็กต์ JavaScript ดิบ
  • เมื่อมีการเพิ่ม เปลี่ยนแปลง หรือลบพร็อพเพอร์ตี้
  • เมื่ออาร์เรย์มีการต่อองค์ประกอบเข้าและออก
  • การเปลี่ยนแปลงโปรโตไทป์ของออบเจ็กต์

ความสำคัญของการเชื่อมโยงข้อมูล

การเชื่อมโยงข้อมูลจะเริ่มมีความสำคัญเมื่อคุณสนใจการแยกการควบคุมโมเดลกับมุมมอง HTML เป็นกลไกการประกาศที่ยอดเยี่ยม แต่เป็นแบบคงที่โดยสมบูรณ์ โดยหลักการแล้ว คุณเพียงต้องประกาศความสัมพันธ์ระหว่างข้อมูลกับ DOM และอัปเดต DOM ให้เป็นข้อมูลล่าสุดอยู่เสมอ ซึ่งจะสร้างประโยชน์และช่วยประหยัดเวลาในการเขียนโค้ดซ้ำๆ มากจริงๆ ซึ่งส่งข้อมูลจาก DOM ไปยังสถานะภายในของแอปพลิเคชันหรือเซิร์ฟเวอร์

การเชื่อมโยงข้อมูลจะมีประโยชน์อย่างยิ่งเมื่อคุณมีอินเทอร์เฟซผู้ใช้ที่ซับซ้อนซึ่งคุณต้องเชื่อมต่อความสัมพันธ์ระหว่างพร็อพเพอร์ตี้หลายรายการในโมเดลข้อมูลกับองค์ประกอบหลายรายการในมุมมอง ซึ่งพบได้ทั่วไปในแอปพลิเคชันหน้าเว็บเดียวที่เรากําลังสร้างอยู่ในปัจจุบัน

เราได้สร้างวิธีสังเกตข้อมูลในเบราว์เซอร์ตั้งแต่ต้นเพื่อให้เฟรมเวิร์ก JavaScript (และไลบรารียูทิลิตีขนาดเล็กที่คุณเขียน) มีวิธีสังเกตการเปลี่ยนแปลงเพื่อจำลองข้อมูลโดยไม่ต้องใช้แฮ็กที่ช้าซึ่งผู้คนใช้กันในปัจจุบัน

สภาพโลกในปัจจุบัน

การตรวจสอบข้อมูล

คุณเคยเห็นการเชื่อมโยงข้อมูลที่ไหนมาก่อน หากคุณใช้ไลบรารี MV* สมัยใหม่ในการสร้างเว็บแอป (เช่น Angular, Knockout) คุณอาจคุ้นเคยกับการเชื่อมโยงข้อมูลโมเดลกับ DOM ต่อไปนี้เป็นตัวอย่างแอปรายชื่อโทรศัพท์ที่เราเชื่อมโยงค่าของโทรศัพท์แต่ละเครื่องในอาร์เรย์ phones (กำหนดไว้ใน JavaScript) กับรายการในลิสต์เพื่อให้ข้อมูลและ UI ของเราซิงค์กันอยู่เสมอ

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

จากนั้นเราจะระบุการเรียกกลับทุกครั้งที่มีการเปลี่ยนแปลง (Mutation) กับออบเจ็กต์ได้ ดังนี้

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() โดยส่งออบเจ็กต์เป็นอาร์กิวเมนต์แรกและส่งการเรียกกลับเป็นอาร์กิวเมนต์ที่ 2

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

ดังที่เห็นด้านล่าง การทำการเปลี่ยนแปลงออบเจ็กต์หลังจากการเรียกใช้นี้จะไม่ส่งผลให้ระบบแสดงรายการระเบียนการเปลี่ยนแปลงอีกต่อไป

การกลายพันธุ์

การระบุการเปลี่ยนแปลงความสนใจ

เราได้ดูพื้นฐานเกี่ยวกับวิธีรับรายการการเปลี่ยนแปลงของวัตถุที่สังเกตแล้ว ในกรณีที่คุณสนใจเฉพาะการเปลี่ยนแปลงบางส่วนที่เกิดขึ้นกับออบเจ็กต์แทนการเปลี่ยนแปลงทั้งหมด ทุกคนต้องใช้ตัวกรองจดหมายขยะ ผู้สังเกตการณ์จะระบุได้เฉพาะการเปลี่ยนแปลงประเภทที่ต้องการทราบผ่านรายการยอมรับ ซึ่งระบุได้โดยใช้อาร์กิวเมนต์ที่ 3 ของ 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() ยังมีแนวคิดเรื่องการแจ้งเตือนด้วย โฆษณาเหล่านี้ไม่ได้น่ารำคาญเหมือนโฆษณาที่คุณเห็นในโทรศัพท์ แต่มีประโยชน์ การแจ้งเตือนคล้ายกับ Mutation Observer ซึ่งจะเกิดขึ้นเมื่อทำไมโครแทสก์เสร็จแล้ว ในบริบทของเบราว์เซอร์ การดำเนินการนี้จะอยู่ที่ส่วนท้ายของตัวแฮนเดิลเหตุการณ์ปัจจุบันเกือบทุกครั้ง

ช่วงเวลานี้เหมาะมากเพราะโดยทั่วไปแล้ว 1 หน่วยของงานจะเสร็จสิ้นแล้ว และตอนนี้ผู้สังเกตการณ์ก็เริ่มทำงานได้ ซึ่งเป็นรูปแบบการประมวลผลแบบผลัดกันทีละฝ่ายที่ยอดเยี่ยม

เวิร์กโฟลว์ในการใช้เครื่องมือแจ้งเตือนมีลักษณะดังนี้

การแจ้งเตือน

มาดูตัวอย่างการใช้งานตัวแจ้งเตือนในทางปฏิบัติเพื่อกำหนดการแจ้งเตือนที่กำหนดเองเมื่อมีการเรียกข้อมูลหรือตั้งค่าพร็อพเพอร์ตี้ในออบเจ็กต์ โปรดติดตามความคิดเห็นที่นี่

// 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() คือแนวคิดเกี่ยวกับตัวสังเกตการณ์การเรียกคืนเพียงรายการเดียว วิธีนี้ช่วยให้ใช้การเรียกกลับรายการเดียวเป็น "ผู้สังเกตการณ์" สําหรับออบเจ็กต์ต่างๆ จำนวนมากได้ ฟังก์ชันการเรียกกลับจะได้รับชุดการเปลี่ยนแปลงทั้งหมดของออบเจ็กต์ทั้งหมดที่สังเกตเห็นเมื่อ "สิ้นสุดไมโครแทสก์" (โปรดสังเกตความคล้ายคลึงกับ Mutation Observer)

การสังเกตวัตถุหลายรายการด้วยคอลแบ็กรายการเดียว

การเปลี่ยนแปลงขนาดใหญ่

อาจเป็นเพราะคุณกําลังทํางานกับแอปขนาดใหญ่มากและต้องทําการเปลี่ยนแปลงขนาดใหญ่เป็นประจำ ออบเจ็กต์อาจต้องการอธิบายการเปลี่ยนแปลงเชิงความหมายขนาดใหญ่ซึ่งจะส่งผลต่อพร็อพเพอร์ตี้จํานวนมากในลักษณะที่กะทัดรัดยิ่งขึ้น (แทนที่จะเผยแพร่การเปลี่ยนแปลงพร็อพเพอร์ตี้จํานวนมาก)

O.o() ช่วยแก้ปัญหานี้ในรูปแบบยูทิลิตี 2 รายการ ได้แก่ 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
    });
  }
}

จากนั้นเรากําหนดตัวสังเกตการณ์ 2 ตัวสําหรับออบเจ็กต์ของเรา ตัวหนึ่งเป็นตัวรับการเปลี่ยนแปลงทั้งหมด และอีกตัวจะรายงานเฉพาะประเภทการยอมรับที่เฉพาะเจาะจงที่เรากําหนด (Thingy.INCREMENT, Thingy.MULTIPLY, Thingy.INCREMENT_AND_MULTIPLY)

var observer, observer2 = {
    records: undefined,
    callbackCount: 0,
    reset: function() {
      this.records = undefined;
      this.callbackCount = 0;
    },
};

observer.callback = function(r) {
    console.log(r);
    observer.records = r;
    observer.callbackCount++;
};

observer2.callback = function(r){
    console.log('Observer 2', r);
}


Thingy.observe = function(thingy, callback) {
  // Object.observe(obj, callback, optAcceptList)
  Object.observe(thingy, callback, [Thingy.INCREMENT,
                                    Thingy.MULTIPLY,
                                    Thingy.INCREMENT_AND_MULTIPLY,
                                    'update']);
}

Thingy.unobserve = function(thingy, callback) {
  Object.unobserve(thingy);
}

ตอนนี้เราเริ่มเล่นกับรหัสนี้ได้ มากำหนด Thingy ใหม่กัน

var thingy = new Thingy(2, 4);

สังเกตดูแล้วทําการเปลี่ยนแปลง สนุกมาก มีหลายสิ่งหลายอย่าง

// Observe thingy
Object.observe(thingy, observer.callback);
Thingy.observe(thingy, observer2.callback);

// Play with the methods thingy exposes
thingy.increment(3);               // { a: 5, b: 7 }
thingy.b++;                        // { a: 5, b: 8 }
thingy.multiply(2);                // { a: 10, b: 16 }
thingy.a++;                        // { a: 11, b: 16 }
thingy.incrementAndMultiply(2, 2); // { a: 26, b: 36 }
การเปลี่ยนแปลงขนาดใหญ่

ทุกอย่างภายใน "ฟังก์ชันการดําเนินการ" จะถือว่าทํางานของ "big-change" ผู้สังเกตการณ์ที่ยอมรับ "big-change" จะได้รับเฉพาะระเบียน "big-change" เท่านั้น ผู้สังเกตการณ์ที่ไม่ได้ดำเนินการดังกล่าวจะได้รับการเปลี่ยนแปลงพื้นฐานที่เกิดจากงานที่ "ดำเนินการฟังก์ชัน" ดำเนินการ

การสังเกตอาร์เรย์

เราคุยกันมานานแล้วเกี่ยวกับการสังเกตการเปลี่ยนแปลงของออบเจ็กต์ แต่แล้วอาร์เรย์ล่ะ เป็นคำถามที่ดีนะ เมื่อมีคนบอกฉันว่า "คำถามดีมาก" ฉันไม่เคยได้ยินคำตอบจากพวกเขาเลย เพราะมัวแต่ยินดีกับตัวเองที่ถามคำถามที่ยอดเยี่ยมเช่นนี้ แต่เราขออนุญาตนอกเรื่องสักนิด นอกจากนี้ เรายังมีวิธีการใหม่ๆ ในการใช้งานอาร์เรย์ด้วย

Array.observe() เป็นเมธอดที่จัดการการเปลี่ยนแปลงขนาดใหญ่กับตัวมันเอง เช่น การต่อ unshift หรือสิ่งใดก็ตามที่เปลี่ยนความยาวโดยนัย ในฐานะระเบียนการเปลี่ยนแปลง "การต่อ" แต่ภายในจะใช้ notifier.performChange("splice",...)

ต่อไปนี้เป็นตัวอย่างที่เราสังเกต "อาร์เรย์" ของโมเดลและรับรายการการเปลี่ยนแปลงกลับคืนมาเมื่อข้อมูลพื้นฐานมีการเปลี่ยนแปลง

var model = ['Buy some milk', 'Learn to code', 'Wear some plaid'];
var count = 0;

Array.observe(model, function(changeRecords) {
  count++;
  console.log('Array observe', changeRecords, count);
});

model[0] = 'Teach Paul Lewis to code';
model[1] = 'Channel your inner Paul Irish';
การสังเกตอาร์เรย์

ประสิทธิภาพ

วิธีคิดเกี่ยวกับผลกระทบด้านประสิทธิภาพการประมวลผลของ O.o() คือให้คิดว่าเป็นแคชการอ่าน โดยทั่วไปแล้ว แคชเป็นตัวเลือกที่ยอดเยี่ยมในกรณีต่อไปนี้ (เรียงตามลําดับความสําคัญ)

  1. ความถี่ของการอ่านมีมากกว่าความถี่ของการเขียน
  2. คุณสามารถสร้างแคชซึ่งแลกเปลี่ยนปริมาณงานที่คงที่ซึ่งเกี่ยวข้องกับการเขียนเพื่อประสิทธิภาพที่ดีขึ้นของอัลกอริทึมในระหว่างการอ่าน
  3. การเขียนที่ช้าลงอย่างต่อเนื่องนั้นยอมรับได้

O.o() ออกแบบมาสำหรับกรณีการใช้งานอย่างเช่น 1)

การตรวจสอบข้อมูลใหม่ต้องเก็บสําเนาของข้อมูลทั้งหมดที่คุณกําลังสังเกต ซึ่งหมายความว่าคุณจะต้องเสียค่าใช้จ่ายด้านหน่วยความจำเชิงโครงสร้างเพื่อการตรวจสอบข้อมูลใหม่ ซึ่งคุณไม่พบใน O.o() แม้ว่าการตรวจสอบข้อมูลใหม่จะเป็นโซลูชันแก้ปัญหาชั่วคราวที่ดี แต่ก็เป็นการแยกแยะข้อมูลแบบรั่วไหลโดยพื้นฐาน ซึ่งอาจทําให้แอปพลิเคชันมีความซับซ้อนโดยไม่จําเป็น

เหตุผล การตรวจสอบข้อมูลที่มีการเปลี่ยนแปลงต้องทำงานทุกครั้งที่ข้อมูลอาจมีการเปลี่ยนแปลง วิธีการที่มีประสิทธิภาพมากในการดำเนินการนี้ไม่มีอยู่จริง และวิธีการใดๆ ก็มีข้อเสียที่สำคัญ (เช่น การตรวจสอบช่วงเวลาการโหวตอาจทำให้เกิดข้อบกพร่องที่มองเห็นได้และเงื่อนไขการแข่งขันระหว่างข้อกังวลเกี่ยวกับโค้ด) นอกจากนี้ การตรวจสอบสถานะแบบไม่สะอาดยังต้องใช้รีจิสทรีส่วนกลางของผู้สังเกตการณ์ ซึ่งก่อให้เกิดอันตรายจากการรั่วไหลของหน่วยความจำและค่าใช้จ่ายในการรื้อถอน ซึ่ง O.o() หลีกเลี่ยง

มาดูตัวเลขกัน

การทดสอบการเปรียบเทียบประสิทธิภาพด้านล่าง (มีอยู่ใน GitHub) ช่วยให้เราเปรียบเทียบการตรวจสอบข้อมูลที่เปลี่ยนแปลงกับ O.o() ได้ โดยมีการกําหนดโครงสร้างเป็นกราฟของขนาดชุดออบเจ็กต์ที่สังเกตได้เทียบกับจํานวนการกลายพันธุ์ ผลลัพธ์ทั่วไปคือประสิทธิภาพของการตรวจสอบข้อมูลใหม่จะแปรผันตามจำนวนออบเจ็กต์ที่สังเกตได้ ในขณะที่ประสิทธิภาพของ O.o() จะแปรผันตามจำนวนการกลายพันธุ์ที่เกิดขึ้น

การตรวจสอบข้อมูล

ประสิทธิภาพของการตรวจสอบข้อมูลที่ไม่ถูกต้อง

Chrome ที่เปิด Object.observe()

สังเกตประสิทธิภาพ

การทดแทน Object.observe()

เยี่ยมเลย แสดงว่า O.o() ใช้ได้ใน Chrome 36 แต่จะใช้ในเบราว์เซอร์อื่นๆ ได้ไหม เรารวบรวมมาให้แล้ว Observe-JS ของ Polymer เป็น polyfill สำหรับ O.o() ซึ่งจะใช้การใช้งานแบบเนทีฟหากมี แต่หากไม่มีก็จะใช้ polyfill และเพิ่ม Sugaring ที่มีประโยชน์บางอย่างไว้ด้านบน ซึ่งแสดงมุมมองรวมของโลกที่สรุปการเปลี่ยนแปลงและส่งรายงานเกี่ยวกับสิ่งที่เปลี่ยนแปลง 2 สิ่งที่มีประโยชน์มากในเครื่องมือนี้ ได้แก่

  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() จะช่วยให้เฟรมเวิร์กและไลบรารีมีโอกาสอย่างมากในการปรับปรุงประสิทธิภาพการเชื่อมโยงข้อมูลในเบราว์เซอร์ที่รองรับฟีเจอร์นี้

Yehuda Katz และ Erik Bryn จาก Ember ยืนยันว่าการเพิ่มการรองรับ O.o() อยู่ในแผนงานระยะสั้นของ Ember Misko Hervy จาก Angular ได้เขียนเอกสารการออกแบบเกี่ยวกับการตรวจหาการเปลี่ยนแปลงที่ปรับปรุงแล้วของ Angular 2.0 แนวทางระยะยาวของทีมคือใช้ประโยชน์จาก Object.observe() เมื่อเปิดตัวใน Chrome เวอร์ชันเสถียร โดยเลือกใช้ Watchtower.js ซึ่งเป็นแนวทางการตรวจหาการเปลี่ยนแปลงของตนเองจนกว่าจะถึงเวลานั้น น่าตื่นเต้นสุดๆ

สรุป

O.o() เป็นการเพิ่มประสิทธิภาพที่ยอดเยี่ยมสำหรับแพลตฟอร์มเว็บที่คุณนำไปใช้ได้ตั้งแต่วันนี้

เราหวังว่าในอนาคตฟีเจอร์นี้จะพร้อมให้บริการในเบราว์เซอร์อื่นๆ มากขึ้น ซึ่งจะช่วยให้เฟรมเวิร์ก JavaScript มีประสิทธิภาพมากขึ้นจากการเข้าถึงความสามารถในการสังเกตวัตถุแบบเนทีฟ ผู้ที่ต้องการกำหนดเป้าหมายไปยัง Chrome ควรใช้ O.o() ใน Chrome 36 (ขึ้นไป) ได้ และฟีเจอร์นี้ควรพร้อมใช้งานใน Opera เวอร์ชันในอนาคตด้วย

ดังนั้น โปรดพูดคุยกับผู้เขียนเฟรมเวิร์ก JavaScript เกี่ยวกับ Object.observe() และวิธีที่ผู้เขียนวางแผนที่จะใช้ Object.observe() เพื่อปรับปรุงประสิทธิภาพการเชื่อมโยงข้อมูลในแอป นี่เป็นช่วงเวลาที่น่าตื่นเต้นอย่างแน่นอน

แหล่งข้อมูล

ขอขอบคุณ Rafael Weinstein, Jake Archibald, Eric Bidelman, Paul Kinlan และ Vivian Cromwell สำหรับข้อมูลและการตรวจสอบ