前言
哈喽大家好,我是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。让我们逐行解析它:
-
lastIndex
和lastStates
变量被声明并初始化为 0 和一个空数组,分别用于记录当前下标和所有的状态值。 -
useState
函数被定义,并接受初始值initValue
作为参数。 -
currentIndex
变量通过闭包捕获了lastIndex
的值,即记录当前状态的下标。 -
在第一次调用
useState
时,会检查对应下标的lastStates
是否已经存在。如果不存在(即为undefined
),则将其初始化为initValue
。 -
setState
函数用于更新状态值lastStates[currentIndex]
。它接受一个新的值newValue
作为参数,并将其赋值给对应下标的lastStates
。 -
在调用
setState
后,通常会触发重新渲染(此处没有给出具体实现),以便在界面上反映最新的状态值。 -
最后,
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。下面逐行解析它:
-
lastCallback
和lastDependencies
变量被声明,用于存储上一次记忆的回调函数和依赖项。 -
useCallback
函数被定义,并接受两个参数:callback
(要记忆的回调函数)和dependencies
(可选的依赖项数组)。 -
首先判断是否传入了依赖项
dependencies
。 -
如果传入了依赖项,则通过比较每个依赖项的值与上一次的依赖项值,判断依赖项是否发生变化。使用
every
方法遍历dependencies
数组,并将每个依赖项与对应位置的lastDependencies[index]
进行比较。如果有任何一个依赖项不相等,表示依赖项发生了变化,将isChange
设置为true
。 -
如果依赖项发生了变化(即
isChange
为true
),则更新lastCallback
为新的回调函数callback
,并将最新的依赖项数组赋值给lastDependencies
。 -
如果没有传入依赖项,说明可以将其当做第一次调用
useCallback
,此时直接将lastCallback
更新为传入的回调函数callback
,并将lastDependencies
设置为undefined
。 -
最后,无论是否传入了依赖项,都会返回记忆的回调函数
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
非常相似,但是返回的不是记忆的函数,而是记忆的结果值。下面对代码进行解析:
-
lastResult
和lastDependencies
变量用于存储上一次记忆的结果和依赖项。 -
useMemo
函数被定义,并接受两个参数:callback
(要记忆的函数)和dependencies
(可选的依赖项数组)。 -
首先判断是否传入了依赖项
dependencies
。 -
如果传入了依赖项,则通过比较每个依赖项的值与上一次的依赖项值,判断依赖项是否发生变化。使用
every
方法遍历dependencies
数组,并将每个依赖项与对应位置的lastDependencies[index]
进行比较。如果有任何一个依赖项不相等,表示依赖项发生了变化,将isChange
设置为true
。 -
如果依赖项发生了变化(即
isChange
为true
),则调用callback
函数以获取新的结果,并将其赋值给lastResult
。 -
如果没有传入依赖项,说明可以将其当做第一次调用
useMemo
,此时直接调用callback
函数以获取初始结果,并将其赋值给lastResult
。 -
最后,无论是否传入了依赖项,都会返回记忆的结果值
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。下面逐行解析这段代码:
-
lastIndex
和lastStates
变量用于记录当前的状态下标和所有的状态值。 -
useReducer
函数被定义,并接受两个参数:reducer
(处理状态的纯函数)和initState
(初始状态值)。 -
currentIndex
变量通过闭包捕获了lastIndex
的值,即记录当前状态的下标。 -
在第一次调用
useReducer
时,会检查对应下标的lastStates
是否已经存在。如果不存在(即为undefined
),则将其初始化为initState
。 -
dispatch
函数用于更新状态值lastStates[currentIndex]
。它接受一个动作对象action
作为参数,并将使用reducer
函数处理当前状态值和动作对象,得到新的状态值。 -
更新状态值后,通常会触发重新渲染(此处没有给出具体实现),以便在界面上反映最新的状态值。
-
最后,
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。这段代码相比之前的示例要简单得多。下面对代码进行分析:
-
contextValues
变量用来存储不同上下文的值,它是一个对象。 -
useContext
函数被定义,并接受一个参数context
(上下文对象)。 -
首先判断
contextValues
对象中是否存在给定的上下文context
的值。如果不存在,说明上下文提供者没有正确设置或没有在当前组件的父组件中传递该上下文的值。 -
如果找不到上下文值,则抛出一个错误,提示需要确保上下文提供者的正确设置。
-
如果能找到上下文值,则将其返回。
这段代码的作用是获取指定上下文的值。它通过检查 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();
}
这段代码相对复杂一些,所以我会逐步解析:
-
effectCallbacks
变量是一个数组,用于存储所有需要执行的副作用回调函数。 -
useEffect
函数被定义,并接受两个参数:callback
(副作用的回调函数)和dependencies
(可选的依赖项数组)。 -
在
useEffect
函数中,首先将callback
和dependencies
封装成一个对象effect
。 -
然后将封装后的
effect
对象添加到effectCallbacks
数组中,以便在适当的时机执行。 -
runEffects
函数用于遍历effectCallbacks
数组,并根据依赖项的变化来触发需要执行的副作用回调函数。 -
在遍历过程中,首先判断当前的依赖项是否存在,如果没有依赖项或者其中任何一个依赖项与前一次保存的依赖项不相等,表示依赖项已经发生了变化,需要调用回调函数。
-
执行完毕后,更新保存的依赖项为当前的依赖项值,以便在下次比较时使用。
-
最后,
render
函数示例了渲染流程,并在适当的时机调用runEffects
函数来运行所有的副作用回调函数。
这段代码的作用是模拟 React 的 useEffect
钩子,用于处理副作用操作和依赖项变化。通过将副作用回调函数和依赖项封装成对象,并存储在数组中,在适当的时机遍历数组并检查依赖项的变化,从而调用相应的副作用回调函数。这有助于在合适的时机执行副作用逻辑,避免重复执行或缺少执行。
useRef
下面是一个简化版的自定义 hook useRef
,类似于 React 中的 useRef
hook:
javascript
let refValue;
function useRef(initialValue) {
if (refValue === undefined) {
refValue = { current: initialValue };
}
return refValue;
}
这段代码相当简单,下面对其进行解析:
-
refValue
变量用于存储引用对象的值。 -
useRef
函数被定义,并接受一个参数initialValue
(引用对象的初始值)。 -
首先检查
refValue
是否为undefined
。如果是,表示引用对象还未初始化。 -
在首次调用
useRef
时,将initialValue
封装在带有current
属性的对象中,并赋值给refValue
。 -
如果已经存在初始值,说明引用对象已经初始化过了,直接返回保存的
refValue
。 -
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.");
}
}
这段代码非常简短,下面对其进行解析:
-
useImperativeHandle
函数被定义,并接受两个参数:ref
(ref 对象)和createHandle
(创建 handle 的回调函数)。 -
首先检查传入的
ref
是否是一个对象且不为null
,并且检查ref.current
属性是否存在且是一个对象。这样做是为了确保传递的ref
符合要求。 -
如果
ref
符合要求,调用createHandle
回调函数,并将ref.current
作为参数传递给它。 -
如果
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]);
}
这段代码相对较长,但基本原理与前面示例相似。下面对其进行逐行解析:
-
useSyncExternalStore
函数被定义,并接受两个参数:externalStore
(外部存储)和updateCallback
(更新回调函数)。 -
在函数内部使用了
useEffect
hook 来实现副作用的处理。 -
在
useEffect
的回调函数中,首先通过调用updateCallback(externalStore.getState())
来将外部存储的当前状态传递给更新回调函数初始化组件的状态。 -
接下来,通过调用
externalStore.subscribe()
方法订阅外部存储的变化。当外部存储发生变化时,会触发订阅的回调函数,即() => { updateCallback(externalStore.getState()); }
,用于更新组件中的状态。 -
最后,返回一个清除函数,在组件卸载时取消订阅,以避免内存泄漏。
-
useEffect
的依赖项数组包含externalStore
和updateCallback
,确保在这两个值发生变化时重新运行副作用。
这段代码的作用是在组件中实现与外部存储的同步,即将外部存储的状态传递给组件的初始状态,并在外部存储发生变化时更新组件的状态。它通过 useEffect
来订阅外部存储的变化,并提供一个更新回调函数来处理状态的更新。这样可以确保组件始终与外部存储保持同步,并在外部存储发生变化时及时更新显示正确的数据。
useTransition
下面是一个简化版的自定义 hook useTransition
,用于处理过渡效果的控制,同时对代码进行分析:
javascript
function useTransition() {
const [isShowing, setIsShowing] = useState(false);
function startTransition() {
setIsShowing(true);
}
function endTransition() {
setIsShowing(false);
}
return {
isShowing,
startTransition,
endTransition
};
}
让我们逐行解析这段代码:
-
useTransition
函数被定义。 -
在函数内部,使用
useState
hook 定义了一个状态变量isShowing
以及对应的更新函数setIsShowing
,初始值为false
。 -
startTransition
函数定义了启动过渡的操作,将isShowing
设置为true
。 -
endTransition
函数定义了结束过渡的操作,将isShowing
设置为false
。 -
最后,通过返回一个对象字面量,将状态和过渡相关的函数暴露给外部使用。
-
返回的对象有三个属性:
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;
}
让我们逐行解析这段代码:
-
useDeferredValue
函数被定义,并接受两个参数:value
(需要延迟的值)和timeout
(延迟时间)。 -
在函数内部,使用
useState
hook 定义了一个状态变量deferredValue
以及对应的更新函数setDeferredValue
,初始值为传入的value
。 -
使用
useEffect
hook 来处理副作用的逻辑。 -
在
useEffect
的回调函数中,通过调用setTimeout
来延迟一定时间后将value
设置为deferredValue
。 -
setDeferredValue
会在经过timeout
时间后更新deferredValue
的值。 -
返回的
useDeferredValue
hook 非常简单,只返回了deferredValue
,即延迟后的值。 -
useEffect
的依赖项数组包含value
和timeout
,确保在这两个值发生变化时重新运行副作用。
这段代码的作用是提供了一种延迟获取值的方式。它使用 useState
来管理一个状态变量 deferredValue
,初始值为传入的 value
。然后,通过使用 useEffect
来设置一个定时器,在经过指定的 timeout
时间后,将 value
的值更新到 deferredValue
。这样,可以在组件中使用 useDeferredValue
获取延迟后的值,并根据需要进行相应的渲染或逻辑操作。
useId
下面是一个简化版的自定义 hook useId
,用于生成唯一的标识符,并对代码进行分析:
javascript
let id = 0;
function useId() {
const [currentId] = useState(() => ++id);
return currentId;
}
现在让我们逐行解析这段代码:
-
首先,定义了一个变量
id
并初始化为0
。 -
然后,定义了一个函数式组件
useId
。 -
在
useId
内部,使用useState
hook 创建了一个状态变量currentId
,初始值通过回调函数() => ++id
来计算,每次调用时将id
值加 1 并返回。 -
最后,
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]);
}
现在让我们逐行解析这段代码:
-
首先,定义了一个函数式组件
useEvent
,接受三个参数:eventName
(事件名称)、handler
(事件处理函数)和可选的element
(触发事件的元素,默认为window
)。 -
在
useEvent
内部,使用useEffect
hook 处理副作用的逻辑。 -
在
useEffect
的回调函数中,定义了一个eventListener
函数,该函数会调用传入的handler
处理事件。 -
使用
addEventListener
将eventListener
添加到指定的element
上,监听指定的eventName
事件。 -
返回一个清除函数,在组件卸载时使用
removeEventListener
移除之前添加的eventListener
。 -
useEffect
的依赖项数组包含element
、eventName
和handler
,确保在这些值发生变化时重新运行副作用。
这段代码的作用是提供一种方便地绑定和解绑事件的方式。它使用 useEffect
来处理副作用,并在组件首次渲染时添加指定的事件监听器,一旦事件触发,将调用传入的事件处理函数 handler
进行处理。当组件卸载时,通过返回的清除函数来移除之前添加的事件监听器,以避免内存泄漏和不必要的事件处理。这种方式使得在实现自定义的事件逻辑时变得更加简单和可控。