Recoil đã giới thiệu mô hình nguyên tử cho thế giới React . Sức mạnh mới của nó phải trả giá bằng đường cong học tập dốc và tài nguyên học tập thưa thớt.
Jotai và Zedux sau đó đã đơn giản hóa các khía cạnh khác nhau của mô hình mới này, cung cấp nhiều tính năng mới và đẩy các giới hạn của mô hình mới tuyệt vời này.
Các bài viết khác sẽ tập trung vào sự khác biệt giữa các công cụ này. Bài viết này sẽ tập trung vào một tính năng lớn mà cả 3 đều có điểm chung:
Họ đã sửa Flux.
Nếu bạn không biết Flux, đây là ý chính nhanh:
Bên cạnh Redux , tất cả các thư viện dựa trên Flux về cơ bản đều tuân theo mẫu sau: Một ứng dụng có nhiều cửa hàng. Chỉ có một Người điều phối có nhiệm vụ cung cấp các hành động cho tất cả các cửa hàng theo đúng thứ tự. "Trật tự thích hợp" này có nghĩa là tự động sắp xếp các phụ thuộc giữa các cửa hàng.
Ví dụ: hãy thiết lập ứng dụng thương mại điện tử:
Ví dụ, khi người dùng di chuyển một quả chuối vào giỏ hàng của họ, PromosStore cần đợi trạng thái của CartStore cập nhật trước khi gửi yêu cầu xem liệu có phiếu giảm giá chuối nào không.
Hoặc có lẽ chuối không thể vận chuyển đến khu vực của người dùng. CartStore cần kiểm tra UserStore trước khi cập nhật. Hoặc có lẽ phiếu giảm giá chỉ có thể được sử dụng một lần một tuần. PromosStore cần kiểm tra UserStore trước khi gửi yêu cầu phiếu giảm giá.
Flux không thích những phụ thuộc này. Từ tài liệu React kế thừa :
Các đối tượng trong ứng dụng Flux được tách biệt cao và tuân thủ rất chặt chẽ Định luật Demeter , nguyên tắc mà mỗi đối tượng trong hệ thống nên biết càng ít càng tốt về các đối tượng khác trong hệ thống.
Lý thuyết đằng sau điều này là vững chắc. 100%. Soo ... tại sao hương vị Flux nhiều cửa hàng này lại chết?
Hóa ra, sự phụ thuộc giữa các vùng chứa trạng thái biệt lập là không thể tránh khỏi. Trên thực tế, để giữ mã theo mô-đun và KHÔ, bạn nên thường xuyên sử dụng các cửa hàng khác.
Trong Flux, các phụ thuộc này được tạo nhanh chóng:
// This example uses Facebook's own `flux` library PromosStore.dispatchToken = dispatcher.register(payload => { if (payload.actionType === 'add-to-cart') { // wait for CartStore to update first: dispatcher.waitFor([CartStore.dispatchToken]) // now send the request sendPromosRequest(UserStore.userId, CartStore.items).then(promos => { dispatcher.dispatch({ actionType: 'promos-fetched', promos }) }) } if (payload.actionType === 'promos-fetched') { PromosStore.setPromos(payload.promos) } }) CartStore.dispatchToken = dispatcher.register(payload => { if (payload.actionType === 'add-to-cart') { // wait for UserStore to update first: dispatcher.waitFor([UserStore.dispatchToken]) if (UserStore.canBuy(payload.item)) { CartStore.addItem(payload.item) } } })
Ví dụ này cho thấy cách các phụ thuộc không được khai báo trực tiếp giữa các cửa hàng - thay vào đó, chúng được ghép lại với nhau trên cơ sở mỗi hành động. Các phụ thuộc không chính thức này yêu cầu đào qua mã triển khai để tìm.
Đây là một ví dụ rất đơn giản! Nhưng bạn đã có thể thấy Flux cảm thấy như thế nào. Các tác dụng phụ, thao tác lựa chọn và cập nhật trạng thái đều được kết hợp với nhau. Colocation này thực sự có thể là loại tốt đẹp. Nhưng hãy trộn một số phụ thuộc không chính thức, nhân ba công thức và phục vụ nó trên một số đĩa soạn sẵn và bạn sẽ thấy Flux bị hỏng nhanh chóng.
Các triển khai Flux khác như Flummox và Reflux đã cải thiện trải nghiệm gỡ lỗi và soạn sẵn. Mặc dù rất hữu ích, nhưng quản lý phụ thuộc là một vấn đề dai dẳng gây khó khăn cho tất cả các triển khai Flux. Sử dụng một cửa hàng khác cảm thấy xấu xí. Các cây phụ thuộc lồng sâu rất khó theo dõi.
Ứng dụng thương mại điện tử này một ngày nào đó có thể có cửa hàng cho OrderHistory, ShippingCalculator, DeliveryEstimate, BananasHoarded, v.v. Một ứng dụng lớn có thể dễ dàng có hàng trăm cửa hàng. Làm cách nào để bạn luôn cập nhật các phụ thuộc trong mọi cửa hàng? Làm thế nào để bạn theo dõi các tác dụng phụ? Còn độ tinh khiết thì sao? Điều gì về gỡ lỗi? Chuối có thực sự là một loại quả mọng?
Đối với các nguyên tắc lập trình do Flux giới thiệu, luồng dữ liệu một chiều là người chiến thắng, nhưng hiện tại, Định luật Demeter thì không.
Tất cả chúng ta đều biết làm thế nào Redux xuất hiện rầm rộ để cứu lấy một ngày. Nó từ bỏ khái niệm về nhiều cửa hàng để ủng hộ mô hình đơn lẻ. Bây giờ mọi thứ có thể truy cập mọi thứ khác mà không có bất kỳ "sự phụ thuộc" nào.
Các bộ giảm tốc là thuần túy, vì vậy tất cả logic xử lý nhiều lát cắt trạng thái phải nằm ngoài cửa hàng. Cộng đồng đưa ra các tiêu chuẩn để quản lý các tác dụng phụ và trạng thái dẫn xuất. Các cửa hàng Redux có thể sửa lỗi đẹp mắt. Lỗ hổng Flux lớn duy nhất mà Redux ban đầu không sửa được là bản soạn sẵn của nó.
RTK sau đó đã đơn giản hóa bản soạn sẵn khét tiếng của Redux. Sau đó, Zustand đã loại bỏ một số lỗi nhỏ với chi phí là một số khả năng sửa lỗi. Tất cả những công cụ này đã trở nên cực kỳ phổ biến trong thế giới React.
Với trạng thái mô-đun, các cây phụ thuộc phát triển phức tạp một cách tự nhiên đến mức giải pháp tốt nhất mà chúng tôi có thể nghĩ ra là, "Tôi đoán là đừng làm điều đó."
Va no đa hoạt động! Cách tiếp cận đơn lẻ mới này vẫn hoạt động đủ tốt cho hầu hết các ứng dụng. Các nguyên tắc của Flux vững chắc đến mức chỉ cần loại bỏ cơn ác mộng phụ thuộc là đã khắc phục được nó.
Hay đã làm nó?
Sự thành công của cách tiếp cận đơn lẻ đặt ra câu hỏi, Flux đã đạt được điều gì ngay từ đầu? Tại sao chúng ta từng muốn có nhiều cửa hàng?
Cho phép tôi làm sáng tỏ điều này.
Với nhiều cửa hàng, các phần trạng thái được chia thành các thùng chứa mô-đun, tự trị của riêng chúng. Những cửa hàng này có thể được thử nghiệm riêng biệt. Chúng cũng có thể được chia sẻ dễ dàng giữa các ứng dụng và gói.
Các cửa hàng độc lập này có thể được chia thành các đoạn mã riêng biệt. Trong một trình duyệt, chúng có thể được tải chậm và cắm ngay lập tức.
Các bộ giảm tốc của Redux cũng khá dễ phân tách mã. Nhờ replaceReducer
, bước bổ sung duy nhất là tạo bộ giảm tốc kết hợp mới. Tuy nhiên, có thể cần nhiều bước hơn khi có tác dụng phụ và phần mềm trung gian.
Với mô hình đơn lẻ, thật khó để biết cách tích hợp trạng thái bên trong của mô-đun bên ngoài với trạng thái của riêng bạn. Cộng đồng Redux đã giới thiệu mẫu Ducks như một nỗ lực để giải quyết vấn đề này. Và nó hoạt động, với chi phí của một bản soạn sẵn nhỏ.
Với nhiều cửa hàng, một mô-đun bên ngoài có thể hiển thị một cửa hàng một cách đơn giản. Ví dụ: một thư viện biểu mẫu có thể xuất một FormStore. Ưu điểm của điều này là tiêu chuẩn là "chính thức", có nghĩa là mọi người ít có khả năng tạo ra các phương pháp của riêng họ. Điều này dẫn đến một hệ sinh thái gói và cộng đồng thống nhất, mạnh mẽ hơn.
Mô hình singleton có hiệu suất đáng ngạc nhiên. Redux đã chứng minh điều đó. Tuy nhiên, mô hình lựa chọn của nó đặc biệt có giới hạn trên cứng. Tôi đã viết một số suy nghĩ về điều này trong cuộc thảo luận Chọn lại này . Một cây chọn lớn, đắt tiền thực sự có thể bắt đầu bị kéo, ngay cả khi kiểm soát tối đa bộ nhớ đệm.
Mặt khác, với nhiều cửa hàng, hầu hết các cập nhật trạng thái được tách biệt với một phần nhỏ của cây trạng thái. Họ không chạm vào bất cứ thứ gì khác trong hệ thống. Điều này có thể mở rộng vượt xa cách tiếp cận đơn lẻ - trên thực tế, với nhiều cửa hàng, rất khó đạt được giới hạn CPU trước khi đạt giới hạn bộ nhớ trên máy của người dùng.
Phá hủy trạng thái không quá khó trong Redux. Giống như trong ví dụ tách mã, nó chỉ cần thêm một vài bước để loại bỏ một phần của hệ thống phân cấp bộ rút gọn. Nhưng nó vẫn đơn giản hơn với nhiều cửa hàng - về lý thuyết, bạn chỉ cần tách cửa hàng khỏi bộ điều phối và cho phép nó được thu gom rác.
Đây là vấn đề lớn mà Redux, Zustand và mô hình singleton nói chung không xử lý tốt. Các tác dụng phụ được tách ra khỏi trạng thái mà chúng tương tác. Logic lựa chọn được tách ra khỏi mọi thứ. Trong khi Flux đa cửa hàng có lẽ quá đông đúc, thì Redux lại đi theo hướng ngược lại.
Với nhiều cửa hàng tự trị, những thứ này sẽ đi cùng nhau một cách tự nhiên. Thực sự, Flux chỉ thiếu một vài tiêu chuẩn để ngăn mọi thứ trở thành một nơi trú ẩn trốn tránh của gobbledygook (xin lỗi).
Bây giờ, nếu bạn biết thư viện OG Flux, bạn sẽ biết rằng nó thực sự không tuyệt vời chút nào. Người điều phối vẫn áp dụng cách tiếp cận toàn cầu - điều phối mọi hành động tới mọi cửa hàng. Toàn bộ điều với các phụ thuộc không chính thức/ngầm cũng làm cho việc phân tách và phá hủy mã trở nên kém hoàn hảo.
Tuy nhiên, Flux có rất nhiều tính năng thú vị dành cho nó. Ngoài ra, cách tiếp cận nhiều cửa hàng có tiềm năng cho nhiều tính năng hơn như Inversion of Control và quản lý trạng thái fractal (còn gọi là cục bộ).
Flux có thể đã phát triển thành một nhà quản lý nhà nước thực sự mạnh mẽ nếu ai đó không đặt tên cho nữ thần Demeter của họ. Tôi nghiêm túc đấy! ... Ok, tôi không. Nhưng bây giờ khi bạn đề cập đến nó, có lẽ luật của Demeter xứng đáng được xem xét kỹ hơn:
Chính xác thì cái gọi là "luật" này là gì? Từ Wikipedia :
- Mỗi đơn vị chỉ nên có kiến thức hạn chế về các đơn vị khác: chỉ những đơn vị có liên quan "chặt chẽ" với đơn vị hiện tại.
- Mỗi đơn vị chỉ nên nói chuyện với bạn bè của mình; không nói chuyện với người lạ.
Luật này được thiết kế dành cho Lập trình hướng đối tượng, nhưng nó có thể được áp dụng trong nhiều lĩnh vực, bao gồm cả quản lý trạng thái React.
Ý tưởng cơ bản là ngăn cửa hàng:
Về chuối, một quả chuối không nên bóc vỏ một quả chuối khác và không nên nói chuyện với một quả chuối trên cây khác. Tuy nhiên, nó có thể nói chuyện với cây kia nếu hai cây này lắp một đường dây điện thoại bằng chuối trước.
Điều này khuyến khích việc phân tách các mối quan tâm và giúp mã của bạn luôn ở dạng mô-đun, KHÔ và RẮN. Lý thuyết vững chắc! Vậy Flux còn thiếu gì?
Chà, sự phụ thuộc giữa các cửa hàng là một phần tự nhiên của một hệ thống mô-đun tốt. Nếu một cửa hàng cần thêm một phần phụ thuộc khác, cửa hàng đó nên làm điều đó và làm điều đó một cách rõ ràng nhất có thể . Đây là một số mã Flux đó một lần nữa:
PromosStore.dispatchToken = dispatcher.register(payload => { if (payload.actionType === 'add-to-cart') { // wait for CartStore to update first: dispatcher.waitFor([CartStore.dispatchToken]) // now send the request sendPromosRequest(UserStore.userId, CartStore.items).then(promos => { dispatcher.dispatch({ actionType: 'promos-fetched', promos }) }) } if (payload.actionType === 'promos-fetched') { PromosStore.setPromos(payload.promos) } })
PromosStore có nhiều phụ thuộc được khai báo theo những cách khác nhau - nó đợi và đọc từ CartStore
và nó đọc từ UserStore
. Cách duy nhất để khám phá những phụ thuộc này là tìm kiếm các cửa hàng trong quá trình triển khai của PromosStore.
Các công cụ dành cho nhà phát triển cũng không thể giúp làm cho các phụ thuộc này dễ khám phá hơn. Nói cách khác, các phụ thuộc quá tiềm ẩn.
Mặc dù đây là một ví dụ rất đơn giản và giả tạo, nhưng nó minh họa cách Flux giải thích sai Luật Demeter. Mặc dù tôi chắc chắn rằng nó chủ yếu được sinh ra từ mong muốn giữ cho các triển khai Flux ở mức nhỏ (quản lý phụ thuộc thực sự là một nhiệm vụ phức tạp!), Đây là điểm mà Flux không đạt được.
Không giống như những anh hùng của câu chuyện này:
Vào năm 2020, Recoil tình cờ xuất hiện tại hiện trường. Mặc dù lúc đầu hơi vụng về, nhưng nó đã dạy cho chúng tôi một mô hình mới giúp hồi sinh cách tiếp cận nhiều cửa hàng của Flux.
Luồng dữ liệu một chiều được chuyển từ chính cửa hàng sang biểu đồ phụ thuộc. Các cửa hàng bây giờ được gọi là nguyên tử. Các nguyên tử hoàn toàn tự trị và có thể chia mã. Họ có những sức mạnh mới như hỗ trợ hồi hộp và hydrat hóa. Và quan trọng nhất, các nguyên tử chính thức tuyên bố sự phụ thuộc của chúng.
Mô hình nguyên tử ra đời.
// a Recoil atom const greetingAtom = atom({ key: 'greeting', default: 'Hello, World!', })
Recoil phải vật lộn với một cơ sở mã cồng kềnh, rò rỉ bộ nhớ, hiệu suất kém, phát triển chậm và các tính năng không ổn định - đáng chú ý nhất là các tác dụng phụ. Nó sẽ từ từ giải quyết một số trong số này, nhưng trong khi chờ đợi, các thư viện khác đã lấy ý tưởng của Recoil và chạy theo chúng.
Jotai bùng nổ tại hiện trường và nhanh chóng thu hút được nhiều người theo dõi.
// a Jotai atom const greetingAtom = atom('Hello, World!')
Bên cạnh việc chỉ chiếm một phần rất nhỏ trong kích thước của Recoil, Jotai còn cung cấp hiệu năng tốt hơn, API bóng bẩy hơn và không bị rò rỉ bộ nhớ nhờ cách tiếp cận dựa trên WeakMap của nó.
Tuy nhiên, nó phải trả giá bằng một số năng lượng - cách tiếp cận WeakMap khiến việc kiểm soát bộ đệm trở nên khó khăn và việc chia sẻ trạng thái giữa nhiều cửa sổ hoặc các lĩnh vực khác gần như là không thể. Và việc thiếu các phím chuỗi, trong khi kiểu dáng đẹp, khiến việc gỡ lỗi trở thành cơn ác mộng. Hầu hết các ứng dụng nên thêm những ứng dụng đó trở lại, làm giảm đáng kể kiểu dáng đẹp của Jotai.
// a (better?) Jotai atom const greetingAtom = atom('Hello, World!') greetingAtom.debugLabel = 'greeting'
Một vài đề cập đáng trân trọng là Reatom và Nanostores . Các thư viện này đã khám phá thêm lý thuyết đằng sau mô hình nguyên tử và cố gắng đẩy kích thước và tốc độ của nó đến giới hạn.
Mô hình nguyên tử nhanh và có tỷ lệ rất tốt. Nhưng cho đến rất gần đây, có một vài lo ngại rằng không có thư viện nguyên tử nào giải quyết tốt:
Đường cong học tập. Các nguyên tử là khác nhau . Làm cách nào để chúng tôi làm cho các khái niệm này có thể tiếp cận được đối với các nhà phát triển React?
Dev X và gỡ lỗi. Làm thế nào để chúng ta làm cho các nguyên tử có thể khám phá được? Làm cách nào để bạn theo dõi các bản cập nhật hoặc thực thi các thông lệ tốt?
Di chuyển gia tăng cho các cơ sở mã hiện có. Làm thế nào để bạn truy cập các cửa hàng bên ngoài? Làm thế nào để bạn giữ nguyên logic hiện có? Làm thế nào để bạn tránh viết lại đầy đủ?
Bổ sung. Làm thế nào để chúng ta làm cho mô hình nguyên tử có thể mở rộng? Nó có thể xử lý mọi tình huống có thể xảy ra không?
Tiêm phụ thuộc. Các nguyên tử xác định các quan hệ phụ thuộc một cách tự nhiên, nhưng liệu chúng có thể được hoán đổi trong quá trình thử nghiệm hoặc trong các môi trường khác nhau không?
Định luật Demeter. Làm cách nào để chúng tôi ẩn chi tiết triển khai và ngăn cập nhật rải rác?
Đây là nơi tôi đến. Hãy xem, tôi là người tạo ra chính một thư viện nguyên tử khác:
Zedux cuối cùng đã xuất hiện vài tuần trước. Được phát triển bởi một công ty Fintech ở New York - công ty tôi làm việc - Zedux không chỉ được thiết kế để có tốc độ nhanh và khả năng mở rộng, mà còn cung cấp trải nghiệm gỡ lỗi và phát triển mượt mà.
// a Zedux atom const greetingAtom = atom('greeting', 'Hello, World!')
Tôi sẽ không đi sâu vào các tính năng của Zedux ở đây - như tôi đã nói, bài viết này sẽ không tập trung vào sự khác biệt giữa các thư viện nguyên tử này.
Chỉ cần nói rằng Zedux giải quyết tất cả các mối quan tâm trên. Ví dụ: đây là thư viện nguyên tử đầu tiên cung cấp Inversion of Control thực sự và là thư viện đầu tiên đưa chúng ta trở lại toàn bộ Luật Demeter bằng cách cung cấp xuất nguyên tử để ẩn chi tiết triển khai.
Các hệ tư tưởng cuối cùng của Flux cuối cùng đã được hồi sinh - không chỉ được hồi sinh mà còn được cải thiện! - nhờ mô hình nguyên tử.
Vậy mô hình nguyên tử chính xác là gì?
Các thư viện nguyên tử này có nhiều điểm khác biệt - chúng thậm chí còn có các định nghĩa khác nhau về "nguyên tử" nghĩa là gì. Sự đồng thuận chung là các nguyên tử là các thùng chứa trạng thái tự trị, nhỏ, bị cô lập, được cập nhật theo phản ứng thông qua Đồ thị tuần hoàn có hướng.
Tôi biết, tôi biết, nghe có vẻ phức tạp, nhưng hãy đợi cho đến khi tôi giải thích nó bằng chuối.
Tôi đùa đấy! Nó thực sự rất đơn giản:
Cập nhật ricochet thông qua biểu đồ. Đó là nó!
Vấn đề là, bất kể cách triển khai hay ngữ nghĩa, tất cả các thư viện nguyên tử này đã làm sống lại khái niệm về nhiều cửa hàng và khiến chúng không chỉ có thể sử dụng được mà còn là một niềm vui thực sự khi làm việc cùng.
6 lý do tôi đưa ra để muốn có nhiều cửa hàng chính xác là lý do khiến mô hình nguyên tử trở nên mạnh mẽ như vậy:
Chỉ riêng các API đơn giản và khả năng mở rộng đã làm cho các thư viện nguyên tử trở thành một lựa chọn tuyệt vời cho mọi ứng dụng React. Nhiều năng lượng hơn và ít bản mẫu hơn Redux? Đây có phải là một giấc mơ?
Thật là một hành trình! Thế giới quản lý trạng thái React không bao giờ hết ngạc nhiên, và tôi rất vui vì đã quá giang.
Chúng ta chỉ mới bắt đầu. Có rất nhiều chỗ cho sự đổi mới với các nguyên tử. Sau nhiều năm tạo và sử dụng Zedux, tôi đã thấy mô hình nguyên tử có thể mạnh mẽ như thế nào. Trên thực tế, sức mạnh của nó là gót chân Achilles:
Khi các nhà phát triển khám phá các nguyên tử, họ thường đào sâu vào các khả năng đến mức họ quay lại nói: "Hãy nhìn vào sức mạnh phức tạp điên rồ này", thay vì "Hãy xem các nguyên tử giải quyết vấn đề này đơn giản và tao nhã như thế nào." Tôi ở đây để thay đổi điều này.
Mô hình nguyên tử và lý thuyết đằng sau nó chưa được dạy theo cách mà hầu hết các nhà phát triển React có thể tiếp cận được. Theo một cách nào đó, trải nghiệm về các nguyên tử trong thế giới React cho đến nay trái ngược với Flux:
Bài viết này là bài thứ hai trong loạt tài nguyên học tập mà tôi đang sản xuất để giúp các nhà phát triển React hiểu cách thức hoạt động của các thư viện nguyên tử và lý do bạn có thể muốn sử dụng một tài nguyên. Hãy xem bài viết đầu tiên - Khả năng mở rộng: Cấp độ bị mất của Quản lý trạng thái phản ứng .
Phải mất 10 năm, nhưng lý thuyết CS vững chắc do Flux giới thiệu cuối cùng đã tác động mạnh mẽ đến các ứng dụng React nhờ mô hình nguyên tử. Và nó sẽ tiếp tục làm như vậy trong nhiều năm tới.