Đã bao giờ tò mò rằng React Hook bản chất là gì chưa? Khi rõ ràng đó là những cú pháp tương đối xa lạ và không giống với lập trình thông thường, từ một hàm được gọi đi gọi lại mỗi lần có trạng thái thay đổi đến những hàm sinh ra để tránh việc gọi đi gọi lại đó. Trong bài này Dũng sẽ cùng các bạn đi tìm hiểu vào sâu bên trong của React để hiểu rõ React Hook là cái gì nhé.

Chẩn bị

Bạn hãy clone hoặc tải mã nguồn của react về và mở lên bằng visual studio code nhé.

React Hook là 1 dạng cú pháp được biên dịch

Bạn hãy tìm đến tập tin Globals.ts trong thư mục compiler/packages/babel-plugin-react-compiler/src. Trong đó babel-plugin-react-compiler chứa mã nguồn sẽ được biên dịch bởi React Compiler. Bạn có thể tìm thấy đoạn mã nguồn sau:

const REACT_APIS: Array<[string, BuiltInType]> = [
  [
    "useContext",
    addHook(DEFAULT_SHAPES, {
      positionalParams: [],
      restParam: Effect.Read,
      returnType: { kind: "Poly" },
      calleeEffect: Effect.Read,
      hookKind: "useContext",
      returnValueKind: ValueKind.Frozen,
      returnValueReason: ValueReason.Context,
    }),
  ],
  [
    "useState",
    addHook(DEFAULT_SHAPES, {
      positionalParams: [],
      restParam: Effect.Freeze,
      returnType: { kind: "Object", shapeId: BuiltInUseStateId },
      calleeEffect: Effect.Read,
      hookKind: "useState",
      returnValueKind: ValueKind.Frozen,
      returnValueReason: ValueReason.State,
    }),
  ],
  [
    "useActionState",
    addHook(DEFAULT_SHAPES, {
      positionalParams: [],
      restParam: Effect.Freeze,
      returnType: { kind: "Object", shapeId: BuiltInUseActionStateId },
      calleeEffect: Effect.Read,
      hookKind: "useActionState",
      returnValueKind: ValueKind.Frozen,
      returnValueReason: ValueReason.State,
    }),
  ],
  [
    "useReducer",
    addHook(DEFAULT_SHAPES, {
      positionalParams: [],
      restParam: Effect.Freeze,
      returnType: { kind: "Object", shapeId: BuiltInUseReducerId },
      calleeEffect: Effect.Read,
      hookKind: "useReducer",
      returnValueKind: ValueKind.Frozen,
      returnValueReason: ValueReason.ReducerState,
    }),
  ],
  [
    "useRef",
    addHook(DEFAULT_SHAPES, {
      positionalParams: [],
      restParam: Effect.Capture,
      returnType: { kind: "Object", shapeId: BuiltInUseRefId },
      calleeEffect: Effect.Read,
      hookKind: "useRef",
      returnValueKind: ValueKind.Mutable,
    }),
  ],
  [
    "useMemo",
    addHook(DEFAULT_SHAPES, {
      positionalParams: [],
      restParam: Effect.Freeze,
      returnType: { kind: "Poly" },
      calleeEffect: Effect.Read,
      hookKind: "useMemo",
      returnValueKind: ValueKind.Frozen,
    }),
  ],
  [
    "useCallback",
    addHook(DEFAULT_SHAPES, {
      positionalParams: [],
      restParam: Effect.Freeze,
      returnType: { kind: "Poly" },
      calleeEffect: Effect.Read,
      hookKind: "useCallback",
      returnValueKind: ValueKind.Frozen,
    }),
  ],
  [
    "useEffect",
    addHook(
      DEFAULT_SHAPES,
      {
        positionalParams: [],
        restParam: Effect.Freeze,
        returnType: { kind: "Primitive" },
        calleeEffect: Effect.Read,
        hookKind: "useEffect",
        returnValueKind: ValueKind.Frozen,
      },
      BuiltInUseEffectHookId
    ),
  ],
  [
    "useLayoutEffect",
    addHook(
      DEFAULT_SHAPES,
      {
        positionalParams: [],
        restParam: Effect.Freeze,
        returnType: { kind: "Poly" },
        calleeEffect: Effect.Read,
        hookKind: "useLayoutEffect",
        returnValueKind: ValueKind.Frozen,
      },
      BuiltInUseLayoutEffectHookId
    ),
  ],
  [
    "useInsertionEffect",
    addHook(
      DEFAULT_SHAPES,
      {
        positionalParams: [],
        restParam: Effect.Freeze,
        returnType: { kind: "Poly" },
        calleeEffect: Effect.Read,
        hookKind: "useInsertionEffect",
        returnValueKind: ValueKind.Frozen,
      },
      BuiltInUseInsertionEffectHookId
    ),
  ],
  [
    "use",
    addFunction(
      DEFAULT_SHAPES,
      [],
      {
        positionalParams: [],
        restParam: Effect.Freeze,
        returnType: { kind: "Poly" },
        calleeEffect: Effect.Read,
        returnValueKind: ValueKind.Frozen,
      },
      BuiltInUseOperatorId
    ),
  ],
];

Đoạn mã nguồn này sẽ được biên dịch thành javascript, khi đó mã nguồn thực tế chúng ta nhận được sẽ khác với những gì chúng ta nhìn thấy.
Ví dụ useEffect có thể được chuyển thành các mã javascript dưới đây chẳng hạn:

var create = effect.create;
{
  if ((flags & Insertion) !== NoFlags$1) {
    setIsRunningInsertionEffect(true);
  }
}
effect.destroy = create();
{
  if ((flags & Insertion) !== NoFlags$1) {
    setIsRunningInsertionEffect(false);
  }
}
{
  if ((flags & Passive$1) !== NoFlags$1) {
    markComponentPassiveEffectMountStopped();
  } else if ((flags & Layout) !== NoFlags$1) {
    markComponentLayoutEffectMountStopped();
  }
}

Luồng thực thi

Để thấy được các hàm của react hook sẽ được nằm ở đâu và thực thi thế nào, chúng ta có thể khai báo một useEffect thế này:

useEffect(() => {
  throw new Error("test")
}, []);

Hàm này sẽ gây ra lỗi để đến khi chúng ta reload lại trang thì sẽ thấy stack strace ở console như sau:

Error: test
    at App.tsx:22:13
    at commitHookEffectListMount (react-dom_client.js?v=d5d7f799:16913:34)
    at commitPassiveMountOnFiber (react-dom_client.js?v=d5d7f799:18154:19)
    at commitPassiveMountEffects_complete (react-dom_client.js?v=d5d7f799:18127:17)
    at commitPassiveMountEffects_begin (react-dom_client.js?v=d5d7f799:18117:15)
    at commitPassiveMountEffects (react-dom_client.js?v=d5d7f799:18107:11)
    at flushPassiveEffectsImpl (react-dom_client.js?v=d5d7f799:19488:11)
    at flushPassiveEffects (react-dom_client.js?v=d5d7f799:19445:22)
    at react-dom_client.js?v=d5d7f799:19326:17
    at workLoop (react-dom_client.js?v=d5d7f799:195:42)

Bạn vẫn còn nhớ Schedule.js chứ? Hàm workLoop nằm trong tập tin này và có nội dung như sau:

function workLoop(hasTimeRemaining: boolean, initialTime: number): boolean {
  let currentTime = initialTime;
  advanceTimers(currentTime);
  currentTask = peek(taskQueue);
  while (
    currentTask !== null &&
    !(enableSchedulerDebugging && isSchedulerPaused)
  ) {
    if (
      currentTask.expirationTime > currentTime &&
      (!hasTimeRemaining || shouldYieldToHost())
    ) {
      // This currentTask hasn't expired, and we've reached the deadline.
      break;
    }
    // $FlowFixMe[incompatible-use] found when upgrading Flow
    const callback = currentTask.callback;
    if (typeof callback === 'function') {
      // $FlowFixMe[incompatible-use] found when upgrading Flow
      currentTask.callback = null;
      // $FlowFixMe[incompatible-use] found when upgrading Flow
      currentPriorityLevel = currentTask.priorityLevel;
      // $FlowFixMe[incompatible-use] found when upgrading Flow
      const didUserCallbackTimeout = currentTask.expirationTime <= currentTime;
      if (enableProfiling) {
        // $FlowFixMe[incompatible-call] found when upgrading Flow
        markTaskRun(currentTask, currentTime);
      }
      const continuationCallback = callback(didUserCallbackTimeout);
      currentTime = getCurrentTime();
      if (typeof continuationCallback === 'function') {
        // If a continuation is returned, immediately yield to the main thread
        // regardless of how much time is left in the current time slice.
        // $FlowFixMe[incompatible-use] found when upgrading Flow
        currentTask.callback = continuationCallback;
        if (enableProfiling) {
          // $FlowFixMe[incompatible-call] found when upgrading Flow
          markTaskYield(currentTask, currentTime);
        }
        advanceTimers(currentTime);

        if (shouldYieldForPaint) {
          needsPaint = true;
          return true;
        } else {
          // If `shouldYieldForPaint` is false, we keep flushing synchronously
          // without yielding to the main thread. This is the behavior of the
          // `toFlushAndYield` and `toFlushAndYieldThrough` testing helpers .
        }
      } else {
        if (enableProfiling) {
          // $FlowFixMe[incompatible-call] found when upgrading Flow
          markTaskCompleted(currentTask, currentTime);
          // $FlowFixMe[incompatible-use] found when upgrading Flow
          currentTask.isQueued = false;
        }
        if (currentTask === peek(taskQueue)) {
          pop(taskQueue);
        }
        advanceTimers(currentTime);
      }
    } else {
      pop(taskQueue);
    }
    currentTask = peek(taskQueue);
  }
  // Return whether there is additional work
  if (currentTask !== null) {
    return true;
  } else {
    const firstTimer = peek(timerQueue);
    if (firstTimer !== null) {
      requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
    }
    return false;
  }
}

Như vậy có thể thấy rằng sau khi được biện dịch và đến khi chương trình khởi động thì các hàm của React Hook sẽ tham gia vào chuối phản ứng (chain of responsibility design pattern) và được thực thi trong mỗi lần lặp của scheduler nếu thoả mãn điều kiện.

Tổng kết

Như vậy chúng ta đã cùng nhau tìm hiểu về hàm bản chất bên trong của React Hook để thấy rằng nó cũng không quá cao siêu, về bề ngoài thì nó là một dạng cú pháp để hỗ trợ chúng ta lập trình nhanh hơn với ít dòng lệnh hơn, còn về bên trong khi được biên dịch ra mã javascript thì nó vẫn làm các thao tác thông thường là đăng ký các hàm vào những chuỗi phản ứng tương ứng để được thực thi trong mỗi lần lặp của scheduler nếu thoả mãn điều kiện.


Cám ơn bạn đã quan tâm đến bài viết này. Để nhận được thêm các kiến thức bổ ích bạn có thể:

  1. Đọc các bài viết của TechMaster trên facebook: https://www.facebook.com/techmastervn
  2. Xem các video của TechMaster qua Youtube: https://www.youtube.com/@TechMasterVietnam nếu bạn thấy video/bài viết hay bạn có thể theo dõi kênh của TechMaster để nhận được thông báo về các video mới nhất nhé.
  3. Chat với techmaster qua Discord: https://discord.gg/yQjRTFXb7a