การปฏิวัติการเชื่อมโยงข้อมูลด้วย Object.observe()

แอดดี้ ออสมานี
แอดดี ออสมานี

เกริ่นนำ

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

ตกลง เพื่อไม่ให้เกิดความล่าช้าเพิ่มเติม เรายินดีที่จะประกาศว่า Object.observe() ได้อยู่ใน Chrome 36 เวอร์ชันเสถียรแล้ว [ว้าว 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 มิลลิวินาทีต่อการอัปเดต และ O.o() ใช้เวลา 1-2 มิลลิวินาทีต่อการอัปเดต (เพิ่มขึ้นเร็วขึ้น 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()

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

ขอย้ำอีกครั้งว่านักพัฒนาเว็บควรคาดหวังให้ไลบรารีช่วยให้การแจ้งเตือนและวิธีต่างๆ ในการคำนวณพร็อพเพอร์ตี้เป็นเรื่องง่าย (และลดต้นแบบ)

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

ข้ามผ่านโค้ดเพื่อดูการทำงานนี้ในเครื่องมือสำหรับนักพัฒนาเว็บ

function Circle(r) {
  var radius = r;
 
  var notifier = Object.getNotifier(this);
  function notifyAreaAndRadius(radius) {
    notifier.notify({
      type: 'update',
      name: 'radius',
      oldValue: radius
    })
    notifier.notify({
      type: 'update',
      name: 'area',
      oldValue: Math.pow(radius * Math.PI, 2)
    });
  }
 
  Object.defineProperty(this, 'radius', {
    get: function() {
      return radius;
    },
    set: function(r) {
      if (radius === r)
        return;
      notifyAreaAndRadius(radius);
      radius = r;
    }
  });
 
  Object.defineProperty(this, 'area', {
    get: function() {
      return Math.pow(radius, 2) * Math.PI;
    },
    set: function(a) {
      r = Math.sqrt(a/Math.PI);
      notifyAreaAndRadius(radius);
      radius = r;
    }
  });
}
 
function observer(changes){
  changes.forEach(function(change, i){
    console.log(change);
  })
}
คอนโซลระเบียนการเปลี่ยนแปลงสังเคราะห์

พร็อพเพอร์ตี้ของตัวเข้าถึง

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

หากคุณกำหนดให้กับ JavaScript ของตัวเข้าถึง จะเพียงแค่เรียกใช้ฟังก์ชันที่นั่น และจากมุมมองของตัวเข้าถึง จะไม่มีอะไรเปลี่ยนแปลง เพียงแต่ให้โอกาสโค้ดบางอย่างทำงานได้

ปัญหาในเชิงความหมาย เราสามารถดูที่ค่าข้างต้นของเรา - 5 ของค่า เราอยากจะรู้ว่าเกิดอะไรขึ้นที่นี่ ปัญหานี้แก้ไขไม่ได้จริงๆ ตัวอย่างนี้แสดงสาเหตุ ไม่มีทางที่ระบบใดจะทราบความหมายของปัญหานี้ เนื่องจากค่านี้อาจเป็นโค้ดที่กำหนดเองได้ ในกรณีนี้ โมเดลนี้สามารถทำอะไรก็ได้ที่ต้องการ เป็นการอัปเดตมูลค่าทุกครั้งที่มีการเข้าถึง การถามว่าสิ่งที่เปลี่ยนแปลงนั้นดูสมเหตุสมผลไหม

การสังเกตหลายออบเจ็กต์ด้วยโค้ดเรียกกลับเดียว

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

การสังเกตหลายออบเจ็กต์ด้วยโค้ดเรียกกลับเดียว

การเปลี่ยนแปลงจำนวนมาก

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

O.o() จะช่วยในเรื่องนี้ในรูปแบบของยูทิลิตี 2 แบบ ได้แก่ notifier.performChange() และ notifier.notify() ซึ่งเราได้แนะนำไปแล้ว

การเปลี่ยนแปลงจำนวนมาก

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

เช่น notifier.performChange('foo', performFooChangeFn);

function Thingy(a, b, c) {
  this.a = a;
  this.b = b;
}

Thingy.MULTIPLY = 'multiply';
Thingy.INCREMENT = 'increment';
Thingy.INCREMENT_AND_MULTIPLY = 'incrementAndMultiply';


Thingy.prototype = {
  increment: function(amount) {
    var notifier = Object.getNotifier(this);

    // Tell the system that a collection of work comprises 
    // a given changeType. e.g
    // notifier.performChange('foo', performFooChangeFn);
    // notifier.notify('foo', 'fooChangeRecord');
    notifier.performChange(Thingy.INCREMENT, function() {
      this.a += amount;
      this.b += amount;
    }, this);

    notifier.notify({
      object: this,
      type: Thingy.INCREMENT,
      incremented: amount
    });
  },

  multiply: function(amount) {
    var notifier = Object.getNotifier(this);

    notifier.performChange(Thingy.MULTIPLY, function() {
      this.a *= amount;
      this.b *= amount;
    }, this);

    notifier.notify({
      object: this,
      type: Thingy.MULTIPLY,
      multiplied: amount
    });
  },

  incrementAndMultiply: function(incAmount, multAmount) {
    var notifier = Object.getNotifier(this);

    notifier.performChange(Thingy.INCREMENT_AND_MULTIPLY, function() {
      this.increment(incAmount);
      this.multiply(multAmount);
    }, this);

    notifier.notify({
      object: this,
      type: Thingy.INCREMENT_AND_MULTIPLY,
      incremented: incAmount,
      multiplied: multAmount
    });
  }
}

จากนั้นเราจะกำหนดตัวสังเกต 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 }
การเปลี่ยนแปลงจำนวนมาก

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

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

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

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() ได้ ซึ่งมีโครงสร้างเป็นกราฟขนาดที่สังเกตการณ์กับจำนวนการปิดเสียง ผลลัพธ์ทั่วไปคือประสิทธิภาพการตรวจสอบสิ่งสกปรกจะเป็นสัดส่วนตามอัลกอริทึมของจำนวนวัตถุที่สังเกตได้ ในขณะที่ประสิทธิภาพ O.o() จะแปรผันตามจำนวนการกลายพันธุ์ที่เกิดขึ้น

การตรวจสอบขยะ

การตรวจสอบประสิทธิภาพที่สกปรก

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

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

Polyfilling Object.observe()

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

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

แหล่งข้อมูล

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