1. Hooks详解
Hooks是react16.8版本以后新增的钩子API; 目的:增加代码的可复用性,逻辑性,弥补无状态组件没有生命周期,没有数据管理状态state的缺陷。
为什么要使用Hooks?
- 开发友好,可扩展性强,抽离公共的方法和组件,Hook 使你在无需修改组件结构的情况下复用状态逻辑;
- 函数式编程,将组件中相互关联的部分根据业务逻辑拆分成更小的函数;
- class更多作为语法糖,没有稳定的提案,且在开发过程中会出现不必要的优化点,Hooks无需学习复杂的函数式或响应式编程技术;
1.1 常见Hooks
1.1.1 useState
const [number, setNumber] = useState(0);
- setState支持stateless组件有自己的state;
- 入参:具体值或一个函数;
- 返回值:数组第一项是state值,第二项负责派发数据更新,组件渲染;
注意:setState会让组件重新执行,所以一般需要配合useMemo或useCallback;
const DemoState = (props) => {
/* number为此时state读取值 ,setNumber为派发更新的函数 */
const [number, setNumber] = useState(0) /* 0为初始值 */
return (
<div>
<span>{ number }</span>
<button onClick={ ()=> {
setNumber(number + 1)
console.log(number) /* 这里的number是不能够即使改变的,返回0 */
}}
/>
</div>
)
}
// 当更新函数之后,state的值是不能即时改变的,只有当下一次上下文执行的时候,state值才随之改变
——————————————————————————————————————————
const a =1
const DemoState = (props) => {
/* useState 第一个参数如果是函数 则处理复杂的逻辑,返回值为初始值 */
let [number, setNumber] = useState(()=>{
// number
return a === 1 ? 1 : 2
}) /* 1为初始值 */
return (<div>
<span>{ number }</span>
<button onClick={ ()=>setNumber(number+1) } ></button>
</div>)
}
1.1.2 useEffect
- 使用条件:当组件init、dom render完成、操纵DOM、请求数据(如componentDidMount)等;
- 不限制条件,组件每次更新都会触发useEffect --> componentDidUpDate 与 componentwillreceiveprops;
- useEffect 第一个参数为处理函数,第二个参数接受数组,为限定条件,当数组里的状态变化时触发,为空数组时只在组件初始化时触发;
- useEffect第一个参数有返回时,一般用来消除副作用(如去除定时器、事件绑定等);
* 模拟数据交互 */
function getUserInfo(a)
return new Promise((resolve)=>{
setTimeout(()=>{
resolve({
name:a,
age:16,
})
},500)
})
}
const Demo = ({ a }) => {
const [ userMessage , setUserMessage ] = useState({})
const [number, setNumber] = useState(0)
const div= useRef()
const handleResize =()=>{}
useEffect(()=>{
getUserInfo(a).then(res=>{
setUserMessage(res)
})
console.log(div.current) /* div */
window.addEventListener('resize', handleResize)
/*
只有当props->a和state->number改变的时候 ,useEffect副作用函数重新执行 ,
如果此时数组为空[],证明函数只有在初始化的时候执行一次相当于componentDidMount
*/
},[ a ,number ])
return (<div ref={div} >
<span>{ userMessage.name }</span>
<span>{ userMessage.age }</span>
<div onClick={ ()=> setNumber(1) } >{ number }</div>
</div>)
}
————————————————————————————————————————————————
const Demo = ({ a }) => {
const handleResize =()=>{}
useEffect(()=>{
const timer = setInterval(()=>console.log(666),1000)
window.addEventListener('resize', handleResize)
/* 此函数用于清除副作用 */
return function(){
clearInterval(timer)
window.removeEventListener('resize', handleResize)
}
},[ a ])
return (<div></div>)
}
注意:useEffect无法直接使用 async await
// Bad
useEffect(async ()=>{
/* 请求数据 */
const res = await getUserInfo(payload)
},[ a ,number ])
————————————————————————————————————————————————
useEffect(() => {
// declare the async data fetching function
const fetchData = async () => {
const data = await fetch('https://xxx.com');
const json = await data.json();
return json;
}
// call the function
const result = fetchData()
.catch(console.error);
// ❌ 无效
setData(result);
}, [])
// 改进版
const fetchData = async () => {
const data = await fetch('https://xxx.com');
const json = await response.json();
setData(json);
}
useEffect(() => {
// call the function
fetchData()
// make sure to catch any error
.catch(console.error);;
}, [])
1.1.3 useLayoutEffect
渲染更新之前的useEffect
useEffect:组件更新挂载完成 --> 浏览器dom绘制完成 -> 执行useEffect回调;
useLayoutEffect:组件更新挂载完成 -> 执行useLayoutEffect回调 -> 浏览器DOM绘制完成
const DemoUseLayoutEffect = () => {
const target = useRef()
useLayoutEffect(() => {
/*我们需要在dom绘制之前,移动dom到制定位置*/
const { x ,y } = getPositon() /* 获取要移动的 x,y坐标 */
animate(target.current,{ x,y })
}, []);
return (
<div >
<span ref={ target } className="animate"></span>
</div>
)
}
1.1.4 useRef
用来获取元素、缓存数据。入参可以作为初始值
// 获取元素
const DemoUseRef = ()=>{
const dom= useRef(null)
const handerSubmit = ()=>{
/* <div >表单组件</div> dom 节点 */
console.log(dom.current)
}
return <div>
<div ref={dom} >表单组件</div>
<button onClick={()=>handerSubmit()} >提交</button>
</div>
}
// 缓存数据,小技巧
// 不同于useState,useRef改变值不会使comp re-render
const currenRef = useRef(InitialData)
currenRef.current = newValue
1.1.5 useContext
用来获取父级组件传递过来的context值,这个当前值就是最近的父级组件 Provider 的value。
从parent comp获取ctx方式;
- useContext(Context);
- Context.Consumer;
/* 用useContext方式 */
const DemoContext = ()=> {
const value = useContext(Context);
/* my name is aaa */
return <div> my name is { value.name }</div>
}
/* 用Context.Consumer 方式 */
const DemoContext1 = ()=>{
return <Context.Consumer>
{/* my name is aaa */}
{ (value)=> <div> my name is { value.name }</div> }
</Context.Consumer>
}
export default ()=>{
return <div>
<Context.Provider value={{ name:'aaa' }} >
<DemoContext />
<DemoContext1 />
</Context.Provider>
</div>
}
1.1.6 useReducer
入参:
- 第一个为函数,可以视为reducer,包括state和action,返回值为根据action的不同而改变后的state;
- 第二个为state的初始值;
出参:
- 第一个更新后的state值
- 第二个时派发更新的dispatch函数;执行dispatch会导致组件re-render;(另一个时useState)
const DemoUseReducer = ()=>{
/* number为更新后的state值, dispatchNumbner 为当前的派发函数 */
const [ number , dispatchNumbner ] = useReducer((state, action) => {
const { payload , name } = action
/* return的值为新的state */
switch(name) {
case 'a':
return state + 1
case 'b':
return state - 1
case 'c':
return payload
}
return state
}, 0)
return <div>
当前值:{ number }
{ /* 派发更新 */ }
<button onClick={()=>dispatchNumbner({ name: 'a' })} >增加</button>
<button onClick={()=>dispatchNumbner({ name: 'b' })} >减少</button>
<button onClick={()=>dispatchNumbner({ name: 'c' , payload:666 })} >赋值</button>
{ /* 把dispatch 和 state 传递给子组件 */ }
<MyChildren dispatch={ dispatchNumbner } State={{ number }} />
</div>
}
业务中经常将 useReducer + useContext 代替 Redux
1.1.7 useMemo
用来根据useMemo的第二个参数deps(数组)判定是否满足当前的限定条件来决定是否执行第一个cb;
// selectList 不更新时,不会重新渲染,减少不必要的循环渲染
useMemo(() => (
<div>{
selectList.map((i, v) => (
<span
className={style.listSpan}
key={v} >
{i.patentName}
</span>
))}
</div>
), [selectList])
————————————————————————————————————————————————————
// listshow, cacheSelectList 不更新时,不会重新渲染子组件
useMemo(() => (
<Modal
width={'70%'}
visible={listshow}
footer={[
<Button key="back" >取消</Button>,
<Button
key="submit"
type="primary"
>
确定
</Button>
]}
>
{ /* 减少了PatentTable组件的渲染 */ }
<PatentTable
getList={getList}
selectList={selectList}
cacheSelectList={cacheSelectList}
setCacheSelectList={setCacheSelectList}
/>
</Modal>
), [listshow, cacheSelectList])
————————————————————————————————————————————————————
// 减少组件更新导致函数重新声明
const DemoUseMemo = () => {
/* 用useMemo 包裹之后的log函数可以避免了每次组件更新再重新声明 ,可以限制上下文的执行 */
const newLog = useMemo(() => {
const log = () => {
console.log(123)
}
return log
}, [])
return <div onClick={()=> newLog() } ></div>
}
————————————————————————————————————————————————————
// 如果没有加相关的更新条件,是获取不到更新之后的state的值的
const DemoUseMemo = () => {
const [ number ,setNumber ] = useState(0)
const newLog = useMemo(() => {
const log = () => {
/* 点击span之后 打印出来的number 不是实时更新的number值 */
console.log(number)
}
return log
/* [] 没有 number */
}, [])
return <div>
<div onClick={() => newLog()} >打印</div>
<span onClick={ () => setNumber( number + 1 ) } >增加</span>
</div>
}
1.1.8 useCallback
useMemo返回cb的运行结果;
useCallback返回cb的函数;
import React, { useState, useCallback } from 'react'
function Button(props) {
const { handleClick, children } = props;
console.log('Button -> render');
return (
<button onClick={handleClick}>{children}</button>
)
}
const MemoizedButton = React.memo(Button);
export default function Index() {
const [clickCount, increaseCount] = useState(0);
const handleClick = () => {
console.log('handleClick');
increaseCount(clickCount + 1);
}
return (
<div>
<p>{clickCount}</p>
<MemoizedButton handleClick={handleClick}>Click</MemoizedButton>
</div>
)
}
// MemoizedButton还是重新渲染了
// Index组件state发生变化,导致组件重新渲染;
// 每次渲染导致重新创建内部函数handleClick ,
// 进而导致子组件Button也重新渲染。
import React, { useState, useCallback } from 'react'
function Button(props) {
const { handleClick, children } = props;
console.log('Button -> render');
return (
<button onClick={handleClick}>{children}</button>
)
}
const MemoizedButton = React.memo(Button);
export default function Index() {
const [clickCount, increaseCount] = useState(0);
// 这里使用了`useCallback`
const handleClick = useCallback(() => {
console.log('handleClick');
increaseCount(clickCount + 1);
}, [])
return (
<div>
<p>{clickCount}</p>
<MemoizedButton handleClick={handleClick}>Click</MemoizedButton>
</div>
)
}
2.Hooks的使用建议
2.1 尽量不要用useCallback
- useCallback 大部分场景没有提升性能 2. useCallback让代码可读性变差
2.2 useMemo建议适当使用
在deps不变,且非简单的基础类型运算的情况下建议使用
// 没有使用 useMemo
const memoizedValue = computeExpensiveValue(a, b);
// 使用 useMemo
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
// 如果没有使用 useMemo,computeExpensiveValue 会在每一次渲染的时候执行;
// 如果使用了 useMemo,只有在 a 和 b 变化时,才会执行一次 computeExpensiveValue。
const a = 1;
const b = 2;
const c = useMemo(()=> a + b, [a, b]);
const c = a + b; // 内存消耗少
2.3 useState的正确使用姿势
- 能用其他状态计算出来就不用单独声明状态。一个state必须不能通过其他State/props 直接计算出来,否则就不用定义 state。 2. 保证数据源唯一,在项目中同一个数据,保证只存储在一个地方 3. useState 适当合并
// Example 1
const SomeComponent = (props) => {
const [source, setSource] = useState([
{type: 'done', value: 1},
{type: 'doing', value: 2},
])
const [doneSource, setDoneSource] = useState([])
const [doingSource, setDoingSource] = useState([])
useEffect(() => {
setDoingSource(source.filter(item => item.type === 'doing'))
setDoneSource(source.filter(item => item.type === 'done'))
}, [source])
return (
<div>
.....
</div>
)
}
const SomeComponent = (props) => {
const [source, setSource] = useState([
{type: 'done', value: 1},
{type: 'doing', value: 2},
])
const doneSource = useMemo(()=> source.filter(item => item.type === 'done'), [source]);
const doingSource = useMemo(()=> source.filter(item => item.type === 'doing'), [source]);
return (
<div>
.....
</div>
)
}
// 避免props层层传递,在CR中很难看清楚
// Example 2 searchKey 是由拼接而来所以不必使用useState,直接定义变量即可。
function SearchBox({ data }) {
const [searchKey, setSearchKey] = useState(getQuery('key'));
const handleSearchChange = e => {
const key = e.target.value;
setSearchKey(key);
history.push(`/movie-list?key=${key}`);
}
return (
<input
value={searchKey}
placeholder="Search..."
onChange={handleSearchChange}
/>
);
}
function SearchBox({ data }) {
const searchKey = parse(localtion.search)?.key;
const handleSearchChange = e => {
const key = e.target.value;
history.push(`/movie-list?key=${key}`);
}
return (
<input
value={searchKey}
placeholder="Search..."
onChange={handleSearchChange}
/>
);
}
// url params 和 state重复了
// Example 3 适当合并
const [firstName, setFirstName] = useState();
const [lastName, setLastName] = useState();
const [school, setSchool] = useState();
const [age, setAge] = useState();
const [address, setAddress] = useState();
const [weather, setWeather] = useState();
const [room, setRoom] = useState();
const [userInfo, setUserInfo] = useState({
firstName,
lastName,
school,
age,
address
});
const [weather, setWeather] = useState();
const [room, setRoom] = useState();
// 更新一个时
setUserInfo(s=> ({
...s,
fristName,
}))