前言
本文适合有一定 react 基础,且学习过 hooks 的同学,通过手写实现 hooks,加深其实现原理。
useState
语法回顾
先来回顾下 useState 的语法:
const [state, setState] = useState(initValue)
useState 接受一个初始值作为参数 initValue, 可以直接传值,也可以传入一个函数,并返回初始值,即:useState(() => initValue)
useState 返回一个数组,数组中第一个元素是我们需要使用的 state,第二个元素是用来更改 state 的回调函数 setState(newValue)
手写实现
实现思路:
- 因为我们会多次使用 useState 创建多个 state,所以为了区分谁是谁,
创建一个全局 lastStates 数组来存放对应的 state
。 - 同时,为了方便后续更新 state,
创建一个全局的索引 lastIndex
找到当前 state 在 lastStates 中的位置。 - 每次调用 useState 时,存值,返回数组。
- 调用 setState 时,通过
lastStates[lastIndex]
修改当前 state,并 Rerender。
代码如下:
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 [lastStatas[lastIndex++], setState]
}
注意:为什么最后返回的是 lastStates[lastIndex++], 而不是lastStates[currentIndex]
?
因为在一个组件中,我们会多次调用 useState 创建不同的 state
, 每一次 lastIndex++ 后,下一次再调用 useState 时,通过 currentIndex = lastIndex 记录最新的下标,通过闭包就能保证 setState 时状态更改是正确的。
useCallback
语法回顾
const memoizedCallback = useCallback(() => {
doSomething()
},dependenices)
useCallback 接受一个 回调函数 和 依赖项数组作为参数,返回一个 记忆值函数,当依赖项发生改变的时候,对记忆值函数进行更新。
通过 useCallback 包裹的函数,可以避免非必要性的重新渲染,达到性能优化的目的。比如父组件传一个 函数props 给子组件,而父组件 state 发生改变时,子组件也会跟着 render,这时可以用该 hook 进行优化。
手写实现
const memoizedCallback = useCallback(() => {
doSomething()
},dependenices)
useCallback 接受一个 回调函数 和 依赖项数组作为参数,返回一个 记忆值函数,当依赖项发生改变的时候,对记忆值函数进行更新。
通过 useCallback 包裹的函数,可以避免非必要性的重新渲染,达到性能优化的目的。比如父组件传一个 函数props 给子组件,而父组件 state 发生改变时,子组件也会跟着 render,这时可以用该 hook 进行优化。
实现思路:
- 创建全局的两个变量: lastCallback、lastDependencies,
用于保存传入的回调函数和依赖项
- 执行 useCallback 时,判断是否传入了依赖项 dependencies
- 若传入,
则对比 dependencies 和 lastDependencies 中的每一个依赖项是否改变
,若改变,则记录最新的 callback 和 依赖项,若没改变,返回记忆值函数 lastCallback - 若没传入,
可以理解为第一次调用 useCallback
,记录 callback 和 依赖项,返回 lastCallback
代码如下:
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
}
useMemo
语法回顾
useMemo 和 useCallback 大同小异,只不过前者缓存结果,后者缓存函数。
const memoizedData= useMemo(() => {
return result
},dependenices)
useMemo 接受一个 回调函数 和 依赖项数组作为参数,返回一个
记忆值结果
,当依赖项发生改变的时候,重新计算并返回结果。
useMemo 底层原理
useMemo 会记录上一次执行计算的结果,并把它绑定在函数组件对应的 fiber 对象上,只要组件不销毁,缓存值就一直存在
,但是 deps 中如果有一项改变,就会重新计算结果,将其作为新的值记录到 fiber 对象上。
手写实现
useMemo 和 useCallback 类似,直接上代码:
let lastResult;
let lastDependencies;
function useCallback(callback, dependencies) {
//1、判断是否传入了依赖项
if (dependencies) {
const isChange = !dependencies.every((item, index) => {
return item === lastDependencies[index]
})
//如果依赖项改变
if (isChange) {
lastResult = callback(); //更新记忆值
lastDependencies = dependencies; //记录最新的依赖项
}
} else {
//没传入依赖项,可以当成是第一次调用 useMemo
lastResult = callback();
lastDependencies = dependencies;
}
//返回记忆值函数
return lastResult
}
useReducer
语法回顾
const [state, dispatch] = useReducer(reducer, initState);
useReducer接收两个参数:
第一个参数:reducer函数,第二个参数:初始化的state。
返回值为最新的state和dispatch函数(dispatch 用来触发reducer函数,计算对应的state)。
按照官方的说法:对于复杂的state操作逻辑,嵌套的state的对象,推荐使用useReducer。
const [state, dispatch] = useReducer(reducer, initState);
useReducer接收两个参数:
第一个参数:reducer函数,第二个参数:初始化的state。 返回值为最新的state和dispatch函数(dispatch 用来触发reducer函数,计算对应的state)。
按照官方的说法:对于复杂的state操作逻辑,嵌套的state的对象,推荐使用useReducer。
我们可以先来看一个例子:
// 官方 useReducer Demo
// 第一个参数:应用的初始化
const initialState = {count: 0};
// 第二个参数:state的reducer处理函数
function reducer(state, action) {
switch (action.type) {
case 'increment':
return {count: state.count + 1};
case 'decrement':
return {count: state.count - 1};
default:
throw new Error();
}
}
function Counter() {
// 返回值:最新的state 和 dispatch函数
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
// useReducer会根据dispatch的action,返回最终的state,并触发rerender
Count: {state.count}
// dispatch 用来接收一个 action参数「reducer中的action」,用来触发reducer函数,更新最新的状态
<button onClick={() => dispatch({type: 'increment'})}>+</button>
<button onClick={() => dispatch({type: 'decrement'})}>-</button>
</>
);
}
手写实现
实现思路:
- 与 useState 类似,创建全局的两个变量:
- lastIndex:当前 state 的下标。
- lastStates:存储 state 的数组。
- 执行 useReducer 时,记录初始值。
- 定义 dispatch(reducer, action) 函数。
- 返回数组 [当前的 state, dispatch]。
代码如下:
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]
}
useContext
语法回顾
const MyContext = React.createContext()
const MyContext = React.createContext()
createContext 能够创建一个 React 的 上下文(context),然后订阅了这个上下文的组件中,可以拿到上下文中提供的数据或者其他信息。
如果要使用创建的上下文,需要通过 Context.Provider 最外层包装组件,并且需要显示的通过 <MyContext.Provider value={{xx:xx}}>
的方式传入 value,指定 context 要对外暴露的信息。
子组件在匹配过程中只会匹配最新的 Provider,也就是说如果有下面三个组件:ContextA.Provider->A->ContexB.Provider->B->C
, 如果 ContextA 和 ContextB 提供了相同的方法,则 C 组件只会选择 ContextB 提供的方法。
通过 React.createContext 创建出来的上下文,在子组件中可以通过 useContext 这个 Hook 获取 Provider 提供的内容
,即:
const {funcName} = useContext(MyContext);
从上面代码可以发现,useContext 需要将 MyContext 这个 Context 实例传入,不是字符串,就是实例本身。
这种用法会存在一个比较尴尬的地方,父子组件不在一个目录中,如何共享 MyContext 这个 Context 实例呢?
一般这种情况下,我会通过 Context Manager 统一管理上下文的实例,然后通过 export 将实例导出,在子组件中在将实例 import 进来。
下面我们看看代码,使用起来非常简单
import React, { useState, useContext } from 'react';
import ReactDOM from 'react-dom';
let AppContext = React.createContext()
function Counter() {
let { state, setState } = useContext(AppContext)
return (
<>
Count: {state.count}
<button onClick={() => setState({ number: state.number + 1 })}>+</button>
</>
);
}
function App() {
let [state, setState] = useState({ number: 0 })
return (
<AppContext.Provider value={{ state, setState }}>
<div>
<h1>{state.number}</h1>
<Counter></Counter>
</div>
</AppContext.Provider>
)
}
function render() {
ReactDOM.render(
<App />,
document.getElementById('root')
);
}
render()
要是用过vue的同学,会发现,这个机制有点类似vue 中提供的provide和inject
手写实现
原理非常简单,由于createContext,Provider 不是ReactHook的内容, 所以这里只需要实现 useContext,如代码所示,只需要一行代码
import React, { useState } from 'react';
import ReactDOM from 'react-dom';
let AppContext = React.createContext()
// useContext 实现
function useContext(context){
return context._currentValue
}
function Counter() {
let { state, setState } = useContext(AppContext)
return (
<>
<button onClick={() => setState({ number: state.number + 1 })}>+</button>
</>
);
}
function App() {
let [state, setState] = useState({ number: 0 })
return (
<AppContext.Provider value={{ state, setState }}>
<div>
<h1>{state.number}</h1>
<Counter></Counter>
</div>
</AppContext.Provider>
)
}
function render() {
ReactDOM.render(
<App />,
document.getElementById('root')
);
}
render()
useEffect
语法回顾
useEffect(() => {
doSomething()
return () => {
//清除副作用,如取消事件订阅、清除定时器
}
}, dependencies);
useEffect(() => {
doSomething()
return () => {
//清除副作用,如取消事件订阅、清除定时器
}
}, dependencies);
useEffect 可以理解为函数组件的生命周期函数
,看作类组件 componentDidMoun、componentDidUpdate
和 componentWillUnmount
这三个生命周期的组合。
-
当 dependencies 为 [] 时,useEffect 相当于 componentDidMount,
只会在组件初次挂在完成后执行
,后续改变 state 时,此时 useEffect 不会再次执行。 -
当 dependencies 不传时,useEffect 相当于 componentDidUpdate,
会在组件初次渲染以及 state 更新时执行
。 -
当 useEffect 返回一个清除函数(cleanHandler)时,
React 将会在执行清除操作时调用它
,比如组件销毁前。 -
当 dependencies 不是空数组时,只有当依赖发生改变时, useEffect才会重新执行。
实现思路
useEffect 的思路和 useMemo、useCallback类似,都会去判断依赖是否发生改变,并记录新依赖,区别是 useEffect 没有返回值,而 useMemo、useCallback 有返回值。
代码如下:
import React, { useState} from 'react';
import ReactDOM from 'react-dom';
let lastEffectDependencies //记录依赖
function useEffect(callback,dependencies){
//是否已存在依赖,若存在,判断依赖是否改变
if(lastEffectDependencies){
let changed = !dependencies.every((item,index)=>{
return item === lastEffectDependencies[index]
})
//若依赖改变,重新执行 callback,记录新的依赖
if(changed){
setTimeout(callback())
lastEffectDependencies = dependencies
}
}else{
//若不存在,则调用 callback,记录依赖
setTimeout(callback())
lastEffectDependencies = dependencies
}
}
function App() {
let [number, setNumber] = useState(0)
useEffect(()=>{
console.log(number);
},[number])
return (
<div>
<h1>{number}</h1>
<button onClick={() => setNumber(number+1)}>+</button>
</div>
)
}
function render() {
ReactDOM.render(
<App />,
document.getElementById('root')
);
}
render()
注意:为什么执行 callback 时要加一个 setTimeout ?
因为实际上 callback 是在
浏览器渲染结束后执行
的。所以我们要加 setTimeout。
useLayoutEffect
useEffect 和 useLayoutEffect 两个 hook 十分类似。
原理
这两个hook基本相同,只是调用时机不同
。
而官方也明确说明:请全部使用useEffect,除非遇到bug或者不可解决的问题,再考虑使用useLayoutEffect
。
上面说到 useEffect 的调用时机是 浏览器渲染结束后执行 的,而 useLayoutEffect 是在 DOM构建完成,浏览器渲染前执行 的。
所以这里需要把宏任务setTimeout改成微任务
import React, { useState} from 'react';
import ReactDOM from 'react-dom';
let lastEffectDependencies //记录依赖
function useLayouyEffect(callback,dependencies){
//是否已存在依赖,若存在,判断依赖是否改变
if(lastEffectDependencies){
let changed = !dependencies.every((item,index)=>{
return item === lastEffectDependencies[index]
})
//若依赖改变,重新执行 callback,记录新的依赖
if(changed){
Promise.resolve().then(callback()) //微任务模拟
lastEffectDependencies = dependencies
}
}else{
//若不存在,则调用 callback,记录依赖
Promise.resolve().then(callback()) //微任务模拟
lastEffectDependencies = dependencies
}
}
function App() {
let [number, setNumber] = useState(0)
useLayouyEffect(()=>{
console.log(number);
},[number])
return (
<div>
<h1>{number}</h1>
<button onClick={() => setNumber(number+1)}>+</button>
</div>
)
}
function render() {
ReactDOM.render(
<App />,
document.getElementById('root')
);
}
render()
结语
以上内容如有错误,欢迎留言指出,一起进步