เกริ่นนำ
การปฏิวัติกำลังจะมาถึง 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.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() คือให้คิดว่านี่เป็นเหมือนแคชการอ่าน โดยทั่วไปแล้ว แคชเป็นตัวเลือกที่ดีในกรณีต่อไปนี้ (ตามลำดับความสำคัญ)
- ความถี่ของการอ่านจะมากกว่าความถี่ของการเขียน
- คุณสามารถสร้างแคชซึ่งแลกเปลี่ยนปริมาณงานที่เกี่ยวข้องระหว่างการเขียนคงที่เพื่อประสิทธิภาพที่ดีขึ้นตามหลักอัลกอริทึมระหว่างการอ่าน
- การชะลอเวลาคงที่ของการเขียนนั้นยอมรับได้
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 เอาไว้และรวมการใส่น้ำตาลที่เป็นประโยชน์ไว้ด้านบน โดยนำเสนอมุมมองโดยรวมของโลกที่สรุปการเปลี่ยนแปลงและส่งรายงานเกี่ยวกับการเปลี่ยนแปลง สิ่งที่มีประสิทธิภาพจริงๆ สองอย่างคือ
- คุณสังเกตเส้นทางได้ ซึ่งหมายความว่าคุณสามารถพูดว่า ฉันต้องการสังเกต "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() จะมอบโอกาสครั้งใหญ่ให้กับเฟรมเวิร์กและไลบรารีในการปรับปรุงประสิทธิภาพการเชื่อมโยงข้อมูลในเบราว์เซอร์ที่รองรับฟีเจอร์นี้
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() บน Harmony wiki>
- การเชื่อมโยงข้อมูลด้วย Object.observe() โดย Rick Waldron
- ทุกอย่างที่คุณต้องการรู้เกี่ยวกับ Object.observe() - JSConf
- เหตุใด Object.observe() จึงเป็นฟีเจอร์ ES7 ที่ดีที่สุด
ขอขอบคุณ Rafael Weinstein, Jake Archibald, Eric Bidelman, Paul Kinlan และ Vivian Cromwell สำหรับข้อมูลและรีวิว