Hook
,不在条件、循环或者嵌套函数中使用Hook
Hook
Hook
存储在组件的私有属性中__hooks_list
数组中。读取和存储都依赖currentIndex
,如果hook
的执行顺序改变,currentIndex
获取的hook
可能是完成错误的。
Effect Hook 可以在函数组件中执行一些具有side effect(副作用)的操作
render
和之后的每次update
后运行,React
保证在DOM
已经更新完成后才会回调。useEffect(() => { // 只有组件render后执行 }, []); useEffect(() => { // 只有count改变时才会执行 }, [count]);
useEffect
的第一个参数可以返回一个函数,这个函数会在页面更新渲染后,执行下次useEffect
之前调用。这个函数是对上一次调用useEffect
进行清理。
export default function HookTest() { const [count, setCount] = useState(0); useEffect(() => { console.log(`执行...当前count: ${count}`); return () => { console.log(`清除...当前count: ${count}`); } }, [count]); return ( <div> <p>You clicked {count} times</p> <button onClick-{() => setCount(count + 1)}>click me</button> </div> ); }
执行上面的代码,并点击几次按钮,会得到下面的结果:
执行...当前count: 1 清理...当前count: 1 执行...当前count: 2 清理...当前count: 2 执行...当前count: 3 清理...当前count: 4
如果加上浏览渲染的情况,结果应该是这样的:
页面渲染...1 执行...当前count: 1 页面渲染...2 清理...当前count: 1 执行...当前count: 2 页面渲染...3 清理...当前count: 2 执行...当前count: 3 页面渲染...4 清理...当前count: 3 执行...当前count: 4
那为什么浏览器在渲染完后,再执行清理的方法还能找到上一次的state
呢?原因很简单,我们再useEffect
中返回的是一个函数,形成了一个闭包,这能保证我们上一次执行函数存储的变量不会被销毁和污染。
举例会更好理解:
let flag = 1; let clean; function effect(flag) { console.log(`effect...curFlag: ${flag}`); return function() { console.log(`clean...curFlag: ${flag}`); } } clean = effect(flag); flag = 2; clean(); clean = effect(flag); flag = 3; clean(); clean = effect(flag); clean(); // effect...curFlag: 1 // clean...curFlag: 1 // effect...curFlag: 2 // clean...curFlag: 2 // effect...curFlag: 3 // clean...curFlag: 3
componentDidMount
等价于useEffect
的回调,仅在页面初始化完成后执行一次。当useEffect
的第二个参数传入一个空数组时就可以实现这种效果。
function uesDidMount(callback) { useEffect(callback, []); }
官方不推荐这种写法,因为可能会导致一些错误。
function useUnMount(callback) { useEffect(() => callback, []); }
不像componentDidMount
或者componentDidUpdate
,useEffect
中使用的effect并不会阻止浏览器渲染页面。这让页面渲染看起来更加流程。
红圈中是同步操作
useLayoutEffect
和useEffect
类似,但不同的是:
useEffect
不会阻塞浏览器的重绘useLayoutEffect
会阻塞浏览器的重绘。如果需要手动修改dom
,推荐使用useLayoutEffect
。因为如果在useEffect
中更新dom
,useEffect
不会阻塞浏览器重绘,用户可能会看到因为更新导致的闪烁。使用useRef Hook
,你可以轻松获取dom
的ref
。
export default function Input() { const inputEl = useRef(null); const onButtonClick = () => { inputEl.current.focus(); }; return ( <div> <input ref={inputEl} type="text" /> <button onClick={onButtonClick}>Focus this input</button> </div> ); }
useRef
不仅仅可以用来当作获取dom
的ref
。还可以通过useRef
产生的ref
的current
属性的可变性,用它来保存任意值。
componentDidUpdate
就相当于第一次调用的useEffect
,借助useRef
生成一个标识,来记录是否为第一次执行:
function useDidUpdate(callback, prop) { const init = useRef(true); useEffect(() => { if (init.current) { init.current = false; } else { return callback(); } }, prop) }
在React
中,性能优化点在于:
setState
,就会触发组件的重新渲染,不论state
是否变化基于以上两点,useCallback
和useMemo
就是解决性能问题的杀手锏。
useCallback
和useMemo
的异同:
共同点:
仅仅是依赖数据发生变化时,才会重新计算结果,起到缓存作用。
两者区别:
useMemo
计算结果是return
回来的值,主要用于缓存计算结果的值。应用场景: 需要计算的状态。useCallback
计算结果是函数
,主要用于缓存函数。useCallback
返回的是缓存的函数,最简单的用法:
const fnA = useCallback(fnB, [a]);
当依赖 a
变更时,会返回新的函数。我们无法很好的判断返回函数是否变更,可以借助ES6
新增的数据类型Set
来判断,具体如下:
const set = new Set(); export default function Callback() { const [count, setCount] = useState(0); const [val, setVal] = useState(''); const callback = useCallback(() => { console.log(count); }, [count]); set.add(callback); return( <div> <h4>{count}</h4> <h4>{set.size}</h4> <div> <button onClick={() => setCount(count + 1)}>add</button> <input value={val} onChange={e => setVal(e.target.value)} /> </div> </div> ); }
每次修改count
,set.size
都会+1,这说明useCallback
依赖变量count
变化时,会返回新的函数。而val
变化时,set.size
无变化,说明返回的是缓存的函数。
知道useCallback
特点后,有什么作用呢?
使用场景:有一个父组件,包含子组件,子组件接收一个函数作为peops
。通常而言,如果父组件更新了,子组件也会执行。但大多数情况下,更新是没有必要的。我们可以借助useCallback
来返回函数,然后把这个函数作为props
传递给子组件;这样,子组件就能避免不必要的更新。
function Parent() { const [count, setCount] = useState(0); const [val, setVal] = useState(''); const callback = useCallback(() => { return count; }, [count]); return ( <div> <h4>{count}</h4> <Child callback={callback} /> <div> <button onClick={() => setCount(count + 1)}></button> <input value={val} onChange={e => setVal(e.target.val)} /> </div> </div> ); } function Child({ callback }) { const [count, setCount] = useState(() => callback()); return ( <div>{count}</div> ); }
我们先来看个反例:
export default function withoutMemo() { const [count, setCount] = useState(1); const [val, setVal] = useState(''); function expensive() { console.log('compute'); let sum = 0; for(let i = 0; i < count * 1000; i ++) { sum += i; } return sum; } return ( <div> <h4>{count}-{val}-{expensive()}</h4> <div> <button onClick={() => setCount(count + 1)}></button> <input value={val} onChange={e => setVal(e.target.val)} /> </div> </div> ); }
这里创建了两个state
,然后通过expensive
函数,执行一次昂贵的计算,拿到count
对应的某个值。我们可以看到:无论是修改count
还是val
,由于组件的重新渲染,都会触发expensive
的执行。但是这里的昂贵计算只依赖于count
的值,在val
修改的时候,是没有必要再次计算的。
在这种情况下,我们就可以使用useMemo
,只在count
的值修改时,执行expensive
计算:
export default function withMemo() { const [count, setCount] = useState(1); const [val, setVal] = useState(''); const expensive = useMemo(() => { console.log('compute'); let sum = 0; for (let i = 0; i < count * 100; i++) { sum += i; } return sum; }, [count]); return ( <div> <h4>{count}-{val}-{expensive()}</h4> <div> <button onClick={() => setCount(count + 1)}></button> <input value={val} onChange={e => setVal(e.target.val)} /> </div> </div> ); }
上面我们可以看到,使用useMemo
来执行昂贵的计算,然后将计算值返回,并且将count
作为依赖值传递进去。这样,就只会在count改变的时候触发expensive
执行,在修改val
的时候,返回上一次缓存的值。
同时也可以用来持久化一个执行函数,避免子组件的重复渲染,例如:
function Parent() { const [count, setCount] = useState(1); const [val, setValue] = useState(''); const getNum = useCallback(() => { return Array.from({length: count * 100}, (v, i) => i).reduce((a, b) => a + b); }, [count]); return ( <div> <Child getNum={getNum} /> <div> <button onClick={() => setCount(count + 1)}>+1</button> <input value={val} onChange={e => setValue(e.target.value)}/> </div> </div> ); } const Child = React.memo(function ({ getNum }) { return (<h4>总和:{getNum()}</h4>); });