Giới thiệu về lan truyền ngữ cảnh trong JavaScript

theanh

Administrator
Nhân viên
React đã phổ biến ý tưởng về truyền ngữ cảnh trong các ứng dụng của chúng tôi với API ngữ cảnh của nó. Trong thế giới React, ngữ cảnh được sử dụng như một giải pháp thay thế cho prop-drilling và đồng bộ hóa trạng thái trên các phần khác nhau của ứng dụng.
“Ngữ cảnh cung cấp một cách để truyền dữ liệu qua cây thành phần mà không cần phải truyền prop xuống thủ công ở mọi cấp độ.”

— React Docs
Bạn có thể tưởng tượng ngữ cảnh của React như một loại "lỗ sâu" mà bạn có thể truyền giá trị qua đâu đó trên cây thành phần của mình và truy cập chúng xa hơn trong các thành phần con của bạn.

Đoạn trích sau đây là một ví dụ khá đơn giản (và khá vô dụng) về API ngữ cảnh của React, nhưng nó chứng minh cách chúng ta có thể sử dụng các giá trị được xác định cao hơn trong cây thành phần mà không cần truyền chúng một cách rõ ràng cho các thành phần con.

Trong đoạn trích bên dưới, chúng ta có ứng dụng có thành phần Color trong đó. Thành phần Color đó hiển thị một thông báo chứa thông báo được xác định trong thành phần cha của nó — ứng dụng, chỉ không truyền trực tiếp thông báo đó dưới dạng prop cho thành phần, mà thay vào đó — thông báo đó sẽ xuất hiện một cách "thần kỳ" thông qua việc sử dụng useContext.
Mã:
import {createContext, useContext} from 'react'const MyContext = createContext();function App() { return (    );}function Color() { const {color} = useContext(MyContext); return Màu của bạn là: {color}}
Mặc dù trường hợp sử dụng để truyền bá ngữ cảnh rất rõ ràng khi xây dựng các ứng dụng hướng đến người dùng bằng một khuôn khổ UI, nhưng vẫn cần có một API tương tự ngay cả khi không sử dụng khuôn khổ UI nào cả hoặc thậm chí khi không xây dựng UI.

Tại sao chúng ta nên quan tâm đến điều này?​

Theo tôi, có hai lý do để thực sự thử và triển khai nó.

Đầu tiên, với tư cách là người dùng một khuôn khổ — điều rất quan trọng là phải hiểu cách thức hoạt động của nó. Chúng ta thường coi các công cụ mình sử dụng là "ma thuật" và những thứ chỉ hoạt động. Cố gắng tự mình xây dựng một phần của chúng sẽ giúp bạn hiểu rõ hơn và thấy rằng không có phép thuật nào liên quan và rằng mọi thứ có thể khá đơn giản.

Thứ hai, API ngữ cảnh cũng có thể hữu ích khi làm việc trên các ứng dụng không phải UI.

Bất cứ khi nào chúng ta xây dựng bất kỳ loại ứng dụng nào từ trung bình đến lớn, chúng ta đều phải đối mặt với các hàm gọi lẫn nhau và ngăn xếp cuộc gọi có thể đi sâu vào nhiều lớp. Việc phải truyền các đối số xuống sâu hơn có thể tạo ra rất nhiều sự lộn xộn — đặc biệt là nếu bạn không sử dụng tất cả các biến này ở mọi cấp độ. Trong thế giới React, chúng tôi gọi đó là "prop drilling".

Ngoài ra, nếu bạn là tác giả thư viện và bạn dựa vào các lệnh gọi lại được người dùng truyền cho bạn, bạn có thể có các biến được khai báo ở các cấp độ khác nhau của thời gian chạy và bạn muốn chúng khả dụng ở xa hơn. Ví dụ, hãy lấy một khuôn khổ kiểm thử đơn vị.
Mã:
describe('add', () => { it('Should add two numbers', () => { expect(add(1, 1)).toBe(2); });});
Trong ví dụ sau, chúng ta có cấu trúc này:
  1. describe được gọi và gọi hàm gọi lại được truyền cho nó.
  2. trong hàm gọi lại, chúng ta có một lệnh gọi it.

Chúng ta muốn thực hiện điều gì?​

Bây giờ chúng ta hãy viết phần triển khai cơ bản cho khuôn khổ kiểm thử đơn vị của chúng ta. Tôi đang áp dụng cách tiếp cận rất ngây thơ và vui vẻ để làm cho mã đơn giản nhất có thể, nhưng tất nhiên, đây không phải là thứ bạn nên sử dụng trong cuộc sống thực.
Mã:
function describe(description, callback) { callback()}function it(text, callback) { try { callback() console.log("✅ " + text)} catch { console.log("🚨 " + text) }}
Trong ví dụ trên, chúng ta có hàm “describe” gọi hàm gọi lại của nó. Hàm gọi lại đó có thể chứa các lệnh gọi khác nhau đến “it”. “it”, đến lượt nó, ghi lại xem thử nghiệm có thành công hay không.

Giả sử rằng, cùng với thông báo thử nghiệm, chúng ta cũng muốn ghi lại thông báo từ “describe”:
Mã:
describe('calculator: Add', () => { it("Should correct add two numbers", () => { expect(add(1, 1)).toBe(2); });});
Sẽ ghi vào bảng điều khiển thông báo thử nghiệm được thêm vào phần mô tả:
Mã:
"calculator: Add > ✅ Should correct add two numbers"
Để thực hiện điều này, bằng cách nào đó, chúng ta cần phải có thông báo mô tả “nhảy qua” mã người dùng và bằng cách nào đó, tìm đường vào phần triển khai hàm “it”.

Chúng ta có thể thử những giải pháp nào?​

Khi cố gắng giải quyết vấn đề này, có nhiều cách tiếp cận mà chúng ta có thể thử. Tôi sẽ cố gắng xem xét một vài cách và chứng minh lý do tại sao chúng có thể không phù hợp trong tình huống của chúng ta.
  • Sử dụng "this"Chúng ta có thể thử khởi tạo một lớp và để dữ liệu lan truyền qua "this", nhưng có hai vấn đề ở đây. "this" rất khó tính. Nó không phải lúc nào cũng hoạt động như mong đợi, đặc biệt là khi phân tích các hàm mũi tên, sử dụng phạm vi từ vựng để xác định giá trị "this" hiện tại, nghĩa là người dùng của chúng ta sẽ phải sử dụng từ khóa function.Cùng với đó, không có mối quan hệ nào giữa "test" và "describe", vì vậy không có cách thực sự nào để chia sẻ phiên bản hiện tại.
  • Phát ra sự kiệnĐể phát ra sự kiện, chúng ta cần ai đó bắt được sự kiện đó. Nhưng nếu chúng ta có nhiều bộ chạy cùng lúc thì sao? Vì chúng ta không có mối quan hệ nào giữa các lệnh gọi thử nghiệm và các “descript” được tôn trọng của chúng, điều gì sẽ ngăn cản các bộ khác cũng bắt được các sự kiện của chúng?
  • Lưu trữ thông điệp trên một đối tượng toàn cụcCác đối tượng toàn cục gặp phải các vấn đề tương tự như phát ra một sự kiện và chúng ta cũng làm ô nhiễm phạm vi toàn cục.Có một đối tượng toàn cục cũng có nghĩa là giá trị ngữ cảnh của chúng ta có thể được kiểm tra và thậm chí sửa đổi từ bên ngoài hàm chạy của chúng ta, điều này có thể rất rủi ro.
  • Ném lỗiVề mặt kỹ thuật, điều này có thể hoạt động: "describe" của chúng ta có thể bắt lỗi do "it" ném ra, nhưng điều đó có nghĩa là khi gặp lỗi đầu tiên, chúng ta sẽ dừng thực thi và không thể chạy thêm bất kỳ bài kiểm tra nào nữa.

Context To The Rescue!​

Bây giờ, bạn hẳn đã đoán được rằng tôi đang ủng hộ một giải pháp có thiết kế tương tự như API ngữ cảnh của riêng React và tôi nghĩ rằng ví dụ kiểm thử đơn vị cơ bản của chúng ta có thể là một ứng cử viên tốt để kiểm thử nó.

Giải phẫu của ngữ cảnh​

Chúng ta hãy phân tích các thành phần tạo nên ngữ cảnh của React:
  1. React.createContext — tạo một ngữ cảnh mới, về cơ bản là định nghĩa một vùng chứa chuyên biệt mới đối với chúng tôi.
  2. Provider — giá trị trả về createContext. Đây là một đối tượng có thuộc tính “provider”. Thuộc tính provider là một thành phần riêng biệt và khi được sử dụng trong ứng dụng React, nó là lối vào “wormhole” của chúng tôi.
  3. React.useContext — một hàm, khi được gọi trong cây React được bao bọc bằng ngữ cảnh, đóng vai trò là điểm thoát khỏi wormhole của chúng tôi và cho phép kéo các giá trị ra khỏi đó.
Chúng ta hãy xem ngữ cảnh riêng của React:



Có vẻ như đối tượng ngữ cảnh React khá phức tạp. Nó chứa một Provider và một Consumer thực chất là các React Element. Hãy ghi nhớ cấu trúc này khi tiến về phía trước.

Biết được những gì chúng ta hiện biết về ngữ cảnh của React, hãy thử nghĩ xem các phần khác nhau của nó sẽ tương tác như thế nào với ví dụ kiểm thử đơn vị của chúng ta. Tôi sẽ tạo một kịch bản vô nghĩa chỉ để chúng ta có thể tưởng tượng các thành phần khác nhau hoạt động trong cuộc sống thực.
Mã:
const TestContext = createContext()function describe(description, callback) { // callback() // }function it(text, callback) { // const { description } = useContext(TestContext); try { callback() console.log(description + " > ✅ " + text) } catch { console.log(description+ " > 🚨 " + text) }}
Nhưng rõ ràng là điều này không thể thực hiện được. Đầu tiên, chúng ta không thể sử dụng React Elements trong mã JS thuần túy của mình. Thứ hai, chúng ta không thể sử dụng ngữ cảnh của React bên ngoài React. Đúng không? Đúng rồi.

Vậy thì hãy chuyển thể cấu trúc đó thành JS thực:
Mã:
const TestContext = createContext()function describe(description, callback) { TestContext.Provider({description}, () => {callback() });}function it(text, callback) { const { description } = useContext(TestContext); try { callback() console.log(description + " > ✅ " + text) } catch { console.log(description+ " > 🚨 " + text) }}
OK, vậy là bắt đầu giống JavaScript hơn rồi. Chúng ta có gì ở đây?

Vâng, chủ yếu là — thay vì thành phần ContextProvider, chúng ta sử dụng TextContext.Provider, thành phần này lấy một đối tượng có tham chiếu đến các giá trị của chúng ta và useContext() đóng vai trò là cổng thông tin của chúng ta — để chúng ta có thể khai thác vào lỗ sâu của mình.

Nhưng liệu điều này có hiệu quả không?Hãy thử xem.

Soạn thảo API của chúng ta​

Bây giờ chúng ta đã có khái niệm chung về cách sử dụng ngữ cảnh của mình, hãy bắt đầu bằng cách định nghĩa các hàm mà chúng ta sẽ trình bày. Vì chúng ta đã biết React Context API trông như thế nào, nên chúng ta có thể dựa vào đó.
Mã:
function createContext() { return { Provider, Consumer } function Provider(value, callback) {} function Consumer() {}}function useContext(ctxRef) {}
Chúng ta đang định nghĩa hai hàm, giống như React. createContextuseContext. createContext trả về một Provider và một Consumer, giống như ngữ cảnh của React, và useContext lấy tham chiếu ngữ cảnh.

Một số khái niệm cần lưu ý trước khi bắt đầu​

Những gì chúng ta sẽ làm từ đây trở đi sẽ dựa trên hai ý tưởng cốt lõi quan trọng đối với các nhà phát triển JavaScript. Tôi sẽ không giải thích chúng ở đây, nhưng nếu bạn cảm thấy không chắc chắn về những chủ đề này, bạn sẽ được khuyến khích tìm hiểu thêm về chúng:
  1. JavaScript ClosuresTừ MDN: “Một closure là sự kết hợp của một hàm được đóng gói lại với nhau (được bao quanh) với các tham chiếu đến trạng thái xung quanh của nó (môi trường từ vựng). Nói cách khác, một closure cho phép bạn truy cập vào phạm vi của một hàm bên ngoài từ một hàm bên trong. Trong JavaScript, các closure được tạo ra mỗi khi một hàm được tạo ra, tại thời điểm tạo hàm.”
  2. Bản chất đồng bộ của JavaScriptVề cơ bản, Javascript là đồng bộ và chặn. Đúng, nó có các lời hứa bất đồng bộ, lệnh gọi lại và async/await — và chúng sẽ yêu cầu một số xử lý đặc biệt, nhưng về cơ bản, hãy coi JavaScript là đồng bộ, vì trừ khi chúng ta đến các miền đó hoặc các triển khai trình duyệt trường hợp ngoại lệ cũ RẤT kỳ lạ, thì mã JavaScript là đồng bộ.
Hai ý tưởng có vẻ không liên quan này là những gì cho phép ngữ cảnh của chúng ta hoạt động. Giả định là, nếu chúng ta đặt một số giá trị trong Provider và gọi lệnh gọi lại của mình, các giá trị của chúng ta sẽ vẫn giữ nguyên và khả dụng trong suốt quá trình chạy hàm đồng bộ của chúng ta. Chúng ta chỉ cần một cách để truy cập vào nó. Đó là mục đích của useContext.

Lưu trữ giá trị trong ngữ cảnh của chúng ta​

Ngữ cảnh được sử dụng để truyền dữ liệu trong toàn bộ ngăn xếp cuộc gọi của chúng ta, vì vậy điều đầu tiên chúng ta muốn làm thực sự là lưu trữ thông tin trên đó.Hãy định nghĩa một biến contextValue trong hàm createContext của chúng ta. Nằm trong closure của createContext, đảm bảo rằng tất cả các hàm được định nghĩa trong createContext sẽ có quyền truy cập vào nó ngay cả sau này.
Mã:
function createContext() { let contextValue = undefined; function Provider(value, callback) {} function Consumer() {} return { Provider, Consumer }}
Bây giờ, chúng ta đã lưu trữ giá trị trong context, hàm Provider của chúng ta có thể lưu trữ giá trị mà nó chấp nhận trên đó và hàm Consumer có thể trả về giá trị đó.
Mã:
function createContext() { let contextValue = undefined; function Provider(value, callback) { contextValue = value; } function Consumer() { return contextValue; } return { Provider, Consumer }}
Để truy cập dữ liệu từ bên trong hàm của chúng ta, chúng ta chỉ cần gọi hàm Consumer, nhưng để giao diện của chúng ta hoạt động chính xác như React, chúng ta cũng phải cho useContext có quyền truy cập vào dữ liệu.
Mã:
function useContext(ctxRef) { return ctxRef.Consumer();}

Gọi Callback của chúng ta​

Bây giờ phần thú vị bắt đầu. Như đã đề cập, phương pháp này dựa trên bản chất đồng bộ của JavaScript. Điều này có nghĩa là từ thời điểm chúng ta chạy hàm gọi lại, chúng ta biết chắc chắn rằng sẽ không có mã nào khác chạy — điều đó có nghĩa là chúng ta không thực sự cần bảo vệ ngữ cảnh của mình khỏi bị sửa đổi trong quá trình chạy, mà thay vào đó, chúng ta chỉ cần dọn dẹp ngay sau khi hàm gọi lại của chúng ta chạy xong.
Mã:
function createContext() { let contextValue = undefined; function Provider(value, callback) { contextValue = value; callback(); contextValue = undefined; } function Consumer() { return contextValue; } return { Provider, Consumer }}
Đó là tất cả những gì cần làm. Thật đấy. Nếu hàm của chúng ta được gọi bằng hàm Provider, trong suốt quá trình thực thi, nó sẽ có quyền truy cập vào giá trị Provider.

Nếu chúng ta có một ngữ cảnh lồng nhau thì sao?​

Lồng ghép ngữ cảnh là điều có thể xảy ra. Ví dụ, khi tôi có describe trong describe. Trong trường hợp như vậy, ngữ cảnh của chúng ta sẽ bị hỏng khi thoát khỏi ngữ cảnh trong cùng, vì sau mỗi lần chạy lệnh gọi lại, chúng ta đặt lại giá trị ngữ cảnh thành undefined và vì cả hai lớp của ngữ cảnh đều chia sẻ cùng một closure — Provider trong cùng sẽ đặt lại giá trị cho các lớp bên trên nó.
Mã:
function Provider(value, callback) { contextValue = value; callback(); contextValue = undefined;}
May mắn thay, việc này rất dễ xử lý. Khi nhập ngữ cảnh, tất cả những gì chúng ta cần làm là lưu giá trị hiện tại của ngữ cảnh đó trong một biến và đặt lại giá trị đó khi chúng ta thoát khỏi ngữ cảnh:
Mã:
function Provider(value, callback) { let currentValue = contextValue; contextValue = value; callback(); contextValue = currentValue;}
Bây giờ, bất cứ khi nào chúng ta bước ra khỏi ngữ cảnh, nó sẽ quay lại giá trị trước đó và nếu không còn lớp ngữ cảnh nào ở trên, chúng ta sẽ quay lại giá trị ban đầu — giá trị này không được xác định.

Một tính năng khác mà chúng ta không triển khai hôm nay là giá trị mặc định cho ngữ cảnh. Trong React, bạn có thể khởi tạo ngữ cảnh bằng giá trị mặc định sẽ được Consumer/useContext trả về trong trường hợp chúng ta không ở trong ngữ cảnh đang chạy.

Nếu bạn đã đi đến bước này, bạn có tất cả kiến thức và công cụ để tự mình thử và triển khai nó — tôi rất muốn xem những gì bạn đưa ra.

Is This Being Used Anywhere?​

Có! Trên thực tế, tôi đã xây dựng gói context trên NPM thực hiện chính xác điều đó, với một số sửa đổi và nhiều tính năng hơn nữa — bao gồm hỗ trợ đầy đủ cho TypeScript, hợp nhất các ngữ cảnh lồng nhau, trả về giá trị từ hàm "Provider", giá trị khởi tạo ngữ cảnh và thậm chí là phần mềm trung gian đăng ký ngữ cảnh.

Bạn có thể kiểm tra mã nguồn đầy đủ của gói tại đây: https://github.com/ealush/vest/blob/latest/packages/context/src/context.ts

Và nó đang được sử dụng rộng rãi bên trong khung xác thực Vest, một khung xác thực biểu mẫu lấy cảm hứng từ các thư viện kiểm thử đơn vị như Mocha hoặc Jest. Context đóng vai trò là runtime chính của Vest, như bạn có thể thấy tại đây.

Tôi hy vọng bạn thích phần giới thiệu ngắn gọn này về truyền ngữ cảnh trong JavaScript và nó cho bạn thấy rằng không có phép thuật nào ẩn sau một số API React hữu ích nhất.

Đọc thêm trên Tạp chí Smashing​

  • “Giới thiệu về API ngữ cảnh của React,” Yusuff Faruq
  • “Biến phản ứng trong GraphQL Apollo Client,” Daniel Don
  • “Thành phần hợp chất trong React,” Ichoku Chinonso
  • “Các móc React hữu ích mà bạn có thể sử dụng trong các dự án của mình,” Ifeanyi Dike
 
Back
Bên trên