什么是Hooks?
React有两个方式编写组件,类组件与函数组件,因为函数组件相比类组件会更少的代码实现相同的功能,所以一般推荐使用函数组件。
React 16.8版本就新增了Hooks特性,在不编写类组件的情况下就可以使用state等其他React特性。
这两种写法并没有代替的意思,在原来基于class写法的React项目可以继续使用Hooks写法,可以相互并存,不需要修改原先的代码。只需写新代码的时候采用Hooks的方式来实现就可以了。
脸书为什么要推出 Hooks?
很久之前,在Class写法是主流的时候,React其实就已经支持函数式的写法,但缺点是缺少状态、生命周期等一些机制,功能一直受限,直到推出了Hooks。
Hooks更好的体现了React的思想:即从State => View的函数式映射。
Hooks的机制
- 术语版:把某个目标结果钩到某个可能会变化的数据源或者事件源上,那么当被钩到的数据或事件发生变化时 ,产生这个目标结果的代码会重新执行,产生更新的结果。(所以Hooks实际上是钩子的意思)
- 俗语版:能够把外部的数据绑定到函数的执行。当这个数据产生变化时,函数就能够重新执行。同样,能影响到外面的UI展示数据,都可以绑定通过这个机制绑定到React的函数组件。
这个数据源/事件源可以是:
- URL的参数
- State状态
- 浏览器的参数(窗口大小变化)
- 等……
好处:
逻辑的复用:
可以说是最大的好处,因为Hooks本质还是普通函数,所以可以在别的函数调用。同时,Hooks中被钩的对象,不仅可以是某个独立的数据源,也可以是另一个Hook执行的结果。
对比Class写法,逻辑复用,必须要借助于高阶组件等复杂的设计模式才能实现,而且这样的组件会产生冗余,使调试变得困难。正因如此,React团队就推出了Hooks写法;
有助于关注分离:如图所示,左为Class写法,右为Hooks写法
Hooks基础API
useState
声明常量:即在组件初始化的时候就会定义
import React, {useState} from "react";
function fun(){
const [count, setCount] = useState(0)
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
)
}
关于useState接受的函数
声明函数:只有在开始渲染的时候函数才会执行
const [count, setCount] = useState(()=>{
const initalCount = someComputation(props);
return initialState;
})
上面代码可以计算一个复杂的state的值,然后返回这个值。
在使用setState传一个函数代表什么?
function App() {
const [n, setN] = useState(0);
const onClick = () => {
setN(n + 1);
setN(n + 1);
};
}
上面有两个setN(n + 1)
,会执行两次的setN(n + 1)
吗?不会,执行一次
但如果改成两个setN(n => n + 1)
,就会执行两次,想执行多少次就写多少个。
注意:
stete是不可局部更新的,例如页面有很多数据,但要求只需要改其中的一小段数据,如果是Vue会直接修改这一小段的数据,但React不会这样做,不会像Vue自动合并没改的数据,改的数据会渲染到页面,而没改的数据,因为没自动合并,就不会渲染到页面,就会消失。尝试以下代码:
mport React, {useState} from "react";
import ReactDOM from "react-dom";
function App() {
const [user,setUser] = useState({name:'Jeff', age: 20})
const onClick = ()=>{
setUser({
name: 'Jack'
})
}
return (
<div className="App">
<h1>{user.name}</h1>
<h2>{user.age}</h2>
<button onClick={onClick}>Click</button>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
还有一点,如果setState(obj)
的内存地址不变,那么React就认为数据没有改变。
useContext
为了使React能够 全局状态管理 就需要使用useContext。能够在这个组件上的树上的所有组件,都能够访问和修改这个Context.
基本用法
import React, { useContext } from 'react';
//首先需要在组件外面创建一个Constxt(声明上下文)
const AppContext = React.createContext({})
//组件1号
const Navbar = () => {
//在组件内引用该上下文
const {one} = useContext(AppContext)
return (
<div>
//这样就可以显示获取到的共享状态
<p>1号{one}</p>
</div>
)
}
//组件2号
const Navbar2 = () => {
//在组件内引用该上下文
const {tow} = useContext(AppContext)
return (
<div>
//这样就可以显示获取到的共享状态
<p>2号{tow}</p>
</div>
)
}
const Index = () => {
return (
//把想要共享状态的组件放入该上下文中,格式必须是 <xxx.Provider value={共享的状态}></xxx.Provider>
//xxx便是声明的上下文名
<AppContext.Provider value={{tow:2, one: 1}}>
<Navbar/>
<Navbar2/>
</AppContext.Provider>
);
};
export default Index;
尽量不要
useEffect
基础用法
基本用法看我之前的博客:React 函数组件详细
什么是副作用?
虽然说是使用生命周期函数,官方觉得,这个会改变页面环境,就是副作用。因此官方还提供了可以清除副作用的特性。
useLayoutEffect
定义:与useEffect
作用一样,但有一点不同,useEffect
在浏览器渲染完成后执行,而useLayoutEffect
是在浏览器渲染前执行。
因此,不管这两者哪个在前哪个在后,第一个执行的都是useLayoutEffect
,其他的按代码顺序来。
那么这两种先使用哪种好?
如果需要改变页面布局之类的,就优先使用useLayoutEffect
,而数据改变,就使用useEffect
,因为用户需要看到数据的加载过程及变化,为了用户体验好些。
useReducer
复杂版的useState
同时也是代替useState
方案,这是用来践行 Flux/Redux 的思想。
步骤分为:
- 创建初始值
initialState
。 - 创建所有操作
reducer(state, action)
。 - 传给
useReducer
,得到读和写API。 - 调用写
({type: '操作类型'})
。
如下:
import React, {useReducer} from "react";
import ReactDOM from "react-dom";
//创造初始值
const initial = {
n: 0
}
//创造操作
const reducer = (state, action) => {
if(action.type === "add"){
return {n: state.n + action.number};
} else if(action.type === "multi"){
return { n: state.n * 2}
} else {
throw new Error("unknown type")
}
}
function App(){
//传入读写API
const [state, dispatch] = useReducer(reducer, initial);
const {n} = state;
const onClick = () => {
//调用操作
dispatch({type: "add", number: 1})
}
const onClick2 = () => {
dispatch({type: "add", number: 2})
}
return (
<div className="App">
<h1>n: {n}</h1>
<button onClick={onClick}>+1</button>
<button onClick={onClick2}>+2</button>
</div>
)
}
memo、useMemo、useCallback
memo
在了解React.useMemo
前,需要了解React.memo
。
React经常为了一些没有变化的组件也全渲染一遍。因为这样的问题存在,就出了React.memo
,memo的作用是,如果props不变,组件就不会重新渲染。
如下例子:
function App() {
const [n, setN] = React.useState(0)
const [m, setM] = React.useState(0)
const onClick = () => {
setN(n + 1)
}
return (
<div className="App">
<button onClick={onClick}>
update n {n}
</button>
<Child data={m}/>
</div>
)
}
function Child(props){
console.log("Child执行了")
console.log("假设这里有大量代码")
return <div>child: {pros.data}</div>
}
加载页面的时候就会渲染一次,点击App组件里的子组件Child按钮,就会执行一次Child里面的代码。不过因为Child数据没有变化,按道理是不应该再渲染一次。所以就用到了React.memo
,在代码下方添加一行const Child2 = React.memo(Child)
,第13行改成<Child2 />
这样Child组件只要props
变了才会重新渲染一次。或者可以简写成:
const Child2 = React.memo((props)=>{
console.log("Child执行了")
console.log("假设这里有大量代码")
return <div>child: {pros.data}</div>
})
useMemo
但问题又来了,如果组件中有函数,即使是有memo,也还是会重新渲染。这时就可以使用useMemo
如下:
const onClick = useMemo(()=>{
return () => {
console.log('1')
}
})
没错,React.useMemo
用法就是useMemo(()=> (x)=> console.log(x))
,有两个箭头函数。
React.useMemo
作用和React.memo
作用是没什么区别,主要是组件里面如果用了函数,可以重用这个函数,而不会在props没变的时候再一次渲染组件。
即使有了memo
,为什么函数会让组件重新渲染?上面还没用函数的例子,是使用了数字,数字值与数字值如果之间是相等的,就不会变,不会变用了memo
就不会渲染。而对象就不行,例如两个空对象,地址能一样吗?因为App会重新渲染,导致App组件里的对象,即使是同个对象,这两次的对象地址就会不一样,间接相当于props变化了,所以就会子组件重新渲染。
所以,React.useMemo
就可以完完全全的,只有props变化了,才会执行。
useCallback
useMemo
的用法对于有些人来说有点奇怪,那么useCallback
可以代替useMemo
,作用完全一样,改写上面的例子如下:
const onClick = useCallback(()=>{
console.log('1')
})
注意
问题:是不是所有回调函数都需要使用 useCallback 或者 useMemo 来封装呢?是不是简单函数就不需要封装?
答:是否应该使用useCallback 或者 useMemo 这与函数的复杂度没有关系。而是回调函数绑定了哪个组件有关系。这是为了避免因为属性频繁的变化而导致不必要的重新渲染。
补充:对于原生的DOM节点,比如 button、input等,是不需要担心重新渲染的。如果事件处理函数是传递给原生节点,就不需要写useCallback 或者 useMemo ,几乎没有影响。如果是自定义组件,或者UI框架的组件,就应该用 useCallback 或者 useMemo 进行封装。
useRef
可以看作是在函数组件之外创建一个容器空间。通过唯一的current属性 设置一个值,从而在函数组件的多次渲染之间共享这个值。同时 ,可以保存DOM节点的能力。
举个例子,实现一个计时器,只需要实现开始和暂停功能两个功能。假设需要用window.setInterval
提供计时功能,需要暂停则需要保存 window.setInterval
返回的计数器的引用,以此来点击暂停按钮时用 window.clearInterval
停止时间。现在问题是,需要在哪里存储这个计数器的应用?
最适合保存的地方就是使用useRef
。如下在多次渲染之间共享数据的例子
import React, {useState, useCallback, useRef} from "react";
const ShowTime = () => {
//保存累计时间
const [time, setTime] = useState(0);
//保存变量
const timer = useRef(null);
//开始
const handleStart = useCallback(() => {
//使用current属性设置 ref 的值
timer.current = window.setInterval(() => {
setTime((time) => time + 1)
}, 100)
}, []);
//暂停
const handlePause = useCallback(() => {
window.clearInterval(timer.current)
timer.current = null
})
return (
<div>
{time / 10}
<button onClick={handleStart}>开始</button>
<button onClick={handlePause}>暂停</button>
</div>
)
}
export default ShowTime;
大多数时候,在React中,并不需要关注真实的DOM,在一些场景中,已经需要获取DOM节点的引用,而React的ref属性以及useRef,就可以保存真实的DOM节点,并对这个节点进行操作。
如下需要点击某按钮时,让某个输入框获得焦点:
const inputButton = () => {
const inputEl = useRef(null)
// current 属性指向了真实的 input 这个 DOM 节点,从而可以调用 focus 方法
const onButtonClick = () => {
inputEl.current.focus() //DOM的获取焦点
}
return (
<>
<input ref={inputEl} type="text" />
<button onClick={onButtonClick}>点击</button>
</>
)
}
以上代码,只要渲染到界面上,就可以通过 current 属性访问到真实的DOM节点的实例。
小问题:useRef与useState有什么区别?
useRef即使改变了值,该组件是不会重新渲染,而useState相反。