Case 1: miss dependency in useEffect
因为没有依赖
,是无法触发页面的unmount
和componentDidUpdate
的, 只会在componentDidMount
时运行一次
同样的值传入setState
多次,只有前两次 会引起re-render;
为什么count
的值一直是0
?
每次渲染都是一次snapshot
,会有自己state
的一个version
因为没有依赖
,所以就没有unmount
的发生,所以clean
不会发生
clean
没有发生,全局就只
有一次setInterval
,会把count闭包
闭进去
闭包是闭了count
这个变量
,而不是
它的值
但是count
这个变量永远停留
在当时渲染的那个状态
(Immutable)
所以setInterval
中读到的count
永远都是0
.
import React , { useState , useEffect } from 'react' ;
const Counter = ( ) => {
const [ count , setCount ] = useState ( 0 ) ;
console . log ( 'render' ) ;
useEffect ( ( ) => {
console . log ( 'componentDidUpdate' )
const id = setInterval ( ( ) => {
console . log ( count )
setCount ( count + 1 )
} , 1000 ) ;
return ( ) => {
clearInterval ( id ) ; // 只会在页面退出时执行
console . log ( 'clean' ) ;
}
} , [ ] ) ; // 是无法触发页面的unmount和componentDidUpdate的, 只会在componentDidMount时运行一次
return (
< h1 >
{ count }
</ h1 >
)
}
Solution 1: append dependency
如果传入count
,会发生什么?
因为设置了count
为dependency
, 每次count
的变动都会引起unmount
, re-render
和useEffect
即clean上一次setInterval
, 重新渲染(更新count
的值), 和重新setInterval
(闭包闭了新的count
)
因此表现起来
会很正常
传入useEffect的dependencies:
state: 如果useEffect中又会引起state的变化,将会陷入无限循环当中
function:
如果function
定义在useEffect
里面,则无需添加到依赖中
如果function
定义在component
里面,将会陷入无限循环的渲染当中, 因为function
每次渲染-引用都会发生变化
如果function
定义在component
外面,则只会渲染一次
如果必须把function
定义在component
里面,但是又不想无限循环,则在使用useCallback
或者useMemo
import React , { useState , useEffect } from 'react' ;
const Counter = ( ) => {
const [ count , setCount ] = useState ( 0 ) ;
console . log ( 'render' ) ;
useEffect ( ( ) => {
console . log ( 'componentDidUpdate' )
const id = setInterval ( ( ) => {
console . log ( count )
setCount ( count + 1 )
} , 1000 ) ;
return ( ) => {
clearInterval ( id ) ;
console . log ( 'clean' ) ;
}
} , [ count ] ) ; // 每次变化将会触发unmount和componentDidUpdate
return (
< h1 >
{ count }
</ h1 >
)
}
export default Counter ;
Solution 2: remove dependency
Solution 1
有什么问题?
按照直觉,状态的更新引起页面的重新渲染我们可以理解
但是状态的更新却同时引起了页面的unmount
和componentDidUpdate
, 其实是没有必要的过程
我们希望只有一个setInterval
,但是页面依然会根据状态变化而重新熏染
Solution A: 使用setState的函数模式
import React , { useState , useEffect } from 'react' ;
const Counter = ( ) => {
const [ count , setCount ] = useState ( 0 ) ;
console . log ( 'render' ) ;
useEffect ( ( ) => {
console . log ( 'componentDidUpdate' )
const id = setInterval ( ( ) => {
setCount ( count => count + 1 ) ; // 仅仅描述了一种行为,每次执行时都会自动读取当时最新count值
} , 1000 ) ;
return ( ) => {
clearInterval ( id ) ;
console . log ( 'clean' ) ;
}
} , [ ] ) ; // 是无法触发页面的unmount和componentDidUpdate的
return (
< h1 >
{ count }
</ h1 >
)
}
export default Counter ;
这种情况适用于: state相互之间存在依赖,需要传入多个进入useEffect
的dependency
建议: 能把initialState
和reducer
都放在component
外面的,就尽量放在外面
因为每次渲染都会形成新的initialState
和reducer
, 是没有必要的
你会发现这里state
的变化也不会引起unmount
的发生
import React , { useEffect , useReducer } from 'react' ;
const initialState = { count : 0 , step : 1 } ;
const reducer = ( state , action ) => {
switch ( action . type ) {
case 'TICK' : return { ...state ,
count : state . count + state . step } ;
case 'STEP' : return { ...state ,
count : state . count + state . step + action . payload ,
step : state . step + action . payload }
default : throw new Error ( ) ;
}
}
const Counter = ( ) => {
const [ state , dispatch ] = useReducer ( reducer , initialState ) ;
console . log ( 'render' ) ;
useEffect ( ( ) => {
console . log ( 'componentDidUpdate' )
const id = setInterval ( ( ) => {
dispatch ( { type : 'TICK' } ) ; // 读取上一次最新的state交给了userReducer去完成
} , 1000 ) ;
return ( ) => {
clearInterval ( id ) ;
console . log ( 'clean' ) ;
}
} , [ ] ) ; // 是无法触发页面的unmount和componentDidUpdate的
return (
< h1 >
{ state . count } < br />
< button onClick = { ( ) => { dispatch ( { type : 'STEP' , payload : 1 } ) } } > Step Up</ button >
</ h1 >
)
}
export default Counter ;
case 2: compute state based on props
Solution 1: pass props to dispatch
这种情形跟case 1
类似:
如果不将 step
例如dependency
, 那么不会触发unmount
, setInterval
全局只有一 个,闭包闭的step
的值一直停留在最初的值上面,因为props
的变化并不 能引起count
的变化
如果将 step
列入dependency
, 将会触发unmount
, setInterval
一直会clean
和重建
,读取到的props也是最新的,但是有一定性能的损耗
.
import React , { useEffect , useReducer } from 'react' ;
const initialState = { count : 0 } ;
const reducer = ( state , action ) => {
switch ( action . type ) {
case 'TICK' : return { ...state , count : state . count + action . step } ;
default : throw new Error ( ) ;
}
}
const Counter = ( props : any ) => {
const { step } = props ;
const [ state , dispatch ] = useReducer ( reducer , initialState ) ;
console . log ( 'render' ) ;
useEffect ( ( ) => {
console . log ( 'componentDidUpdate' )
const id = setInterval ( ( ) => {
dispatch ( { type : 'TICK' , step } ) ; // 读取上一次最新的state交给了userReducer去完成
} , 1000 ) ;
return ( ) => {
clearInterval ( id ) ;
console . log ( 'clean' ) ;
}
} , [ step ] ) ; // react 保证dispatch在每次渲染中都是一样的
return (
< h1 >
{ state . count } < br />
{ /* <button onClick={() => { dispatch({ type: 'STEP', payload: step }) }}>Step Up</button> */ }
</ h1 >
)
}
export default Counter ;
Solution 2: move reducer inside component
因为dependency
为[]
, 因此全局只有一个setInterval
props
的更新本身就会引起重新渲染
,因此reducer
里面读到的永远是最新的props
import React , { useEffect , useReducer } from 'react' ;
const initialState = { count : 0 } ;
const Counter = ( props : any ) => {
const { step } = props ;
const reducer = ( state , action ) => {
switch ( action . type ) {
case 'TICK' : return { ...state , count : state . count + step } ;
default : throw new Error ( ) ;
}
}
const [ state , dispatch ] = useReducer ( reducer , initialState ) ;
console . log ( 'render' ) ;
useEffect ( ( ) => {
console . log ( 'componentDidUpdate' )
const id = setInterval ( ( ) => {
dispatch ( { type : 'TICK' } ) ;
} , 1000 ) ;
return ( ) => {
clearInterval ( id ) ;
console . log ( 'clean' ) ;
}
} , [ ] ) ;
return (
< h1 >
{ state . count } < br />
</ h1 >
)
}
export default Counter ;
case 3: put function outside of effect
特征:
函数
本身的值 不会发生变化(stable )
函数本身未引用
任何外部变量 ,
useEffect
中引用外部
的变量 (仅仅是变量不包括函数 )会被提醒
要加入到dependency
中
所以这里的fetchData
并未 被要求加入到dep
s中
因为fetchData
定义是在component
内部,useEffect
的外部,如果加入到dep
s中,一定会引起页面的无限刷新
import React , { useState , useEffect } from 'react' ;
import axios from 'axios' ;
const SearchResult = ( ) => {
const [ data , setData ] = useState ( { hits : [ ] } ) ;
const fetchData = async ( ) => {
const result = await axios ( 'https://hn.algolia.com/api/v1/search?query=react' )
setData ( result . data ) ;
}
useEffect ( ( ) => {
// 将fetchData放到这里其实效果一样,性能也基本上没有差异
fetchData ( ) ; // 未引用任何变量,所以不会被提醒加入到deps中
} , [ ] ) // 如果把函数加入到deps中,反而会造成页面的无限循环
return (
< div > { data . hits . map ( ( item : any ) => (
< div > { item . title } </ div >
) ) } </ div >
)
}
export default SearchResult ;
case 4: function references any external variables
Solution : put function inside useEffect with variable as deps in useEffect
import React , { useState , useEffect } from 'react' ;
import axios from 'axios' ;
const SearchResult = ( ) => {
const [ query , setQuery ] = useState ( 'react' ) ;
const [ data , setData ] = useState ( { hits : [ ] } ) ;
useEffect ( ( ) => {
const fetchData = async ( ) => { // define inside useEffect
const result = await axios ( 'https://hn.algolia.com/api/v1/search?query=' + query ) ;
setData ( result . data ) ;
}
fetchData ( ) ;
} , [ query ] ) // add external variables here
return (
< div >
< ul >
{ data . hits . map ( ( item : any ) => (
< li key = { item . objectID } > { item . title } </ li >
) ) }
</ ul >
{ query }
</ div >
)
}
export default SearchResult ;
case 5: function references any external variables but is used by multiple useEffects
特征:
如果有一个函数必须放到useEffect之外,例如想要在多个useEffect中复用这个函数
该函数要依赖外部变量时
解决:
useCallback的使用能够使得函数在每次页面渲染时候保持稳定,因此不会引起页面的再次渲染
useCallback的本质其实是将原本定义在useEffect中的函数分离出来的一层,最终还是依赖于变量的
问题:
只是'缓存'了变量,并未缓存api请求的结果,可以进一步优化
import React , { useState , useEffect , useCallback } from 'react' ;
import axios from 'axios' ;
const SearchResult = ( ) => {
const [ query , setQuery ] = useState ( 'react' ) ;
const [ data1 , setData1 ] = useState ( { hits : [ ] } ) ;
const [ data2 , setData2 ] = useState ( { hits : [ ] } ) ;
const MemorizedGetFetchUrl = useCallback ( ( ) => 'https://hn.algolia.com/api/v1/search?query=' + query , [ query ] ) ;
useEffect ( ( ) => {
const fetchData = async ( ) => {
const result = await axios ( MemorizedGetFetchUrl ( ) ) ;
setData1 ( result . data ) ;
}
fetchData ( ) ;
} , [ MemorizedGetFetchUrl ] )
useEffect ( ( ) => {
const fetchData = async ( ) => {
const result = await axios ( MemorizedGetFetchUrl ( ) )
setData2 ( result . data ) ;
}
fetchData ( )
} , [ MemorizedGetFetchUrl ] )
return (
< div >
< ul >
{ data1 . hits . map ( ( item : any ) => (
< li key = { item . objectID } > { item . title } </ li >
) ) }
</ ul >
< br />
< ul >
{ data2 . hits . map ( ( item : any ) => (
< li key = { item . objectID } > { item . title } </ li >
) ) }
</ ul >
</ div >
)
}
export default SearchResult ;
Optimization: useCallback
这里优化,本质上是因为deps的缘故,只有在query
改变时才会进行api请求
import React , { useState , useEffect , useCallback } from 'react' ;
import axios from 'axios' ;
const SearchResult = ( ) => {
const [ query1 , setQuery1 ] = useState ( 'react' ) ;
const [ query2 , setQuery2 ] = useState ( 'redux' ) ;
const [ refresh , setRefresh ] = useState ( 0 )
const [ data1 , setData1 ] = useState ( { hits : [ ] } ) ;
const [ data2 , setData2 ] = useState ( { hits : [ ] } ) ;
const MemorizedGetFetchUrl = useCallback ( ( query , id ) => {
const fetchData = async ( ) => {
const result = await axios ( 'https://hn.algolia.com/api/v1/search?query=' + query ) ;
if ( id === 1 ) setData1 ( result . data ) ;
if ( id === 2 ) setData2 ( result . data ) ;
}
fetchData ( )
} , [ ] ) ;
useEffect ( ( ) => {
MemorizedGetFetchUrl ( query1 , 1 ) ;
} , [ MemorizedGetFetchUrl , query1 ] )
useEffect ( ( ) => {
MemorizedGetFetchUrl ( query2 , 2 ) ;
} , [ MemorizedGetFetchUrl , query2 ] )
return (
< div >
< ul >
{ data1 . hits . map ( ( item : any ) => (
< li key = { item . objectID } > { item . title } </ li >
) ) }
</ ul >
< br />
< ul >
{ data2 . hits . map ( ( item : any ) => (
< li key = { item . objectID } > { item . title } </ li >
) ) }
</ ul >
< button onClick = { ( ) => { setRefresh ( refresh + 1 ) } } > Re-render Page</ button >
</ div >
)
}
export default SearchResult ;
Case 6: clean up last api influence to avoid Race Condition
何时需要cleanup
函数?
useEffect存在deps
且deps
会变化 的时候
例如在useEffect
中的api请求依赖于某个变量
。
因为无法保证api请求的返回顺序 ,我们需要在新的请求之前,禁止上一次请求的结果对页面元素产生影响 !
代码分析:
本质原因是js的闭包
,闭包闭的variable而不是值
因此在第一次useEffect
执行未完成,但是unmount
已经触发的情况下,第一次·的值会被修改为·
这时候setData
的操作就不会被出发
const SearchResult = ( ) => {
const [ query , setQuery ] = useState ( '' ) ;
const [ search , setSearch ] = useState ( '' )
const [ data , setData ] = useState ( { hits : [ ] } ) ;
const MemorizedGetFetchUrl = useCallback ( ( didCancel ) => {
const fetchData = async ( ) => {
const result = await axios ( 'https://hn.algolia.com/api/v1/search?query=' + search ) ;
! didCancel && setData ( result . data ) ;
! ! didCancel && console . log ( 'cancel setting result' ) ;
}
search !== '' && fetchData ( )
} , [ search ] ) ;
useEffect ( ( ) => {
let didCancel = false ;
! didCancel && MemorizedGetFetchUrl ( didCancel ) ;
return ( ) => {
didCancel = true ;
}
} , [ MemorizedGetFetchUrl ] )
return (
< div >
< ul >
{ data . hits . map ( ( item : any ) => (
< li key = { item . objectID } > { item . title } </ li >
) ) }
</ ul >
< input type = "text" value = { query } onChange = { evt => setQuery ( evt . target . value ) } />
< button onClick = { ( ) => setSearch ( query ) } > Search</ button >
</ div >
)
}
export default SearchResult ;
Case 7: how to choose between useMemo and useCallback
useCallback
可以直接传参数 ,而useMemo
不 能直接
传参数 , 但是二者都可以直接读取定义在文件内的变量
useMemo
重点在与缓存了昂贵计算
函数运行的结果 ,而useCallback
的重点在于缓存了函数本身 ; 使用useCallback无法达到缓存昂贵计算的结果 ;
useCallback
中可以进行api
请求,而useMemo
内部传入函数时只适合做用在渲染过程
中的昂贵计算 上,比如重交互的图表 和动画 等
结论: 单纯为了持久化 函数或者多 个useEffect
函数复用一个函数,使用useCallback
; 为了能够减少昂贵计算的次数,使用useMemo
useEffect
依赖的变动会引起
unmount
render
componentDidUpdate
什么会引起componentWillUnmount
?
setState会引起什么?
render
componentDidUpdate
不会 引起unmount
props的改变会引起什么?
子组件的render
,如果是不必要的,可以使用memo + shallowEqual的方式避免子组件的重新渲染
子组件的componentDidUpdate
不会 引起unmount
不会引起子组件state的reset
import React , { useState , memo } from "react" ;
import shallowCompare from "react-addons-shallow-compare" ;
const App = ( ) => {
const [ a , setA ] = useState ( 0 ) ;
console . log ( "app render" ) ;
return (
< div >
< div > Hello World</ div >
{ a }
< button onClick = { ( ) => setA ( a => a + 1 ) } > change app</ button >
< Counter a = { a } />
</ div >
) ;
} ;
const Counter = memo ( ( { a } : any ) => {
const [ b , setB ] = useState ( a ) ;
console . log ( "counter render" ) ;
return (
< div >
{ b }
< button onClick = { ( ) => setB ( b => b + 1 ) } > change counter</ button >
</ div >
) ;
} , shallowCompare ) ;
export default App ;
为什么要减少dependency?
为了减少dependency变化带来的componentWillUnmount
的执行
useEffect会提醒你把什么东西加入到deps中?
外部变量
包含外部变量 的函数
不提醒 你把不包含外部变量 的函数
加入
什么样的变量适合放入state
中?
定义在hooks
之外,组件之内的变量和函数有什么特征?
经验之谈:
放在deps
里面的不是props
就是state
, 共同特征: 会引起页面的重新渲染
因为component
中的hooks
之外的变量和函数都是不稳定的,会引起无限渲染
如果要放在component
之外,说明不需要被复用,其实可以直接放入到hooks
里面
如果没办法放在component之内,有必须在hooks之外的(复用),则需要使用useCallback
, useMemo
来原始化
本质: hooks的本质就是在immutable中使用mutable
useReducer vs useState: 前者具备了数据在上一次和此次沟通的能力,也便利了状态之间的沟通