Hook
Hook 是 React 16.8 的新增特性。它可以让你在不编写 class类组件 的情况下使用 state 以及其他的 React 特性
import React, { useState } from 'react';
function Example() {
// 声明一个新的叫做 “count” 的 state 变量
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
上面的useState就是我们要学习的第一个hook
什么是hook
Hook 是一些可以让你在函数组件里“钩入” React state 及生命周期等特性的函数。Hook 不能在 class 组件中使用
使用Hook的动机
- Hook 使你在无需修改组件结构的情况下复用状态逻辑。
- class类组件不能很好的压缩,并且会使热重载出现不稳定的情况,Hook是更利于优化代码的一套API
React 内置了一些像 useState 这样的 Hook。你也可以创建你自己的 Hook 来复用不同组件之间的状态逻辑。我们会先介绍这些内置的 Hook。
内置Hook
useState
在函数组件里调用它来给组件添加一些内部 state。React 会在重复渲染时保留这个 state。useState 接收一个state的初始值,然后返回一对值:当前state状态值和一个更新该state的函数,你可以在事件处理函数中或其他一些地方调用这个函数。它类似 class 组件的 this.setState
useState()接收的initValue值类型是多样的,我们接下来就来看一下它接收常见的几种类型值的情况:
1、基本类型(数字、布尔、字符串)
import React,{useState} from 'react'
function Fn (){
const [age,setAge] =useState(0) //该组件中有一个age的state值,初始为0
const [name,setName] =useState('哈哈') //该组件中有一个name的state值,初始为”哈哈“
const [isStudent,setIsStudent] =useState(false) //该组件中有一个isStudent的state值,初始为false
return (
<>
<h1>{我叫`${name},今年${age}岁,${isStudent ?'是' : "不是"}一个学生`}</h1>
<button onClick={()=>{setAge(6)}}>点我改变age</button>
<button onClick={()=>{setName(”呵呵’)}}>点我改变name</button>
<button onClick={()=>{setIsStudent(true)}}>点我改变isStudent</button>
</>
)
}
export default Fn;
页面初始状态效果如下:
当分别点击三个button时,就会去调用相应的set方法,改变对应state的值,点击后效果如下:
2、对象类型:
如果state为对象类型,在set改变state值的时候,必须传一个新对象,我们来看下面三种情况
import React,{useState} from 'react'
function Fn (){
const [obj,setObj] =useState({name:'张三'}) //该组件中有一个age的state值,初始为0
return (
<>
<h1>{obj.name}</h1>
<button onClick={()=>{setObj({name:'李四'})}}>直接复制一个新对象</button> //OK
<button onClick={()=>{setObj({...obj,name:"李四"})}}>解构赋值</button> //OK
<button onClick={()=>{setObj(Object.assign(obj,{name:"李四"}))}}>Object.assign复制对象属性</button> //error
</>
)
}
export default Fn;
说明:从上面三个方法可以看出,只有最后一个Object.assign()方法不能正常改变state值,这是因为该方法的返回值还是原先的obj对象,只不过是复制了一个属性而已,所以它不满足上面所说的传值规则
3、函数类型
//useState接收一个函数,state的初始值就看函数的返回值,这样写和直接写0是一样的效果
import React,{useState} from 'react'
function Fn (){
const [fn,setFn] =useState(function(){
return 0
})
return (
<>
<h1>{fn}</h1>
<button onClick={()=>{setFn(6)}}>change</button>
</>
)
}
export default Fn;
_________________________________________________________
//这样写和直接写{name: "张三"}是一样的
import React,{useState} from 'react'
function Fn (){
const [fn,setFn] =useState(function(){
return {name:'张三'}
})
return (
<>
<h1>{fn.name}</h1>
<button onClick={()=>{setFn({name:"李四"})}}>change</button>
</>
)
}
export default Fn;
_________________________________________________________
// 如果接收的函数没有返回值,则该state值就会undefined
import React,{useState} from 'react'
function Fn (){
const [fn,setFn] =useState(function(){
console.log(1111)
})
return (
<>
{console.log(fn,'fn的值')} //undefined
<h1>{fn}</h1>
</>
)
}
export default Fn;
useEffect
什么是 effect 副作用
简单来说,就是当前操作不止会影响函数本身的状态、返回值等,还会对当前函数作用域之外的事务造成影响,比如在函数内进行DOM操作、发送网络请求等
什么是useEffect
它是一个让函数型组件也拥有处理副作用的能力,类似生命周期函数的这么一个hook,它告诉 React 组件需要在渲染后执行某些操作。React 会保存你传递的函数(我们将它称之为 “effect”),并且在执行 DOM 更新之后调用它,来看个简单例子:
function Example() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `You clicked ${count} times`;
});
return (
<>
<div>{count}</div>
<button onClick={()=>{setCount(count+1)}}>change</button>
</>
)
}
export default Example;
代码详解:
我们声明了 count state 变量,并告诉 React 我们需要使用 effect。紧接着传递函数给 useEffect Hook。此函数就是我们的 effect。然后使用 document.title 浏览器 API 设置 document 的 title。我们可以在 effect 中获取到最新的 count 值,因为当 React 渲染组件时,会保存已使用的 effect,并在更新完 DOM 后执行它,这个过程在每次渲染时都会发生,包括首次渲染。
如何控制useEffect中effect函数的执行时机
从刚才的例子中可以看出,如果useEffect单单接受一个函数的话(effect),那么组件每次渲染结束后(render)都会走一遍effect,如果有些时候我们并不想重复去执行effect函数怎么办?要如何去限制effect的执行呢?
答案:通过useEffect的第二个参数来控制effect的执行
当useEffect的第二个参数,传一个空数组,这样的写法就相当于class类组件中的componentDidMount生命周期函数,都只在第一次渲染结束后执行一次
function Example() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `You clicked ${count} times`;
console.log('渲染一次')
},[]);
return (
<>
<div>{count}</div>
<button onClick={()=>{setCount(count+1)}}>change</button>
</>
)
}
export default Example;
如果给useEffect第二个参数传递一个变量的时候,那么只有当该变量改变的时候才会执行effect函数
function Home(){
const [count,setCount] = useState(0)
const [name,setName] = useState("哈哈")
useEffect(()=>{
console.log(count,"执行effect11")
},[count])
return (
<div>
<h1>{count}</h1>
<h1>{name}</h1>
<button onClick={()=>{setCount(count+1)}}>change</button>
</div>
)
}
如果传递的参数不变(比如传一个常量或者一个没做改变的state变量),那就不会执行effect,就和之前传空数组是同样的道理,只会在第一次渲染的时候执行
function Home(){
const [count,setCount] = useState(0)
const [name,setName] = useState("哈哈")
//传一个常量3
useEffect(()=>{
console.log(count,"执行effect11")
},[3])
//传一个别的不变的state变量name,其值一直是’哈哈‘
useEffect(()=>{
console.log(name,"执行effect22")
},[name])
return (
<div>
<h1>{count}</h1>
<h1>{name}</h1>
<button onClick={()=>{setCount(count+1)}}>change</button>
</div>
)
}
useEffect什么时候清理
1、当useEffect函数的第二个参数发生改变时,会先走effect中的清理函数(effect中return的函数),然后再执行effect函数
function Test1() {
const [count, setCount] = useState(0);
useEffect(() => {
console.log(count,"重新执行effect") //后执行,此时的count是最新的state值
return ()=>{
console.log(count,"组件销毁了") //先执行,此时的count是老state值
}
},[count]);
return (
<>
<div>{count}</div>
<button onClick={()=>{setCount(count+1)}}>change</button>
</>
)
}
export default Test1;
2、组件卸载的时候会执行清理,但此时并不会走effect函数的内容,单单只执行return中的清理函数,而且是组件中所有的useEffect中的清理函数都会执行
当点击显示\隐藏按钮后,home组件销毁了,其中两个useEffect的清理函数都会按顺序执行:
function Home(){
const [count,setCount] = useState(0)
const [name,setName] = useState("哈哈")
//第一个useEffect函数
useEffect(()=>{
console.log(count,"执行effect11")
return ()=>{
console.log(count,"第一个useEffect函数执行销毁了")
}
},[count])
//第二个useEffect函数
useEffect(()=>{
console.log(name,"执行effect22")
return ()=>{
console.log(name,"第二个useEffect函数执行销毁了")
}
},[name])
return (
<div>
<h1>{count}</h1>
<h1>{name}</h1>
<button onClick={()=>{setCount(count+1)}}>change</button>
</div>
)
}
function Test1 (){
const [show,setShow] =useState(true)
return (
<div>
{show && <Home />}
<button onClick={()=>{setShow(!show)}}>显示\隐藏</button>
</div>
)
}
useReducer
const [state, dispatch] = useReducer(reducer, initialArg, init);
这个hook是useState 的一种替代方案,适合处理那些比较复杂的state数据,它的作用其实和之前用过的redux是差不多的,其实它是同一个作者写的,该作者原先是redux的开发者后被微软招入参与hooks项目
入参:
1、形如 (state, action) => newState 的 reducer函数,这个函数就是用于处理不同action,然后返回最新的state值;
2、初始state值,可以是常规的数字,也可以是对象,反正就是初始state的值;
3、init函数,这个函数可以用来改变初始的state值,它可以将用于计算 state 的逻辑提取到 reducer 外部,这也为将来对重置 state 的 action 做处理提供了便利,它接受一个参数(useReducer的第二个参数,此时有了init函数,第二个参数就不一定是初始state了,因为init函数的返回值才会被最终作为state的最新值)
出参:
返回当前最新的 state 以及与其配套的 dispatch 方法,例如:
const [ state, dispatch ] = useReducer( reducer,initState,init )
我们来看个简单例子:(点击+,数字+1,点-,数字-1,点重置,数字归0)
const initValue = 0 //定义一个初始值变量
function reducer(state,action){ // reducer函数,接受老的state和当前action(dispatch函数接受的对象),返回最新的state
switch(action.type){
case 'add':
return {count:state.count+1};
case 'reset':
return {count:action.payload};
case 'jian':
return {count:state.count-1};
}
}
function init(initValue){ //init函数,返回处理后的真正的state值
return {count:initValue}
}
function Home(){
const [state,dispatch] = useReducer(reducer,initValue,init)
return (
<div>
<h1>{state.count}</h1>
<button onClick={()=>{dispatch({type:'add'})}}>+</button>
<button onClick={()=>{dispatch({type:'jian'})}}>-</button>
<button onClick={()=>{dispatch({type:'reset',payload:initValue})}}>重置</button>
</div>
)
}
export default Home;
useContext
const value = useContext(MyContext);
接收一个 context 对象(React.createContext 的返回值)并返回该 context 的当前值。当前的 context 值由上层组件中距离当前组件最近的 <MyContext.Provider> 的 value 决定。
当组件上层最近的 <MyContext.Provider> 更新时,该 Hook 会触发重渲染,并使用最新传递给 MyContext provider 的 context value 值。即使祖先使用 React.memo 或 shouldComponentUpdate,也会在组件本身使用 useContext 时重新渲染
import React,{useState,useContext,createContext} from 'react'
const myContext = createContext();
function Demo (){
const { count,setCount } = useContext(myContext);
return (
<>
<h1>{count}</h1>
<button onClick={()=>{setCount(count+1)}}>+</button>
</>
)
}
function Home(){
const [ count,setCount ] = useState(0);
return (
<myContext.Provider value={{count,setCount}}>
<div>
<Demo />
</div>
</myContext.Provider>
)
}
export default Home;
注意:
1、使用useContext()后,子组件不需要像原先React中的context那样用consumer组件包裹来接受value,如果react原先的context还不是很了解的话,可以去看下我之前的博文React组件间通信方式详解
2、useContext(MyContext) 只是让你能够读取 context 的值以及订阅 context 的变化。虽然子组件不需要用到consumer,但你仍然需要在上层组件树中使用 <MyContext.Provider> 来为下层组件提供 context
3、像上面例子中,由于是demo代码,所以组件都写在一块了,所以myContext对象可以直接使用,正常开发中组件与组件之间是分离的文件,myContext对象记得通过props传给子组件,不然子组件中就不能正常使用useContext了
useCallback
它返回的是一个函数;
我们知道像下面这种情况,当父组件count值发生改变时,子组件就会重新渲染,然后子组件中就会重新生成一个全新的fn函数,这样就会浪费增加重复创建函数的开销
import React,{useState,useContext,createContext} from 'react'
function Fu(){
const [ count,setCount ] = useState(0);
const [ name,setName] = useState("ha");
return (
<div>
<h1>{count}</h1>
<button onClick={()=>{setCount(count+1)}}>change</button>
<Zi name={name}/>
</div>
)
}
function Zi({name}){
const fn = ()=>{
console.log("111")
}
return (
<div>
<h1>{name}</h1>
</div>
)
}
export default Fu;
使用useCallback后,简单理解就是它会将传入的回调函数注册到react中,然后当子组件重新渲染的时候,react内部会判断,其依赖的第二个参数是否改变,如果改变,就会重新生成一个新的函数,如果没变,那么该函数变量还是引用的原先的那个函数,看下面这个例子就可以看出,父组件count的改变,会引起子组件的重新渲染,但是在子组件中,此时的fn和handleClick两个函数是相等的,意味着重新渲染后handleClick函数并没有重新生成,因为子组件中useCallback第二个参数是name,name并没有改变,所以并不会重新生成handleClick函数
import React,{useState,useCallback} from 'react'
let fn =null;
function Fu(){
const [ count,setCount ] = useState(0);
const [ name,setName] = useState("ha");
return (
<div>
<h1>{count}</h1>
<button onClick={()=>{setCount(count+1)}}>change</button>
<Zi name={name}/>
</div>
)
}
function Zi({name}){
const handleClick = useCallback(()=>{
console.log(1111)
},[name])
console.log(Object.is(fn,handleClick));
fn=handleClick;
return (
<div>
<h1>{name}</h1>
</div>
)
}
export default Fu;
应用场景小结:
1、像上面例子中那样,由于函数组件如果在组件层面没用useMemo来优化的话(这个后面会讲),那么当父组件状态改变时,子组件就会每次重新渲染执行,如果子组件中有多个函数定义甚至说函数执行逻辑也复杂,这种情况用useCallback就可以有效的减少子组件内部函数多次重复创建和执行的开销;
2、还有一种常见的应用场景就是在向子组件传递函数props时,每次 父组件render 都会创建新函数,导致子组件不必要的渲染,浪费性能,这个时候,useCallback 可以保证,无论 render 多少次,我们的函数都是同一个函数,减小不断创建的开销。
小疑问:组件内的函数可以避免重新生成,那整个组件有没有类似的这块优化呢?比如数据没变,子组件就不重新渲染
答案:当然是有的,类组件可以用pureCompnent,函数组件可以用React.memo()来实现,分别来看下面的效果
function Fu(){
const [ count,setCount ] = useState(0);
const [ name,setName] = useState("ha");
return (
<div>
<h1>{count}</h1>
<button onClick={()=>{setCount(count+1)}}>change</button>
<Zi1 name={name}/>
<Zi2 name={name} />
</div>
)
}
class Zi2 extends React.PureComponent{
constructor(props) {
super(props)
}
render(){
console.log("子组件2重新渲染")
return(
<h1>我是子组件2</h1>
)
}
}
const Zi1= React.memo(()=>{
console.log("子组件1重新渲染")
return (
<h1>我是子组件1</h1>
)
})
export default Fu;
代码解析:当父组件中count值改变,但两个子组件依赖的props数据并没有发生改变(name没变),所以此时子组件就不会重新渲染,这种方式用在那些复杂且成本高的子组件上非常好,可以很好的避免重复渲染
useMemo
useMemo接受2个参数,第一个是函数,第二个是依赖列表(数组),useMemo将调用该函数并返回其返回值,并且会依据依赖值来记忆(缓存)这个函数返回结果,依赖值发生改变,则会重新调用函数,生成最新的返回值,如果不变则会记忆缓存之前的返回值,这个返回结果可以是任何,函数,对象等,甚至一个组件,所以接下来我们就用useMemo同样来实现下上面React.memo和pureComponent的效果
我们来看个例子:
优化前代码:(父组件改变state,导致子组件每次重新渲染)
function Zi({name}){ // 子组件
console.log('子组件渲染');
return(
<div>
<h1>{name}</h1>
</div>
)
}
function Fu(){ // 父组件
const [ count,setCount ] = useState(0);
const [ name,setName] = useState("ha");
return (
<div>
<h1>{count}</h1>
<button onClick={()=>{setCount(count+1)}}>change</button>
<Zi name={name} />
</div>
)
}
export default Fu;
用useMemo优化后:
function Fu(){
const [ count,setCount ] = useState(0);
const [ name,setName] = useState("ha");
const Zi = useMemo(()=>{ // 用useMemo缓存整个子组件,只有当name改变时,才会重新渲染子组件,当然这里的name是一个自定义的依赖项,实际开发中按组件所需的props数据为依赖
console.log('子组件渲染');
return ()=>{
return(
<div>
<h1>{name}</h1>
</div>
)
}
},[name]);
return (
<div>
<h1>{count}</h1>
<button onClick={()=>{setCount(count+1)}}>change</button>
<Zi />
</div>
)
}
总结:
1、useMemo、useCallback等hook都只能在函数组件中使用,不能在组件外定义使用;
2、useMemo/useCallback两个hook很相似,都是用来优化性能的,但是区别在于useCallback只能缓存函数的引用,换句话说就是它的返回值是一个函数,一般用于父子组件props的传值优化上,但useMemo可以缓存值、函数、组件等任何值,常见场景是缓存计算量大的结果 或者 不需要根据状态改变的渲染元素(也就是组件)
3、useMemo 会在渲染的时候执行,而不是渲染之后执行,这一点和 useEffect 有区别,所以 useMemo 不建议有 副作用相关的逻辑
useRef
const refContainer = useRef(initialValue);
useRef是一个方法,且useRef返回一个可变的ref对象(对象!!!)
initialValue被赋值给其返回值的.current属性
相信有过React使用经验的人对ref都会熟悉,它可以用来获取组件实例对象或者是DOM对象。
我们先来看下其传统的用法:
function T(){
const [count ,setCount] =useState(0);
const testRef =useRef(null)
const change =()=>{
testRef.current.focus()
}
return (
<div>
<button onClick={change}>change</button>
<input ref={testRef} />
</div>
)
}
上述代码中,用useRef创建了一个实例对象testRef,将其赋值给了input元素的ref属性,这样,就能通过testRef.current访问到input元素了,所以当点击button时就能调用input的focus方法,使input框获得焦点;
而且ref 对像保存的值发生改变时并不会引起重新渲染,我们看下面的例子
function T() {
const r = useRef(0);
const add = () => {
r.current += 1;
console.log(`r.current:${r.current}`);
};
return (
<div className="App">
{console.log("render")}
<h1>r的current:{r.current}</h1>
<button onClick={add}>点击+1</button>
</div>
);
}
那如果我们想要让ref改变,同步渲染显示最新的ref值该如何操作?这时就需要借助state的帮助了,因为state值改变都会重新render,所以我们将以上代码进行改造:
function T() {
const [count,setCount] =useState(0);
const r = useRef(0);
useEffect(()=>{
r.current+=1
})
return (
<div className="App">
{console.log("render")}
<h1>r的current:{r.current}</h1>
<button onClick={()=>setCount(count+1)}>点击+1</button>
</div>
);
}
定义一个state值,用来协助ref值的更新渲染,从上面例子中也能看出,useRef保存的值并不会随着组件重新渲染而丢失之前的值,所以useRef在这层面表现出了类似类组件中的实例对象的特性
总结:
1、本质上,useRef就是一个其.current属性保存着一个可变值“盒子”
2、useRef是一个方法,且useRef返回一个可变的ref对象
3、可以保存任何类型的值:dom、对象等任何可变值
4、useRef对象的值发生改变之后,不会触发组件重新渲染
5、useRef对象类似与类组件中的实例对象,除组件销毁外,可以一直保存某一数据