Hook 简介
Hook 是 React 16.8 的新增特性。它可以让我们在不编写 class 的情况下使用 state 以及其他的 React 特性。hook主要是为了提升性能的,因为我们的页面其实就是一棵树(DOM树),树上有很多分支节点,如果其中任何一个节点变化都重新渲染树对性能极为不友好,hook就是解决这个问题的。
注意:hook只适用于函数组件,不适于类组件(类组件性能优化也有PureComponent供我们使用)
memo
React.memo
为高阶组件。此方法用来优化react的性能,它与 React.PureComponent
非常相似,但它适用于函数组件,但不适用于 class 组件。如果我们的函数组件在给定相同 props 的情况下渲染相同的结果,那么我们可以通过将其包装在 React.memo
中调用,以此通过记忆组件渲染结果的方式来提高组件的性能表现。这意味着在这种情况下,React 将跳过渲染组件的操作并直接复用最近一次渲染的结果。
React.memo
仅影响 props 变更。如果函数组件被 React.memo
包裹,且其实现中拥有 useState
或 useContext
的 Hook,当 context 发生变化时,它仍会重新渲染。
默认情况下其只会对复杂对象做浅层对比,如果你想要控制对比过程,那么请将自定义的比较函数通过第二个参数传入来实现;
举例:
export default () => {
const [a, setA] = useState(1)
const [b, setB] = useState(2)
const updateA = () => setA(3)
const updateB = () => setB(4)
return <div>
<Show data={a} onClick={updateA} />
<Show data={b} onClick={updateB} />
</div>
}
//show组件
export default({ data = "show", onClick = () => console.log("click") }) => {
console.log("in update", data);
return (
<div>
<p>{data}</p>
<button onClick={onClick}>修改</button>
</div>
)
}
我们有如上组件,我们想要实现a和b两个值的单独更新和渲染,然而下面的这个组件是无法实现的,因为不管是a还是b单独改变值后,由于每次react比对updateA和updateB方法每次都会返回比对结果为false(不同),相当于state值改变了,因此整个组件都会重新渲染。
所以我们可以通过引入React.memo的方式来实现局部更新,优化性能。优化后的Show组件如下:
import React, { memo } from "react";
export default memo(
({ data = "show", onClick = () => console.log("click") }) => {
console.log("in update", data);
return (
<div>
<p>{data}</p>
<button onClick={onClick}>修改</button>
</div>
)
},//下面是memo的第二个参数
(prev,next)=>{
if(prev.data === next.data){
return true
}
return false
}//如果两次传入的data相等,返回true,不会更新,相反返回false,更新组件
)
注意memo的第二个参数也是一个回调函数如果没传第二个函数,memo会默认自己去判断上一次个这一次传进来的props一一进行比对。如果我们需要自己判断,则需要传入这个回调。
**注意:**与 class 组件中 shouldComponentUpdate()
方法不同的是,如果 props 相等,areEqual
会返回 true
;如果 props 不相等,则返回 false
。这与 shouldComponentUpdate
方法的返回值相反。
useMemo
当然针对上面出的重复刷新的问题我们也有其他的解决办法,React中提供了一个叫做useMemo的hook,它的用法很简单,比如上面问题出现其实是由于每次react比对updateA和updateB方法每次都会返回不同,因此我们可以用useMemo将我们自定义的方法缓存起来:
上面的show组件如果memo方法我们没有传第二个参数,依然会导致组建的多余刷新,这时我们可以在父级组建中使用useMemo方法缓存我们自定义的方法:
export default () => {
const [a, setA] = useState(1)
const [b, setB] = useState(2)
const updateA = useMemo(() => {
console.log("usememo");
return () => setA(3)
},[])
const updateB = useMemo(() => {
console.log("usememo");
return ()=>{setB(4)}
},[])
return <div>
<Show data={a} onClick={updateA} />
<Show data={b} onClick={updateB} />
</div>
}
//show组件
export default memo(
({ data = "show", onClick = () => console.log("click") }) => {
console.log("in update", data);
return (
<div>
<p>{data}</p>
<button onClick={onClick}>修改</button>
</div>
)
}
)
这样子同样可以实现a和b的分别刷新互不干扰。
**但是现在新的问题又来了:**比如假如我们需要将a的值和b的值每次都在上一次的基础上+1,我们可能会很自然的想到可以将useMemo中的返回值由固定的setA(3)改为setA(a+1),然而执行起来并不是这样的,这是因为useMemo的作用本来就是将我们的封装的方法缓存起来避免组建的多余刷新影响性能,我们第一次执行函数的时候,a被初始化为1,updateA函数返回setA(a+1)也就是setA(2),同时useMemo将函数setA(2)缓存起来了,等待后边的使用,所以当修改按钮点击时,a的值被修改为a+1也就是2,但是当第二次调用是,本来应该是setA(3),但是由于上一次调用时已经缓存了setA(2)所以又将setA(2)返回给整个组件,因此每次调用时都是setA(1+1)也就是setA(2),因此a的值在第一次改变后就不会变了。
**解决办法:**useMemo的第二个参数传一个a,意义就是react灰根据传的参数来判断这个值是否被改变,如果传入的值发生变化,这个函数会被重新返回(重新执行updateA函数),这样子就可以实现上面的需求:将a的值和b的值每次都在上一次的基础上+1;
代码:
const updateA = useMemo(() => {
console.log("usememo");
return () => setA(a+1)
},[a]) //通过判断传入的a是否变化来决定是否重新返回和缓存
const updateB = useMemo(() => {
console.log("usememo");
return ()=>{setB(b+1)}
},[b]) //通过判断传入的b是否变化来决定是否重新返回和缓存
useCallback
useCallback和useMemo的功能十分类似,他们的区别是useMemo的第一个参数传的是传的是一个回调函数,但是它缓存的是该函数的返回值而不是整个函数,而useCallback缓存的是第一个参数整,代码更简洁,如果我们需要这个函数有其他的事情做而不只是缓存,需要使用useMemo,如果我们只需要缓存一个方法,使用useCallback更加简洁;这里我们可以把update和updateB方法用useCallback缓存起来,代码:
const updateA = useCallback(()=>setA(a+1),[a])
const updateB = useCallback(()=>setB(b+1),[b])
useReducer
import React, { useState, useReducer, useCallback } from "react";
import Title from "./title";
import Ibutton from "./Ibutton";
const options = {
add:"ADD",
dec:"MINUS"
}
export default () => {
//reducer默认接收两个参数,action是一个描述对象,用来描述用户针对状态state的操作行为
//action对象有一个type属性,用来描述用户行为的类型,固定属性,不能自己命名
const reducer = (state, action) => {//通过action.type来判断用户是什么操作,并且做出相应的操作
const {add,dec} = options;
switch (action.type) {
case add:
return state + 1;
case dec:
return state - 1;
default:
return state;
}
}
const [count, dispatch] = useReducer(reducer, 20) //把处理器交给useReducer来执行,0是初始值
//用户触发reduce人执行
const add = useCallback( ()=>{dispatch({type:options.add})},[])
const dec = useCallback( ()=>{dispatch({type:options.dec})},[])
return (<div>
<Title count={count} />
<Ibutton onClick={add} name="+" />
<Ibutton onClick={dec} name="-" />
</div>)
}
//Title组件
import React, { memo } from "react";
export default memo((props) => {
return (
<h1>{props.count}</h1>
)
})
//Ibutton组件
import React from "react";
export default (props) => {
return <button onClick={props.onClick}>{props.name}</button>
}
分析代码:useReducer是useState 的替代方案。这个方法接收一个形如 (state, action) => newState
的 reducer,并返回当前的 state 以及与其配套的 dispatch
方法;我们先定义一个reducer函数;
const reducer = (state, action) => {
}
const [count,dispatch] = useReducer(reducer,0)
useReducer方法(处理器)的第一个参数就是操作状态的方法,第二个参数是这个状态的默认值,该方法可以返回一个数组,第一项和第二项分别为状态值count和dispatch,用户通过dispatch来触发reducer处理器来更新state,dispatch的参数传的是用户的操作类型,也就是reducer函数的action参数,我们可以通过数组解构的方式拿到它们。
上面代码中定义的reducer函数,默认接收两个参数,action是一个描述对象,用来描述用户针对状态state的操作行为,action对象有一个type属性,用来描述用户行为的类型,state就是这个处理器需要处理的值,固定属性,不能自己命名;
同时我们还定义了add和得出两个方法,并且用useCallback缓存起来,在外面定义了options对象来存储用户的操作,根据action.type来判断用户的操作然后给到相应的更改state状态值的操作,实现我们需要更改状态值并且足够考虑性能的这样一个操作。
自定义hook
上面讲的都是React提供给我们使用的hooks,其实React也有给我们提供自定义hook的操作方法,比如我们上面用自定义hook来重构useReducer的项目,以此来练习和加强对自定义hook的理解;
**注意:**我们自定义的hook在命名方面必须和React自带的hook相同,比如useCount等等;
我们先新建一个defineHook.js的文件来创建我们的useCount这个自定义hook;
import { useReducer, useCallback } from "react";
const options = {
add: "ADD",
dec: "MINUS"
}
export const useCount = () => {
const reducer = (state, action) => {//通过action.type来判断用户是什么操作,并且做出相应的操作
const { add, dec } = options;
switch (action.type) {
case add:
return state + 1;
case dec:
return state - 1;
default:
return state;
}
}
const [count, dispatch] = useReducer(reducer, 20) //把处理器交给useReducer来执行,0是初始值
//用户触发reduce人执行
const add = useCallback(() => { dispatch({ type: options.add }) }, [])
const dec = useCallback(() => { dispatch({ type: options.dec }) }, [])
return [count, { add, dec }]
}
然后在父级组建中直接引用和使用该hook即可
import React from "react";
import Title from "./title";
import Ibutton from "./Ibutton";
import {useCount} from "./defineHook"
export default () => {
const [count, {add,dec}] = useCount() //把处理器交给useReducer来执行,0是初始值
return (<div>
<Title count={count} />
<Ibutton onClick={add} name="+" />
<Ibutton onClick={dec} name="-" />
</div>)
}