Đổi mới liên kết dữ liệu bằng Object.observe()

Addy Osmani
Addy Osmani

Giới thiệu

Một cuộc cách mạng sắp đến. JavaScript có một tính năng mới sẽ thay đổi mọi thứ mà bạn nghĩ mình biết về tính năng liên kết dữ liệu. Điều này cũng sẽ thay đổi cách nhiều thư viện MVC tiếp cận các mô hình quan sát để chỉnh sửa và cập nhật. Bạn đã sẵn sàng nhận một số ưu đãi tăng hiệu suất hấp dẫn cho những ứng dụng quan tâm đến việc quan sát thuộc tính chưa?

OK. Không cần trì hoãn thêm, tôi rất vui được thông báo rằng Object.observe() đã có phiên bản ổn định của Chrome 36. [WOOOO. CROWD GOES WILD].

Object.observe(), một phần của tiêu chuẩn ECMAScript trong tương lai, là một phương thức để quan sát không đồng bộ các thay đổi đối với đối tượng JavaScript… mà không cần thư viện riêng. Lớp này cho phép đối tượng tiếp nhận dữ liệu nhận được một chuỗi bản ghi thay đổi theo thứ tự thời gian, trong đó mô tả tập hợp các thay đổi diễn ra đối với một tập hợp các đối tượng được quan sát.

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

});

Bất cứ khi nào có thay đổi, hệ thống sẽ báo cáo:

Đã báo cáo thay đổi.

Với Object.observe() (tôi muốn gọi là O.o() hoặc Oooooooo), bạn có thể triển khai liên kết dữ liệu hai chiều mà không cần khung.

Điều này không có nghĩa là bạn không nên sử dụng công cụ này. Đối với những dự án lớn có logic kinh doanh phức tạp, các khung làm việc cố định là vô giá và bạn nên tiếp tục sử dụng chúng. Các mẫu này đơn giản hoá hướng dẫn cho các nhà phát triển mới, yêu cầu ít bảo trì mã hơn và áp đặt các mẫu về cách thực hiện các nhiệm vụ phổ biến. Khi không cần, bạn có thể sử dụng các thư viện nhỏ hơn, tập trung hơn như Polymer (đã tận dụng O.o()).

Ngay cả khi bạn thấy mình đang phải sử dụng nhiều khung hoặc thư viện MV*, O.o() vẫn có khả năng mang đến cho họ một số điểm cải tiến về hiệu suất lành mạnh, với cách triển khai nhanh hơn, đơn giản hơn mà vẫn giữ nguyên API. Ví dụ: năm ngoái Angular nhận thấy rằng trong điểm chuẩn khi có các thay đổi được thực hiện đối với một mô hình, quá trình kiểm tra sửa đổi mất 40 mili giây mỗi lần cập nhật và O.o() mất 1-2 mili giây mỗi lần cập nhật (tăng nhanh hơn 20-40 lần).

Việc liên kết dữ liệu mà không cần đến hàng tấn mã phức tạp cũng có nghĩa là bạn không còn phải thăm dò ý kiến về các thay đổi nữa, nhờ đó, thời lượng pin sẽ dài hơn!

Nếu bạn đã bán trên O.o(), hãy bỏ qua phần giới thiệu tính năng hoặc đọc tiếp để biết thêm về các vấn đề mà tính năng này giải quyết.

Chúng ta muốn quan sát điều gì?

Khi nói về chế độ quan sát dữ liệu, chúng ta thường đề cập đến việc theo dõi một số loại thay đổi cụ thể:

  • Thay đổi đối với đối tượng JavaScript thô
  • Khi các thuộc tính được thêm, thay đổi, xoá
  • Khi mảng có các phần tử được ghép vào và tách ra
  • Các thay đổi đối với nguyên mẫu của đối tượng

Tầm quan trọng của tính năng liên kết dữ liệu

Tính năng liên kết dữ liệu bắt đầu trở nên quan trọng khi bạn quan tâm đến việc tách biệt chế độ điều khiển thành phần hiển thị và mô hình. HTML là một cơ chế khai báo tuyệt vời, nhưng hoàn toàn tĩnh. Tốt nhất là bạn chỉ muốn khai báo mối quan hệ giữa dữ liệu của bạn và DOM và luôn cập nhật DOM. Điều này tạo ra đòn bẩy và giúp bạn tiết kiệm nhiều thời gian viết mã lặp đi lặp lại chỉ gửi dữ liệu đến và đi từ DOM giữa trạng thái nội bộ của ứng dụng hoặc máy chủ.

Liên kết dữ liệu đặc biệt hữu ích khi bạn có giao diện người dùng phức tạp, trong đó bạn cần kết nối các mối quan hệ giữa nhiều thuộc tính trong mô hình dữ liệu với nhiều thành phần trong chế độ xem. Điều này khá phổ biến trong các ứng dụng trang đơn mà chúng ta đang xây dựng.

Bằng cách áp dụng phương pháp quan sát dữ liệu một cách tự nhiên trong trình duyệt, chúng tôi cung cấp cho các khung JavaScript (và các thư viện tiện ích nhỏ mà bạn viết) một cách để quan sát các thay đổi đối với dữ liệu mô hình mà không cần dựa vào một số kỹ thuật tấn công chậm mà thế giới đang sử dụng.

Thế giới ngày nay trông như thế nào

Kiểm tra bụi bẩn

Trước đây, bạn đã từng thấy tính năng liên kết dữ liệu ở đâu? Nếu sử dụng thư viện MV* hiện đại để xây dựng ứng dụng web (ví dụ: Angular, Knockout), có thể bạn đã quen với việc liên kết dữ liệu mô hình với DOM. Để ôn lại, sau đây là ví dụ về ứng dụng Danh sách điện thoại, trong đó chúng ta liên kết giá trị của từng điện thoại trong mảng phones (được xác định trong JavaScript) với một mục danh sách để dữ liệu và giao diện người dùng luôn đồng bộ:

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

và JavaScript cho trình điều khiển:

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.'}
  ];
});

Bất cứ khi nào dữ liệu mô hình cơ bản thay đổi, danh sách của chúng tôi trong DOM sẽ được cập nhật. Angular làm thế nào để đạt được điều này? Chà, đằng sau nó đang làm một việc gì đó gọi là kiểm tra bẩn.

Kiểm tra bẩn

Ý tưởng cơ bản của việc kiểm tra trạng thái thay đổi là bất cứ khi nào dữ liệu có thể thay đổi, thư viện phải kiểm tra xem dữ liệu đó có thay đổi hay không thông qua một chu kỳ thay đổi hoặc tóm tắt. Trong trường hợp của Angular, một chu kỳ tóm tắt sẽ xác định tất cả biểu thức được đăng ký để theo dõi xem có thay đổi nào không. Mô hình biết về các giá trị trước đó của mô hình và nếu các giá trị đó thay đổi, sự kiện thay đổi sẽ được kích hoạt. Đối với nhà phát triển, lợi ích chính ở đây là bạn có thể sử dụng dữ liệu đối tượng JavaScript thô, dễ sử dụng và soạn khá tốt. Nhược điểm là ứng dụng có hành vi thuật toán xấu và có thể rất tốn kém.

Đang kiểm tra xem có bị bẩn không.

Chi phí của thao tác này tỷ lệ thuận với tổng số đối tượng được quan sát. Tôi có thể cần phải kiểm tra nhiều lần. Ngoài ra, bạn cũng có thể cần một cách để kích hoạt tính năng kiểm tra dữ liệu không sạch khi dữ liệu có thể đã thay đổi. Có rất nhiều thủ thuật thông minh mà các khung sử dụng cho việc này. Không rõ liệu việc này có bao giờ hoàn hảo hay không.

Hệ sinh thái web cần có nhiều khả năng đổi mới và phát triển các cơ chế khai báo riêng, ví dụ:

  • Hệ thống mô hình dựa trên ràng buộc
  • Hệ thống tự động duy trì (ví dụ: duy trì các thay đổi đối với IndexedDB hoặc localStorage)
  • Đối tượng vùng chứa (Ember, Backbone)

Đối tượng Vùng chứa là nơi khung tạo các đối tượng bên trong chứa dữ liệu. Họ có các trình truy cập vào dữ liệu và có thể ghi lại những gì bạn đã thiết lập hoặc nhận được rồi truyền tin nội bộ. Cách này hoạt động hiệu quả. Phương thức này tương đối hiệu quả và có hành vi thuật toán tốt. Bạn có thể xem ví dụ về các đối tượng vùng chứa sử dụng Ember bên dưới:

// 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

Chi phí để khám phá những gì đã thay đổi ở đây tỷ lệ thuận với số lượng những thay đổi. Một vấn đề khác là hiện tại bạn đang sử dụng loại đối tượng khác này. Nói chung, bạn phải chuyển đổi từ dữ liệu nhận được từ máy chủ sang các đối tượng này để quan sát được.

Mã này không kết hợp đặc biệt tốt với mã JS hiện có vì hầu hết mã đều giả định rằng mã có thể hoạt động trên dữ liệu thô. Không dành cho những loại đối tượng chuyên biệt này.

Introducing Object.observe()

Lý tưởng nhất là những gì chúng ta muốn là điều tốt nhất ở cả hai thế giới - một cách để quan sát dữ liệu có hỗ trợ các đối tượng dữ liệu thô (đối tượng JavaScript thông thường) nếu chúng ta chọn AND mà không cần phải kiểm tra sửa đổi mọi thứ mọi lúc. Nội dung có hành vi thuật toán tốt. Nội dung kết hợp tốt và được đưa vào nền tảng. Đây là điểm mạnh của Object.observe().

Công cụ này cho phép chúng ta quan sát một đối tượng, thay đổi các thuộc tính và xem báo cáo về những nội dung đã thay đổi. Nhưng về lý thuyết, hãy cùng xem một số mã!

Object.observe()

Object.observe() và Object.unobserve()

Hãy tưởng tượng rằng chúng ta có một đối tượng JavaScript đơn giản đại diện cho một mô hình:

// A model can be a simple vanilla object
var todoModel = {
  label: 'Default',
  completed: false
};

Sau đó, chúng ta có thể chỉ định một lệnh gọi lại cho bất cứ khi nào có đột biến (thay đổi) đối với đối tượng:

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

Sau đó, chúng ta có thể quan sát những thay đổi này bằng cách sử dụng O.o(), truyền đối tượng làm đối số đầu tiên và lệnh gọi lại làm đối số thứ hai:

Object.observe(todoModel, observer);

Hãy bắt đầu thay đổi đối tượng mô hình Việc cần làm:

todoModel.label = 'Buy some more milk';

Khi xem bảng điều khiển, chúng ta sẽ nhận được một số thông tin hữu ích! Chúng ta biết thuộc tính nào đã thay đổi, cách thay đổi và giá trị mới là gì.

Báo cáo trên bảng điều khiển

Tuyệt vời! Tạm biệt tính năng kiểm tra trạng thái thay đổi! Bia mộ của bạn phải được khắc trong truyện tranh Comic Sans. Hãy thay đổi một thuộc tính khác. Lần này completeBy:

todoModel.completeBy = '01/01/2014';

Như chúng ta có thể thấy, chúng ta đã nhận lại thành công một báo cáo thay đổi:

Thay đổi báo cáo.

Vậy thì tuyệt quá! Nếu bây giờ chúng ta quyết định xoá thuộc tính "đã hoàn tất" khỏi đối tượng của mình thì sao:

delete todoModel.completed;
Ðã hoàn tất

Như chúng ta có thể thấy, báo cáo về các thay đổi được trả về bao gồm thông tin về việc xoá. Như dự kiến, giá trị mới của thuộc tính hiện không xác định. Vì vậy, chúng tôi hiện đã biết bạn có thể biết thời điểm các cơ sở lưu trú được thêm vào. Khi chúng đã bị xoá. Về cơ bản, nhóm các thuộc tính trên một đối tượng ("mới", "đã xoá", "đã định cấu hình lại") và nguyên mẫu đang thay đổi (proto).

Giống như mọi hệ thống quan sát, cũng có một phương thức để ngừng theo dõi các thay đổi. Trong trường hợp này, đó là Object.unobserve() có cùng chữ ký với O.o() nhưng có thể được gọi như sau:

Object.unobserve(todoModel, observer);

Như chúng ta có thể thấy dưới đây, mọi đột biến được thực hiện cho đối tượng sau khi chạy không còn dẫn đến danh sách bản ghi thay đổi được trả về nữa.

Đột biến

Chỉ định các thay đổi quan tâm

Vậy là chúng ta đã tìm hiểu những thông tin cơ bản về cách khôi phục danh sách các thay đổi đối với một đối tượng được quan sát. Điều gì sẽ xảy ra nếu bạn chỉ quan tâm đến một số thay đổi đã được thực hiện đối với một đối tượng thay vì tất cả các thay đổi đó? Mọi người cần có bộ lọc thư rác. Vâng, trình quan sát chỉ có thể chỉ định những loại thay đổi mà họ muốn thông qua danh sách chấp nhận. Bạn có thể chỉ định đối số này bằng cách sử dụng đối số thứ ba cho O.o() như sau:

Object.observe(obj, callback, optAcceptList)

Hãy xem qua một ví dụ về cách sử dụng điều này:

// 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

Tuy nhiên, nếu chúng ta xoá nhãn này, hãy lưu ý rằng loại thay đổi này sẽ được báo cáo:

delete todoModel.label;

Nếu bạn không chỉ định danh sách các loại chấp nhận cho O.o(), thì theo mặc định, thuộc tính này sẽ áp dụng các loại thay đổi đối tượng "nội tại" (add, update, delete, reconfigure, preventExtensions (khi một đối tượng không thể mở rộng không quan sát được)).

Thông báo

O.o() cũng đi kèm với khái niệm thông báo. Những thông báo này không giống như những thông báo phiền toái mà bạn nhận được trên điện thoại, mà rất hữu ích. Thông báo tương tự như Mutation Observer (Trình quan sát biến đổi). Các sự kiện này xảy ra khi kết thúc tác vụ vi mô. Trong ngữ cảnh trình duyệt, phần này hầu như luôn nằm ở cuối trình xử lý sự kiện hiện tại.

Thời điểm này rất phù hợp vì thường thì một đơn vị công việc đã hoàn tất và giờ đây, trình quan sát có thể thực hiện công việc của mình. Đây là một mô hình xử lý theo lượt rất hay.

Quy trình sử dụng trình thông báo sẽ có dạng như sau:

Thông báo

Hãy xem xét một ví dụ về cách sử dụng trình thông báo trong thực tế để xác định thông báo tuỳ chỉnh khi hệ thống lấy hoặc thiết lập thuộc tính trên một đối tượng. Theo dõi bình luận ở đây:

// 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);
Bảng điều khiển thông báo

Tại đây, chúng ta báo cáo thời điểm giá trị của các thuộc tính dữ liệu thay đổi ("cập nhật"). Mọi thứ khác mà quá trình triển khai của đối tượng chọn báo cáo (notifier.notifyChange()).

Nhiều năm kinh nghiệm trên nền tảng web đã dạy chúng tôi rằng phương pháp đồng bộ là điều đầu tiên bạn nên thử, vì đó là cách dễ nhất. Vấn đề là việc này tạo ra một mô hình xử lý nguy hiểm về cơ bản. Giả sử bạn đang viết mã và cập nhật thuộc tính của một đối tượng, thì bạn không thực sự muốn một tình huống cập nhật thuộc tính của đối tượng đó có thể đã mời một số mã tuỳ ý thực hiện bất cứ điều gì nó muốn. Bạn không nên vô hiệu hoá các giả định khi đang chạy giữa một hàm.

Nếu là người quan sát, bạn tốt nhất không nên muốn nhận cuộc gọi nếu ai đó đang ở giữa một nội dung nào đó. Bạn không muốn được yêu cầu làm công việc trong một tình trạng thế giới không nhất quán. Cuối cùng, bạn phải kiểm tra lỗi nhiều hơn nữa. Cố gắng chấp nhận nhiều tình huống xấu hơn và nói chung, đây là một mô hình khó làm việc. Async khó xử lý hơn nhưng về lâu dài, đây là mô hình tốt hơn.

Giải pháp cho vấn đề này là bản ghi thay đổi tổng hợp.

Bản ghi thay đổi tổng hợp

Về cơ bản, nếu muốn có phương thức truy cập hoặc thuộc tính được tính toán, bạn có trách nhiệm thông báo khi các giá trị này thay đổi. Tuy hơi phức tạp nhưng được thiết kế như một loại tính năng hạng nhất của cơ chế này và các thông báo này sẽ được gửi cùng với phần còn lại của các thông báo từ các đối tượng dữ liệu cơ bản. Từ các thuộc tính dữ liệu.

Bản ghi thay đổi tổng hợp

Bạn có thể giải quyết việc quan sát phương thức truy cập và các thuộc tính được tính toán bằng notifier.notify – một phần khác của O.o(). Hầu hết các hệ thống quan sát đều muốn có một số hình thức quan sát các giá trị phái sinh. Có nhiều cách để thực hiện việc này. O.o không đánh giá cách nào là "đúng". Tài sản đã tính toán phải là trình truy cập và sẽ thông báo khi trạng thái nội bộ (riêng tư) thay đổi.

Xin nhắc lại rằng các nhà phát triển web nên mong đợi các thư viện có thể giúp việc thông báo và nhiều phương pháp khác nhau trở nên dễ dàng đối với các tài sản được tính toán (và giảm bớt mã nguyên mẫu).

Hãy thiết lập ví dụ tiếp theo, đó là một lớp hình tròn. Ý tưởng ở đây là chúng ta có một vòng tròn và có một thuộc tính bán kính. Trong trường hợp này, bán kính là một phương thức truy cập và khi giá trị của bán kính thay đổi, giá trị này sẽ tự thông báo rằng giá trị đã thay đổi. Nội dung này sẽ được phân phối cùng với tất cả các thay đổi khác đối với đối tượng này hoặc bất kỳ đối tượng nào khác. Về cơ bản, nếu bạn đang triển khai một đối tượng mà bạn muốn có các thuộc tính tổng hợp hoặc được tính toán, thì bạn phải chọn một chiến lược để đối tượng này hoạt động. Sau khi bạn thực hiện, toàn bộ hệ thống sẽ phù hợp với ứng dụng.

Bỏ qua mã để xem cách hoạt động này trong DevTools.

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);
  })
}
Bảng điều khiển bản ghi thay đổi tổng hợp

Thuộc tính phương thức truy cập

Lưu ý nhanh về thuộc tính phương thức truy cập. Chúng ta đã đề cập trước đó rằng chỉ có thể ghi nhận được những thay đổi về giá trị đối với các thuộc tính dữ liệu. Không dành cho các thuộc tính được tính toán hoặc phương thức truy cập. Lý do là JavaScript không thực sự có khái niệm về thay đổi giá trị đối với phương thức truy cập. Trình truy cập chỉ là một tập hợp các hàm.

Nếu bạn chỉ định cho một trình truy cập, JavaScript chỉ cần gọi hàm tại đó và từ quan điểm của trình truy cập thì không có gì thay đổi. Nó chỉ cho một số mã cơ hội để chạy.

Vấn đề là về mặt ngữ nghĩa, chúng ta có thể xem phép gán ở trên theo giá trị - 5 cho nó. Chúng ta nên biết chuyện gì đã xảy ra ở đây. Đây thực sự là một vấn đề không thể giải quyết. Ví dụ sau đây minh hoạ lý do. Thực sự không có cách nào để bất kỳ hệ thống nào biết được ý nghĩa của thuộc tính này vì đây có thể là mã tuỳ ý. Trong trường hợp này, lớp này có thể làm bất cứ điều gì nó muốn. Cập nhật giá trị mỗi lần được truy cập nên việc hỏi xem có thay đổi hay không là không hợp lý.

Quan sát nhiều đối tượng bằng một lệnh gọi lại

Một mẫu khác có thể có với O.o() là khái niệm về một trình quan sát lệnh gọi lại duy nhất. Điều này cho phép dùng một lệnh gọi lại làm "trình quan sát" cho nhiều đối tượng khác nhau. Lệnh gọi lại sẽ được phân phối tập hợp đầy đủ các thay đổi cho tất cả đối tượng mà lệnh gọi lại này quan sát được ở “kết thúc của vi tác vụ” (Lưu ý sự tương đồng với Trình quan sát đột biến).

Quan sát nhiều đối tượng bằng một lệnh gọi lại

Thay đổi trên quy mô lớn

Có thể bạn đang làm việc trên một ứng dụng thực sự lớn và thường xuyên phải xử lý các thay đổi trên quy mô lớn. Các đối tượng có thể mô tả các thay đổi ngữ nghĩa lớn hơn sẽ ảnh hưởng đến nhiều thuộc tính theo cách ngắn gọn hơn (thay vì truyền tải hàng tấn thay đổi thuộc tính).

O.o() giúp giải quyết vấn đề này dưới dạng hai tiện ích cụ thể: notifier.performChange()notifier.notify() mà chúng tôi đã giới thiệu.

Thay đổi trên quy mô lớn

Hãy xem ví dụ này về cách mô tả các thay đổi trên quy mô lớn, trong đó chúng ta xác định đối tượng Thingy bằng một số tiện ích toán học (multiply, increment, incrementAndMultiply). Bất cứ khi nào một tiện ích được sử dụng, tiện ích đó sẽ cho hệ thống biết rằng một tập hợp công việc bao gồm một loại thay đổi cụ thể.

Ví dụ: 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
    });
  }
}

Sau đó, chúng ta xác định hai trình quan sát cho đối tượng của mình: một trình quan sát toàn diện cho các thay đổi và một trình quan sát sẽ chỉ báo cáo lại các loại chấp nhận cụ thể mà chúng ta đã xác định (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);
}

Bây giờ, chúng ta có thể bắt đầu chơi với mã này. Hãy xác định một Thingy mới:

var thingy = new Thingy(2, 4);

Hãy quan sát và sau đó thực hiện một số thay đổi. Ôi, thật thú vị. Quá nhiều thứ thú vị!

// 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 }
Thay đổi trên quy mô lớn

Mọi thứ bên trong "hàm thực hiện" được coi là công việc của "thay đổi lớn". Những trình quan sát chấp nhận "thay đổi lớn" sẽ chỉ nhận được bản ghi "thay đổi lớn". Những trình quan sát không nhận được các thay đổi cơ bản phát sinh từ công việc mà "thực hiện hàm" đã thực hiện.

Quan sát các mảng

Chúng ta đã nói khá nhiều về việc quan sát các thay đổi đối với đối tượng, nhưng còn về mảng thì sao?! Câu hỏi hay đó! Khi có người nói với tôi: "Câu hỏi hay". Tôi chưa bao giờ nghe câu trả lời của họ vì tôi đang bận tự chúc mừng mình vì đã đặt một câu hỏi hay như vậy. Nhưng tôi lạc đề rồi. Chúng tôi cũng có các phương thức mới để xử lý mảng!

Array.observe() là một phương thức xử lý các thay đổi trong chính nó trên quy mô lớn, chẳng hạn như splice (mối ghép), unshift (huỷ dịch chuyển) hoặc các thay đổi ngầm thay đổi độ dài của phương thức đó, ở dạng bản ghi thay đổi "splice" (ghép nối). Trong nội bộ, API này sử dụng notifier.performChange("splice",...).

Sau đây là một ví dụ mà chúng ta quan sát "mảng" mô hình và tương tự, nhận lại danh sách các thay đổi khi có bất kỳ thay đổi nào đối với dữ liệu cơ bản:

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';
Quan sát mảng

Hiệu suất

Cách suy nghĩ về tác động của hàm O.o() đối với hiệu suất tính toán là coi hàm này giống như một bộ nhớ đệm đọc. Nói chung, bộ nhớ đệm là một lựa chọn tuyệt vời khi (theo mức độ quan trọng):

  1. Tần suất đọc chiếm ưu thế hơn tần suất ghi.
  2. Bạn có thể tạo một bộ nhớ đệm để trao đổi lượng công việc không đổi liên quan trong quá trình ghi để có hiệu suất tốt hơn về mặt thuật toán trong quá trình đọc.
  3. Có thể chấp nhận làm chậm thời gian ghi liên tục.

O.o() được thiết kế cho các trường hợp sử dụng như 1).

Để kiểm tra xem có sửa đổi không, bạn cần lưu giữ bản sao của tất cả dữ liệu bạn đang quan sát. Điều này đồng nghĩa với việc bạn phải tốn chi phí bộ nhớ có cấu trúc cho việc kiểm tra sửa đổi bộ nhớ mà bạn không nhận được với O.o(). Kiểm tra sửa đổi, tuy là một giải pháp dừng lỗ hổng hợp lý, cũng là một mô hình trừu tượng rò rỉ về cơ bản, có thể tạo ra sự phức tạp không cần thiết cho các ứng dụng.

Tại sao? Chạy quy trình kiểm tra dữ liệu bất cứ khi nào dữ liệu có thể đã thay đổi. Không có cách nào hiệu quả để thực hiện việc này và mọi phương pháp đều có những nhược điểm đáng kể (ví dụ: việc kiểm tra khoảng thời gian thăm dò ý kiến có thể dẫn đến các cấu phần phần mềm ảo và tình trạng tương tranh giữa các vấn đề về mã). Tính năng kiểm tra thay đổi cũng yêu cầu một sổ đăng ký toàn cầu của trình quan sát, tạo ra các mối nguy hiểm rò rỉ bộ nhớ và chi phí tháo dỡ mà O.o() tránh được.

Hãy cùng xem xét một vài số liệu.

Các bài kiểm thử điểm chuẩn dưới đây (có trên GitHub) cho phép chúng ta so sánh tính năng kiểm tra thay đổi với O.o(). Các bài kiểm thử này được cấu trúc dưới dạng biểu đồ của Kích thước-tập-hợp-đối-tượng-đã-quan-sát so với Số-lượng-sự-thay-đổi. Kết quả chung là hiệu suất kiểm tra sửa đổi tỷ lệ theo thuật toán với số lượng đối tượng được quan sát, trong khi hiệu suất O.o() tỷ lệ thuận với số lần đột biến được thực hiện.

Kiểm tra bụi bẩn

Hiệu suất kiểm tra lỗi

Chrome có bật Object.observe()

Quan sát hiệu suất

Polyfilling Object.observe()

Tuyệt vời - vậy O.o() có thể được sử dụng trong Chrome 36, nhưng còn việc sử dụng nó trong các trình duyệt khác thì sao? Chúng tôi đã chuẩn bị sẵn thông tin cho bạn. Observe-JS của polymer là một polyfill cho O.o(), nó sẽ sử dụng phương thức triển khai gốc nếu có, nếu không thì sẽ polyfill nó và bao gồm một số đường hữu ích ở trên cùng. Tính năng này cung cấp thông tin tổng hợp về thế giới, tóm tắt các thay đổi và đưa ra báo cáo về những thay đổi đó. Hai yếu tố thực sự mạnh mẽ mà hệ thống này cho thấy là:

  1. Bạn có thể quan sát các đường dẫn. Điều này có nghĩa là bạn có thể nói rằng tôi muốn quan sát "foo.bar.baz" từ một đối tượng nhất định và chúng sẽ cho bạn biết thời điểm giá trị tại đường dẫn đó thay đổi. Nếu đường dẫn không thể truy cập được, đường dẫn sẽ coi giá trị là không xác định.

Ví dụ về cách quan sát một giá trị tại một đường dẫn từ một đối tượng cho trước:

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. Phương thức này sẽ cho bạn biết về các mối nối mảng. Về cơ bản, ghép nối mảng là tập hợp các thao tác ghép nối tối thiểu mà bạn sẽ phải thực hiện trên một mảng để chuyển đổi phiên bản cũ của mảng thành phiên bản mới của mảng. Đây là một kiểu biến đổi hoặc một khung hiển thị khác của mảng. Đây là lượng công việc tối thiểu bạn cần làm để chuyển từ trạng thái cũ sang trạng thái mới.

Ví dụ về cách báo cáo các thay đổi đối với một mảng dưới dạng tập hợp các điểm nối tối thiểu:

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

Khung và Object.observe()

Như đã đề cập, O.o() sẽ mang đến cho các khung và thư viện một cơ hội lớn để cải thiện hiệu suất của tính năng liên kết dữ liệu trong các trình duyệt hỗ trợ tính năng này.

Yehuda Katz và Erik Bryn của Ember xác nhận rằng việc thêm tính năng hỗ trợ cho O.o() nằm trong lộ trình sắp tới của Ember. Misko Hervy của Angular đã viết một tài liệu thiết kế về tính năng phát hiện thay đổi được cải thiện của Angular 2.0. Về lâu dài, họ sẽ tận dụng Object.observe() khi chuyển sang phiên bản ổn định của Chrome, chọn Watchtower.js, phương pháp phát hiện thay đổi của riêng họ cho đến thời điểm đó. Suuuuper thú vị đấy.

Kết luận

O.o() là một tính năng bổ sung mạnh mẽ cho nền tảng web mà bạn có thể sử dụng ngay hôm nay.

Chúng tôi hy vọng rằng theo thời gian, tính năng này sẽ xuất hiện trên nhiều trình duyệt hơn, cho phép các khung JavaScript tăng hiệu suất nhờ quyền truy cập vào các tính năng quan sát đối tượng gốc. Những người đang nhắm mục tiêu Chrome đó có thể sử dụng O.o() trong Chrome 36 (trở lên) và tính năng này cũng sẽ có trong bản phát hành Opera trong tương lai.

Vì vậy, hãy liên hệ và trao đổi với các tác giả của khung JavaScript về Object.observe() cũng như cách họ dự định sử dụng công cụ này để cải thiện hiệu suất của tính năng liên kết dữ liệu trong ứng dụng của bạn. Chắc chắn sẽ có những khoảnh khắc thú vị ở phía trước!

Tài nguyên

Cảm ơn Rafael Weinstein, Jake Archibald, Eric Bidelman, Paul Kinlan và Vivian Cromwell vì những đóng góp và bài đánh giá của họ.