Đổ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 diễn ra. JavaScript có tính năng bổ sung mới sẽ thay đổi mọi thứ bạn nghĩ mình biết về liên kết dữ liệu. Phiên bản này cũng sẽ thay đổi số lượng 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 để tăng hiệu suất đáng kể cho những ứng dụng quan tâm đến tính năng quan sát thuộc tính chưa?

OK. Không trì hoãn thêm nữa, tôi rất vui được thông báo rằng Object.observe() đã có mặt trong phiên bản ổn định của Chrome 36. [Ồ! [TÊN NGƯỜI].

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 các đối tượng JavaScript... mà không cần một thư viện riêng. Cho phép đối tượng tiếp nhận dữ liệu nhận được một chuỗi các bản ghi thay đổi theo thứ tự thời gian, trong đó mô tả tập hợp các thay đổi xảy ra 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 thay đổi được thực hiện, thay đổi đó sẽ được 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 Đối với những dự án lớn có logic kinh doanh phức tạp, khung làm việc cố định là vô giá và bạn nên tiếp tục sử dụng chúng. Chúng giúp đơn giản hoá việc định hướng cho các nhà phát triển mới, ít yêu cầu bảo trì mã hơn và áp đặt các mẫu về cách đạt được các tác vụ phổ biến. Khi không cần một, bạn có thể sử dụng các thư viện nhỏ hơn và tập trung hơn như Polymer (đã tận dụng O.o()).

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

Việc liên kết dữ liệu mà không cầ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 để tìm ra các thay đổi, nhờ đó, kéo dài thời lượng pin!

Nếu bạn đã bán sản phẩm 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 được.

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

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

  • Thay đổi đối với đối tượng JavaScript thô
  • Khi cơ sở lưu trú được thêm, thay đổi, xoá
  • Khi mảng có các phần tử ghép vào và tách ra khỏi mảng
  • Thay đổi đối với nguyên mẫu của đối tượng

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

Việc 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 phân tách quyền kiểm soát chế độ xem 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ỉ cần khai báo mối quan hệ giữa dữ liệu của mình 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 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ó một giao diện người dùng phức tạp. Trong đó, bạn cần liên kết mối quan hệ giữa nhiều thuộc tính trong mô hình dữ liệu với nhiều phần tử trong khung hiển thị của mình. Đ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 hiện nay.

Bằng cách bắt đầu một cách để quan sát dữ liệu trong trình duyệt một cách tự nhiên, 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ố thủ thuật tấn công chậm trên thế giới ngày nay.

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

Kiểm tra độ 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 sẽ quen với việc liên kết dữ liệu mô hình với DOM. Để nhắc lại, đâ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 mỗi đ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 của chúng ta luôn được đồ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 bộ đ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 của mô hình cơ bản thay đổi, danh sách của chúng ta trong DOM sẽ được cập nhật. Angular đạt được điều này bằng cách nào? Chà, ở hậu trường, hệ thống này đang thực hiện một việc gọi là kiểm tra sửa đổi.

Kiểm tra độ sai lệch

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

Đang kiểm tra mức độ sai sót.

Chi phí của thao tác này tỷ lệ với tổng số đối tượng được quan sát. Có thể tôi sẽ phải kiểm tra kỹ lưỡng. Ngoài ra, có thể cần một cách để kích hoạt tính năng kiểm tra sửa đổi khi dữ liệu có thể đã thay đổi. Có rất nhiều khung thủ thuật thông minh sử dụng cho trường hợp này. Không rõ liệu phương pháp này có hoàn thiện 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ơ chế khai báo của riêng nó hơn, ví dụ:

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

Đối tượng Vùng chứa là nơi khung tạo các đối tượng bên trong lưu giữ dữ liệu. Họ có quyền truy cập vào dữ liệu và có thể ghi lại nội dung bạn đặt hoặc nhận và phát sóng nội bộ. Cách này khá hiệu quả. Thử nghiệm này có hiệu suất tương đối và hành vi thuật toán tốt. Bạn có thể xem một 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 thứ 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 để có thể quan sát được.

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

Introducing Object.observe()

Lý tưởng nhất là điều chúng ta muốn là điều tốt nhất trong cả hai thế giới – 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 kiểm tra sửa mọi thứ mọi lúc. Một nội dung nào đó có hành vi thuật toán tốt. Một số yếu tố soạn thảo hợp lý và được đưa vào nền tảng. Đây là vẻ đẹp của những gì Object.observe() mang đến.

Nó cho phép chúng ta quan sát một đối tượng, thay đổi thuộc tính và xem báo cáo thay đổi về những gì đã thay đổi. Nhưng về lý thuyết là đủ, hãy cùng xem xét một số đoạn 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 vanilla đơ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 lệnh gọi lại bất cứ khi nào các đột biến (thay đổi) được thực hiện đố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 vào đố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 thực hiện một số thay đổi đối với đối tượng mô hình Việc cần làm:

todoModel.label = 'Buy some more milk';

Nhìn vào bảng điều khiển, chúng ta sẽ tìm được một số thông tin hữu ích! Chúng tôi biết thuộc tính nào đã thay đổi, thuộc tính đó đã thay đổi như thế nào 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, kiểm tra bẩn! Bia mộ của bạn phải được chạm khắc trong 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ư có thể thấy, một lần nữa, chúng ta lấy lại thành công 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 thì sao:

delete todoModel.completed;
Ðã kết thúc

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 là không xác định. Giờ đây, chúng tôi đã biết rằng bạn có thể biết được thời điểm thêm cơ sở lưu trú. Thời điểm xoá những tệp đó. Về cơ bản, tập hợp thuộc tính trên một đối tượng ("mới", "đã xoá", "được định cấu hình lại") và đang thay đổi nguyên mẫu (proto).

Giống như trong bất kỳ hệ thống quan sát nào, 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 bên dưới, mọi thay đổi đối với đối tượng sau khi chạy sẽ không còn dẫn đến danh sách các bản ghi thay đổi được trả về.

Đột biến

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

Vì vậy, chúng ta đã xem xét những kiến thức cơ bản đằng sau cách lấy lại danh sách các thay đổi đối với đố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 phần nhỏ các thay đổi được thực hiện đối với một đối tượng thay vì tất cả các thay đổi đó? Ai cũng cần có bộ lọc thư rác. Đối tượng tiếp nhận dữ liệu chỉ có thể chỉ định những loại thay đổi mà họ muốn biết thông qua danh sách chấp nhận. Điều này có thể được chỉ định bằng cách sử dụng đối số thứ ba cho O.o() như sau:

Object.observe(obj, callback, optAcceptList)

Hãy cùng xem một ví dụ về cách sử dụng công cụ 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 bây giờ chúng ta xoá nhãn, hãy lưu ý rằng loại thay đổi sau vẫn được báo cáo:

delete todoModel.label;

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

Thông báo

O.o() cũng đi kèm với khái niệm thông báo. Những nội dung này không giống như những nội dung gây khó chịu bạn thấy trên điện thoại, nhưng khá hữu ích. Thông báo tương tự như Mutation Observer (Trình quan sát thay đổi). Chúng xảy ra vào cuối nhiệm 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.

Đây là thời gian hợp lý vì nhìn chung một đơn vị công việc đã được hoàn thành và bây giờ người quan sát bắt đầu 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 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 các thuộc tính trên một đối tượng được thiết lập hoặc được thiết lập. Theo dõi các bình luận tại đâ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

Ở đây, chúng tôi báo cáo khi giá trị của các thuộc tính dữ liệu thay đổi ("cập nhật"). Mọi hoạt động 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 tiếp cận đồng bộ là điều đầu tiên bạn thử vì đây là phương pháp dễ nhất để bạn khám phá. Vấn đề là việc này tạo ra một mô hình xử lý về cơ bản nguy hiểm. Nếu bạn đang viết mã và nói là 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 xảy ra 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ì muốn. Bạn không nên vô hiệu hoá các giả định của mình khi đang chạy ở giữa một hàm.

Nếu là người quan sát, thì bạn không nên bị người khác gọi đến nếu đang làm việc gì đó. Bạn sẽ không muốn bị yêu cầu làm việc khi tình trạng không nhất quán của thế giới. Phải kiểm tra nhiều lỗi hơn nữa. Việc cố gắng chấp nhận nhiều tình huống xấu hơn và nhìn chung thì thật khó để chấp nhận. Không đồng bộ khó xử lý hơn nhưng mô hình tốt hơn vào cuối ngày.

Giải pháp cho vấn đề này là các 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ó các trình truy cập hoặc thuộc tính đã tính toán, bạn có trách nhiệm thông báo khi các giá trị này thay đổi. Một số công việc cần làm thêm 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 các thông báo còn lại 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

Các trình truy cập quan sát và thuộc tính đã tính toán có thể được giải quyết 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 muốn có một số hình thức quan sát các giá trị phát sinh. Có rất nhiều cách để thực hiện việc này. O.o không phán xét "đúng cách". Các thuộc tính được tính toán phải là trình truy cập notify 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 kỳ vọng các thư viện sẽ giúp việc thông báo và nhiều phương pháp khác nhau đối với các thuộc tính đã tính toán trở nên dễ dàng (và giảm bớt mã nguyên mẫu).

Hãy thiết lập ví dụ tiếp theo. Đây là lớp vòng tròn. Ý tưởng ở đây là chúng ta có vòng tròn này 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 trình truy cập và khi giá trị của bán kính thay đổi, nó thực sự sẽ tự thông báo rằng giá trị đã thay đổi. Đối tượng 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 đã tính toán, hoặc bạn phải chọn một chiến lược về cách hoạt động của đối tượng này. Sau khi bạn làm như vậy, điều này sẽ phù hợp với tổng thể hệ thống của bạn.

Bỏ qua mã để xem mã này hoạt động trong Công cụ cho nhà phát triển.

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 trình truy cập

Lưu ý nhanh về các thuộc tính của trình truy cập. Như đã đề cập trước đó, bạn chỉ có thể quan sát 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 hoặc trình truy cập đã tính toán. Lý do là JavaScript không thực sự có khái niệm về thay đổi giá trị đối với trình 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ỉ gọi hàm đó và từ quan điểm của nó thì không có gì thay đổi. Nó chỉ giúp cho một số mã có thể chạy.

Vấn đề về mặt ngữ nghĩa, chúng ta có thể xem xét bài tập được chỉ định ở trên cho giá trị - 5 cho nó. Chúng tôi phải biết điều gì đã xảy ra ở đây. Đây thực sự là một vấn đề không thể giải quyết. Ví dụ này minh hoạ lý do. Thực sự không có cách nào để bất cứ hệ thống nào biết được ý nghĩa của điều này vì đây có thể là mã tuỳ ý. Trong trường hợp này, bot có thể làm bất cứ điều gì nó muốn. Mô-đun này cập nhật giá trị mỗi khi được truy cập, vì vậy, việc hỏi liệu giá trị có thay đổi hay không sẽ 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ể sử dụng với O.o() là khái niệm về trình quan sát lệnh gọi lại đơn lẻ. Điều này cho phép dùng một lệnh gọi lại duy nhất làm "trình quan sát" cho nhiều đối tượng. Lệnh gọi lại sẽ gửi một tập hợp đầy đủ các thay đổi cho tất cả đối tượng mà nó quan sát được ở "điểm cuối của vi tác vụ" (Lưu ý điểm tương tự như Mutation Observer).

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 phát triển một ứng dụng thực sự lớn và thường xuyên phải làm việc với những thay đổi trên quy mô lớn. Các đối tượng nên mô tả các thay đổi lớn hơn về ngữ nghĩa. Những thay đổi này sẽ ảnh hưởng đến nhiều thuộc tính theo cách ngắn gọn hơn (thay vì phải truyền đi rất nhiều thay đổi về thuộc tính).

O.o() giúp thực hiện việc này ở dạng hai tiện ích cụ thể: notifier.performChange()notifier.notify(), mà chúng ta đã giới thiệu.

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

Hãy xem điều này trong một ví dụ về cách mô tả các thay đổi trong quy mô lớn, khi chúng ta xác định một đối tượng Thingy bằng một số tiện ích toán học (nhân, tăng, tăngAndNhân). 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 đối tượng tiếp nhận dữ liệu cho đối tượng của mình: một là đối tượng chung cho các thay đổi và một đối tượng khác sẽ chỉ báo cáo lại về 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 bằng đoạn mã này. Hãy xác định một Thingy mới:

var thingy = new Thingy(2, 4);

Hãy quan sát rồi thực hiện một số thay đổi. Quá tuyệt vời. Quá nhiều thứ!

// 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 xem là công việc của "thay đổi lớn". Những người 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 đối tượng tiếp nhận dữ liệu không nhận được những thay đổi cơ bản do tác vụ "thực hiện hàm" đã thực hiện.

Quan sát mảng

Chúng ta đã trò chuyện một chút về việc quan sát thay đổi của các đối tượng, nhưng còn về mảng thì sao?! Câu hỏi hay đó! Khi ai đó nói: "Một câu hỏi hay". Tôi không bao giờ nghe thấy họ trả lời vì tôi đang bận rộn tự chúc mừng vì đã hỏi một câu hỏi hay như vậy, nhưng tôi lại lạc lõng. Chúng ta 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 quy mô lớn đối với chính nó – ví dụ: điểm nối, chế độ huỷ thay đổi hoặc bất cứ điều gì ngầm thay đổi độ dài của điểm nối – dưới dạng bản ghi thay đổi "mối nối". Trong nội bộ, ứng dụng này sử dụng notifier.performChange("splice",...).

Dưới đây là một ví dụ mà chúng ta quan sát một "mảng" mô hình và tương tự sẽ nhận lại một 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

Hãy xem xét tác động của O.o() đối với hiệu suất tính toán 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 phối tần suất ghi.
  2. Bạn có thể tạo một bộ nhớ đệm giúp trao đổi lượng công việc không đổi liên quan trong quá trình ghi nhằm đạt được hiệu suất tốt hơn bằng thuật toán trong quá trình đọc.
  3. Việc giảm tốc độ liên tục trong thời gian ghi là có thể chấp nhận được.

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

Quy trình kiểm tra bẩn đòi hỏi bạn phải lưu giữ bản sao của tất cả dữ liệu bạn đang quan sát. Điều này có nghĩa là bạn phải chịu chi phí bộ nhớ có cấu trúc cho việc kiểm tra sửa lỗi mà bạn không nhận được với O.o(). Kiểm tra bẩn, mặc dù là một giải pháp ngăn chặn hợp lý, nhưng về cơ bản cũng là một sự trừu tượng bị rò rỉ và 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à, quy trình kiểm tra sửa đổi phải chạy bất cứ khi nào dữ liệu có thể thay đổi. Đơn giản là không có một cách hiệu quả để thực hiện việc này và mọi phương pháp tiếp cận đề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ò có nguy cơ tạo ra cấu phần mềm trực quan và điều kiện tranh đấu giữa các vấn đề về mã). Việc kiểm tra bẩn cũng đòi hỏi phải có danh sách toàn cầu cho những người quan sát, tạo ra các mối nguy rò rỉ bộ nhớ và hao tổn chi phí (O.o()).

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

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

Kiểm tra độ bẩn

Kiểm tra hiệu suất sai lệch

Chrome 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 nhiều mẹo cho bạn. Observe-JS của polymer là một polyfill cho O.o() sẽ sử dụng phương thức triển khai gốc nếu có, nhưng nếu không thì sẽ chèn polyfill cho nó và bao gồm một số đường hữu ích ở trên cùng. Công cụ này cung cấp cái nhìn 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 gì đã thay đổi. Hai tính năng này thực sự có tác động mạnh mẽ, đó là:

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

Ví dụ về việc quan sát một giá trị tại đườ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. Thao tác này sẽ cho bạn biết về các điểm nối mảng. Mối nối mảng về cơ bản là tập hợp các thao tác 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à kiểu biến đổi hoặc thành phần hiển thị khác của mảng. Đó 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 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 từ 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 gần đây 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 tiến của Angular 2.0. Cách tiếp cận dài hạn của họ sẽ là tận dụng Object.observe() khi Object.observe() xuất hiện trên phiên bản ổn định của Chrome, chọn sử dụng Watchtower.js, phương pháp phát hiện thay đổi của riêng họ cho đến thời điểm đó. Thật thú vị.

Kết luận

O.o() là một phần 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 tính năng này sẽ xuất hiện trong nhiều trình duyệt hơn trong thời gian tới, cho phép các khung JavaScript tăng hiệu suất nhờ khả năng quan sát đối tượng gốc. Những mục tiêu mà Chrome nhắm đến 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 của Opera trong tương lai.

Vì vậy, hãy tiếp tục 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 liên kết dữ liệu trong ứng dụng của bạn. Chắc chắn là có những thời điểm thú vị nhất ở phía trước!

Tài nguyên

Xin cảm ơn Rafael Weinstein, Jake Archibald, Eric Bidelman, Paul Kinlan và Vivian Cromwell đã cung cấp thông tin và bài đánh giá.