React 源碼解析 - Hooks 系列

2023-10-27

前言

這篇要來研究下每次寫 react 都在使用的 Hooks。說到 hooks,大家一定都用過,我自己也常用,但是一直都不知道 hook 背後到底是怎麼運作的,出現 bug 時就是百度解決辦法,半猜半試解決,其實都有點不明所以。所以,今天就要來把 react hooks 慢慢深入解析一下。

正文

調用 React Hook 到底背後發生了什麼?

前情提要下,這篇分析 hooks 所用的 React 源碼是 v16.13.1 的,因為最新版本的源碼關於 hooks 這塊個人能力不夠,不是看的很清楚,所以還是先以穩定版本來看,不過具體實現也都沒有大問題的。

初探 Hooks 源碼

當我們在調用 useXXX() 時到底背後發生了什麼?雖然看源碼真的有點累、挺難頂的,但是要想真正理解原理,還是看源碼最實在。

平時我們在項目中引入 hooks 時一般都是這樣寫的:

import React, { useState } from 'react';

後面的 from 'react' 其實就提示了我們 useState 這個 hook 源碼藏在 /react 這個目錄下,所以順藤摸瓜,最後就找到了導出所有 hooks 的地方:react/packages/react/src/React.js,跟 hooks 相關的源碼如下:

...

import {
  useCallback,
  useContext,
  useEffect,
  useImperativeHandle,
  useDebugValue,
  useLayoutEffect,
  useMemo,
  useReducer,
  useRef,
  useState,
  useResponder,
  useTransition,
  useDeferredValue,
} from './ReactHooks';

...
...

export {
  Children,
  createRef,
  Component,
  PureComponent,
  ...
  useCallback,
  useContext,
  useEffect,
  useImperativeHandle,
  useDebugValue,
  useLayoutEffect,
  useMemo,
  useReducer,
  useRef,
  useState,
  ...
};

這個文件中,我們看到其實所有 hooks 都來自 /ReactHooks.js,所以當然就再去看看啦~

Dispatcher

  • /ReactHooks.js 源碼如下:
/**
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @flow
 */

import type {Dispatcher} from 'react-reconciler/src/ReactInternalTypes';
import type {
  MutableSource,
  MutableSourceGetSnapshotFn,
  MutableSourceSubscribeFn,
  ReactContext,
} from 'shared/ReactTypes';
import type {OpaqueIDType} from 'react-reconciler/src/ReactFiberHostConfig';

import ReactCurrentDispatcher from './ReactCurrentDispatcher';

type BasicStateAction<S> = (S => S) | S;
type Dispatch<A> = A => void;

function resolveDispatcher() {
  const dispatcher = ReactCurrentDispatcher.current;
  if (__DEV__) {
    if (dispatcher === null) {
      console.error(
        'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for' +
          ' one of the following reasons:\n' +
          '1. You might have mismatching versions of React and the renderer (such as React DOM)\n' +
          '2. You might be breaking the Rules of Hooks\n' +
          '3. You might have more than one copy of React in the same app\n' +
          'See https://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem.',
      );
    }
  }
  // Will result in a null access error if accessed outside render phase. We
  // intentionally don't throw our own error because this is in a hot path.
  // Also helps ensure this is inlined.
  return ((dispatcher: any): Dispatcher);
}

export function getCacheForType<T>(resourceType: () => T): T {
  const dispatcher = resolveDispatcher();
  // $FlowFixMe This is unstable, thus optional
  return dispatcher.getCacheForType(resourceType);
}

export function useContext<T>(Context: ReactContext<T>): T {
  const dispatcher = resolveDispatcher();
  if (__DEV__) {
    // TODO: add a more generic warning for invalid values.
    if ((Context: any)._context !== undefined) {
      const realContext = (Context: any)._context;
      // Don't deduplicate because this legitimately causes bugs
      // and nobody should be using this in existing code.
      if (realContext.Consumer === Context) {
        console.error(
          'Calling useContext(Context.Consumer) is not supported, may cause bugs, and will be ' +
            'removed in a future major release. Did you mean to call useContext(Context) instead?',
        );
      } else if (realContext.Provider === Context) {
        console.error(
          'Calling useContext(Context.Provider) is not supported. ' +
            'Did you mean to call useContext(Context) instead?',
        );
      }
    }
  }
  return dispatcher.useContext(Context);
}

export function useState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
  const dispatcher = resolveDispatcher();
  return dispatcher.useState(initialState);
}

export function useReducer<S, I, A>(
  reducer: (S, A) => S,
  initialArg: I,
  init?: I => S,
): [S, Dispatch<A>] {
  const dispatcher = resolveDispatcher();
  return dispatcher.useReducer(reducer, initialArg, init);
}

export function useRef<T>(initialValue: T): {|current: T|} {
  const dispatcher = resolveDispatcher();
  return dispatcher.useRef(initialValue);
}

export function useEffect(
  create: () => (() => void) | void,
  deps: Array<mixed> | void | null,
): void {
  const dispatcher = resolveDispatcher();
  return dispatcher.useEffect(create, deps);
}

export function useLayoutEffect(
  create: () => (() => void) | void,
  deps: Array<mixed> | void | null,
): void {
  const dispatcher = resolveDispatcher();
  return dispatcher.useLayoutEffect(create, deps);
}

export function useCallback<T>(
  callback: T,
  deps: Array<mixed> | void | null,
): T {
  const dispatcher = resolveDispatcher();
  return dispatcher.useCallback(callback, deps);
}

export function useMemo<T>(
  create: () => T,
  deps: Array<mixed> | void | null,
): T {
  const dispatcher = resolveDispatcher();
  return dispatcher.useMemo(create, deps);
}

export function useImperativeHandle<T>(
  ref: {|current: T | null|} | ((inst: T | null) => mixed) | null | void,
  create: () => T,
  deps: Array<mixed> | void | null,
): void {
  const dispatcher = resolveDispatcher();
  return dispatcher.useImperativeHandle(ref, create, deps);
}

export function useDebugValue<T>(
  value: T,
  formatterFn: ?(value: T) => mixed,
): void {
  if (__DEV__) {
    const dispatcher = resolveDispatcher();
    return dispatcher.useDebugValue(value, formatterFn);
  }
}

export const emptyObject = {};

export function useTransition(): [boolean, (() => void) => void] {
  const dispatcher = resolveDispatcher();
  return dispatcher.useTransition();
}

export function useDeferredValue<T>(value: T): T {
  const dispatcher = resolveDispatcher();
  return dispatcher.useDeferredValue(value);
}

export function useOpaqueIdentifier(): OpaqueIDType | void {
  const dispatcher = resolveDispatcher();
  return dispatcher.useOpaqueIdentifier();
}

export function useMutableSource<Source, Snapshot>(
  source: MutableSource<Source>,
  getSnapshot: MutableSourceGetSnapshotFn<Source, Snapshot>,
  subscribe: MutableSourceSubscribeFn<Source, Snapshot>,
): Snapshot {
  const dispatcher = resolveDispatcher();
  return dispatcher.useMutableSource(source, getSnapshot, subscribe);
}

export function useCacheRefresh(): <T>(?() => T, ?T) => void {
  const dispatcher = resolveDispatcher();
  // $FlowFixMe This is unstable, thus optional
  return dispatcher.useCacheRefresh();
}

在這個文件裡,我們看到以 useXXX 命名的函數,但這還不是 hooks 真正的源碼,畢竟哪可能這麼短hh。看到這邊經常出現的單詞 dispatcher,我們其實大概率可以猜到這個文件其實是起到一個 調度 的作用,我們來分析下。

useState Hook

useState 來說:

export function useState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
  const dispatcher = resolveDispatcher();
  return dispatcher.useState(initialState);
}

我們先跳過函數的參數以及返回值類型,直接看邏輯。看到首先調用了一個 resolveDispatcher 函數,接著就通過了 return 的這個 dispatcher 調用了 useState 函數。所以其實本質上就是通過 resolveDispatcher() 這個方法生成一個 dispatcher 分發器,然後通過調度器去調取實際執行用戶所希望的 hook 的真實邏輯。

那我們當然就好奇了,這個 resolveDispatcher 函數中的 dispatcher 到底是什麼?於是繼續往下挖源碼:

/**
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @flow
 */

import type {Dispatcher} from 'react-reconciler/src/ReactFiberHooks';

/**
 * Keeps track of the current dispatcher.
 */
const ReactCurrentDispatcher = {
  /**
   * @internal
   * @type {ReactComponent}
   */
  current: (null: null | Dispatcher),
};

export default ReactCurrentDispatcher;

在這個文件裡面,我們看到一個比較特別的語法:import type,這其實是一個 flow.js 的語法,不過因為不是本篇的重點所以在此暫不展開說明。

其實開頭的 import type 就是從另一個文件模塊中導入一個數據類型,也就是 /packages/react-reconciler/src/ReactFiberHooks.jsDispatcher 類型。當然我們就是繼續去看看 ReactFiberHooks.js 看看裡面到底有啥。

來到 ReactFiberHooks.js,我們發現這是一個 2000 多行的怪物。所有官方提供的 hooks 源代碼的秘密就都藏在這個文件裡面。文件開頭也就看到了我們循跡而來的 Dispatcher 類型,後面就是每個 hooks 的具體實現了。

Hook 的實現

我們首先就來看看下面這段源碼:

const HooksDispatcherOnMount: Dispatcher = {
  readContext,

  useCallback: mountCallback,
  useContext: readContext,
  useEffect: mountEffect,
  useImperativeHandle: mountImperativeHandle,
  useLayoutEffect: mountLayoutEffect,
  useMemo: mountMemo,
  useReducer: mountReducer,
  useRef: mountRef,
  useState: mountState,
  useDebugValue: mountDebugValue,
  useResponder: createDeprecatedResponderListener,
  useDeferredValue: mountDeferredValue,
  useTransition: mountTransition,
};

const HooksDispatcherOnUpdate: Dispatcher = {
  readContext,

  useCallback: updateCallback,
  useContext: readContext,
  useEffect: updateEffect,
  useImperativeHandle: updateImperativeHandle,
  useLayoutEffect: updateLayoutEffect,
  useMemo: updateMemo,
  useReducer: updateReducer,
  useRef: updateRef,
  useState: updateState,
  useDebugValue: updateDebugValue,
  useResponder: createDeprecatedResponderListener,
  useDeferredValue: updateDeferredValue,
  useTransition: updateTransition,
};

我們看到,每一個 hook 都有兩個相關的函數:mountXXX 以及 updateXXX。這兩個函數其實分別是 hook 在 mount 階段(即組件的掛載,或者說初始化階段,又或者說是第一次執行 useXXX 的時候),和 update 階段(即組件更新,或者說組件重新渲染)的邏輯。

而為了方便管理和調用,react 團隊的工程師們把 Hook 在 Mount 階段的邏輯存到 HooksDispatcherOnMount 對象中,把 Update 階段的邏輯存到 HooksDispatcherOnUpdate 對象中,並以 Hook 自己的名字 useXXX 作為 key。

也就是說,同一個 hook 在 mount 和 update 階段所做的事情是不一樣的。

Hook 在 Mount 階段(useState)

每個 hook 都有各自實現的功能,而由於篇幅有限,在這裏我只用最最最基本的 hooks useState 進行分析。

首先來看看 useState mount 階的源碼 mountState()

function mountState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
  const hook = mountWorkInProgressHook();
  if (typeof initialState === 'function') {
    // $FlowFixMe: Flow doesn't like mixed types
    initialState = initialState();
  }
  hook.memoizedState = hook.baseState = initialState;
  const queue = (hook.queue = {
    pending: null,
    dispatch: null,
    lastRenderedReducer: basicStateReducer,
    lastRenderedState: (initialState: any),
  });
  const dispatch: Dispatch<
    BasicStateAction<S>,
  > = (queue.dispatch = (dispatchAction.bind(
    null,
    currentlyRenderingFiber,
    queue,
  ): any));
  return [hook.memoizedState, dispatch];
}

我們就來分析下關於 useState 在 mount 階段的邏輯。首先就看到一個又看不懂的函數,mountWorkInProgressHook(),當然我們也只能再去找找這個函數的源碼,來看看這個函數幹了啥。

mountWorkInProgressHook() - 存儲 Hook 的數據結構 - 循環鏈表

馬上找到 mountWorkInProgressHook() 源碼如下:

function mountWorkInProgressHook(): Hook {
  const hook: Hook = {
    memoizedState: null,

    baseState: null,
    baseQueue: null,
    queue: null,

    next: null,
  };

  // workInProgressHook 指向當前組件的 hook 鏈表
  if (workInProgressHook === null) {
    // 如果当前组件的 Hook 链表为空,那么就将刚刚新建的 Hook 作为 Hook 链表的第一个节点(头结点) 
    // This is the first hook in the list
    currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
  } else {
    // 如果当前组件的 Hook 链表不为空,那么就将刚刚新建的 Hook 添加到 Hook 链表的末尾(作为尾结点)
    // Append to the end of the list
    workInProgressHook = workInProgressHook.next = hook;
  }
  return workInProgressHook;
}

我們首先看到,函數一開始定義了一個 Hook 類型的 hook 變量,其實就是一個簡單的 js 對象,裡面有五個屬性,其實這個就是一個存儲 hook 的鏈表的節點結構。

下面的 workInProgressHook 指向當前組件的 hook 鏈表,邏輯就是如上註解在代碼上。

所以我們很清楚可以看到,一個函數組件的的所有 hook 就是以 鏈表 的形式存儲的,而鏈表中的每個節點就是一個 hook,每個 hook 節點結構的定義如下:

export type Hook = {|
  memoizedState: any,
  baseState: any,
  baseQueue: Update<any, any> | null,
  queue: UpdateQueue<any, any> | null,
  next: Hook | null,
|};

這邊對 hook 節點的結構做一下解釋:

  • memoizedState:Hook 自身維護的狀態

  • queue:Hook 自身維護的更新隊列

  • next:指向下一個 hook 節點

回到 mount 階段。所以在 mount 階段(組件初始化),調用 useStatemountState 就會調用 mountWorkInProgressHook 方法來創建一個新的 hook 節點,並把他添加到組件自身的 Hook 鏈表上。來舉個例子,假如現在有一個函數組件,並且第一次執行下面代碼:

const [firstName, setFirstName] = useState('尼古拉斯');
const [lastName, setLastName] = useState('赵四');
useEffect(() => {});

由於是第一次執行,也就是在 mount 階段,會創建如下圖的一個 hook 鏈表:

整個 Hook 鏈表保存在哪裡

其實關於這個問題,在上面的 mountWorkInProgressHook 已經有大致提示了。

function mountWorkInProgressHook(): Hook {
  ...
  if (workInProgressHook === null) {
    // This is the first hook in the list
    currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
  } else {
    ...
  }
  ...
}

假如原來還沒有 hook 鏈表,那麼就會將新建的 hook 節點作為 hook 鏈表的頭節點,然後把 hook 鏈表的頭節點保存在 currentlyRenderingFiber.memoizedState 中,也就是 當前 FiberNode 的 memoziedState 屬性 (注意,currentlyRenderingFiber 不是 Hook 類型,而是一個 Fiber 類型)。這樣一個簡單的小小的赋值语句,就可以把當前組件和裡面的所有 hook 關聯起來。

useState 怎麼處理 state 更新

用過 useState 的人都知道,useState 返回一個數組,其中第一個元素是當前 state 的值,而第二個元素是一個用來設置,或者說更新 state 的函數,一般會名為 setXXX。

在上面我們看 mountState 源碼的時候,會新建一個更新隊列鏈表 queue,這個鏈表的作用其實就是用來存放更新操作,鏈表中的每一個節點就是一次更新 state 操作,也就是調用了一次 setXXX,以便後面的 update 階段可以拿到最新的 state。那我們當然就要來看看這個 queue 的源碼:

const queue = (hook.queue = {
  pending: null,
  dispatch: null,
  lastRenderedReducer: basicStateReducer,
  lastRenderedState: (initialState: any),
});
  • pending:最近一個等待執行的更新

  • dispatch:更新 state 的方法,setXXX

  • lastRenderedReducer:這個字段是組件最近一次渲染時使用的 reducer。其實 useState 本身就是一個簡化版的 useReducer,所以我們在使用 useState 時才不需要傳入 reducer,這是因為 useState 使用的 reducer 是 react 官方默認幫我們寫好的 reducer,basicStateReducer

  • lastRenderedState:組件最近一次渲染的 state

創建完這個隊列後,接著是一個 dispatch 變量,其實也是個方法,也就是最後要返回的第二個參數 setXXX,而這個變量實際上就是 dispatchAction 函數,通過 bind 來改變 this 的指向,將當前 fiber,更新隊列和 dispatch 關聯起來。

所以其實當我們調用了 useState 的更新方法 setXXX 時,其實本質上就是調用了 dispatchAction 這個方法。那我們當然就要繼續往下挖挖,看看 dispatchAction 的源碼:

function dispatchAction<S, A>(
  fiber: Fiber,
  queue: UpdateQueue<S, A>,
  action: A,
) {
  if (__DEV__) {
    if (typeof arguments[3] === 'function') {
      console.error(
        "State updates from the useState() and useReducer() Hooks don't support the " +
          'second callback argument. To execute a side effect after ' +
          'rendering, declare it in the component body with useEffect().',
      );
    }
  }

  const currentTime = requestCurrentTimeForUpdate();
  const suspenseConfig = requestCurrentSuspenseConfig();
  const expirationTime = computeExpirationForFiber(
    currentTime,
    fiber,
    suspenseConfig,
  );

  // 为当前更新操作新建 update 对象
  const update: Update<S, A> = {
    expirationTime,
    suspenseConfig,
    action,
    eagerReducer: null,
    eagerState: null,
    next: (null: any),
  }

  if (__DEV__) {
    update.priority = getCurrentPriorityLevel();
  }

  // Append the update to the end of the list.
  // pending 指向的是最新的 update 对象
  const pending = queue.pending;
  if (pending === null) {
    // This is the first update. Create a circular list.
    // 如果更新队列为空,那么将当前的更新作为更新队列的第一个节点,并且让它的 next 属性指向自身,以此来保持为循环链表
    update.next = update;
  } else {
    // 如果更新队列为非空,那么就将当前的更新对象插入到列表的头部
    update.next = pending.next;
    // 链表的尾结点指向最新的头节点,以保持为一个循环链表
    pending.next = update;
  }
  // 让 queue.pending 指向最新的头节点
  queue.pending = update;

  const alternate = fiber.alternate;
  if (
    fiber === currentlyRenderingFiber ||
    (alternate !== null && alternate === currentlyRenderingFiber)
  ) {
    // This is a render phase update. Stash it in a lazily-created map of
    // queue -> linked list of updates. After this render pass, we'll restart
    // and apply the stashed updates on top of the work-in-progress hook.
    didScheduleRenderPhaseUpdate = true;
    update.expirationTime = renderExpirationTime;
    currentlyRenderingFiber.expirationTime = renderExpirationTime;
  } else {
    if (
      fiber.expirationTime === NoWork &&
      (alternate === null || alternate.expirationTime === NoWork)
    ) {
      // The queue is currently empty, which means we can eagerly compute the
      // next state before entering the render phase. If the new state is the
      // same as the current state, we may be able to bail out entirely.
      const lastRenderedReducer = queue.lastRenderedReducer;
      if (lastRenderedReducer !== null) {
        let prevDispatcher;
        if (__DEV__) {
          prevDispatcher = ReactCurrentDispatcher.current;
          ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnUpdateInDEV;
        }
        try {
          const currentState: S = (queue.lastRenderedState: any);
          const eagerState = lastRenderedReducer(currentState, action);
          // Stash the eagerly computed state, and the reducer used to compute
          // it, on the update object. If the reducer hasn't changed by the
          // time we enter the render phase, then the eager state can be used
          // without calling the reducer again.
          update.eagerReducer = lastRenderedReducer;
          update.eagerState = eagerState;
          if (is(eagerState, currentState)) {
            // Fast path. We can bail out without scheduling React to re-render.
            // It's still possible that we'll need to rebase this update later,
            // if the component re-renders for a different reason and by that
            // time the reducer has changed.
            return;
          }
        } catch (error) {
          // Suppress the error. It will throw again in the render phase.
        } finally {
          if (__DEV__) {
            ReactCurrentDispatcher.current = prevDispatcher;
          }
        }
      }
    }
    if (__DEV__) {
      // $FlowExpectedError - jest isn't a global, and isn't recognized outside of tests
      if ('undefined' !== typeof jest) {
        warnIfNotScopedWithMatchingAct(fiber);
        warnIfNotCurrentlyActingUpdatesInDev(fiber);
      }
    }
    scheduleWork(fiber, expirationTime);
  }
}

這段代碼真的還蠻複雜的,我也還沒完全看懂,不過大致上看懂了基本關鍵的思路部分。下面就來說說整理一下~

我們用栗子還帶入,假設現在有三個更新 state 的操作如下:

const [firstName, setFirstName] = useState('');

setFirstName('Tom');
setFirstName('Allen');
setFirstName('Bill');
  1. 執行 setFirstName('Tom')
1-1. const update = {...} // 新建一個 update 對象,為了方便說明這邊叫做 update0

1-2. queue.pending === null,所以 update.next = update // update0 的 next 指向它自己

1-3. queue.pending = update // queue.pending 指向 update0

  1. 執行 setFirstName('Allen')
2-1. const update = {...} // 新建一個 update 對象,為了方便說明這邊叫做 update1

2-2. const pending = queue.pending // 此時的 queue.pending 指向 update0

2-3. queue.pending !== null,所以

     2-3-1. update.next = pending.next // 相當於是 update1.next = update0.next,又因為 update0.next 就指向自己,所以這一部相當於 update1.next = update0

     2-3-2. pending.next = update // 相當於 update0.next = update1

2-4. queue.pending = update // queue.pending 指向 update1

  1. 執行 setFirstName('Bill')
2-1. const update = {...} // 新建一個 update 對象,為了方便說明這邊叫做 update2

2-2. const pending = queue.pending // 此時的 queue.pending 指向 update1

2-3. queue.pending !== null,所以

     2-3-1. update.next = pending.next // 相當於是 update2.next = update1.next,又因為前面執行 setFirstName('Allen') 時 update1.next 其實指向 update0,所以這步實際上就是 update2.next = update0

     2-3-2. pending.next = update // 相當於 update1.next = update2

2-4. queue.pending = update // queue.pending 指向 update2

到此就是執行完三次 setState 之後的 hook 鏈表,整體最後的最終狀態如下圖:

觸發組件更新(重新渲染)使組建達到 Update 階段

在將 update 對象添加到 Hook 的更新隊列鏈表後,dispatchAction() 還會去判斷當前調用 setXxx(action) 傳入的值(action)和上一次渲染的 state(此時正展示的 UI)作對比,看看有沒有變化,如果有變化,則調用 scheduleWork() 安排 fiberNode 的更新工作(組件重新渲染),如果沒變化,則直接跳過,不安排更新(組件重新渲染。

關於 scheduleWork() 是如何安排更新工作的問題,這邊先不展開講了,因為涉及到另外更多的邏輯和機制,並且其實我自己也還沒看懂,實在是有點頂。暫時就先知道關於 setState 這個 hook 還有這些提升效率的機制這樣~

Hook 在 Update 階段做了什麼?

接下來,我們來看看 Update 階段(組件重新渲染)時幹了啥。還是以 useState 這個 Hook 來舉例子。在 Update 階段的時候,也就是組件重新渲染,即第二、第三、第 N 次執行 useState 的時候,這時就不是執行 mountState() 了,而是執行 updateState() 了。來看一波 updateState() 的源碼:

function updateState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
  return updateReducer(basicStateReducer, (initialState: any));
}

可以發現,updateState() 實際上調用的是 updateReducer(),這也就再次證明了 useState 實際上只是簡化版的 useReducer 而已。因為我們調用 useState 的時候不會傳入 reducer,所以這裏會默認傳一個 basicStateReducer 進去作為 reducer。

function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S {
  // $FlowFixMe: Flow doesn't like mixed types
  return typeof action === 'function' ? action(state) : action;
}

在使用 useState 時其實我們的 action 通常是一個值,而不會是一個函數,所以其實 basicStateReducer 就會直接把這個值返回出來而已。

那麼接下來當然就要來看看 updateReducer 的代碼啦~

function updateReducer<S, I, A>(
  reducer: (S, A) => S,
  initialArg: I,
  init?: I => S,
): [S, Dispatch<A>] {
  // 获取正在执行的处于更新阶段 Hook 节点
  const hook = updateWorkInProgressHook();
  // 获取更新队列链表
  const queue = hook.queue;
  invariant(
    queue !== null,
    'Should have a queue. This is likely a bug in React. Please file an issue.',
  );

  queue.lastRenderedReducer = reducer;

  const current: Hook = (currentHook: any);

  // The last rebase update that is NOT part of the base state.
  let baseQueue = current.baseQueue;

  // The last pending update that hasn't been processed yet.
  const pendingQueue = queue.pending;
  if (pendingQueue !== null) {
    // We have new updates that haven't been processed yet.
    // We'll add them to the base queue.
    if (baseQueue !== null) {
      // Merge the pending queue and the base queue.
      const baseFirst = baseQueue.next;
      const pendingFirst = pendingQueue.next;
      baseQueue.next = pendingFirst;
      pendingQueue.next = baseFirst;
    }
    if (__DEV__) {
      if (current.baseQueue !== baseQueue) {
        // Internal invariant that should never happen, but feasibly could in
        // the future if we implement resuming, or some form of that.
        console.error(
          'Internal error: Expected work-in-progress queue to be a clone. ' +
            'This is a bug in React.',
        );
      }
    }
    current.baseQueue = baseQueue = pendingQueue;
    queue.pending = null;
  }

  if (baseQueue !== null) {
    // We have a queue to process.

    // 获取更新队列最初(第一個)的 update 对象节点
    const first = baseQueue.next;
    // 初始化 newState
    let newState = current.baseState;

    let newBaseState = null;
    let newBaseQueueFirst = null;
    let newBaseQueueLast = null;
    let update = first;
    do {
      // 循环遍历更新队列链表
      // 从最早的那个 update 对象开始遍历,每次遍历执行一次更新,去更新状态
      
      const updateLane = update.lane;
      if (!isSubsetOfLanes(renderLanes, updateLane)) {
        // Priority is insufficient. Skip this update. If this is the first
        // skipped update, the previous update/state is the new base
        // update/state.
        const clone: Update<S, A> = {
          lane: updateLane,
          action: update.action,
          eagerReducer: update.eagerReducer,
          eagerState: update.eagerState,
          next: (null: any),
        };
        if (newBaseQueueLast === null) {
          newBaseQueueFirst = newBaseQueueLast = clone;
          newBaseState = newState;
        } else {
          newBaseQueueLast = newBaseQueueLast.next = clone;
        }
        // Update the remaining priority in the queue.
        // TODO: Don't need to accumulate this. Instead, we can remove
        // renderLanes from the original lanes.
        currentlyRenderingFiber.lanes = mergeLanes(
          currentlyRenderingFiber.lanes,
          updateLane,
        );
        markSkippedUpdateLanes(updateLane);
      } else {
        // This update does have sufficient priority.

        if (newBaseQueueLast !== null) {
          const clone: Update<S, A> = {
            // This update is going to be committed so we never want uncommit
            // it. Using NoLane works because 0 is a subset of all bitmasks, so
            // this will never be skipped by the check above.
            lane: NoLane,
            action: update.action,
            eagerReducer: update.eagerReducer,
            eagerState: update.eagerState,
            next: (null: any),
          };
          newBaseQueueLast = newBaseQueueLast.next = clone;
        }

        // Process this update.
        if (update.eagerReducer === reducer) {
          // If this update was processed eagerly, and its reducer matches the
          // current reducer, we can use the eagerly computed state.
          newState = ((update.eagerState: any): S);
        } else {
          // 具體就是在這邊更新了 newState 以不斷更新獲取到最新的 state
          const action = update.action;
          newState = reducer(newState, action);
        }
      }
      update = update.next;
    } while (update !== null && update !== first);

    if (newBaseQueueLast === null) {
      newBaseState = newState;
    } else {
      newBaseQueueLast.next = (newBaseQueueFirst: any);
    }

    // Mark that the fiber performed work, but only if the new state is
    // different from the current state.
    if (!is(newState, hook.memoizedState)) {
      markWorkInProgressReceivedUpdate();
    }

    hook.memoizedState = newState;
    hook.baseState = newBaseState;
    hook.baseQueue = newBaseQueueLast;

    queue.lastRenderedState = newState;
  }

  // Interleaved updates are stored on a separate queue. We aren't going to
  // process them during this render, but we do need to track which lanes
  // are remaining.
  const lastInterleaved = queue.interleaved;
  if (lastInterleaved !== null) {
    let interleaved = lastInterleaved;
    do {
      const interleavedLane = interleaved.lane;
      currentlyRenderingFiber.lanes = mergeLanes(
        currentlyRenderingFiber.lanes,
        interleavedLane,
      );
      markSkippedUpdateLanes(interleavedLane);
      interleaved = ((interleaved: any).next: Update<S, A>);
    } while (interleaved !== lastInterleaved);
  } else if (baseQueue === null) {
    // `queue.lanes` is used for entangling transitions. We can set it back to
    // zero once the queue is empty.
    queue.lanes = NoLanes;
  }

  const dispatch: Dispatch<A> = (queue.dispatch: any);
  return [hook.memoizedState, dispatch];
}

updateReducer() 會去遍歷更新隊列鏈表,執行每一個節點裏面的更新操作,得到最新的狀態並返回,以此來保證我們每次刷新組件都能拿到當前最新的狀態。useState 的 reducer 是 baseStateReducer,因為傳入的 update.action 是一個值,所以直接返回了 update.aciton

小結一下,當 Update 階段,即第二次、第三次…第 N 次執行 useState 時,所做的事情是:

  1. 獲取正在執行的處於更新階段 Hook 節點;

  2. 獲取該 Hook 節點的更新隊列鏈表;

  3. 從該更新隊列的最早的 update 對象節點開始遍歷,一直遍歷到最近添加的(最新的)update 對象節點,遍歷到每個節點的時候執行該節點的更新操作,將該次更新的 state 值存到 newState 中;

  4. 當遍歷完最近的一個 update 對象節點後,此時 newState 裏存放的就是最新的 state 值,最後返回 newState,於是用戶就拿到了最新的 state;

useState 的運行流程

上面介紹了 useState(useReducer)在 mount 階段、 update 階段分別做的事情以及組件何時觸發組件更新,最後來總結一下 useState 整體的運行流程:

組件初次渲染 (mount) 時

此時是第一次執行 useState,也就是 mount 階段,所以執行的是 mountState

  1. 在 Hook 鏈表上添加該 useState 的 Hook 節點

  2. 初始化 state 的值

  3. 返回此次渲染的 state 和 修改 state 的方法(dispatch)

當調用 setXxx/dispatchAction 時

  1. 創建 update 對象,並將 update 對象添加到該 Hook 節點的更新隊列鏈表

  2. 判斷傳入的值(action)和當前 UI 渲染的 state 值是否相同,若相同則略過,若不相同,則調用 scheduleWork 安排組件的重新渲染

  3. 當前所有 setXxx 都逐一執行完後,假如其中有能滿足(2)的條件,即有調用 scheduleWork 的話,則觸發更新(組件重新渲染),進入 Update 階段

組件重新渲染 (update) 時

組件重新渲染,進入 Update 階段,即第 2 、第 3 、… n 次執行 useState

  1. 獲取該 useState Hook 的更新隊列鏈表

  2. 遍歷這個更新隊列鏈表,從最早的那一個 update 對象進行遍歷,直至遍歷到最近的添加那一個 update 對象,最後得到最新的 state 並返回,作為組件此次渲染的 state

  3. 返回此次渲染的 state 和 修改 state 的方法

useEffect Hook

如果能看到這裡,真的已經很堅持了,本人在學習這段時到這邊也快頂不住了,著實是有點太複雜。不過既然都已經大概理解了 useState 的基本原理,不仿再堅持一下。這邊我們打鐵趁熱,繼續挖掘下 useEffect 這個也是超常用的 Hook 的原理吧~

經過了上面的分析,我們知道,一個 Hook 在組件的初次渲染(掛載)和重新渲染的時候所執行的操作是不一樣的。在初次渲染的時候執行的是 mountXxx() 方法,而在重新渲染的時候,執行的是 updateXxx() 方法。

useEffect 這個 Hook 也同理,它對應著也有 mountEffectupdateEffect

mountEffect

mountEffect 源碼如下:

function mountEffect(
  create: () => (() => void) | void,
  deps: Array<mixed> | void | null,
): void {
  if (__DEV__) {
    // $FlowExpectedError - jest isn't a global, and isn't recognized outside of tests
    if ('undefined' !== typeof jest) {
      warnIfNotCurrentlyActingEffectsInDEV(currentlyRenderingFiber);
    }
  }
  if (
    __DEV__ &&
    enableStrictEffects &&
    (currentlyRenderingFiber.mode & StrictEffectsMode) !== NoMode
  ) {
    return mountEffectImpl(
      MountPassiveDevEffect | PassiveEffect | PassiveStaticEffect,
      HookPassive,
      create,
      deps,
    );
  } else {
    // 重點就是這段,上面開發環境代碼都可以先略過
    return mountEffectImpl(
      PassiveEffect | PassiveStaticEffect,
      HookPassive,
      create,
      deps,
    );
  }
}

很明顯,重點就是 mountEffectImpl 這個方法,mountEffect 只是作為一個入口,那麼就接著往下看看:

function mountEffectImpl(fiberFlags, hookFlags, create, deps): void {
  // 获取当前 Hook 节点,同时将当前的 Hook 節點添加到所屬的 FiberNode 所指向的 Hook 链表中
  const hook = mountWorkInProgressHook();

  // 获取依赖
  const nextDeps = deps === undefined ? null : deps;

  currentlyRenderingFiber.flags |= fiberFlags;
  hook.memoizedState = pushEffect(
    HookHasEffect | hookFlags,
    create,
    undefined,
    nextDeps,
  );
}

看到在這個 mountEffectImpl 方法中又調用了一個 pushEffect,繼續看下去:

function pushEffect(tag, create, destroy, deps) {
  const effect: Effect = {
    tag,
    create,
    destroy,
    deps,
    // Circular
    next: (null: any),
  };
  // 获取当前 FiberNode 的 updateQueue
  let componentUpdateQueue: null | FunctionComponentUpdateQueue = (currentlyRenderingFiber.updateQueue: any);
  if (componentUpdateQueue === null) {
    // 如果 updateQueue 为空,那就创建一个新的 updateQueue,其中 lastEffect 指向最新添加进来的 effect
    componentUpdateQueue = createFunctionComponentUpdateQueue();
    currentlyRenderingFiber.updateQueue = (componentUpdateQueue: any);
    componentUpdateQueue.lastEffect = effect.next = effect;
  } else {
    const lastEffect = componentUpdateQueue.lastEffect;
    if (lastEffect === null) {
      // 假如 lastEffect 指向 null,说明此时链表还不是循环链表的结构,那么就要控制最新的 effect 的 next 的指向,使其变为循环链表的结构 
      componentUpdateQueue.lastEffect = effect.next = effect;
    } else {
      // 将当前 effect 添加到 updateQueue 中
      const firstEffect = lastEffect.next;
      lastEffect.next = effect;
      effect.next = firstEffect;
      // 令 lastEffect 始终指向最新添加进来的 effect
      componentUpdateQueue.lastEffect = effect;
    }
  }
  return effect;
}

其實這個 pushEffect 邏輯還算比較清晰。pushEffect 所做的事情其實就是會依次獲取當前 Hook 節點以及 useEffect 的依賴,並調用 pushEffect 將當前 effect 添加到 FiberNodeupdateQueue 隊列中,最後將 effect 保存在當前 Hook 節點的 memoizedState 屬性中。

可以看到,react 在處理 useEffect 時底層的思想也是應用到了單向的循環鏈表。重要的幾個參數,createdestroydeps 其實就是我們使用的 useEffect 時的 effect 函數,銷毀函數和 effect 依賴。

useEffect(() => {
  // create

  return () => {
    // destroy
  }
}, [ /* deps */ ]) 

那這邊先小結一下,所以 useEffect 在 mount 階段的 mountEffect 所做的事如下:

  1. 獲取當前 Hook 節點,並把它添加到 Hook 鏈表中;

  2. 獲取本次 effect 的 deps 依賴;

  3. effect 添加到 fiberNodeupdateQueue 中。updateQueuelastEffect 屬性指向的始終是最新添加進隊列的 effect,lastEffect 的 next 始終指向最早添加進來的 effect,以次形成一個循環鏈表的結構。

updateEffect

看完 mountEffect 當然就再來看看 updateEffect 啦~

function updateEffect(
  create: () => (() => void) | void,
  deps: Array<mixed> | void | null,
): void {
  if (__DEV__) {
    // $FlowExpectedError - jest isn't a global, and isn't recognized outside of tests
    if ('undefined' !== typeof jest) {
      warnIfNotCurrentlyActingEffectsInDEV(currentlyRenderingFiber);
    }
  }
  return updateEffectImpl(PassiveEffect, HookPassive, create, deps);
}

不用多說,繼續往下找找 updateEffectImpl 函數的源碼:

function updateEffectImpl(fiberFlags, hookFlags, create, deps): void {
  // 获取当前 Hook 节点,并把它添加到 Hook 链表中
  const hook = updateWorkInProgressHook();

  // 获取依赖
  const nextDeps = deps === undefined ? null : deps;
  // 初始化清除 effect 函数
  let destroy = undefined;

  if (currentHook !== null) {
    // 获取上一次渲染的 Hook 节点的 effect
    const prevEffect = currentHook.memoizedState;

    // 获取上一次渲染的 Hook 节点的 effect 的清除函数
    destroy = prevEffect.destroy;
    if (nextDeps !== null) {
      // 获取上一次渲染的 Hook 节点的 effect 的依赖
      const prevDeps = prevEffect.deps;

      // 对比前后依赖的值是否相同
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        // 如果依赖的值相同,即依赖没有变化,那么只会给这个 effect 打上一个 HookPassive tag,然后在组件渲染完以后会跳过这个 effect 的执行
        hook.memoizedState = pushEffect(hookFlags, create, destroy, nextDeps);
        return;
      }
    }
  }

  currentlyRenderingFiber.flags |= fiberFlags;

  // pushEffect 的作用是将当前 effect 添加到 FiberNode 的 updateQueue 中,然后返回这个当前 effect
  // 然后是把返回的当前 effect 保存到 Hook 节点的 memoizedState 属性中
  hook.memoizedState = pushEffect(
    HookHasEffect | hookFlags,
    create,
    destroy,
    nextDeps,
  );
}

update 階段的 useEffect 和 mount 階段所做的事情基本相似,唯獨不一樣就是 update 階段會考慮 effect 的依賴是否有變化,如果沒有變化,那麽就只會給這次 effect 打上 HookPassive tag,在最後 commit 階段(組件視圖渲染完成後)會跳過 effect 的執行;如果有依賴有變化,那麽就會給這次 effect 打上 HookPassiveHookHasEffect 兩個 tag,表示這個 effect 將會在組件視圖渲染完成後執行。

小結一下,update 階段的 useEffect 做了一下事情:

  1. 獲取當前 Hook 節點,並把它添加到 Hook 鏈表中;

  2. 獲取本次 effect 的 deps 依賴;

  3. 拿本次 effect 的 依賴和上一次渲染時的 effect 的依賴做對比:

  • 假如沒有依賴沒有發生改變,那麽就只給這次 effect 打上 HookPassive 的 tag,在 commit 階段(組件視圖渲染完成後),跳過這一次 effect 的執行;

  • 假如依賴有發生改變,那麽就會給這次 effect 打上 HookPassiveHookHasEffect 兩個 tag,在 commit 階段(組件視圖渲染完成後),執行這一次 effect;

  1. 將本次 effect 添加到 fiberNodeupdateQueue 中,並將本次 effect 保存在當前 Hook 節點的 memoizedState 屬性中

useEffect 運行流程

一個使用 useEffect Hook 的函數組件,在運行的時候的運行流程如下:

組件初次渲染(mount)

  1. 執行 useEffect 時,將 useEffect Hook 添加到 Hook 鏈表中,然後創建 fiberNodeupdateQueue,並把本次 effect 添加到 updateQueue 中;

  2. 渲染組件的 UI;

  3. 完成 UI 渲染後,執行本次 effect;

組件重新渲染(update)

  1. 執行 useEffect 時,將 useEffect Hook 添加到 Hook 鏈表中,判斷依賴:
  • 假如沒有傳入依賴(useEffect 沒有傳入第二個參數),那麽直接給這個 effect 打上 “需要執行” 的 tag(HookHasEffect);

  • 假如有傳入依賴 deps 並且當前依賴和上次渲染時的依賴對比有發生改變,那麽就給這個 effect 打上 “需要執行” 的 tag(HookHasEffect);

  • 假如有傳入依賴 deps,但是依賴沒有發生改變,則不會給這個 effect “需要執行” 的 tag;

  • 假如有傳入依賴 deps,但是傳入的是一個空數組 [],那麽也不會給這個 effect “需要執行” 的 tag;

  1. 渲染組件的 UI;

  2. 假如有清除函數(effect 中的 return 內容),則執行上一次渲染的清除函數;如果依賴是 [],則先不用執行清除函數,而是等到組件銷毀時才執行;

  3. 判斷本次 effect 是否有“需要執行” 的 tag(HookHasEffect),如果有,就執行本次 effect;如果沒有,就直接跳過,不執行本次 effect;

組件銷毀時

在組件銷毀之前,先執行完組件上次渲染時的清除函數。

關於 useEffect 源碼研究的部分,這邊還留有一個疑問:就是並沒有在源碼中看到對於 destroy 清除函數的判斷邏輯。我個人的猜測是 react 會在 commit 階段也就是 UI 渲染完後對這部分做邏輯處理,但純屬個人猜測。關於 react Hooks 這部分的學習其實還有一個部分在這篇中不打算再提了,可能會在別篇源碼研究中總結,就是關於 react 的幾個階段,begincommitcompleteunwind。了解這些階段的處理應該可以更進一步的理解 react 對 Hooks 的一些做法和時機點,鑑於篇幅實在過長,這邊就暫時不寫了。

結語

這篇因為其實在實習之前就開始想寫,也陸陸續續開始,但是之後因為實習還有一些別的事所以擱置了好長一段時間,終於在不知道多久之後完成了這篇。這篇關於 react hooks 的源碼學習也是寫的有夠長,真的是蠻複雜得,且即使花了這麼多時間也才看了 useStateuseEffect 兩個最基本的 hook,而且其實有一些部分還是沒有完全理解,只能說是粗淺的入門了一下。不過總是要有這麼一段過程的,這篇也真的寫得太累了,關於 react 源碼的學習陸續也都繼續更新,先撤了~

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

React 源碼解析 - Hooks 系列 的相关文章

  • 为什么 React 组件在 props 没有改变的情况下会重新渲染?

    我在 ReactJS 16 8 5 和 React Redux 3 7 2 上构建了一个应用程序 当应用程序加载应用程序安装时 将设置初始存储并针对 Firebase 实时数据库设置数据库订阅 该应用程序包含侧边栏 标题和内容部分 通过使用
  • React 和 ES6 继承

    Note 这篇文章是在 React 不支持 ES6 v12 时发布的 我有一个 ES6 课程 class BaseClass getInitialState return message Hello render return div di
  • 反应:无效的挂钩调用。钩子只能在函数组件体内调用

    我花了两天时间试图了解错误在哪里 知道吗 App js import React useState useEffect from react import Menu from components Menu function App ret
  • React.js 的状态基于其他状态

    我遇到了 React js 的一些问题 并且在调用 setState 时状态没有立即设置 我不确定是否有更好的方法来解决这个问题 或者这是否真的只是 React 的一个缺点 我有两个状态变量 其中一个基于另一个 原始问题的小提琴 http
  • 如何正确使用Formik的setError方法? (反应库)

    我正在使用 React 与后端进行通信 现在尝试正确实现 Formik 表单库 主要问题 如何正确使用Formik的setError方法 客户端验证错误显示正确 但现在我尝试设置 显示后端验证错误 这些错误通过状态代码 400 的响应返回
  • 使用应用程序文件夹中的 Next.js 读取 URL 动态参数/slug

    我在获取动态路由中的参数时遇到问题 我在阅读时不断收到错误 类型错误 无法解构 router query 的属性 themenID 因为它未定义 另外 我无法使用next router但必须集成路由器next navigation 但这个没
  • 严格模式与 React 18 的工作方式是否不同?

    考虑下面的片段 有了 React 18 count每次渲染时都会在控制台上打印两次 但在 React 17 中它只打印一次 反应 18 示例 function App const count setCount React useState
  • eslint/prettier 在 html 标签中添加不需要的空间

    我正在尝试向我的项目添加配置 以便代码自动格式化 我对 VS Code 中的用户设置进行了一些更新 还安装了 eslint 和 prettier 现在 每当我尝试保存我的代码时 都会从中得到改变 div div div div div di
  • 使用 Apollo-Client 对 GitHub API v4 进行身份验证

    与之前的版本一样 GitHub 的新 GraphQL API 需要使用令牌进行身份验证 那么 我们如何添加一个 Header 信息到HttpLink里面Apollo Client const client new ApolloClient
  • 设置时区 React-Datepicker

    我正在使用反应日期选择器 https www npmjs com package react datepicker让用户选择一个日期 但是 现在它使用本地时间 PDT 但我想对其进行硬编码以使用特定时区 PST 我尝试使用utcOffset
  • 如何为 NextAuth.js 创建自定义 UI(使用 AWS Cognito)?

    是否可以为 AWS Cognito 提供自定义 UINextAuth js https next auth js org 当我使用 NextAuth js 调用 API 时 我看到这个 主观上丑陋的 屏幕 只有一个按钮可以将您重定向到真正的
  • 样式组件如何影响性能?

    使用样式组件是否比样式表更会降低 Web 应用程序的速度 如果我关心性能并且没有任何依赖于 props 的样式 我是否应该放弃样式组件并使用样式表 当您有很多小组件时 同时使用样式化组件渲染 性能开销可能会很有意义 绝对值得测试以删除小元素
  • 获取React组件的类型propTypes定义

    假设有以下代码 TestComponent propTypes text React PropTypes string myEnum React PropTypes oneOf News Photos 我在另一个文件中执行了以下操作 使用
  • 具有两组子组件的 React 组件

    我正在创建一个组件 需要接收两组子组件并放置在组件的两个不同部分中 let CreditCardForm icons fields gt div div div div icons div div div fields div let Cr
  • Menu.Item 中的 Antd 自动换行

    可以在 Menu Item 组件中应用自动换行吗 示例不起作用
  • Nextjs 无法在生产环境中的“.next”目录中找到有效的构建

    我正在 docker 中运行我的应用程序 但我的生产构建和启动脚本仅在 docker 环境中失败 虽然node env开发在docker环境下运行良好 这是我的脚本 无法进行生产构建并启动服务器 我正在使用nodemon和babel bui
  • 使用 nginx 在云上部署 django 和 React

    我有一个 digitalocean 服务器 并且已经使用 Gunicorn 和 nginx 部署了 Django 后端服务器 如何在同一台服务器上部署 React 应用程序 您可以构建 React 应用程序并使用 Nginx 提供其静态文件
  • 从回调中访问状态

    我在从回调访问组件状态时遇到问题 国家的价值num更改正确 但此类更改对于加载时定义的回调函数不可见 import React useState from react class MyObject callback gt void cons
  • React autoFocus 将光标设置为输入值的开头

    我有一个受控输入 最初显示一个值 我已将该输入设置为自动聚焦 但当我希望它出现在末尾时 光标出现在输入的开头 我知道这可能是因为自动对焦是在值之前添加的 但我不能 100 确定 在输入字段末尾完成光标初始化的最佳方法是什么 var Test
  • 创建反应应用程序:npm 错误!解析“...”附近时 JSON 输入意外结束

    描述错误 npx create react app my app不适合我 我试过npm cache clean force但我一次又一次地遇到同样的错误 重现步骤 npm cache clean force npm npx create r

随机推荐

  • 编程中,有哪些好的习惯一开始就值得坚持?

    嗨 小伙伴们大家好 我是沉默王二 就是那个吹自己既有颜值又有才华的家伙 今天这个话题我觉得还是值得和大家探讨的 因为好的编程习惯对程序员的职业生涯真的非常重要 记得我在学驾照的时候 遇到一哥们 之前开过车 属于无证驾驶的那种 但是 这哥们科
  • 一键部署WeBase-FISCO-BCOS联盟链管理平台

    文章目录 一键部署 前置条件 实际使用环境 一 环境安装 ubuntu18 04 1 Java安装及环境变量配置 2 Mysql的安装及配置 3 python配置 PyMySQL部署 Python3 4 二 拉取webase部署脚本 修改w
  • PageHelper 分页查询「实现篇」

    两种情况 分两种情况 查出来为 PageQueryRespDTO 查出来直接是 Model 查出来为 PageQueryRespDTO public PageInfo
  • WS-Discovery(网络服务动态发现)协议再解读

    WS Discovery 网络服务动态发现 协议再解读 文章目录 WS Discovery 网络服务动态发现 协议再解读 1 概述 2 协议资料 3 协议介绍 1 适用范围 2 不适用 4 协议示例及wireshark抓包分析 1 客户端探
  • html想相关概念

    HTML Hypertext Markup Language 是一种用于创建网页的标记语言 它为开发者提供了一种结构化的方式来描述网页的内容和外观 本文将介绍HTML的基础知识和关键点 让读者了解如何使用HTML来构建现代网页 HTML的基
  • 将日志打印的SQL组装为可执行SQL_sublime版

    java项目日志打印的sql和条件参数是分离的 使用起来很不方便 之前为了解决这种问题 使用notepad 录制宏解决了这个问题 但是后期不方便维护 遂转用sublime 这个跟notepad 比起来有些东西不是很习惯 但是好处是插件可以自
  • 关于“网页埋点”的简单介绍与实现

    网页埋点的简单实现 本文抛开数据增长方法和埋点技术原理的大道理 通过一个简单的埋点小实例帮助大家更好的了解埋点 一 什么是数据埋点 埋点是网站和APP等产品进行日常改进及数据分析的数据采集基础 根据采集得到的用户行为数据 例如 页面访问路径
  • React源码分析(二)=> Reac初次渲染分析

    文章目录 1 render阶段 legacyRenderSubtreeIntoContainer 1 1 legacyCreateRootFromDOMContainer 1 1 1 ReactRoot 1 1 1 1 createFibe
  • [CM311-1A]-买了一个机顶盒准备刷成 Linux 盒子!

    目录 移动魔百盒 CM311 1A CM311 1A 配置 烧录系统盘将机顶盒刷成 Armbian 服务器教程发布啦 为什么非要买个盒子 拆后介绍 使用介绍 启动盒子 机顶盒怎么截屏 键盘鼠标操作 系统应用 系统设置 网络设置 空间管理 分
  • axios vue 加载效果动画_Vue 使用 axios 或者 vue-router 配置页面加载动画

    先提一笔 这篇文章是关于 Vue 页面加载动画的实现 以下介绍两种方法 使用 axios 配置全局中当发生 ajax 请求时 显示一个加载动画 当 ajax 请求结束后 隐藏加载动画 使用 Vue router 实现当页面跳转时有一个加载动
  • 图像I、B、P帧介绍

    I p b 帧 I帧 帧内编码帧 尽可能去除图像空间冗余信息来压缩传输数据量的帧内编码图像 P帧 前向预测编码帧 通过充分将低于图像序列中前面已编码帧的时间冗余信息来压缩传输数据量的编码图像 也叫预测帧 B帧 双向预测内插编码帧 既考虑与源
  • “区块链+物联网”的一场“网链”能顺利奔现吗?

    前不久 物联网安全高峰论坛在京举行 相关行业的专家在探讨物联网安全问题时不可避免地谈及区块链技术在物联网安全方面的独特价值 有专家指出 目前全球年产近百亿的智能终端控制芯片 真正能投入联网的还不足1 而阻碍物联网进一步发展融入大众生活中的
  • 使用Red Hat Insights注册RedHat系统

    文章目录 前因 Step 1 确认所选择的系统 Step 2 将系统注册到Red Hat Insights Step 3 具体操作演示 前因 使用SSH命令远程连接红帽系统 提示需要使用下面提示的命令进行系统注册订阅 C Users xyb
  • 【YOLOv7调整detect.py】1.调整检测框粗细,2.设定标签颜色,3.只显示与标签数目相同的检测结果

    目录 1 调整检测框粗细 2 设定标签颜色 3 只显示与标签数目相同的检测结果 1 调整检测框粗细 在detect py中按住Ctrl F检索line thickness定位过去 在129行左右 更改line thickness的大小即可
  • 启动nginx报错找不到 libssl.so.1.1

    项目场景 centOS7安装openresty后启动nginx报错 nginx error while loading shared libraries libssl so 1 1 cannot open shared object fil
  • python 存根_pyi文件是干吗的?(一文读懂Python的存根文件和类型检查)

    参考资料 html 写这篇文章的缘由是我在网上下载的项目中看到了 pyi文件 可是寻找了不少资料才对这些个概念 存根文件 类型检查 有了一个直观的印象 这篇随笔的目的是用最短的篇幅让你理解这些概念 测试 首先我介绍几个论断 this 1 P
  • React composing components

    bottom up div div
  • PyTorch源码安装小记

    Torch是Facebook AIR去年6月开源的深度学习框架 向G家的Tensorflow看起 基于Lua开发 可以充分利用GPU进行计算加速 PyTorch是FAIR今年1月发布的 在原来基础上提供python接口支持 也是正面刚支持p
  • 基于深度图与RGB图-->点云数据

    在这里记录一下 以备后续查阅 输入 对齐的深度图与RGB图 输出 点云数据 include
  • React 源碼解析 - Hooks 系列

    React 源碼解析 Hooks 系列 前言 正文 調用 React Hook 到底背後發生了什麼 初探 Hooks 源碼 Dispatcher useState Hook Hook 的實現 Hook 在 Mount 階段 useState