บทนำ
การปฏิวัติกำลังจะเกิดขึ้น 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);
});
});
ระบบจะรายงานการเปลี่ยนแปลงทุกครั้งที่ดำเนินการดังนี้
![การเปลี่ยนแปลงที่รายงาน](https://web.developers.google.cn/static/articles/es7-observe/image/change-reported-2d04b2fe4d7e5.png?hl=th)
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 ทําสิ่งเหล่านี้ได้อย่างไร เบื้องหลังคือมีสิ่งที่เรียกว่าการตรวจสอบข้อมูลใหม่
![การตรวจสอบแบบไม่สะอาด](https://web.developers.google.cn/static/articles/es7-observe/image/dirty-checking-d67f969846396.png?hl=th)
แนวคิดพื้นฐานของการตรวจสอบข้อมูลที่มีการเปลี่ยนแปลงคือเมื่อใดก็ตามที่ข้อมูลอาจมีการเปลี่ยนแปลง คลังต้องตรวจสอบว่าข้อมูลมีการเปลี่ยนแปลงหรือไม่ผ่านวงจรข้อมูลสรุปหรือการเปลี่ยนแปลง ในกรณีของ Angular รอบข้อมูลสรุปจะระบุนิพจน์ทั้งหมดที่ลงทะเบียนเพื่อเฝ้าดูว่ามีการเปลี่ยนแปลงหรือไม่ ฟีเจอร์นี้ทราบค่าก่อนหน้าของโมเดล และหากค่ามีการเปลี่ยนแปลง ระบบจะเรียกเหตุการณ์การเปลี่ยนแปลง ประโยชน์หลักสำหรับนักพัฒนาแอปคือคุณจะได้ใช้ข้อมูลออบเจ็กต์ JavaScript ดิบซึ่งใช้งานง่ายและคอมโพสิทได้ดีพอสมควร ข้อเสียคือมีพฤติกรรมของอัลกอริทึมที่ไม่ดีและอาจมีค่าใช้จ่ายสูงมาก
![การตรวจสอบข้อมูลใหม่](https://web.developers.google.cn/static/articles/es7-observe/image/dirty-checking-43ad997ca6175.png?hl=th)
ค่าใช้จ่ายของการดำเนินการนี้จะแปรผันตามจำนวนวัตถุทั้งหมดที่สังเกตได้ เราอาจต้องตรวจสอบแบบไม่สมบูรณ์เป็นจำนวนมาก นอกจากนี้ คุณอาจต้องใช้วิธีเรียกใช้การตรวจสอบข้อมูลที่เปลี่ยนแปลงเมื่อข้อมูลอาจมีการเปลี่ยนแปลง เฟรมเวิร์กใช้เทคนิคที่ชาญฉลาดมากมายสำหรับการดำเนินการนี้ เราไม่แน่ใจว่าระบบนี้จะทำงานได้อย่างสมบูรณ์แบบไหม
ระบบนิเวศของเว็บควรมีความสามารถในการสร้างสรรค์และพัฒนากลไกการประกาศของตนเองได้มากขึ้น เช่น
- ระบบรูปแบบที่อิงตามข้อจำกัด
- ระบบการคงข้อมูลไว้โดยอัตโนมัติ (เช่น การเปลี่ยนแปลงที่คงไว้ใน 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()](https://web.developers.google.cn/static/articles/es7-observe/image/objectobserve-05f1a3cde8b4f.png?hl=th)
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';
เมื่อตรวจสอบคอนโซลแล้ว เราได้รับข้อมูลที่เป็นประโยชน์บางอย่าง เราทราบพร็อพเพอร์ตี้ที่เปลี่ยนแปลง ลักษณะการเปลี่ยนแปลง และค่าใหม่
![รายงานคอนโซล](https://web.developers.google.cn/static/articles/es7-observe/image/console-report-055777178bf69.png?hl=th)
เย่! ลาก่อน การตรวจสอบข้อมูลใหม่ จารึกบนหลุมศพควรเป็น Comic Sans มาเปลี่ยนพร็อพเพอร์ตี้อื่นกัน completeBy
ครั้งนี้
todoModel.completeBy = '01/01/2014';
เราได้รับรายงานการเปลี่ยนแปลงอีกครั้งเรียบร้อยแล้วดังที่เห็นด้านล่าง
![เปลี่ยนรายงาน](https://web.developers.google.cn/static/articles/es7-observe/image/change-report-a791b15278ca5.png?hl=th)
เยี่ยมเลย จะเกิดอะไรขึ้นหากตอนนี้เราตัดสินใจลบพร็อพเพอร์ตี้ "เสร็จสมบูรณ์" ออกจากออบเจ็กต์
delete todoModel.completed;
![เสร็จสมบูรณ์](https://web.developers.google.cn/static/articles/es7-observe/image/completed-e55c09dfea155.png?hl=th)
ดังที่เราเห็น รายงานการเปลี่ยนแปลงที่แสดงผลมีข้อมูลเกี่ยวกับการลบ ค่าใหม่ของพร็อพเพอร์ตี้เป็น "ไม่ระบุ" ตามที่เราคาดไว้ ตอนนี้เราทราบแล้วว่าคุณสามารถดูได้ว่ามีการเพิ่มที่พักเมื่อใด เมื่อมีการลบ โดยพื้นฐานแล้ว ชุดพร็อพเพอร์ตี้บนออบเจ็กต์ ("ใหม่" "ถูกลบ" "ได้รับการกำหนดค่าใหม่") และการเปลี่ยนแปลงโปรโตไทป์ (proto)
ระบบสังเกตการณ์ทุกระบบจะมีวิธีการหยุดการฟังการเปลี่ยนแปลงด้วย ในกรณีนี้ Object.unobserve()
ซึ่งมีลายเซ็นเดียวกับ O.o() แต่เรียกได้ดังนี้
Object.unobserve(todoModel, observer);
ดังที่เห็นด้านล่าง การทำการเปลี่ยนแปลงออบเจ็กต์หลังจากการเรียกใช้นี้จะไม่ส่งผลให้ระบบแสดงรายการระเบียนการเปลี่ยนแปลงอีกต่อไป
![การกลายพันธุ์](https://web.developers.google.cn/static/articles/es7-observe/image/mutations-92a5d9df68699.png?hl=th)
การระบุการเปลี่ยนแปลงความสนใจ
เราได้ดูพื้นฐานเกี่ยวกับวิธีรับรายการการเปลี่ยนแปลงของวัตถุที่สังเกตแล้ว ในกรณีที่คุณสนใจเฉพาะการเปลี่ยนแปลงบางส่วนที่เกิดขึ้นกับออบเจ็กต์แทนการเปลี่ยนแปลงทั้งหมด ทุกคนต้องใช้ตัวกรองจดหมายขยะ ผู้สังเกตการณ์จะระบุได้เฉพาะการเปลี่ยนแปลงประเภทที่ต้องการทราบผ่านรายการยอมรับ ซึ่งระบุได้โดยใช้อาร์กิวเมนต์ที่ 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 หน่วยของงานจะเสร็จสิ้นแล้ว และตอนนี้ผู้สังเกตการณ์ก็เริ่มทำงานได้ ซึ่งเป็นรูปแบบการประมวลผลแบบผลัดกันทีละฝ่ายที่ยอดเยี่ยม
เวิร์กโฟลว์ในการใช้เครื่องมือแจ้งเตือนมีลักษณะดังนี้
![การแจ้งเตือน](https://web.developers.google.cn/static/articles/es7-observe/image/notifications-a017cbcb0783a.png?hl=th)
มาดูตัวอย่างการใช้งานตัวแจ้งเตือนในทางปฏิบัติเพื่อกำหนดการแจ้งเตือนที่กำหนดเองเมื่อมีการเรียกข้อมูลหรือตั้งค่าพร็อพเพอร์ตี้ในออบเจ็กต์ โปรดติดตามความคิดเห็นที่นี่
// 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);
![คอนโซลการแจ้งเตือน](https://web.developers.google.cn/static/articles/es7-observe/image/notifications-console-9498345f0d213.png?hl=th)
ในส่วนนี้ เราจะรายงานเมื่อค่าของพร็อพเพอร์ตี้ข้อมูลมีการเปลี่ยนแปลง ("อัปเดต") ข้อมูลอื่นๆ ที่การติดตั้งใช้งานออบเจ็กต์เลือกที่จะรายงาน (notifier.notifyChange()
)
ประสบการณ์หลายปีในแพลตฟอร์มเว็บทำให้เราทราบว่าแนวทางแบบซิงค์เป็นสิ่งแรกที่คุณควรลองใช้เนื่องจากเข้าใจได้ง่ายที่สุด ปัญหาคือการสร้างรูปแบบการประมวลผลที่เป็นอันตรายโดยพื้นฐาน หากคุณเขียนโค้ดและบอกว่าอัปเดตพร็อพเพอร์ตี้ของออบเจ็กต์ คุณไม่ต้องการให้เกิดสถานการณ์ที่การอัปเดตพร็อพเพอร์ตี้ของออบเจ็กต์นั้นอาจเชิญชวนให้โค้ดที่กําหนดเองทําตามที่ต้องการ ไม่ควรทำให้สมมติฐานของคุณเป็นโมฆะขณะที่ดำเนินการอยู่กลางคัน
หากคุณเป็นผู้สังเกตการณ์ คุณก็คงไม่อยากได้รับสายหากมีคนกำลังทำสิ่งใดสิ่งหนึ่งอยู่ คุณไม่ต้องการให้ระบบขอให้คุณไปทำงานในสถานะที่โลกไม่สอดคล้องกัน ส่งผลให้ต้องตรวจสอบข้อผิดพลาดมากขึ้น การพยายามทนกับสถานการณ์ที่เลวร้ายมากขึ้น และโดยทั่วไปแล้ว รูปแบบนี้ใช้งานยาก การทำงานแบบแอซิงค์นั้นจัดการได้ยากกว่า แต่ท้ายที่สุดแล้วจะเป็นรูปแบบที่ดีกว่า
โซลูชันสำหรับปัญหานี้คือระเบียนการเปลี่ยนแปลงสังเคราะห์
ระเบียนการเปลี่ยนแปลงสังเคราะห์
โดยทั่วไปแล้ว หากต้องการใช้ตัวรับค่าหรือพร็อพเพอร์ตี้ที่คำนวณแล้ว คุณมีหน้าที่แจ้งให้ทราบเมื่อค่าเหล่านี้มีการเปลี่ยนแปลง การดำเนินการนี้อาจต้องใช้เวลาเพิ่มอีกเล็กน้อย แต่ออกแบบมาเพื่อเป็นฟีเจอร์ชั้นยอดของกลไกนี้ และการแจ้งเตือนเหล่านี้จะส่งไปพร้อมกับการแจ้งเตือนที่เหลือจากออบเจ็กต์ข้อมูลพื้นฐาน จากพร็อพเพอร์ตี้ข้อมูล
![ระเบียนการเปลี่ยนแปลงสังเคราะห์](https://web.developers.google.cn/static/articles/es7-observe/image/synthetic-change-records-44d49a708cc65.png?hl=th)
การสังเกตการณ์ตัวรับค่าและพร็อพเพอร์ตี้ที่คำนวณแล้วสามารถแก้ไขได้ด้วย 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);
})
}
![คอนโซลระเบียนการเปลี่ยนแปลงสังเคราะห์](https://web.developers.google.cn/static/articles/es7-observe/image/synthetic-change-records-42c510594dcb.png?hl=th)
พร็อพเพอร์ตี้ตัวรับค่า
หมายเหตุสั้นๆ เกี่ยวกับพร็อพเพอร์ตี้ตัวรับค่า เราได้กล่าวไปก่อนหน้านี้ว่าคุณจะสังเกตเห็นเฉพาะการเปลี่ยนแปลงค่าสำหรับพร็อพเพอร์ตี้ข้อมูล ไม่ใช่สำหรับพร็อพเพอร์ตี้ที่คำนวณแล้วหรือตัวรับค่า เนื่องจาก JavaScript ไม่มีแนวคิดเรื่องการเปลี่ยนแปลงค่าในตัวแปรการเข้าถึง ตัวรับเป็นเพียงคอลเล็กชันของฟังก์ชัน
หากคุณกําหนดค่าให้กับตัวรับค่า JavaScript จะเรียกใช้ฟังก์ชันนั้น และจากมุมมองของตัวรับค่า ทุกอย่างจะเหมือนเดิม เพียงแค่ให้โอกาสโค้ดบางส่วนทำงาน
ปัญหาคือความหมายที่เราดูการกําหนดค่าด้านบนเป็นค่า -5 เราน่าจะทราบสิ่งที่เกิดขึ้น ปัญหานี้แก้ไม่ได้ ตัวอย่างนี้แสดงให้เห็นถึงเหตุผล ไม่มีระบบใดที่ทราบความหมายของข้อความนี้ เนื่องจากอาจเป็นโค้ดที่กำหนดเอง ในกรณีนี้ ผู้ใช้จะทำสิ่งใดก็ได้ที่ต้องการ เนื่องจากระบบจะอัปเดตค่าทุกครั้งที่มีการเข้าถึง ดังนั้นการถามว่าค่ามีการเปลี่ยนแปลงหรือไม่จึงไม่ค่อยสมเหตุสมผล
การสังเกตวัตถุหลายรายการด้วยคอลแบ็กรายการเดียว
รูปแบบอื่นที่ใช้ได้กับ O.o() คือแนวคิดเกี่ยวกับตัวสังเกตการณ์การเรียกคืนเพียงรายการเดียว วิธีนี้ช่วยให้ใช้การเรียกกลับรายการเดียวเป็น "ผู้สังเกตการณ์" สําหรับออบเจ็กต์ต่างๆ จำนวนมากได้ ฟังก์ชันการเรียกกลับจะได้รับชุดการเปลี่ยนแปลงทั้งหมดของออบเจ็กต์ทั้งหมดที่สังเกตเห็นเมื่อ "สิ้นสุดไมโครแทสก์" (โปรดสังเกตความคล้ายคลึงกับ Mutation Observer)
![การสังเกตวัตถุหลายรายการด้วยคอลแบ็กรายการเดียว](https://web.developers.google.cn/static/articles/es7-observe/image/observing-multiple-object-e686a32b2cfb4.png?hl=th)
การเปลี่ยนแปลงขนาดใหญ่
อาจเป็นเพราะคุณกําลังทํางานกับแอปขนาดใหญ่มากและต้องทําการเปลี่ยนแปลงขนาดใหญ่เป็นประจำ ออบเจ็กต์อาจต้องการอธิบายการเปลี่ยนแปลงเชิงความหมายขนาดใหญ่ซึ่งจะส่งผลต่อพร็อพเพอร์ตี้จํานวนมากในลักษณะที่กะทัดรัดยิ่งขึ้น (แทนที่จะเผยแพร่การเปลี่ยนแปลงพร็อพเพอร์ตี้จํานวนมาก)
O.o() ช่วยแก้ปัญหานี้ในรูปแบบยูทิลิตี 2 รายการ ได้แก่ notifier.performChange()
และ notifier.notify()
ซึ่งเราได้แนะนำไปแล้ว
![การเปลี่ยนแปลงขนาดใหญ่](https://web.developers.google.cn/static/articles/es7-observe/image/large-scale-changes-1d3c06846790f.png?hl=th)
เรามาดูตัวอย่างวิธีอธิบายการเปลี่ยนแปลงขนาดใหญ่กัน โดยเราจะกำหนดออบเจ็กต์ 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 }
![การเปลี่ยนแปลงขนาดใหญ่](https://web.developers.google.cn/static/articles/es7-observe/image/large-scale-changes-fa5179980d0be.png?hl=th)
ทุกอย่างภายใน "ฟังก์ชันการดําเนินการ" จะถือว่าทํางานของ "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';
![การสังเกตอาร์เรย์](https://web.developers.google.cn/static/articles/es7-observe/image/observing-arrays-ffe8e636502fc.png?hl=th)
ประสิทธิภาพ
วิธีคิดเกี่ยวกับผลกระทบด้านประสิทธิภาพการประมวลผลของ O.o() คือให้คิดว่าเป็นแคชการอ่าน โดยทั่วไปแล้ว แคชเป็นตัวเลือกที่ยอดเยี่ยมในกรณีต่อไปนี้ (เรียงตามลําดับความสําคัญ)
- ความถี่ของการอ่านมีมากกว่าความถี่ของการเขียน
- คุณสามารถสร้างแคชซึ่งแลกเปลี่ยนปริมาณงานที่คงที่ซึ่งเกี่ยวข้องกับการเขียนเพื่อประสิทธิภาพที่ดีขึ้นของอัลกอริทึมในระหว่างการอ่าน
- การเขียนที่ช้าลงอย่างต่อเนื่องนั้นยอมรับได้
O.o() ออกแบบมาสำหรับกรณีการใช้งานอย่างเช่น 1)
การตรวจสอบข้อมูลใหม่ต้องเก็บสําเนาของข้อมูลทั้งหมดที่คุณกําลังสังเกต ซึ่งหมายความว่าคุณจะต้องเสียค่าใช้จ่ายด้านหน่วยความจำเชิงโครงสร้างเพื่อการตรวจสอบข้อมูลใหม่ ซึ่งคุณไม่พบใน O.o() แม้ว่าการตรวจสอบข้อมูลใหม่จะเป็นโซลูชันแก้ปัญหาชั่วคราวที่ดี แต่ก็เป็นการแยกแยะข้อมูลแบบรั่วไหลโดยพื้นฐาน ซึ่งอาจทําให้แอปพลิเคชันมีความซับซ้อนโดยไม่จําเป็น
เหตุผล การตรวจสอบข้อมูลที่มีการเปลี่ยนแปลงต้องทำงานทุกครั้งที่ข้อมูลอาจมีการเปลี่ยนแปลง วิธีการที่มีประสิทธิภาพมากในการดำเนินการนี้ไม่มีอยู่จริง และวิธีการใดๆ ก็มีข้อเสียที่สำคัญ (เช่น การตรวจสอบช่วงเวลาการโหวตอาจทำให้เกิดข้อบกพร่องที่มองเห็นได้และเงื่อนไขการแข่งขันระหว่างข้อกังวลเกี่ยวกับโค้ด) นอกจากนี้ การตรวจสอบสถานะแบบไม่สะอาดยังต้องใช้รีจิสทรีส่วนกลางของผู้สังเกตการณ์ ซึ่งก่อให้เกิดอันตรายจากการรั่วไหลของหน่วยความจำและค่าใช้จ่ายในการรื้อถอน ซึ่ง O.o() หลีกเลี่ยง
มาดูตัวเลขกัน
การทดสอบการเปรียบเทียบประสิทธิภาพด้านล่าง (มีอยู่ใน GitHub) ช่วยให้เราเปรียบเทียบการตรวจสอบข้อมูลที่เปลี่ยนแปลงกับ O.o() ได้ โดยมีการกําหนดโครงสร้างเป็นกราฟของขนาดชุดออบเจ็กต์ที่สังเกตได้เทียบกับจํานวนการกลายพันธุ์ ผลลัพธ์ทั่วไปคือประสิทธิภาพของการตรวจสอบข้อมูลใหม่จะแปรผันตามจำนวนออบเจ็กต์ที่สังเกตได้ ในขณะที่ประสิทธิภาพของ O.o() จะแปรผันตามจำนวนการกลายพันธุ์ที่เกิดขึ้น
การตรวจสอบข้อมูล
![ประสิทธิภาพของการตรวจสอบข้อมูลที่ไม่ถูกต้อง](https://web.developers.google.cn/static/articles/es7-observe/image/dirty-checking-performanc-8a8ae6504f80d.png?hl=th)
Chrome ที่เปิด Object.observe()
![สังเกตประสิทธิภาพ](https://web.developers.google.cn/static/articles/es7-observe/image/observe-performance-dcbffb3c8732e.png?hl=th)
การทดแทน Object.observe()
เยี่ยมเลย แสดงว่า O.o() ใช้ได้ใน Chrome 36 แต่จะใช้ในเบราว์เซอร์อื่นๆ ได้ไหม เรารวบรวมมาให้แล้ว Observe-JS ของ Polymer เป็น polyfill สำหรับ O.o() ซึ่งจะใช้การใช้งานแบบเนทีฟหากมี แต่หากไม่มีก็จะใช้ polyfill และเพิ่ม Sugaring ที่มีประโยชน์บางอย่างไว้ด้านบน ซึ่งแสดงมุมมองรวมของโลกที่สรุปการเปลี่ยนแปลงและส่งรายงานเกี่ยวกับสิ่งที่เปลี่ยนแปลง 2 สิ่งที่มีประโยชน์มากในเครื่องมือนี้ ได้แก่
- คุณสามารถสังเกตเส้นทาง ซึ่งหมายความว่าคุณสามารถพูดว่า "ฉันต้องการสังเกต "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()
เพื่อปรับปรุงประสิทธิภาพการเชื่อมโยงข้อมูลในแอป นี่เป็นช่วงเวลาที่น่าตื่นเต้นอย่างแน่นอน
แหล่งข้อมูล
- Object.observe() ในวิกิ Harmony>
- การเชื่อมโยงข้อมูลด้วย Object.observe() โดย Rick Waldron
- ทุกสิ่งที่คุณต้องการทราบเกี่ยวกับ Object.observe() - JSConf
- เหตุใด Object.observe() จึงถือเป็นฟีเจอร์ที่ดีที่สุดของ ES7
ขอขอบคุณ Rafael Weinstein, Jake Archibald, Eric Bidelman, Paul Kinlan และ Vivian Cromwell สำหรับข้อมูลและการตรวจสอบ