Giải thích tốt nhất về tính phản ứng của JavaScript

Nhiều khung JavaScript phía trước (Ví dụ: Angular, React và Vue) có các công cụ Reactivity của riêng chúng. Bằng cách hiểu tính phản ứng là gì và cách thức hoạt động, bạn có thể cải thiện kỹ năng phát triển của mình và sử dụng hiệu quả hơn các khung JavaScript. Trong video và bài viết dưới đây, chúng tôi xây dựng cùng loại Phản ứng mà bạn thấy trong mã nguồn Vue.

Nếu bạn xem video này thay vì đọc bài viết, hãy xem video tiếp theo trong loạt bài thảo luận về phản ứng và proxy với Evan You, người tạo ra Vue.

Hệ thống phản ứng

Hệ thống phản ứng Vue, có thể trông giống như ma thuật khi bạn thấy nó hoạt động lần đầu tiên. Sử dụng ứng dụng Vue đơn giản này:

Bằng cách nào đó Vue chỉ biết rằng nếu giá thay đổi, nó sẽ làm ba điều:

  • Cập nhật giá trị trên trang web của chúng tôi.
  • Tính toán lại biểu thức nhân giá * số lượng và cập nhật trang.
  • Gọi lại hàm TotalpriceWithTax và cập nhật trang.

Nhưng chờ đã, tôi nghe bạn tự hỏi, làm thế nào Vue biết phải cập nhật những gì khi giá thay đổi, và làm thế nào để theo dõi mọi thứ?

Đây không phải là cách lập trình JavaScript thường hoạt động

Nếu nó không rõ ràng với bạn, vấn đề lớn chúng ta phải giải quyết là lập trình thường không hoạt động theo cách này. Ví dụ: nếu tôi chạy mã này:

Bạn nghĩ nó sẽ ra sao? Vì chúng tôi không sử dụng Vue, nên nó sẽ in 10.

Trong Vue, chúng tôi muốn tổng số được cập nhật bất cứ khi nào giá hoặc số lượng được cập nhật. Chúng tôi muốn:

Thật không may, JavaScript là thủ tục, không phản ứng, vì vậy điều này không hoạt động trong cuộc sống thực. Để làm cho toàn bộ phản ứng, chúng tôi phải sử dụng JavaScript để làm cho mọi thứ hoạt động khác đi.

Vấn đề

Chúng ta cần lưu lại cách chúng tôi tính toán tổng số, vì vậy chúng tôi có thể chạy lại khi giá hoặc số lượng thay đổi.

Giải pháp

Trước hết, chúng tôi cần một số cách để nói với ứng dụng của chúng tôi, mã Mã tôi sắp chạy, lưu trữ cái này, tôi có thể cần bạn chạy nó vào lúc khác. Sau đó, chúng tôi sẽ muốn chạy mã và nếu giá hoặc các biến số lượng được cập nhật, chạy lại mã được lưu trữ.

Chúng tôi có thể làm điều này bằng cách ghi lại chức năng để chúng tôi có thể chạy lại nó.

Lưu ý rằng chúng ta lưu trữ một hàm ẩn danh bên trong biến mục tiêu và sau đó gọi một hàm bản ghi. Sử dụng cú pháp mũi tên ES6 tôi cũng có thể viết điều này như sau:

Định nghĩa của hồ sơ chỉ đơn giản là:

Chúng tôi lưu trữ mục tiêu (trong trường hợp của chúng tôi là {tổng = giá * số lượng}) để chúng tôi có thể chạy nó sau này, có lẽ với chức năng phát lại chạy tất cả những thứ chúng tôi đã ghi lại.

Điều này đi qua tất cả các hàm ẩn danh mà chúng ta đã lưu trữ bên trong mảng lưu trữ và thực thi từng hàm.

Sau đó, trong mã của chúng tôi, chúng tôi chỉ có thể:

Đủ đơn giản, phải không? Ở đây, toàn bộ mã trong đó Toàn bộ mã nếu bạn cần đọc qua và cố gắng nắm bắt nó thêm một lần nữa. FYI, tôi đang mã hóa điều này theo một cách đặc biệt, trong trường hợp bạn Were đang tự hỏi tại sao.

Vấn đề

Chúng tôi có thể tiếp tục ghi các mục tiêu khi cần, nhưng thật tuyệt khi có một giải pháp mạnh mẽ hơn sẽ mở rộng quy mô với ứng dụng của chúng tôi. Có lẽ một lớp quan tâm đến việc duy trì một danh sách các mục tiêu được thông báo khi chúng ta cần chúng để chạy lại.

Giải pháp: Một lớp phụ thuộc

Một cách chúng ta có thể bắt đầu giải quyết vấn đề này là bằng cách gói hành vi này vào lớp riêng của nó, Lớp phụ thuộc thực hiện mẫu quan sát lập trình chuẩn.

Vì vậy, nếu chúng ta tạo một lớp JavaScript để quản lý các phụ thuộc của mình (gần với cách Vue xử lý mọi thứ), thì nó có thể trông như thế này:

Lưu ý thay vì lưu trữ, chúng tôi hiện đang lưu trữ các chức năng ẩn danh của mình trong các thuê bao. Thay vì chức năng ghi của chúng tôi, bây giờ chúng tôi gọi phụ thuộc và bây giờ chúng tôi sử dụng thông báo thay vì phát lại. Để chạy cái này:

Nó vẫn hoạt động, và bây giờ mã của chúng tôi cảm thấy có thể tái sử dụng nhiều hơn. Điều duy nhất vẫn cảm thấy hơi kỳ lạ là cài đặt và chạy mục tiêu.

Vấn đề

Trong tương lai, chúng tôi sẽ có một lớp Dep cho mỗi biến và nó sẽ rất hay để gói gọn hành vi tạo các hàm ẩn danh cần được theo dõi để cập nhật. Có lẽ một chức năng theo dõi có thể là để chăm sóc hành vi này.

Vì vậy, thay vì gọi:

(đây chỉ là mã từ phía trên)

Thay vào đó chúng ta có thể gọi:

Giải pháp: Chức năng của Watcher

Bên trong chức năng Watcher của chúng tôi, chúng tôi có thể thực hiện một số điều đơn giản:

Như bạn có thể thấy, hàm watcher lấy đối số myFunc, đặt đó là thuộc tính mục tiêu toàn cầu của chúng ta, gọi dep.depend () để thêm mục tiêu của chúng tôi làm thuê bao, gọi hàm mục tiêu và đặt lại mục tiêu.

Bây giờ khi chúng ta chạy như sau:

Bạn có thể tự hỏi tại sao chúng tôi thực hiện mục tiêu như một biến toàn cục, thay vì chuyển nó vào các chức năng của chúng tôi khi cần thiết. Có một lý do tốt cho việc này, nó sẽ trở nên rõ ràng vào cuối bài viết của chúng tôi.

Vấn đề

Chúng ta có một lớp Dep duy nhất, nhưng điều chúng ta thực sự muốn là mỗi biến của chúng ta có Dep riêng. Hãy để tôi chuyển mọi thứ vào tài sản trước khi chúng ta đi xa hơn.

Hãy để Giả sử trong một phút rằng mỗi tài sản của chúng tôi (giá và số lượng) có lớp Dep nội bộ của riêng chúng.

Bây giờ khi chúng tôi chạy:

Vì giá trị data.price được truy cập (chính là nó), tôi muốn thuộc tính giá của lớp Dep Dep đẩy chức năng ẩn danh của chúng tôi (được lưu trữ trong mục tiêu) lên mảng thuê bao của nó (bằng cách gọi dep.depend ()). Vì data.quantity được truy cập, tôi cũng muốn lớp Dep thuộc tính số lượng đẩy hàm ẩn danh này (được lưu trữ trong mục tiêu) vào mảng thuê bao của nó.

Nếu tôi có một hàm ẩn danh khác, nơi chỉ truy cập data.price, tôi muốn điều đó được đẩy chỉ đến lớp thuộc tính giá.

Khi nào tôi muốn dep.notify () được gọi trên các thuê bao giá rẻ? Tôi muốn chúng được gọi khi giá được đặt. Đến cuối bài viết tôi muốn có thể vào bảng điều khiển và làm:

Chúng ta cần một số cách để móc vào một thuộc tính dữ liệu (như giá hoặc số lượng) để khi nó truy cập, chúng ta có thể lưu mục tiêu vào mảng thuê bao của mình và khi nó thay đổi, hãy chạy các chức năng được lưu trữ mảng thuê bao của chúng ta.

Giải pháp: Object.defineProperty ()

Chúng ta cần tìm hiểu về hàm Object.defineProperty () là JavaScript ES5 đơn giản. Nó cho phép chúng ta xác định các hàm getter và setter cho một thuộc tính. Để tôi chỉ cho bạn cách sử dụng rất cơ bản, trước khi tôi chỉ cho bạn cách chúng tôi sẽ sử dụng nó với lớp Dep của chúng tôi.

Như bạn có thể thấy, nó chỉ ghi hai dòng. Tuy nhiên, nó không thực sự nhận hoặc đặt bất kỳ giá trị nào, vì chúng tôi đã sử dụng quá nhiều chức năng. Hãy để thêm nó trở lại bây giờ. get () dự kiến ​​sẽ trả về một giá trị và set () vẫn cần cập nhật một giá trị, vì vậy, hãy để thêm một biến InternalValue để lưu trữ giá trị giá hiện tại của chúng tôi.

Bây giờ, get và set của chúng tôi đang hoạt động bình thường, bạn nghĩ gì sẽ in ra giao diện điều khiển?

Vì vậy, chúng tôi có một cách để nhận thông báo khi chúng tôi nhận và đặt giá trị. Và với một số đệ quy, chúng ta có thể chạy nó cho tất cả các mục trong mảng dữ liệu của mình, phải không?

FYI, Object.keys (dữ liệu) trả về một mảng các khóa của đối tượng.

Bây giờ mọi thứ đều có getters và setters, và chúng ta thấy điều này trên bàn điều khiển.

Đặt cả hai ý tưởng lại với nhau

Khi một đoạn mã như thế này được chạy và nhận được giá trị của giá, chúng ta muốn giá ghi nhớ hàm ẩn danh này (mục tiêu). Theo cách đó, nếu giá được thay đổi hoặc được đặt thành một giá trị mới, nó sẽ kích hoạt chức năng này để chạy lại, vì nó biết dòng này phụ thuộc vào nó. Vì vậy, bạn có thể nghĩ về nó như thế này.

Nhận => Hãy nhớ chức năng ẩn danh này, chúng tôi sẽ chạy lại khi giá trị của chúng tôi thay đổi.

Đặt => Chạy chức năng ẩn danh đã lưu, giá trị của chúng tôi vừa thay đổi.

Hoặc trong trường hợp của lớp Dep của chúng tôi

Giá đã truy cập (get) => gọi dep.depend () để lưu mục tiêu hiện tại

Đặt giá => gọi dep.notify () về giá, chạy lại tất cả các mục tiêu

Hãy kết hợp hai ý tưởng này và xem qua mã cuối cùng của chúng tôi.

Và bây giờ hãy nhìn vào những gì xảy ra trong bảng điều khiển của chúng tôi khi chúng tôi chơi xung quanh.

Chính xác những gì chúng tôi đã hy vọng! Cả giá cả và số lượng thực sự là phản ứng! Tổng mã của chúng tôi được chạy lại bất cứ khi nào giá trị của giá hoặc số lượng được cập nhật.

Hình minh họa này từ các tài liệu Vue sẽ bắt đầu có ý nghĩa ngay bây giờ.

Bạn có thấy vòng tròn Dữ liệu màu tím tuyệt đẹp đó với các getters và setters không? Nó trông quen quen! Mỗi phiên bản thành phần có một phiên bản trình theo dõi (màu xanh lam) thu thập các phụ thuộc từ getters (dòng màu đỏ). Khi một setter được gọi sau, nó sẽ thông báo cho trình theo dõi khiến thành phần này được kết xuất lại. Ở đây, hình ảnh lại một lần nữa với một số chú thích của riêng tôi.

Vâng, hiện tại điều này có ý nghĩa hơn nhiều không?

Rõ ràng làm thế nào Vue làm điều này dưới vỏ bọc phức tạp hơn, nhưng bây giờ bạn đã biết những điều cơ bản.

Vậy chúng ta đã học được gì?

  • Cách tạo một lớp Dep thu thập các phụ thuộc (phụ thuộc) và chạy lại tất cả các phụ thuộc (thông báo).
  • Cách tạo trình theo dõi để quản lý mã chúng tôi đang chạy, có thể cần phải thêm (mục tiêu) làm phụ thuộc.
  • Cách sử dụng Object.defineProperty () để tạo getters và setters.

Tiếp theo là gì?

Nếu bạn thích học cùng tôi trong bài viết này, bước tiếp theo trong lộ trình học của bạn là tìm hiểu về Reactivity với Proxy. Chắc chắn hãy xem video miễn phí của tôi về chủ đề này trên VueM Abbey.com nơi tôi cũng nói chuyện với Evan You, người tạo ra Vue.js.

Được xuất bản lần đầu tại www.vuem Abbey.com.