8.1 React-hooks 8.1.1 hooks使命 逻辑组件复用 社区一直致力于逻辑层面的复用,像 render props / HOC,不过它们都有对应的问题,Hooks是目前为止相对完美的解决方案
hooks 解决的问题 render props
Avator 组件是一个渲染头像的组件,里面包含其中一些业务逻辑,User组件是纯ui组件,用于展示用户昵称
export default funtion APP ( ) { return ( <div className ="App" > <Avatar > {data=> <User name ={data}/ > } </Avatar > </div > ) }
通过渲染props来实现逻辑组件复用 render props 通过嵌套组件实现,在真实的业务中,会出现嵌套多层,以及梭理props不清晰的问题 Hoc
class Avatar extends Component { render ( ) { return <div > {this.props.name}</div > } } funtion HocAvatar (Component ) { return ()=> <Component name ='王艺瑾' /> }
通过对现有组件进行扩展、增强的方式来实现复用,通常采用包裹方法来实现 高阶组件的实现会额外地增加元素层级,使得页面元素的数量更加臃肿 Hooks
import React,{useState} from 'react' export function HooksAvatar ( ) { const [name,setName]=useState('王一瑾' ) return <> {name}</> }
React 16.8引入的Hooks,使得实现相同功能而代码量更少成为现实 通过使用Hooks,不仅在编码层面减少代码的数量,同样在编译之后的代码也会更少 8.1.2 hooks实践 Hook官方APi(大概率用到的) useState 函数组件中的state方法 useEffect 函数组件处理副作用的方法,什么是副作用?异步请求、订阅原生的dom实事件、setTimeoutd等 useContext 接受一个context对象(React.createContext的返回值)并返回该context的当前值,当前的context由上层组件中距离最近的<Mycontext.provider></Mycontext.provider>
的 value prop决定 useReducer 另一种”useState”,跟redux有点类似 useRef 返回一个突变的ref对象,对象在函数的生命周期内一直存在 useMemo 缓存数值 useCallback 缓存函数 useCustom 自定义Hooks组件 useState import React,{useState} from 'react' const HooksTest = () => { const [count,setCount]=useState(0 ) return ( <div> {} <button onClick={()=> { setCount(count+1 ) }} >Add</button> {count} <div> ) }
useEffect import React,{useEffect} from 'react' ;const HooksTest = () => { const [count, setCount] = useState(0 ); useEffect(()=> { document .title = `You clicked ${count} times` ; return ()=> { clearInterval (timer) } }) useEffect(()=> { document .title = `You clicked ${count} times` ; },[count]) useEffect(()=> { document .title = `You clicked ${count} times` ; },[]) useEffect(()=> { axios.get('login' ) },[]) return ( <div > <p > You clicked {count} times</p > <button onClick ={() => setCount(count + 1)}> Click me </button > </div > ) }
useContext import React from 'react' export const ItemsContext = React.createContext({ name : '' }) import React from 'react' import Child from './child' import { ItemsContext } from './context-manager' import './index.scss' const items = { name : '测试' }const Father = () => { return ( <div className ='father' > <ItemsContext.Provider value ={items} > <Child > </Child > </ItemsContext.Provider > </div > ) } export default Fatherimport React ,{useContext} from 'react' import { ItemsContext } from './context-manager' import './index.scss' const Child = () => { const items=useContext(ItemsContext) return ( <div className ='child' > 子组件 {items.name} </div > ) } export default Child
useReducer useReducer是useState的替代方案,它接受一个形如(state,action)=>newState的reducer,并返回当前的state以及与其配套的dispatch方法
import React,{useReducer} from 'react' const initialState={count :0 }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 () } } const [state.dispatch]=useReducer(reducer,initialState)const HooksTest = () => { return ( <div> {state.count} <button onClick={()=> { dispatch({type :'increment' }) }}>increment</button> <button onClick ={() => { dispatch({type:'decrement'}) }}>increment</button > <div> ) }
useRef import React,{useRef} from 'react' const HooksTest = () => { const inputEl=useRef(null ) function onButtion ( ) { inputEl.current.focus() } return ( <div> <input type='text' ref={inputEl}> <button onClick={onButtion} >Add</button> {count} <div> ) }
因为在函数式组件里没有this来存放一些实例的变量,所以React建议使用useRef来存放有一些会发生变化的值,useRef 不单是为了DOM的ref,同时也是为了存放实例属性
const intervalRef=useRef()useEffect(()=> { intervalRef.current=setInterVal(()=> {}) return ()=> { clearInterval (intervalRef.current) } })
useImperativeHandle 可以让你在使用ref时自定义暴露给父组件的实例值,useImperativeHandle 应当与forwardRef 一起使用,这样可以父组件可以调用子组件的方法
function Father ( ) { const modelRef = useRef(null ); function sureBtn ( ) { inputRef.current.model(); } return ( <> <Button onClick ={sureBtn} > 确定</Button > <Children ref ={modelRef} > </Children > </> ) } const Children = React.forwardRef((props,ref )=> {const [visible, setVisible] = useState(false ); useImperativeHandle(ref, () => ({ model : () => { setVisible(true ); }, })); })
useMemo useMemo的理念和memo差不多,都是根据判断是否满足当前的有限条件来决定是否执行useMemo的callback函数,第二个参数是一个deps数组,数组里的参数变化决定了useMemo是否更新回调函数。
useMemo和useCallback参数一样,区别是useMemo的返回的是缓存的值,useCallback返回的是函数。
useMemo减少不必要的渲染 {useMemo(() => ( <div > { list.map((i, v) => ( <span key ={v} > {i.patentName} </span > ))} </div > ), [list])}
useMemo减少子组件的渲染次数 useMemo(() => ( { } <PatentTable getList={getList} selectList={selectList} cacheSelectList={cacheSelectList} setCacheSelectList={setCacheSelectList} /> ), [listshow, cacheSelectList])
const Demo=()=> { const newLog = useMemo(()=> { const log =()=> { } return log },[]) const log2 = useMemo(()=>{ return },[list]) return <div onClick ={() => newLog()} >{log2}</div > }
useCallback useMemo和useCallback接收的参数都是一样,都是依赖项发生变化后才会执行;useMemo返回的是函数运行结果,useCallback返回的是函数;父组件传递一个函数 给子组件的时候,由于函数组件每一次都会生成新的props函数,这就使的每次一个传递给子组件的函数都发生的变化,这样就会触发子组件的更新,有些更新是没有必要的。
const Father=({ id } )=> { const getInfo = useCallback((sonName )=> { console .log(sonName) },[id]) return <div > {/* 点击按钮触发父组件更新 ,但是子组件没有更新 */} <button onClick ={ ()=> setNumber(number+1) } >增加</button > <DemoChildren getInfo ={getInfo} /> </div > } const Children = React.memo((props )=> { console .log('子组件更新' ,props.getInfo()) return <div > 子组件</div > })
useCallback必须配合 react.memo pureComponent,否则不但不会提升性能,还有可能降低性能。
react-hooks的诞生,也不是说它能够完全代替class声明的组件,对于业务比较复杂的组件,class组件还是首选,只不过我们可以把class组件内部拆解成funciton组件,根据业务需求,哪些负责逻辑交互,哪些需要动态渲染,然后配合usememo等api,让性能提升起来。react-hooks使用也有一些限制条件,比如说不能放在流程控制语句中,执行上下文也有一定的要求。
8.1.5扩展资料 React Hooks 官方文档
useEffect 完整指南
8.2 React-hooks原理解析 8.2.1 前言 ::: warning 阅读以下内容之前先了解一下,hooks出现的动机 ,同时也要熟悉hooks的用法,可以参考上一篇文章;看完useState
、useEffect
源码,我相信你已经基本掌握了hooks;其它的很简单。 :::
废话不多说,我首先克隆一份代码下来
git clone --branch v17.0.2 https://github.com/facebook/react.git
hooks导出部分在react/packages/react/src/ReactHooks.js
,虽然在react导出,但是真正实现在react-reconciler
这个包里面。
前置知识点:
fiber Fiber是一种数据结构,React使用链表把VirtualDOM节点表示一个Fiber,Fiber是一个执行单元,每次执行完一个执行单元,React会检查现在还剩多少时间,如果没有时间就将控制权让出去,去执行一些高优先级的任务。
循环链表
是一种链式存储结构,整个链表形成一个环 它的特点是最后一个节点的指针指向头节点 读源码,我们逐个击破的方式:
useState
useEffect
useRef
useCallback
useMemo
hooks不是一个新api也不是一个黑魔法,就是单纯的一个数组,看下面的例子hooks api返回一个数组,一个是当前值,一个是设置当前值的函数。
hooks中的useState import React ,{useState}from 'react' ;const App = () => { const [name,setName]=useState('王艺瑾' ) return (<div > <div > {name}</div > <button onClick ={() => setName('张艺凡')} >切换</button > </div > ); } export default App;
react 包中导出的useState 源码出处:react/packages/react/src/ReactHooks.js
react包中导出的usesate,其实没什么东西,大致看一下就能明白
export function useState <S >( initialState: (() => S) | S, ) { const dispatcher = resolveDispatcher(); return dispatcher.useState(initialState); }
在ReactHooks.js
搜索到了useState,函数里先执行了resolveDispatcher
,我们先看看resolveDispatcher函数做了写什么?resolveDispatcher
函数的执行,获取了ReactCurrentDispatcher
的current,那我们在看看ReactCurrentDispatcher
是什么?
function resolveDispatcher ( ) { const dispatcher = ReactCurrentDispatcher.current; invariant( dispatcher !== null , '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.' , ); return dispatcher; }
源码出处:react/packages/react/src/ReactCurrentDispatcher.js
const ReactCurrentDispatcher = { current : (null : null | Dispatcher), }; export default ReactCurrentDispatcher;
ReactCurrentDispatcher
现在是null,到这里我们线索好像中断了,因为current要有个hooks方法才行;我们可以断点的形式,去看看在mount阶段,react执行了什么?也就是在mount阶段ReactCurrentDispatcher.current挂载的hooks,蓝色部分就是react在初始化阶段执行的函数
下面才是正文,千万不要放弃
源码出处:react/packages/react-reconciler/src/ReactFiberHooks.new.js
renderWithHooks
因为renderWithhooks
是调用函数组件的主要函数,所有的函数组件执行,都会执行这个方法。
下面我说的hooks
代表组件中的hooks,例如:useState;hook对象
是每次执行hooks
所创建的对象
const HooksDispatcherOnMount = { useState : mountState, useEffect :mountEffect ...... } const HooksDispatcherOnUpdate = { useState : updateState, useEffect :updateEffect ...... } let currentlyRenderingFiber; let workInProgressHook = null let currentHook=null export function renderWithHooks ( current, workInProgress, Component, props, secondArg, ) { currentlyRenderingFiber = workInProgress; workInProgress.memoizedState = null ; workInProgress.updateQueue = null ; ReactCurrentDispatcher.current = current === null || current.memoizedState === null ? HooksDispatcherOnMount : HooksDispatcherOnUpdate; let children = Component(props,secondArg); ReactCurrentDispatcher.current = ContextOnlyDispatcher; currentlyRenderingFiber = null ; workInProgressHook = null ; currentHook = null ; return children; }
const ContextOnlyDispatcher = { useState :throwInvalidHookError } function throwInvalidHookError ( ) { invariant( false , '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://fb.me/react-invalid-hook-call for tips about how to debug and fix this problem.' , ); }
renderWithHooks主要做的事情:
判断是mount阶段还是update阶段给ReactCurrentDispatcher.current赋值。 执行组件函数,执行hooks。 清空在执行hooks所赋值的全局对象,下一次更新函数需要再次用到。 8.2.2 useState :tomato: mount阶段 1. mountState 初次挂载的时候,useState对应的函数是mountState
function basicStateReducer (state, action ) { return typeof action === 'function' ? action(state) : action; } function mountState ( initialState ) { const hook = mountWorkInProgressHook(); if (typeof initialState === 'function' ) { initialState = initialState(); } hook.memoizedState = hook.baseState = initialState; const queue = (hook.queue = { pending : null , dispatch : null , lastRenderedReducer : basicStateReducer, lastRenderedState : initialState, }); const dispatch = (queue.dispatch = (dispatchAction.bind( null , currentlyRenderingFiber, queue, ))); return [hook.memoizedState, dispatch]; }
mountState主要做的事情:
创建hook对象,在上面存上hooks信息,下次更新的时候可以从对象上获取。 返回一个数组,包括初始化的值和更新函数2. mountWorkInProgressHook 构建hooks单向链表,将组件中的hooks函数以链表的形式串连起来,并赋值给workInProgress的memoizedState;
例子:
function work ( ) { const [name,setName]=useState('h' ) const age=useRef(20 ) useEffect(()=> { },[]) } currentlyRenderingFiber.memoizedState={ memoizedState :'h' , next :{ memoizedState :'20' , next :{ memoizedState :effect, next :null } } }
为什么构建一个单向链表?
因为我们在组件更新阶段,需要拿到上次的值,拿到上次的值与本次设置的值做对比来判断是否更新
function mountWorkInProgressHook ( ) { const hook = { memoizedState : null , baseState : null , baseQueue : null , queue : null , next : null , }; if (workInProgressHook === null ) { currentlyRenderingFiber.memoizedState = workInProgressHook = hook; } else { workInProgressHook = workInProgressHook.next = hook; } return workInProgressHook; }
如果上面构建hooks单向链表没有看懂,请看下面解析
if (workInProgressHook === null ) { currentlyRenderingFiber.memoizedState = workInProgressHook = hook; } else { workInProgressHook = workInProgressHook.next = hook; }
第一次我们创建了hook对象,在堆内存中开辟了一块空间, currentlyRenderingFiber.memoizedState
、workInProgressHook
都指向了这个值,对象是引用类型值;我们称这个值为hooks1吧。 currentlyRenderingFiber.memoizedState = hooks1
第二次我们再次创建了hook对象,在堆内存中又开辟了一块空间,我们称这个值为hooks2吧,workInProgressHook.next
指向了hooks2,也就是hooks1.next指向了hook2;因为当前的workInProgressHook
和hooks1指向同一个地址,只要有一个修改内存里的值,其他变量只要引用该值了,也会随之发生变化;最后又把hooks2又赋值给workInProgressHook
,那么workInProgressHook
又指向了hooks2。 hooks1.next= hooks2
workInProgressHook=hooks2
第三次我们再次创建了hook对象,在堆内存中又开辟了一块空间,我们称这个值为hooks3吧,hooks3又赋值给了workInProgressHook.next
,现在的workInProgressHook和hooks2指向是同一个地址,那么我改变workInProgressHook.next
就是改变hooks2的next。 hooks2.next= hooks3
workInProgressHook=hooks3
workInProgressHook始终和最新hook对象指向同一个地址,这样就方便修改上一个hook对象的next
3. dispatchAction function dispatchAction (fiber, queue, action ) { const update= { action, eagerReducer : null , eagerState : null , next : null , } const pending = queue.pending; if (pending === null ) { update.next = update; } else { update.next = pending.next; pending.next = update; } queue.pending = update; const currentState = queue.lastRenderedState; const eagerState = lastRenderedReducer(currentState, action); update.eagerState = eagerState; if (is(eagerState, currentState)) { return } scheduleUpdateOnFiber(fiber); }
类组件更新调用setState
,函数组件hooks更新调用dispatchAction
,都会产生一个update对象,里面记录此处更新的信息; 把update对象放在queue.pending
上。
为什么创建update对象?
每次创建update对象,是希望形成一个环状链表。我们看下面一个例子,三次setCount的update对象会暂时放在queue.pending
上,组件里的state不会立即更新,在下一次函数组件执行的时候,三次update会被合并到baseQueue上,我们要获取最新的状态,会一次执行update上的每一个action,得到最新的state。
function work ( ) { const [count,setCount]=useState(0 ) function add ( ) { setCount(1 ) setCount(2 ) setCount(3 ) } return ( <button onClick ={add} > </button > ) }
为什么不是直接执行最后一个setCount?
如果setCount((state)=>{state+1})
参数是函数,那么需要依赖state,下一个要依赖上一个的state;所以需要都执行一遍才能 拿到准确的值。
:tomato: update阶段 1.updateState function basicStateReducer (state, action ) { return typeof action === 'function' ? action(state) : action; } function updateState ( initialState ) { return updateReducer(basicStateReducer, initialState); } function updateReducer (reducer, initialArg ) { let hook = updateWorkInProgressHook(); const queue = hook.queue; queue.lastRenderedReducer = reducer; const current = currentHook; const pendingQueue = queue.pending; if (pendingQueue!==null ){ let first = pendingQueue.next; let newState = current.memoizedState; let update = first; do { const action = update.action; newState = reducer(newState,action); update = update.next; }while (update !== null && update !== first); queue.pending = null ; hook.memoizedState = newState; queue.lastRenderedState = newState; } const dispatch = dispatchAction.bind(null , currentlyRenderingFiber, queue); return [hook.memoizedState, dispatch]; }
2. updateWorkInProgressHook function updateWorkInProgressHook ( ) { let nextCurrentHook; if (currentHook === null ){ let current = currentlyRenderingFiber.alternate; if (current !== null ) { nextCurrentHook = current.memoizedState; } else { nextCurrentHook = null ; } }else { nextCurrentHook=currentHook.next; } currentHook=nextCurrentHook; const newHook = { memoizedState :currentHook.memoizedState, queue :currentHook.queue, next :null } if (workInProgressHook === null ){ currentlyRenderingFiber.memoizedState = workInProgressHook = newHook; }else { workInProgressHook = workInProgressHook.next = newHook; } return workInProgressHook; }
8.2.3 useEffect :tomato: mount阶段 1. mountEffect const PassiveEffect = 0b000000001000000000 ; const PassiveStaticEffect = 0b001000000000000000 ; function mountEffect ( create, deps, ) { if (__DEV__) {} return mountEffectImpl( PassiveEffect | PassiveStaticEffect, HookPassive, create, deps, ); }
2. mountEffectImpl const HookHasEffect= 0b001 ; hookFlags = 0b100 ; function mountEffectImpl (fiberFlags, hookFlags, create, deps ) { const hook = mountWorkInProgressHook(); const nextDeps = deps === undefined ? null : deps; currentlyRenderingFiber.flags |= fiberFlags; hook.memoizedState = pushEffect( HookHasEffect | hookFlags, create, undefined , nextDeps, ); }
3. pushEffect pushEffect 创建effec对象,并形成环状链表存值与updateQueue上
function createFunctionComponentUpdateQueue ( ) { return { lastEffect : null , }; } function pushEffect (tag, create, destroy, deps ) { const effect = { tag, create, destroy, deps, next :null }; let componentUpdateQueue = currentlyRenderingFiber.updateQueue; if (componentUpdateQueue === null ) { componentUpdateQueue = createFunctionComponentUpdateQueue(); currentlyRenderingFiber.updateQueue = componentUpdateQueue componentUpdateQueue.lastEffect = effect.next = effect; } else { const lastEffect = componentUpdateQueue.lastEffect; const firstEffect = lastEffect.next; lastEffect.next = effect; effect.next = firstEffect; componentUpdateQueue.lastEffect = effect; } } return effect; }
useEffect(()=> {consoe.log(1 )},[]) useEffect(()=> {consoe.log(2 )},[]) useEffect(()=> {consoe.log(3 )},[]) const effect1={ create :()=> {consoe.log(1 )}, deps :[] next :effect1 } const effect1={ create :()=> {consoe.log(1 )}, deps :[] next :effect2 } const effect2={ create :()=> {consoe.log(1 )}, deps :[] next :effect1 } const effect2={ create :()=> {consoe.log(1 ), deps :[] next :effect3 } const effect3={ create :()=> {consoe.log(1 ), deps :[] next :effect1 }
:tomato: update阶段 1. updateEffect function updateEffect ( create, deps, ) { return updateEffectImpl(PassiveEffect, HookPassive, create, deps); }
2. updateEffectImpl function areHookInputsEqual ( nextDeps, prevDeps, ) { for (let i = 0 ; i < prevDeps.length && i < nextDeps.length; i++) { if (is(nextDeps[i], prevDeps[i])) { continue ; } return false ; } return true ; } function updateEffectImpl (fiberFlags, hookFlags, create, deps ) { const hook = updateWorkInProgressHook(); const nextDeps = deps === undefined ? null : deps; let destroy = undefined ; if (currentHook !== null ) { const prevEffect = currentHook.memoizedState; destroy = prevEffect.destroy; if (nextDeps !== null ) { const prevDeps = prevEffect.deps; if (areHookInputsEqual(nextDeps, prevDeps)) { pushEffect(hookFlags, create, destroy, nextDeps); return ; } } } currentlyRenderingFiber.flags |= fiberFlags; hook.memoizedState = pushEffect( HookHasEffect | hookFlags, create, destroy, nextDeps, ); }
8.2.3 useRef mountRef (mount阶段) 看起来很简单,就是把initialValue 赋值给hook.memoizedState, 所以说只要弄懂useState、useEffect ,其他的看一眼就明白
function mountRef (initialValue ) { const hook = mountWorkInProgressHook(); const ref = initialValue; hook.memoizedState = ref; return ref; }
updateRef (update阶段) 拿到上一次的值并返回
function updateRef (initialValue ) { const hook = mountWorkInProgressHook(); const ref = initialValue; hook.memoizedState = ref; return ref; }
8.2.4 useCallback mountCallback (mount阶段) 把函数和依赖数组存到hook.memoizedState,并返回函数
function mountCallback (callback, deps ) { const hook = mountWorkInProgressHook(); const nextDeps = deps === undefined ? null : deps; hook.memoizedState = [callback, nextDeps]; return callback; }
updateCallback (update阶段) 对比依赖是否变化,变化就返回最新的函数,没有变化就返回上一个函数
function updateCallback (callback, deps ) { const hook = updateWorkInProgressHook(); const nextDeps = deps === undefined ? null : deps; const prevState = hook.memoizedState; if (prevState !== null ) { if (nextDeps !== null ) { const prevDeps = prevState[1 ]; if (areHookInputsEqual(nextDeps, prevDeps)) { return prevState[0 ]; } } } hook.memoizedState = [callback, nextDeps]; return callback; }
8.2.5 useMemo mountMemo (mount阶段) 调用传入函数拿到返回值,把值和依赖数组存到hook.memoizedState,并返回值
function mountMemo ( nextCreate, deps, ) { const hook = mountWorkInProgressHook(); const nextDeps = deps === undefined ? null : deps; const nextValue = nextCreate(); hook.memoizedState = [nextValue, nextDeps]; return nextValue; }
updateMemo (update阶段) 对比依赖是否变化,变化就返回最新的值,没有变化就返回上一个值
function updateMemo ( nextCreate, deps, ) { const hook = updateWorkInProgressHook(); const nextDeps = deps === undefined ? null : deps; const prevState = hook.memoizedState; if (prevState !== null ) { if (nextDeps !== null ) { const prevDeps = prevState[1 ]; if (areHookInputsEqual(nextDeps, prevDeps)) { return prevState[0 ]; } } } const nextValue = nextCreate(); hook.memoizedState = [nextValue, nextDeps]; return nextValue; }
8.3 使用hooks会遇到的问题 react hooks遇到的问题
React Hooks完全上手指南
在工程中必须引入lint插件,并开启相应规则,避免踩坑。
{ "plugins" : ["react-hooks" ], "rules" : { "react-hooks/rules-of-hooks" : "error" , "react-hooks/exhaustive-deps" : "warn" } }
这2条规则,对于新手,这个过程可能是比较痛苦的,如果你觉得这2个规则对你编写代码造成了困扰,说明你还未完全掌握hooks,对于某写特殊场景,确实不需要「exhaustive-deps」,可在代码处加eslint-disable-next-line react-hooks/exhaustive-deps;切记只能禁止本处代码,不能偷懒把整个文件都禁了。
8.3.1 useEffect相关问题 依赖变量问题 function ErrorDemo ( ) { const [count, setCount] = useState(0 ); const dom = useRef(null ); useEffect(() => { dom.current.addEventListener('click' , () => setCount(count + 1 )); }, [count]); return <div ref ={dom} > {count}</div > ;
像这种情况,每次count变化都会重新绑定一次事件,那我们怎么解决呢?
function ErrorDemo ( ) { const [count, setCount] = useState(0 ); const dom = useRef(null ); useEffect(() => { dom.current.addEventListener('click' , () => setCount(count + 1 )); }, []); return <div ref ={dom} > {count}</div > ;
把依赖count变量去掉吗?如果把依赖去掉的话,意味着hooks只在组件挂载的时候运行一次,count的值永远不会超过1;因为在effect 执行时,我们会创建一个闭包,并将count的值保存在闭包当中,且初始值为0
思路1:消除依赖 useEffect(() => { dom.current.addEventListener('click' , () => setCount((precount )=> ++precount); }, [])
setCount也可以接收一个函数,这样就不用依赖count了
思路1: 重新绑定事件 useEffect(() => { const $dom = dom.current; const event = () => { setCount(count); }; $dom.addEventListener('click' , event); return $dom.removeEventListener('click' , event); }, [count]);
思路2:ref 你可以 使用一个 ref 来保存一个可变的变量。然后你就可以对它进行读写了
当你实在找不到更好的办法的时候,才这么做,因为依赖的变更使组件变的难以预测
const [count, setCount] = useState(0 );const dom = useRef(null );const countRef=useRef(count)useEffect(() => { countRef.current=count }); useEffect(() => { dom.current.addEventListener('click' , () => setCount(countRef.current + 1 )); }, []);
依赖函数问题 只有 当函数(以及它所调用的函数)不引用 props、state 以及由它们衍生而来的值时,你才能放心地把它们从依赖列表中省略。下面这个案例有一个 Bug:
function ProductPage ({ productId } ) { const [product, setProduct] = useState(null ); async function fetchProduct ( ) { const response = await fetch('http://myapi/product/' + productId); const json = await response.json(); setProduct(json); } useEffect(() => { fetchProduct(); }, []);
思路1:推荐的修复方案是把那个函数移动到你的 effect 内部 这样就能很容易的看出来你的 effect 使用了哪些 props 和 state,并确保它们都被声明了:
function ProductPage ({ productId } ) { const [product, setProduct] = useState(null ); useEffect(() => { async function fetchProduct ( ) { const response = await fetch('http://myapi/product/' + productId); const json = await response.json(); setProduct(json); } fetchProduct(); }, [productId]); }
思路2: useCallback 把函数加入 effect 的依赖但 把它的定义包裹 进 useCallback Hook。这就确保了它不随渲染而改变,除非 它自身 的依赖发生了改变
function ProductPage ({ productId } ) { const [product, setProduct] = useState(null ); const fetchProduct = useCallback(() => { const response = await fetch('http://myapi/product/' + productId); const json = await response.json(); setProduct(json); } }, [productId]); } useEffect(() => { fetchProduct(); }, [fetchProduct]);