什么是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(){//传入读写APIconst [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相反。

