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

Addy Osmani
Addy Osmani

บทนำ

การปฏิวัติกำลังจะเกิดขึ้น 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, กระดูกสันหลัง)

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

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

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

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

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

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

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

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

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

การตรวจสอบสิ่งสกปรก

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

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

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

การใช้ Object.observe() แบบ Polyfill

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

บทสรุป

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

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

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

แหล่งข้อมูล

ขอขอบคุณ Rafael Weinstein, Jake Archibald, Eric Bidelman, Paul Kinlan และ Vivian Cromwell ที่ให้ข้อมูลและความคิดเห็น