React笔记(六)
1.React Hooks
- 在之前的React中,函数式组件相对于类式组件,缺少了很多部分,比如函数式组件内没有状态,没有生命周期,没有this等。这些导致了React函数式组件只能进行简单的展示,而无法实现通过状态切换或管理页面。而编写类式组件,又使得程序过重。因此React在16.8之后提供了一个可以在不编写类式组件的情况下,使用state或其他的React特性的方法:Hooks。
- 在使用时,需要哪些功能,就可以将对应的Hooks引入到函数式组件内,甚至在有特殊需要时,书写自己的Hooks。
2.基础Hooks
-
useState
用来控制函数组件中的状态。在之前的函数组件中,是不存在state的。但当使用这个Hooks后,在函数式组件内部也会存在有可以读写的state状态。具体使用方法为const [xxx,setXxx] = React.useState(initValue);`
其中
xxx
代表函数组件内部当前的状态值,setXxx
代表更新此状态值的函数,initValue
代表第一次初始化时的状态值。在函数式组件中,如果需要多个状态值,可以使用多次useState
定义多个状态值与用于更改状态值的函数。并且区别于类式组件内的state,在修改状态时传给setState
的是一个state对象;函数式组件在使用setXxx
的时候传入为此方法对应的状态值。下面为一个示例。class Demo extends Component { state ={ count:0 }; render() { const {count} = this.state; return <div> <p>{this.state.count}</p> <button onClick={()=>{this.setState({count:count+1})}}>点我加一</button> </div> } } function DemoF(props) { const [ count, setCount ] = React.useState(0); return (<div> <p>{count}</p> <button onClick={() => { setCount(count + 1) }}>点我加一</button> </div>) }
两种组件都实现了点击按钮将数字加1的功能,相比较,函数式组件的代码量更少,结构更简单。在使用
State Hook
时也需要注意一点:setXxx与setState相似,都是异步操作,那么也就是说,如果在单一操作内对相同的状态值重复更改,其会进行覆盖。export default function DemoF(props) { const [ count, setCount ] = React.useState(0); //使用直接赋状态值的方法,后项覆盖前项,结果+1 function addCount(){ setCount(count + 1); setCount(count + 1); } //根据旧状态值更新状态值,结果+2 function addCountF(){ setCount(pre=>pre + 1); setCount(pre=>pre + 1); } //在定时器中操作状态,状态更新是同步的,结果+2 function addCountAs(){ setTimeout(()=>{ setCount(pre=>pre + 1); setCount(pre=>pre + 1); },1000); } //在定时器中操作状态,状态更新是同步的,结果+2 function addCountAsF(){ setTimeout(() => { setCount(pre=>pre + 1); setCount(pre=>pre + 1); }, 1000); } return (<div> <p>{count}</p> <button onClick={addCount}>点我加二(直接赋状态值)</button> <button onClick={addCountF}>点我加二(跟据旧状态值更新状态值)</button> <button onClick={addCountAs}>点我加二(直接赋状态值,异步)</button> <button onClick={addCountAsF}>点我加二(跟据旧状态值更新状态值,异步)</button> </div>) }
-
useEffect
可以在函数组件中执行副作用操作(例如发送ajax请求,设置订阅/启动定时器,手动更改真实DOM等)。这个Hook相当于类组件的生命周期函数ComponentDidMount
,ComponentDidUpdate
,ComponentWillUnmount
。并且也可以在函数式组件中使用多次。export default function Root(props) { const [show, setShow] = React.useState(true); return <div> {show && <DemoF />} <button onClick={() => { setShow(false) }}>点我卸载组件</button> </div> } function DemoF(props) { const [count, setCount] = React.useState(0); const [time, setTime] = React.useState(new Date()); function addCount() { setCount(count + 1); } //监控着count状态值的变化,一但发生变化,就进行真实DOM的更新 //充当着ComponentDidUpdate的作用,但更为精确,只会监控配置的状态值的变化 React.useEffect(() => { console.log('ComponentDidUpdate'); document.title = `你已经点了${count}次了`; }, [count]); //不监控任何状态值的变化,也就是说在整个周期中,只运行一次 //充当着ComponentDidMount的作用 //返回值的函数代表着组件卸载前进行的,充当着ComponentWillUnmount的作用 React.useEffect(() => { console.log('ComponentDidMount'); var timer = setInterval(() => { setTime(new Date()); }, 1000); return function () { console.log('ComponentWillUnmount'); clearInterval(timer); } }, []); return (<div> <p>{count}</p> <p>{time.toString()}</p> <button onClick={addCount}>点我加一,同时刷新title</button> </div>) }
同样对于useEffect也存在着一部分补充。例如,useEffect所监控内容的变化时,其对比前后内容是否发生变化所采取的是浅比较,也就是说,如果是引用类型,其比较的是地址是否发生了变化,这与setState触发render相同。此外监控的内容并不局限于使用state Hook创建的状态值,也可以状态值内部的属性,或是一些变量。
export default function DemoF(props) { const [count, setCount] = React.useState({value:0}); //由于count的地址未发生改变,因此即使count内部的value发生了改变,也不会触发useEffect //并且当将useEffect中监控的内容改成count.value时,其也不会触发 React.useEffect(() => { console.log('ComponentDidUpdate'); document.title = `你已经点了${count.value}次了`; }, [count]); //当仅使用useState时,也需要注意此项内容,因为其也不会重新渲染 function addCount(){ console.log('点击',count.value); count.value++; setCount(count); } return (<div> <p>{count.value}</p> <button onClick={addCount}>点我加一,同时刷新title</button> </div>) }
另外,对于useEffect返回值,其不仅仅只应用于组件卸载前,甚至可以用于消除状态带来的副作用。当状态值由当前向另一个新的变化时,首先触发的是页面的重新渲染,在之后会清除掉原状态的副作用,最后为函数添加一个新的副作用。也就是:render=>clear prev effect=>add next effect。
export default function DemoF(props) { const [count, setCount] = React.useState(0); React.useEffect(() => { //最后触发 console.log('ComponentDidUpdate'); document.title = count; return function(){ //其次触发 console.log('clear inner',count); document.title = ''; } }, [count]); function addCount(){ setCount(count+1); } //首先触发 console.log('render'); return (<div> <button onClick={addCount}>点我title+1</button> </div>) }
-
useContext
其用来实现祖组件与后代组件的通信,在类式组件中使用this.context
来获取传入的context,但在函数组件中由于不存在this,所以无法使用此方法,但通过使用Context Hook可以实现对此内容的应用const ValueVontext = React.createContext(); export default function Grand(props) { const value = '内容'; return (<div> 我是祖组件直接获取:{value} <ValueVontext.Provider value={value}> <Parent value={value}/> </ValueVontext.Provider> </div>) } function Parent(props){ return <div> 我是父组件从props获取:{props.value} <Child/> </div> } function Child(props){ //接收内容:传入Context const value = React.useContext(ValueVontext); return <div> 我是子组件从context获取:{value} </div> }
其整体与类式组件中的context用法一致,只有接收时不一样。并且需要注意的是,对于调用此Hook的组件,在
context
变化时,其一定会进行重新渲染。
3.额外Hooks
-
useReducer
,其实现了Redux的状态管理的功能,具体的使用方法为//state:管理的状态 //dispatch:为此状态绑定action的方法 //reducer:形为(state, action) => newState的方法,接收action与旧state并返回新state //initialArg:默认初始值 //init:处理默认初始值,并返回一个新的初始值 const [state, dispatch] = useReducer(reducer, initialArg, init);
需要注意的是,由于React的浅比较,因此当调用了reducer后的state与原state相同,此组件依旧不会进行再次渲染。
function reducer(state,action){ const {count} = state; switch (action.type) { case 'add': return {count:count+1}; case 'sub': return {count:count-1}; case 'reset': return {count:action.count}; // 惰性初始化 // return initState(action.count); default: throw new Error(); } } function initState(count){ return {count:count}; } export default function DemoF(props) { const [state,dispatch] = React.useReducer(reducer,{count:0}); // 惰性初始化 // const [state,dispatch] = React.useReducer(reducer,0,initState); return <div> <p>{state.count}</p> <button onClick={()=>{dispatch({type:'add'})}}>加一</button> <button onClick={()=>{dispatch({type:'sub'})}}>减一</button> <button onClick={()=>{dispatch({type:'reset',count:0})}}>重置</button> </div> }
-
useMemo
,其常常应用于性能优化,其把创建函数与函数的依赖项数组作为参数传入,在使用时与useEffect
类似。但useEffect执行的是画面的副作用,而useMemo是返回一个记忆值(当后项相同时,前项返回值不变);类似于一个纯函数的功能。//在父组件中点击按钮修改的是count,不会修改子组件中使用的name //但子组件依旧重新渲染了,这种就是不必要的渲染,因此需要使用memo。 export default function Demo(props){ const [name,setName] = React.useState('xxx'); const [count,setCount] = React.useState(0); return <div> <p>{count}</p> <button onClick={()=>{setCount(count+1)}}>点我加一</button> <Child name={name}/> </div> } function Child(props){ console.log('childRender了'); return <p>{props.name}</p> }
对上面这段代码进行简单的修改后。
export default function Demo(props){ const [name,setName] = React.useState('xxx'); const [count,setCount] = React.useState(0); //使用useMemo,只有后项name改变了,前项才会改变 //返回的JSX元素节点也随之改变,在name不变的情况下减少了页面渲染 const ChildEle = React.useMemo(()=>{ return <Child name={name}/>; },[name]) return <div> <p>{count}</p> <button onClick={()=>{setCount(count+1)}}>点我加一</button> {ChildEle} </div> } function Child(props){ console.log('childRender了'); return <p>{props.name}</p> }
虽然上面这段代码也可以通过
useEffect
实现同样的效果,但useEffect
面向的更多的是副作用,相比较而言,memo
更合适。并且在下面这个例子中也可以看出memo
执行于子页面刷新之前,其更像类式组件生命周期中的shouldComponentUpdate
。export default function Demo(props){ const [name,setName] = React.useState('xxx'); const [count,setCount] = React.useState(0); const ChildEleM = React.useMemo(()=>{ console.log('memo 替换') return <Child name={name}/>; },[name]); var ChildEleE = <Child name={name}/>; React.useEffect(()=>{ console.log('Effect 替换') ChildEleE = <Child name={name}/>; },[name]); return <div> <p>{count}</p> <button onClick={()=>{setCount(count+1)}}>点我加一</button> <button onClick={()=>{setName([...name,'x'])}}>点我改名</button> {ChildEleM} </div> } function Child(props){ console.log('childRender了'); return <p>{props.name}</p> }
-
useRef
与类式中的createRef
相同,创建一个Ref,当挂载上之后,会将这个Ref中的current
指向被挂载的元素。同样也不可以对函数式组件使用ref
属性。export default function Demo(props){ const ChildRef = React.useRef(); return <div> <input ref={ChildRef}/> <button onClick={()=>{ChildRef.current.focus()}}>聚焦输入框</button> </div> }
-
但凡事无绝对,在使用
forwardRef
创建的函数式组件就可以使用ref属性。forwardRef
接收一个渲染函数作为参数,这个渲染函数在函数式组件的基础上,增加了一个ref参数用于转发,将ref参数传递给内部的元素。export default function Demo(props){ const ChildRef = React.useRef(); return <div> <Child ref={ChildRef}/> <button onClick={()=>{ChildRef.current.focus()}}>聚焦输入框</button> <button onClick={()=>{console.log(ChildRef);ChildRef.current.value='123'}}>填写内容</button> </div> } function Child(props,ref){ return <input ref = {ref}/> } Child = React.forwardRef(Child);
但此时,挂载在父组件的ref上的元素过于庞大,这时可以采用
useImperativeHandle
来控制暴露给父组件的实例值。用法为//ref=>ref对象 //createHandle=>创建自定义的实例对象 //deps=>依赖项:可选 React.useImperativeHandle(ref, createHandle, [deps])
现在上方的示例中,父组件可以聚焦子组件的输入框,或为其填写内容。但如果在子组件中存在着两个输入框,父组件可以分别为两个子组件填写内容,这时可以使用这个Hook。
export default function Demo(props){ const ChildRef = React.useRef(); return <div> <Child ref={ChildRef}/> <button onClick={()=>{ChildRef.current.focus()}}>聚焦输入框</button> {/* 点击后,查看控制台,也会发现ref对象中的current比较干净,只存在自定义的一些属性 */} <button onClick={()=>{console.log(ChildRef);ChildRef.current.setValueA('123')}}>填写A内容</button> <button onClick={()=>{ChildRef.current.setValueB('456')}}>填写B内容</button> </div> } function Child(props,ref){ const inputRefA = React.useRef(); const inputRefB = React.useRef(); React.useImperativeHandle(ref,()=>({ focus:()=>inputRefA.current.focus(), setValueA:value=>{inputRefA.current.value=value}, setValueB:value=>{inputRefB.current.value=value} })); return <div> A:<input ref = {inputRefA}/> B:<input ref = {inputRefB}/> </div> } Child = React.forwardRef(Child);
-
useLayoutEffect
用法与useEffect
相同,但其只会在所有的DOM变更后才会同步调用内部effect,可以使用其阻塞浏览器渲染,减少页面的错误渲染。在下面的示例中。由于存在一个耗时的操作,因此数字会先变,但数字的奇偶性没有改变,过了三秒后才正常显示。但当将useEffect替换成useLayoutEffect时,则不会出现此问题。export default function DemoF(props) { const [count, setCount] = React.useState(0); const [name, setName] = React.useState('偶数'); React.useEffect(() => { //存在一个耗时的操作 const pre = Date.now(); while(Date.now() - pre < 3000) {} if(count%2==0){ setName('偶数'); } else{ setName('奇数'); } }, [count]); function addCount(){ setCount(count+1); } return (<div> <p>{name}</p> <p>{count}</p> <button onClick={addCount}>点我+1</button> </div>) }
-
useDebugValue
为自定义Hook服务,其可以在React开发工具中显示hook的标签,一般情况下不推荐使用,只有在自定义Hook被复用时,才最有价值。function useCustom(ok){ //一般用法 React.useDebugValue(ok?'OK':'不OK'); //延迟格式化用法 React.useDebugValue(ok,(ok)=(ok?'OK':'不OK')); return ok?'OK':'不OK'; }
4.Hooks规则
- 由于Hooks本质上是一个函数,因此不要在循环,条件或嵌套语句中使用,这样会导致Hook被重复调用或不调用。同样,也需要在return前使用Hook,防止不被调用。
- 另外Hook只能在React的函数式组件或自定义Hook中被调用
5.自定义Hook
-
通过自定义Hook,可以将一部分组件中的逻辑提取出来,提取到可重用的函数中。在下面的示例中就将判断奇偶数的这部分逻辑提取出来,形成了一个自定义Hook。在定义时,建议使用use开头,使得可以一眼看出是一个自定义Hook。
export default function DemoF(props) { const [count,setCount] = React.useState(0); const a = useCustom(count); return <div> <p>{count}</p> <p>{a}</p> <button onClick={()=>{setCount(count+1)}}>点我加一</button> </div> } function useCustom(num){ const [name,setName] = React.useState(null); React.useEffect(()=>{ setName(num%2==0?'偶数':'奇数'); }); return name; }
参考文章
Hook API 索引 – React (reactjs.org)
useState用法指南_前端精髓的博客-CSDN博客_usestate
useEffect使用指南 - 简书 (jianshu.com)