一、什么是Hook?
Hook 是一个特殊的函数,它可以让你“钩入” React 的特性。例如,useState 是允许你在 React 函数组件中添加 state 的 Hook。
二、为什么要用Hook?
参考官方文档 使用hooks的动机
三、Hook API索引
useState
useEffect
useContext
useReducer
useCallback
useMemo
useRef
useImperativeHandle
useLayoutEffect
useDebugValue
四、useState
4.1 useState 和 setState
useState唯一的参数就是初始 state,或者初始化方法- 与 class 组件中的
setState方法不同,useState不会自动合并更新对象。你可以用函数式的setState结合展开运算符来达到合并更新对象的效果 - 与 class 组件中的
setState方法不同,如果你修改状态的时候,传的状态值没有变化,则不重新渲染
默认情况,只要父组件状态变了(不管子组件依不依赖该状态),子组件也会重新渲染。
思考:类组件、函数组件如何减少不必要的渲染,减少渲染次数
4.2 惰性初始化 state
- initialState 参数只会在组件的初始化渲染中起作用,后续渲染时会被忽略
- 如果初始 state 需要通过复杂计算获得,则可以传入一个函数,在函数中计算并返回初始的 state,此函数只在初始渲染时被调用
4.3 函数式更新
- 如果新的 state 需要通过使用先前的 state 计算得出,那么可以将回调函数当做参数传递给 setState。该回调函数将接收先前的 state,并返回一个更新后的值。
<button onClick={() => setCount(prevCount => prevCount + 1)}>+</button>
五、useEffect与useLayoutEffect
使用 Effect Hook – React (reactjs.org)
5.1 useEffect
5.1.1 无需清除的effect
有时候,我们只想在 React 更新 DOM 之后运行一些额外的代码。 比如发送网络请求,手动变更 DOM,记录日志,这些都是常见的无需清除的操作。因为我们在执行完这些操作之后,就可以忽略他们了。 ——官方文档
与
componentDidMount或componentDidUpdate不同,使用useEffect调度的 effect 不会阻塞浏览器更新屏幕,这让你的应用看起来响应更快。大多数情况下,effect 不需要同步地执行。在个别情况下(例如测量布局),有单独的useLayoutEffectHook 供你使用,其 API 与useEffect相同。 ——官方文档
5.1.2 需要清除的effect
之前,我们研究了如何使用不需要清除的副作用,还有一些副作用是需要清除的。例如订阅外部数据源。这种情况下,清除工作是非常重要的,可以防止引起内存泄露!——官方文档
import {useEffect,useState} from 'react'
export default function Counter(){
let [count,setCount]=useState(2)
useEffect(()=>{
let $timer = setInterval(()=>{
setCount(count=>count+1);
},1000);
// useEffect 如果返回一个函数的话,该函数会在组件卸载和更新时调用
// useEffect 在执行副作用函数之前,会先调用上一次返回的函数
// 如果要清除副作用,要么返回一个清除副作用的函数
return ()=>{
console.log('destroy effect');
clearInterval($timer);
}
},[count])
return <div className="App">
{count}
</div>
}
执行下一个 effect 之前,上一个 effect 就已被清除
5.2 useLayoutEffect
- useEffect 在全部渲染完毕后才会执行
- useLayoutEffect 会在 浏览器 layout 之后,painting 之前执行
- 其函数签名与 useEffect 相同,但它会在所有的 DOM 变更之后同步调用 effect, 可以使用它来读取 DOM 布局并同步触发重渲染
- 在浏览器执行绘制之前 useLayoutEffect 内部的更新计划将被同步刷新
- 尽可能使用标准的 useEffect 以避免阻塞视图更新 , 建议将修改 DOM 的操作里放到 useLayoutEffect,避免引起浏览器的回流重绘
useLayoutEffect执行阶段,DOM已经被修改,但但浏览器渲染线程依旧处于被阻塞阶段,所以还没有发生回流、重绘过程。由于内存中的 DOM 已经被修改,通过useLayoutEffect可以拿到最新的 DOM 节点,并且在此时对 DOM 进行样式上的修改,假设修改了元素的 height,这些修改会和 react 做出的更改一起被一次性渲染到屏幕上,依旧只有一次回流、重绘的代价。 如果放在useEffect里,useEffect的函数会在组件渲染到屏幕之后执行,此时对 DOM 进行修改,会触发浏览器再次进行回流、重绘,增加了性能上的损耗。
5.3 useState与useEffect原理
Hook规则
只在最顶层使用 Hook,不要在循环,条件或嵌套函数中调用 Hook, 确保总是在你的 React 函数的最顶层以及任何 return 之前调用他们。遵守这条规则,你就能确保 Hook 在每一次渲染中都按照同样的顺序被调用。这让 React 能够在多次的useState和useEffect调用之间保持 hook 状态的正确。
1. 为什么只能在函数最外层调用 Hook?为什么不要在循环、条件判断或者子函数中调用。
2. “Capture Value” 特性是如何产生的?
import ReactDOM from 'react-dom';
let memoizedState = []; // hooks 存放在这个数组
let cursor = 0; // 当前 memoizedState 下标
function useState(initialValue) {
memoizedState[cursor] = memoizedState[cursor] || initialValue;
const currentCursor = cursor;
function setState(newState) {
memoizedState[currentCursor] = newState;
render();
}
return [memoizedState[cursor++], setState]; // 返回当前 state,并把 cursor 加 1
}
function useEffect(callback, depArray) {
const hasNoDeps = !depArray;
const deps = memoizedState[cursor];
const hasChangedDeps = deps
? !depArray.every((el, i) => el === deps[i])
: true;
if (hasNoDeps || hasChangedDeps) {
callback();
memoizedState[cursor] = depArray;
}
cursor++;
}
export default function Counter(){
let [count,setCount]=useState(2)
useEffect(()=>{
console.log('...',count)
},[count])
return <div className="App">
{count}
<button onClick={()=>setCount(count+1)}>add</button>
</div>
}
const rootElement = document.getElementById("root");
function render() {
cursor = 0;
ReactDOM.render(<Counter />, rootElement);
}
Q:为什么只能在函数最外层调用 Hook?为什么不要在循环、条件判断或者子函数中调用。
A:memoizedState 数组是按 hook定义的顺序来放置数据的,如果 hook 顺序变化,memoizedState 并不会感知到。
Q:“Capture Value” 特性是什么又是如何产生的?
A:援引文章 精读《useEffect 完全指南》 中对 Capture Value 概念的解释:每次 Render 的内容都会形成一个快照并保留下来,因此当状态变更而 Rerender 时,就形成了 N 个 Render 状态,而每个 Render 状态都拥有自己固定不变的 Props 与 State。
每一次 ReRender 的时候,都是重新去执行函数组件了,对于之前已经执行过的函数组件,并不会做任何操作。
- 每一次渲染都有它自己的 Props 和 State
- 每一次渲染都有它自己的事件处理函数
- 当点击更新状态的时候,函数组件都会重新被调用,那么每次渲染都是独立的,取到的值不会受后面操作的影响
六、useMemo与useCallback
两者都起到缓存的作用,具体有何异同,请继续往下看。
6.1 useMemo
//函数签名
function useMemo<T>(factory: () => T, deps: DependencyList | undefined): T;
先上反例,不使用useMemo
import {useState,useMemo} from 'react'
export function WithoutMemo(){
const [count,setCount]=useState(1);
const [val,setValue]=useState('');
function expensive(){
console.log("WithoutMemo...val变化引起重新计算,无语子.....")
let sum=0;
for(let i=0;i<count*100;i++){
sum+=i;
}
return sum
}
return <div>
<h4>{count}-{val}-{expensive()}</h4>
<div>
<button onClick={()=>setCount(count+1)}>+</button>
<input value={val} onChange={e=>setValue(e.target.value)}/>
</div>
</div>
}
使用useMemo优化
export function WithMemo(){
const [count,setCount]=useState(1);
const [val,setValue]=useState('');
const expensive=useMemo(()=>{
console.log("WithMemo...expensive.....")
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=>setValue(e.target.value)}/>
</div>
</div>
}
6.2 useCallback
useCallback同样会缓存,与useMemo的区别在于,它缓存的是函数
//函数签名
function useCallback<T extends (...args: any[]) => any>(callback: T, deps: DependencyList): T;
举例
let set=new Set()
export function Callback(){
const [count,setCount]=useState(1);
const [val,setValue]=useState('');
// const cb=()=>{
// alert(count)
// }
const cb=useCallback(()=>{
alert(count)
},[count])
//用set去记录函数的引用
set.add(cb)
return <div>
<h4>useCallback>>>>>{count}-{val}</h4>
<h4>第{set.size}次渲染useCallback</h4>
<div>
<button onClick={()=>setCount(count+1)}>+</button>
<input value={val} onChange={e=>setValue(e.target.value)}/>
</div>
</div>
}
其实使用useCallback会造成额外的性能;
因为增加了额外的deps变化判断
6.3 useMemo和useCallback的区别,什么时候使用?
这两个hooks都返回缓存的值,useMemo返回缓存的变量,useCallback返回缓存的函数。
useMemo应用场景,依赖状态需要计算。
useCallback其实是利用memoize减少不必要的子组件重新渲染
useCallback应该和React.memo配套使用
import React,{memo,useCallback,useState,useEffect} from "react"
// 没有用memo,子组件会渲染
function ListItem(props){
let addItem=props.addItem;
useEffect(()=>{
console.log('子组件ListItem渲染')
},[])
useEffect(()=>{
console.log('子组件渲染')
})
return(
<div onClick={addItem}>{props.children}</div>
)
}
// const ListItem=memo((props)=>{
// let addItem=props.addItem;
// useEffect(()=>{
// console.log('子组件ListItem渲染')
// },[])
// useEffect(()=>{
// console.log('子组件渲染')
// })
// return(
// <div onClick={addItem}>{props.children}</div>
// )
// })
let count=0,set=new Set();
function List(){
let [list,setList]=useState([]);
let [name,setName]=useState('Mary');
useEffect(()=>{
setList([
'微笑',
'上班',
'吃饭',
'下班',
'嗨',
])
},[])
const addI=useCallback(()=>{
list.push('行程'+count++)
setList([...list])
},[list])
const modifyName=()=>{
setName('Mary'+(++count))
}
set.add(addI)
return <>
<h4>{set.size}</h4>{/*set的size没有变化,说明addI的值并没有改变,但是ListItem组件还是渲染了 */}
{list.map((item,index)=>{
return <ListItem key={index} addItem={addI}>{item}</ListItem>
})}
名字:{name} <button onClick={modifyName}>修改</button>
</>
}
export default List
七、useRef、useImperativeHandle、forwardRef
7.1 useRef
useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue)。返回的 ref 对象在组件的整个生命周期内保持不变。
//官方例子
function TextInputWithFocusButton() {
const inputEl = useRef(null);
const onButtonClick = () => {
// `current` 指向已挂载到 DOM 上的文本输入元素
inputEl.current.focus();
};
return (
<>
<input ref={inputEl} type="text" />
<button onClick={onButtonClick}>Focus the input</button>
</>
);
}
7.2 forwardRef
- 因为函数组件没有实例,所以函数组件无法像类组件一样可以接收 ref 属性
function Parent() {
return (
<>
// <Child ref={xxx} /> 这样是不行的
<Child />
<button>+</button>
</>
)
}
- forwardRef 可以在父组件中操作子组件的 ref 对象
- forwardRef 可以将父组件中的 ref 对象转发到子组件中的 dom 元素上
- 子组件接受 props 和 ref 作为参数
function Child(props,ref){
return (
<input type="text" ref={ref}/>
)
}
Child = React.forwardRef(Child);
function Parent(){
let [number,setNumber] = useState(0);
// 在使用类组件的时候,创建 ref 返回一个对象,该对象的 current 属性值为空
// 只有当它被赋给某个元素的 ref 属性时,才会有值
// 所以父组件(类组件)创建一个 ref 对象,然后传递给子组件(类组件),子组件内部有元素使用了
// 那么父组件就可以操作子组件中的某个元素
// 但是函数组件无法接收 ref 属性 <Child ref={xxx} /> 这样是不行的
// 所以就需要用到 forwardRef 进行转发
const inputRef = useRef();//{current:''}
function getFocus(){
inputRef.current.value = 'focus';
inputRef.current.focus();
}
return (
<>
<Child ref={inputRef}/>
<button onClick={()=>setNumber({number:number+1})}>+</button>
<button onClick={getFocus}>获得焦点</button>
</>
)
}
7.3 useImperativeHandle
useImperativeHandle可以让你在使用 ref 时,自定义暴露给父组件的实例值,不能让父组件想干嘛就干嘛- 在大多数情况下,应当避免使用 ref 这样的命令式代码。useImperativeHandle 应当与 forwardRef 一起使用
- 父组件可以使用操作子组件中的多个 ref
import React,{useState,useEffect,createRef,useRef,forwardRef,useImperativeHandle} from 'react';
function Child(props,parentRef){
// 子组件内部自己创建 ref
let focusRef = useRef();
let inputRef = useRef();
useImperativeHandle(parentRef,()=>(
// 这个函数会返回一个对象
// 该对象会作为父组件 current 属性的值
// 通过这种方式,父组件可以使用操作子组件中的多个 ref
return {
focusRef,
inputRef,
name:'计数器',
focus(){
focusRef.current.focus();
},
changeText(text){
inputRef.current.value = text;
}
}
});
return (
<>
<input ref={focusRef}/>
<input ref={inputRef}/>
</>
)
}
Child = forwardRef(Child);
function Parent(){
const parentRef = useRef();//{current:''}
function getFocus(){
parentRef.current.focus();
// 因为子组件中没有定义这个属性,实现了保护,所以这里的代码无效
parentRef.current.addNumber(666);
parentRef.current.changeText('<script>alert(1)</script>');
console.log(parentRef.current.name);
}
return (
<>
<ForwardChild ref={parentRef}/>
<button onClick={getFocus}>获得焦点</button>
</>
)
}
7.4 useRef使用
除了访问dom外,还能这么用...
//思考:如何使setTimeout时,打印出最新的number
function Counter2(){
let [number,setNumber] = useState(0);
function alertNumber(){
setTimeout(()=>{ // alert 只能获取到点击按钮时的那个状态
alert(number); },3000);
}
return ( <>
<p>{number}</p>
<button onClick={()=>setNumber(number+1)}>+</button>
<button onClick={alertNumber}>alertNumber</button>
</> )
}
本质上,
useRef就像是可以在其.current属性中保存一个可变值的“盒子”。useRef()比ref属性更有用。它可以很方便地保存任何可变值,其类似于在 class 中使用实例字段的方式。
这是因为它创建的是一个普通 Javascript 对象。而
useRef()和自建一个{current: ...}对象的唯一区别是,useRef会在每次渲染时返回同一个 ref 对象。
请记住,当 ref 对象内容发生变化时,
useRef并不会通知你。变更.current属性不会引发组件重新渲染。如果想要在 React 绑定或解绑 DOM 节点的 ref 时运行某些代码,则需要使用回调 ref 来实现。
//利用useRef获取
import React,{useState,useRef,useEffect} from 'react'
export default function Counter(){
const [text,setText]=useState('');
const preRef = useRef('');
useEffect(()=>{
preRef.current=text;
})
const preText=preRef.current;
return <div>
<div>修改前:{preText}</div>
<div>修改后:{text}</div>
<input type="text" onBlur={e=>setText(e.target.value)}/>
</div>
}
八、自定义hook
-
自定义 Hook 更像是一种约定,而不是一种功能。如果函数的名字以 use 开头,并且调用了其他的 Hook,则就称其为一个自定义 Hook
-
有时候我们会想要在组件之间重用一些状态逻辑,之前要么用 render props ,要么用高阶组件,要么使用 redux
将上面的例子中获取修改前值的逻辑抽取出来,实现一个自定义hook
// 自定义hook
function usePre(value){
const ref = useRef('');
useEffect(()=>{
ref.current=value;
})
return ref.current
}
export default function Counter(){
const [text,setText]=useState('');
const preText=usePre(text);
return <div>
<div>修改前:{preText}</div>
<div>修改后:{text}</div>
<input type="text" onBlur={e=>setText(e.target.value)}/>
</div>
}
最后
谢谢!
关于思考======
减少渲染
默认情况,只要父组件状态变了(不管子组件依不依赖该状态),子组件也会重新渲染
一般的优化:
- 类组件:可以使用
pureComponent;- 函数组件:使用
React.memo,将函数组件传递给memo之后,就会返回一个新的组件,新组件的功能:如果接受到的属性不变,则不重新渲染函数;但是怎么保证属性不会变尼?这里使用 useState ,每次更新都是独立的,
const [number,setNumber] = useState(0)也就是说每次都会生成一个新的值(哪怕这个值没有变化),即使使用了React.memo,也还是会重新渲染 更深入的优化:
- useCallback:接收一个内联回调函数参数和一个依赖项数组(子组件依赖父组件的状态,即子组件会使用到父组件的值) ,useCallback 会返回该回调函数的 memoized 版本,该回调函数仅在某个依赖项改变时才会更新
- useMemo:把创建函数和依赖项数组作为参数传入
useMemo,它仅会在某个依赖项改变时才重新计算 memoized 值。这种优化有助于避免在每次渲染时都进行高开销的计算