手写React Hooks并逐行分析

前言

哈喽大家好,我是Lotzinfly ,一位前端小猎人 。欢迎大家来到前端丛林 ,在这里你将会遇到各种各样的前端猎物 ,我希望可以把这些前端猎物统统拿下,嚼碎了服用 ,并成为自己身上的骨肉 。当我们学习React 的时候,React Hooks 是重中之重。学会React Hooks 将大大提高我们的开发效率,所以一定要掌握好React Hooks 。这篇文章让我们手写React Hooks 并逐行分析,最近秋招就要来了,学会手写React Hooks 及其原理就可以大胆和面试官畅所欲言和自信谈薪啦!😏😏😏 击败React Hooks嚼碎了服用将会给我们带来大量经验,你们准备好了吗?话不多说,现在开启我们今天的前端丛林冒险之旅吧!

ps:由于篇幅原因,本篇文章不再介绍React Hooks 的基本用法,默认读者已了解或掌握React Hooks ,若对React Hooks不了解可翻阅相关文章进行学习后再阅读本文章。

useState

js 复制代码
let lastIndex = 0;   //记录当前下标
let lastStates = []; //记录 states

function useState(initValue) {
    //1、记录当前 state 的下标(这里使用了闭包的原理)
    const currentIndex = lastIndex;
    
    //2、记录初始值
    lastStates[currentIndex] = lastStates[currentIndex] ?? initValue;
    
    //3、setState 回调修改 state
    function setState(newValue) {
        lastStates[currentIndex] = newValue
        //修改后,重新render
        render()
    }
    
    //4、返回数组
    return [lastStates[lastIndex++], setState]
}

这段代码是一个简化版的自定义状态管理函数 useState,类似于 React 中的 useState hook。让我们逐行解析它:

  1. lastIndexlastStates 变量被声明并初始化为 0 和一个空数组,分别用于记录当前下标和所有的状态值。

  2. useState 函数被定义,并接受初始值 initValue 作为参数。

  3. currentIndex 变量通过闭包捕获了 lastIndex 的值,即记录当前状态的下标。

  4. 在第一次调用 useState 时,会检查对应下标的 lastStates 是否已经存在。如果不存在(即为 undefined),则将其初始化为 initValue

  5. setState 函数用于更新状态值 lastStates[currentIndex]。它接受一个新的值 newValue 作为参数,并将其赋值给对应下标的 lastStates

  6. 在调用 setState 后,通常会触发重新渲染(此处没有给出具体实现),以便在界面上反映最新的状态值。

  7. 最后,useState 函数返回一个包含两个元素的数组,第一个元素是当前状态值(lastStates[lastIndex++]),第二个元素是更新状态的回调函数 setState

对于使用该 useState 函数的组件,在每次状态更新后会触发重新渲染,以在界面上显示最新的状态值。

useCallback

js 复制代码
let lastCallback;
let lastDependencies;

function useCallback(callback, dependencies) {
    //1、判断是否传入了依赖项
    if (dependencies) {
        const isChange = !dependencies.every((item, index) => {
            return item === lastDependencies[index]
        })
        //如果依赖项改变
        if (isChange) {
            lastCallback = callback; //更新记忆值函数
            lastDependencies = dependencies;  //记录最新的依赖项
        }
    } else {
        //没传入依赖项,可以当成是第一次调用 useCallback
        lastCallback = callback;
        lastDependencies = dependencies;
    }
    
    //返回记忆值函数
    return lastCallback
}

这段代码是一个简化版的自定义 hook useCallback,类似于 React 中的 useCallback hook。下面逐行解析它:

  1. lastCallbacklastDependencies 变量被声明,用于存储上一次记忆的回调函数和依赖项。

  2. useCallback 函数被定义,并接受两个参数:callback(要记忆的回调函数)和 dependencies(可选的依赖项数组)。

  3. 首先判断是否传入了依赖项 dependencies

  4. 如果传入了依赖项,则通过比较每个依赖项的值与上一次的依赖项值,判断依赖项是否发生变化。使用 every 方法遍历 dependencies 数组,并将每个依赖项与对应位置的 lastDependencies[index] 进行比较。如果有任何一个依赖项不相等,表示依赖项发生了变化,将 isChange 设置为 true

  5. 如果依赖项发生了变化(即 isChangetrue),则更新 lastCallback 为新的回调函数 callback,并将最新的依赖项数组赋值给 lastDependencies

  6. 如果没有传入依赖项,说明可以将其当做第一次调用 useCallback,此时直接将 lastCallback 更新为传入的回调函数 callback,并将 lastDependencies 设置为 undefined

  7. 最后,无论是否传入了依赖项,都会返回记忆的回调函数 lastCallback

这段代码的作用是在每次调用 useCallback 时检查依赖项数组是否发生变化。如果依赖项发生变化,则更新记忆的回调函数和依赖项,以便在使用该回调函数时避免不必要的重新创建,提高性能。

useMemo

下面是一个简化版的自定义 hook useMemo,类似于 React 中的 useMemo hook:

javascript 复制代码
let lastResult;
let lastDependencies;

function useMemo(callback, dependencies) {
    //判断是否传入了依赖项 `dependencies`
    if (dependencies) {
        const isChange = !dependencies.every((item, index) => item === lastDependencies[index]);
        
        if (isChange) {
            lastResult = callback();
            lastDependencies = dependencies;
        }
    } else {
        lastResult = callback();
        lastDependencies = dependencies;
    }
    //无论是否传入了依赖项,都会返回记忆的结果值 `lastResult`
    return lastResult;
}

这段代码与前面介绍的 useCallback 非常相似,但是返回的不是记忆的函数,而是记忆的结果值。下面对代码进行解析:

  1. lastResultlastDependencies 变量用于存储上一次记忆的结果和依赖项。

  2. useMemo 函数被定义,并接受两个参数:callback(要记忆的函数)和 dependencies(可选的依赖项数组)。

  3. 首先判断是否传入了依赖项 dependencies

  4. 如果传入了依赖项,则通过比较每个依赖项的值与上一次的依赖项值,判断依赖项是否发生变化。使用 every 方法遍历 dependencies 数组,并将每个依赖项与对应位置的 lastDependencies[index] 进行比较。如果有任何一个依赖项不相等,表示依赖项发生了变化,将 isChange 设置为 true

  5. 如果依赖项发生了变化(即 isChangetrue),则调用 callback 函数以获取新的结果,并将其赋值给 lastResult

  6. 如果没有传入依赖项,说明可以将其当做第一次调用 useMemo,此时直接调用 callback 函数以获取初始结果,并将其赋值给 lastResult

  7. 最后,无论是否传入了依赖项,都会返回记忆的结果值 lastResult

这段代码的作用是在每次调用 useMemo 时检查依赖项数组是否发生变化。如果依赖项发生变化,则重新计算 callback 的结果,并使用记忆的结果值。这有助于避免在使用该结果值的地方重复执行昂贵的计算操作,提高性能。

useReducer

js 复制代码
let lastIndex = 0;
let lastStates = [];

function useReducer(reducer, initState) {
    //1、记录当前 state 下标
    const currentIndex = lastIndex;
    //2、记录初始值
    lastStates[currentIndex] = lastStates[currentIndex] ?? initState;
    
    //3、定义 dispatch
    function dispatch(action) {
        // reducer 处理 state,返回处理后的结果
        lastStates[currentIndex] = reducer(lastStates[currentIndex], action);
        // 重新渲染
        render();
    }
    
    return [lastStates[lastIndex++], dispatch]
}

这段代码是一个简化版的自定义 hook useReducer,类似于 React 中的 useReducer hook。下面逐行解析这段代码:

  1. lastIndexlastStates 变量用于记录当前的状态下标和所有的状态值。

  2. useReducer 函数被定义,并接受两个参数:reducer(处理状态的纯函数)和 initState(初始状态值)。

  3. currentIndex 变量通过闭包捕获了 lastIndex 的值,即记录当前状态的下标。

  4. 在第一次调用 useReducer 时,会检查对应下标的 lastStates 是否已经存在。如果不存在(即为 undefined),则将其初始化为 initState

  5. dispatch 函数用于更新状态值 lastStates[currentIndex]。它接受一个动作对象 action 作为参数,并将使用 reducer 函数处理当前状态值和动作对象,得到新的状态值。

  6. 更新状态值后,通常会触发重新渲染(此处没有给出具体实现),以便在界面上反映最新的状态值。

  7. 最后,useReducer 函数返回一个包含两个元素的数组,第一个元素是当前状态值(lastStates[lastIndex++]),第二个元素是可以触发状态更新的 dispatch 函数。

这段代码的作用是在每次调用 useReducer 时,对状态进行管理、更新和重新渲染。它通过记录状态下标和使用闭包来实现,同时使用纯函数 reducer 来处理状态更新逻辑。返回的数组第一个元素是当前状态值,第二个元素是接受动作对象并触发状态更新的 dispatch 函数。

useContext

js 复制代码
let contextValues = {};

function useContext(context) {
    if (!contextValues[context]) {
        throw new Error(`Cannot find context value for ${context}. Make sure the context provider is properly set.`);
    }
    
    return contextValues[context];
}

这段代码是一个简化版的自定义 hook useContext,类似于 React 中的 useContext hook。这段代码相比之前的示例要简单得多。下面对代码进行分析:

  1. contextValues 变量用来存储不同上下文的值,它是一个对象。

  2. useContext 函数被定义,并接受一个参数 context(上下文对象)。

  3. 首先判断 contextValues 对象中是否存在给定的上下文 context 的值。如果不存在,说明上下文提供者没有正确设置或没有在当前组件的父组件中传递该上下文的值。

  4. 如果找不到上下文值,则抛出一个错误,提示需要确保上下文提供者的正确设置。

  5. 如果能找到上下文值,则将其返回。

这段代码的作用是获取指定上下文的值。它通过检查 contextValues 对象是否包含给定上下文的值来实现。如果找到了上下文值,则将其返回;如果找不到,则抛出错误。这可以帮助开发人员在使用上下文时及早发现潜在的问题,例如缺少上下文提供者或传递了错误的上下文对象。

useEffect

下面是一个简化版的自定义 hook useEffect,类似于 React 中的 useEffect hook:

javascript 复制代码
const effectCallbacks = [];

function useEffect(callback, dependencies) {
    // 将回调函数和依赖项封装成对象
    const effect = { callback, dependencies };
    
    // 将封装后的对象添加到 effectCallbacks 数组中
    effectCallbacks.push(effect);
}

function runEffects() {
    effectCallbacks.forEach(effect => {
        // 首次运行或者依赖项改变时调用回调函数
        if (!effect.dependencies || effect.dependencies.some((dep, index) => dep !== effect.prevDependencies[index])) {
            effect.callback();
        }
        
        // 更新保存的依赖项
        effect.prevDependencies = effect.dependencies;
    });
}

// 在适当的时机调用 runEffects 函数,比如在渲染完成后调用
function render() {
    // 渲染逻辑...
    
    // 运行所有的副作用回调函数
    runEffects();
}

这段代码相对复杂一些,所以我会逐步解析:

  1. effectCallbacks 变量是一个数组,用于存储所有需要执行的副作用回调函数。

  2. useEffect 函数被定义,并接受两个参数:callback(副作用的回调函数)和 dependencies(可选的依赖项数组)。

  3. useEffect 函数中,首先将 callbackdependencies 封装成一个对象 effect

  4. 然后将封装后的 effect 对象添加到 effectCallbacks 数组中,以便在适当的时机执行。

  5. runEffects 函数用于遍历 effectCallbacks 数组,并根据依赖项的变化来触发需要执行的副作用回调函数。

  6. 在遍历过程中,首先判断当前的依赖项是否存在,如果没有依赖项或者其中任何一个依赖项与前一次保存的依赖项不相等,表示依赖项已经发生了变化,需要调用回调函数。

  7. 执行完毕后,更新保存的依赖项为当前的依赖项值,以便在下次比较时使用。

  8. 最后,render 函数示例了渲染流程,并在适当的时机调用 runEffects 函数来运行所有的副作用回调函数。

这段代码的作用是模拟 React 的 useEffect 钩子,用于处理副作用操作和依赖项变化。通过将副作用回调函数和依赖项封装成对象,并存储在数组中,在适当的时机遍历数组并检查依赖项的变化,从而调用相应的副作用回调函数。这有助于在合适的时机执行副作用逻辑,避免重复执行或缺少执行。

useRef

下面是一个简化版的自定义 hook useRef,类似于 React 中的 useRef hook:

javascript 复制代码
let refValue;

function useRef(initialValue) {
    if (refValue === undefined) {
        refValue = { current: initialValue };
    }
    
    return refValue;
}

这段代码相当简单,下面对其进行解析:

  1. refValue 变量用于存储引用对象的值。

  2. useRef 函数被定义,并接受一个参数 initialValue(引用对象的初始值)。

  3. 首先检查 refValue 是否为 undefined。如果是,表示引用对象还未初始化。

  4. 在首次调用 useRef 时,将 initialValue 封装在带有 current 属性的对象中,并赋值给 refValue

  5. 如果已经存在初始值,说明引用对象已经初始化过了,直接返回保存的 refValue

  6. useRef 返回一个包含 current 属性的对象,属性值为传入的 initialValue(或者在首次调用后已经保存的初始值)。

这段代码的作用是创建并获取一个可以保存任意值的引用对象。它通过检查 refValue 的状态来确定是否需要初始化引用对象。在首次调用 useRef 时,会将初始值封装在对象中并保存在 refValue 中,后续调用则一直返回保存的引用对象。这可以帮助在组件多次渲染时,始终使用同一个引用对象来存储数据,并且保留了对该对象的可变性。

useImperativeHandle

下面是一个简化版的自定义 hook useImperativeHandle,类似于 React 中的 useImperativeHandle hook:

javascript 复制代码
function useImperativeHandle(ref, createHandle) {
    if (typeof ref === 'object' && ref !== null && typeof ref.current === 'object') {
        createHandle(ref.current);
    } else {
        console.warn("A ref object with a 'current' property is required for useImperativeHandle.");
    }
}

这段代码非常简短,下面对其进行解析:

  1. useImperativeHandle 函数被定义,并接受两个参数:ref(ref 对象)和 createHandle(创建 handle 的回调函数)。

  2. 首先检查传入的 ref 是否是一个对象且不为 null,并且检查 ref.current 属性是否存在且是一个对象。这样做是为了确保传递的 ref 符合要求。

  3. 如果 ref 符合要求,调用 createHandle 回调函数,并将 ref.current 作为参数传递给它。

  4. 如果 ref 不符合要求,会发出警告提示需要传递一个带有 current 属性的 ref 对象。

这段代码的作用是允许组件暴露特定的方法或属性给父组件,通过使用 ref 来引用子组件实例,并通过回调函数 createHandle 创建 handle。它会在确认传入的 ref 符合要求后,将 ref.current(子组件实例)传递给 createHandle。这样可以在组件之间进行方法或属性的传递和交互。如果 ref 不符合要求,则会发出警告提示开发者传递正确的 ref 对象。

useSyncExternalStore

下面是一个简化版的自定义 hook useSyncExternalStore,用于同步外部存储,同时我会对代码进行分析:

javascript 复制代码
function useSyncExternalStore(externalStore, updateCallback) {
    useEffect(() => {
        updateCallback(externalStore.getState());

        const unsubscribe = externalStore.subscribe(() => {
            updateCallback(externalStore.getState());
        });

        return () => {
            unsubscribe();
        };
    }, [externalStore, updateCallback]);
}

这段代码相对较长,但基本原理与前面示例相似。下面对其进行逐行解析:

  1. useSyncExternalStore 函数被定义,并接受两个参数:externalStore(外部存储)和 updateCallback(更新回调函数)。

  2. 在函数内部使用了 useEffect hook 来实现副作用的处理。

  3. useEffect 的回调函数中,首先通过调用 updateCallback(externalStore.getState()) 来将外部存储的当前状态传递给更新回调函数初始化组件的状态。

  4. 接下来,通过调用 externalStore.subscribe() 方法订阅外部存储的变化。当外部存储发生变化时,会触发订阅的回调函数,即 () => { updateCallback(externalStore.getState()); },用于更新组件中的状态。

  5. 最后,返回一个清除函数,在组件卸载时取消订阅,以避免内存泄漏。

  6. useEffect 的依赖项数组包含 externalStoreupdateCallback,确保在这两个值发生变化时重新运行副作用。

这段代码的作用是在组件中实现与外部存储的同步,即将外部存储的状态传递给组件的初始状态,并在外部存储发生变化时更新组件的状态。它通过 useEffect 来订阅外部存储的变化,并提供一个更新回调函数来处理状态的更新。这样可以确保组件始终与外部存储保持同步,并在外部存储发生变化时及时更新显示正确的数据。

useTransition

下面是一个简化版的自定义 hook useTransition,用于处理过渡效果的控制,同时对代码进行分析:

javascript 复制代码
function useTransition() {
    const [isShowing, setIsShowing] = useState(false);

    function startTransition() {
        setIsShowing(true);
    }

    function endTransition() {
        setIsShowing(false);
    }

    return {
        isShowing,
        startTransition,
        endTransition
    };
}

让我们逐行解析这段代码:

  1. useTransition 函数被定义。

  2. 在函数内部,使用 useState hook 定义了一个状态变量 isShowing 以及对应的更新函数 setIsShowing,初始值为 false

  3. startTransition 函数定义了启动过渡的操作,将 isShowing 设置为 true

  4. endTransition 函数定义了结束过渡的操作,将 isShowing 设置为 false

  5. 最后,通过返回一个对象字面量,将状态和过渡相关的函数暴露给外部使用。

  6. 返回的对象有三个属性:isShowing(表示过渡是否正在进行中的状态)、startTransition(启动过渡的函数)和 endTransition(结束过渡的函数)。

这段代码的作用是提供一个简单的 API 来控制组件的过渡效果。通过使用 useState 来管理一个布尔类型的状态 isShowing,来表示过渡是否正在进行中。使用 startTransition 函数来将 isShowing 设置为 true,从而开始过渡效果。使用 endTransition 函数来将 isShowing 设置为 false,表示过渡结束。这样,可以通过监听 isShowing 的状态改变来触发相应的过渡动画或渲染逻辑。

useDeferredValue

下面是一个简化版的自定义 hook useDeferredValue,用于延迟获取某个值,并对代码进行分析:

javascript 复制代码
function useDeferredValue(value, timeout) {
    const [deferredValue, setDeferredValue] = useState(value);

    useEffect(() => {
        const timeoutId = setTimeout(() => {
            setDeferredValue(value);
        }, timeout);

        return () => {
            clearTimeout(timeoutId);
        };
    }, [value, timeout]);

    return deferredValue;
}

让我们逐行解析这段代码:

  1. useDeferredValue 函数被定义,并接受两个参数:value(需要延迟的值)和 timeout(延迟时间)。

  2. 在函数内部,使用 useState hook 定义了一个状态变量 deferredValue 以及对应的更新函数 setDeferredValue,初始值为传入的 value

  3. 使用 useEffect hook 来处理副作用的逻辑。

  4. useEffect 的回调函数中,通过调用 setTimeout 来延迟一定时间后将 value 设置为 deferredValue

  5. setDeferredValue 会在经过 timeout 时间后更新 deferredValue 的值。

  6. 返回的 useDeferredValue hook 非常简单,只返回了 deferredValue,即延迟后的值。

  7. useEffect 的依赖项数组包含 valuetimeout,确保在这两个值发生变化时重新运行副作用。

这段代码的作用是提供了一种延迟获取值的方式。它使用 useState 来管理一个状态变量 deferredValue,初始值为传入的 value。然后,通过使用 useEffect 来设置一个定时器,在经过指定的 timeout 时间后,将 value 的值更新到 deferredValue。这样,可以在组件中使用 useDeferredValue 获取延迟后的值,并根据需要进行相应的渲染或逻辑操作。

useId

下面是一个简化版的自定义 hook useId,用于生成唯一的标识符,并对代码进行分析:

javascript 复制代码
let id = 0;

function useId() {
    const [currentId] = useState(() => ++id);
    return currentId;
}

现在让我们逐行解析这段代码:

  1. 首先,定义了一个变量 id 并初始化为 0

  2. 然后,定义了一个函数式组件 useId

  3. useId 内部,使用 useState hook 创建了一个状态变量 currentId,初始值通过回调函数 () => ++id 来计算,每次调用时将 id 值加 1 并返回。

  4. 最后,useId 返回 currentId,即生成的唯一标识符。

这段代码的作用是提供了一个简单的方式来生成唯一的标识符。它通过使用 useState 来创建一个状态变量 currentId,并使用计算属性的方式在组件首次渲染时生成并保存一个增加后的 id 值。每当组件重新渲染时,都会返回相同的 currentId 值。这样,可以在组件中使用 useId hook 来获取一个唯一的标识符,适用于各种场景如生成独立的 id、DOM 元素等,确保元素或实体之间的唯一性和辨识度。

useEvent

下面是一个简化版的自定义 hook useEvent,用于处理事件绑定和解绑的逻辑,并对代码进行分析:

javascript 复制代码
function useEvent(eventName, handler, element = window) {
    useEffect(() => {
        const eventListener = (event) => {
            handler(event);
        };

        element.addEventListener(eventName, eventListener);

        return () => {
            element.removeEventListener(eventName, eventListener);
        };
    }, [element, eventName, handler]);
}

现在让我们逐行解析这段代码:

  1. 首先,定义了一个函数式组件 useEvent,接受三个参数:eventName(事件名称)、handler(事件处理函数)和可选的 element(触发事件的元素,默认为 window)。

  2. useEvent 内部,使用 useEffect hook 处理副作用的逻辑。

  3. useEffect 的回调函数中,定义了一个 eventListener 函数,该函数会调用传入的 handler 处理事件。

  4. 使用 addEventListenereventListener 添加到指定的 element 上,监听指定的 eventName 事件。

  5. 返回一个清除函数,在组件卸载时使用 removeEventListener 移除之前添加的 eventListener

  6. useEffect 的依赖项数组包含 elementeventNamehandler,确保在这些值发生变化时重新运行副作用。

这段代码的作用是提供一种方便地绑定和解绑事件的方式。它使用 useEffect 来处理副作用,并在组件首次渲染时添加指定的事件监听器,一旦事件触发,将调用传入的事件处理函数 handler 进行处理。当组件卸载时,通过返回的清除函数来移除之前添加的事件监听器,以避免内存泄漏和不必要的事件处理。这种方式使得在实现自定义的事件逻辑时变得更加简单和可控。

相关推荐
小华同学ai2 分钟前
vue-office:Star 4.2k,款支持多种Office文件预览的Vue组件库,一站式Office文件预览方案,真心不错
前端·javascript·vue.js·开源·github·office
APP 肖提莫4 分钟前
MyBatis-Plus分页拦截器,源码的重构(重构total总数的计算逻辑)
java·前端·算法
问道飞鱼15 分钟前
【前端知识】强大的js动画组件anime.js
开发语言·前端·javascript·anime.js
k093316 分钟前
vue中proxy代理配置(测试一)
前端·javascript·vue.js
傻小胖18 分钟前
React 脚手架使用指南
前端·react.js·前端框架
程序员海军30 分钟前
2024 Nuxt3 年度生态总结
前端·nuxt.js
m0_7482567841 分钟前
SpringBoot 依赖之Spring Web
前端·spring boot·spring
web135085886351 小时前
前端node.js
前端·node.js·vim
m0_512744641 小时前
极客大挑战2024-web-wp(详细)
android·前端
若川1 小时前
Taro 源码揭秘:10. Taro 到底是怎样转换成小程序文件的?
前端·javascript·react.js