React-Hooks学习

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对象类似与类组件中的实例对象,除组件销毁外,可以一直保存某一数据

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Ronychen’s blog

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值