react-hooks
首先hook指的类似useState、useRef这样的函数,hooks是对这类函数的统称。
Hook 是 React 16.8 的新增特性,它可以让我们在不编写class的情况下使用state以及其他的React特性(比如生命周期)。
class组件的优势
(1)class组件可以定义自己的state,用来保存组件自己内部的状态;而函数式组件不可以,因为函数每次调用都会产生新的临时变量;
(2)class组件有自己的生命周期,我们可以在对应的生命周期中完成自己的逻辑,比如在componentDidMount中发送网络请求,并且该生命周期函数只会执行一次;而函数式组件在学习hooks之前,如果在函数中发送网络请求,意味着每次重新渲染都会重新发送一次网络请求;
(3)class组件可以在状态改变时只会重新执行render函数以及我们希望重新调用的生命周期函数componentDidUpdate等;而函数式组件在重新渲染时,整个函数都会被执行,似乎没有什么地方可以只让它们调用一次;
所以,在Hook出现之前,对于上面这些情况我们通常都会编写class组件。
class组件存在的问题
(1)复杂组件变得难以理解:我们在最初编写一个class组件时,往往逻辑比较简单,并不会非常复杂。但是随着业务的增多,我们的class组件会变得越来越复杂,比如componentDidMount中,可能就会包含大量的逻辑代码:包括网络请求、一些事件的监听(还需要在componentWillUnmount中移除);而对于这样的class实际上非常难以拆分:因为它们的逻辑往往混在一起,强行拆分反而会造成过度设计,增加代码的复杂度;
(2)难以理解的class: 很多人发现学习ES6的class是学习React的一个障碍。比如在class中,我们必须搞清楚this的指向到底是谁,所以需要花很多的精力去学习this; 虽然我认为前端开发人员必须掌握this,但是依然处理起来非常麻烦;
(3)组件复用状态很难: 在前面为了一些状态的复用我们需要通过高阶组件或render props; 像我们之前学习的redux中connect或者react-router中的withRouter,这些高阶组件设计的目的就是为了状态的复用;或者类似于Provider、Consumer来共享一些状态,但是多次使用Consumer时,我们的代码就会存在很多嵌套;这些代码让我们不管是编写和设计上来说,都变得非常困难;
使用hook的两个额外规则
(1)只能在函数的最外层进行调用,不能在if条件判断或者for循环中去使用(原因是React hook 底层是基于链表(Object)实现,每次组件被 render 的时候都会顺序执行所有的 hooks,因为底层是链表,每一个 hook 的 next 是指向下一个 hook 的,所以要求开发者不能在不同 hooks 调用中使用判断条件,因为 if 会导致顺序不正确,从而导致报错)
(2)只能在函数组件中使用。
useState
state的hook API就是useState。其本身是一个函数,来自react包。
参数:初始化值,如果不设置为undefined;
返回值:数组,包含两个元素;
(1)元素一:当前状态的值(第一调用为初始化值);
(2)元素二:设置状态值的函数;
import React, { useState } from 'react'
export default function ComplexHookState() {
//复杂状态的使用修改
const [students, setStudents] = useState([
{ id: 110, name: "why", age: 18 },
{ id: 111, name: "kobe", age: 30 },
{ id: 112, name: "lilei", age: 25 },
])
function incrementAgeWithIndex(index) {
//推荐使用下面的方式去修改数据
const newStudents = [...students];
newStudents[index].age += 1;
setStudents(newStudents);
}
return (
<div>
<h2>学生列表</h2>
<ul>
{
students.map((item, index) => {
return (
<li key={item.id}>
<span>名字: {item.name} 年龄: {item.age}</span>
<button onClick={e => incrementAgeWithIndex(index)}>age+1</button>
</li>
)
})
}
</ul>
</div>
)
}
useEffect
useEffect可以我们帮助完成类似于类组件中生命周期函数的功能。
第一个参数为回调函数,当页面初次渲染或者页面重新渲染时都会来到这里。
第二个参数:(主要用来做性能优化的)
(1)如果不传入空的数组,那么当state发生变化时,所有的useEffect都会被重新执行一遍;
(2)如果传入一个空的数组,那么该useEffect只会在被初次创建时执行一次,例如:网络请求、订阅事件。
(3)如果传入一个依赖某个state数据的数组,那么该useEffect只会在页面所依赖的这个state数据发生变化时,才会重新渲染执行一次。
import React, { useState, useEffect } from 'react'
export default function MultiEffectHookDemo() {
const [count, setCount] = useState(0);
const [isLogin, setIsLogin] = useState(true);
//多个useEffect一起使用, 避免了像类组件生命周期那样将所有逻辑存放在一起导致阅读性很差
//再一个重要的优化便是:第二个参数只有当依赖的state数据发生变化时, 这个生命周期才会被重新执行(尤其当使用多个useEffect时候),不用像函数式组件那样所有东西都会被重新执行一次
useEffect(() => {
console.log("修改DOM", count);
}, [count]);
useEffect(() => {
console.log("订阅事件");
return () => {
console.log("取消订阅");
}
}, []);
useEffect(() => {
console.log("网络请求");
}, []);
return (
<div>
<h2>MultiEffectHookDemo</h2>
<h2>{count}</h2>
<button onClick={e => setCount(count + 1)}>+录/1</button>
<h2>{isLogin ? "coderwhy": "未登录"}</h2>
<button onClick={e => setIsLogin(!isLogin)}>登注销</button>
</div>
)
}
useContext
在之前的开发中,我们要在组件中使用共享的Context有两种方式:
(1)类组件可以通过 类名.contextType = MyContext方式,在类中获取context;
(2)多个Context或者在函数式组件中通过 MyContext.Consumer 方式共享context; 但是多个Context共享时的方式会存在大量的嵌套: Context Hook允许我们通过Hook来直接获取某个Context的值;
注意事项:当组件上层最近的 <MyContext.Provider> 更新时,该 Hook 会触发重新渲染,并使用最新传递给 MyContext provider 的 context value 值
export const UserContext = createContext();
export const ThemeContext = createContext();
export default function App() {
return (
<div>
<UserContext.Provider value={{name: "why", age: 18}}>
<ThemeContext.Provider value={{fontSize: "30px", color: "red"}}>
<ContextHookDemo/>
</ThemeContext.Provider>
</UserContext.Provider>
</div>
)
}
import React, { useContext } from 'react';
import { UserContext, ThemeContext } from "../App";
export default function ContextHookDemo(props) {
const user = useContext(UserContext);
const theme = useContext(ThemeContext);
console.log(user, theme);
return (
<div>
<h2>ContextHookDemo</h2>
</div>
)
}
useReducer
useReducer是useState的一种替代方案:在某些情况下,如果state的处理逻辑比较复杂,可以通过useReducer对其进行拆分;或者这次修改的state需要依赖之前的state时,也可以使用。
需要传入两个参数:第一个参数为reducer,第二个参数为state的初始值。
它的返回值也是一个数组,数组中有两个元素,第一个元素为当前的state,第二个元素为dispatch,它是用来当我们需要修改state’时需要dispatch一个action对象,而action对象中又包括了type和payload(一些额外的值)两个属性。
需要注意的是:数据是不会共享的,它们只是使用了相同的Reducer函数而已,所以,useReducer只是useState的一种替代品,并不能替代Redux。
export default function reducer(state, action) {
switch(action.type) {
case "increment":
return {...state, counter: state.counter + 1};
case "decrement":
return {...state, counter: state.counter - 1};
default:
return state;
}
}
import React, { useReducer } from 'react';
import reducer from './reducer';
export default function Home() {
const [state, dispatch] = useReducer(reducer, {counter: 0});
return (
<div>
<h2>Home当前计数: {state.counter}</h2>
<button onClick={e => dispatch({type: "increment"})}>+1</button>
<button onClick={e => dispatch({type: "decrement"})}>-1</button>
</div>
)
}
useCallback
其实际的目的是为了进行性能优化。
需要传入两个参数:第一个参数为回调函数;第二个参数为需要在空的数组中传入相关的state依赖(因为只有当相关的依赖发生改变时,才会发生更新)。
返回值为一个函数。
它会返回一个函数的memoized(记忆)值。
在依赖不变的情况下,多次定义的时候,返回的值是一样的。
重点:如果单独的写useCallback的话,和我普通的写法比起来是没有任何的性能优化的,因为当state改变时,都有重新创建执行过程。
useCallBack的真正用处在于:在将父组件的一个事件处理函数传递给子组件使用时,在父组件页面重新渲染时,只要我这个事件处理函数所依赖的state数据不发生改变,那么我的子组件是不会重新渲染的,这便是它的性能优化的地方。
import React, {useState, useCallback, memo} from 'react';
const HYButton = memo((props) => {
console.log("HYButton重新渲染: " + props.title);
return <button onClick={props.increment}>HYButton +1</button>
});
export default function CallbackHookDemo02() {
console.log("CallbackHookDemo02重新渲染");
const [count, setCount] = useState(0);
const [show, setShow] = useState(true);
const increment1 = () => {
console.log("执行increment1函数");
setCount(count + 1);
}
const increment2 = useCallback(() => {
console.log("执行increment2函数");
setCount(count + 1);
}, [count]);
return (
<div>
<h2>CallbackHookDemo01: {count}</h2>
{/* <button onClick={increment1}>+1</button>
<button onClick={increment2}>+1</button> */}
<HYButton title="btn1" increment={increment1}/>
<HYButton title="btn2" increment={increment2}/>
<button onClick={e => setShow(!show)}>show切换</button>
</div>
)
}
useMemo
实际的目的也是为了进行性能优化。
需要传入两个参数:第一个参数为回调函数;第二个参数为需要在空的数组中传入相关的state依赖(因为只有当相关的依赖发生改变时,才会发生更新)。
它会返回一个memoized(记忆)值。
在依赖不变的情况下,多次定义的时候,返回的值是一样的。
场景一:复杂计算的应用
import React, {useState, useMemo} from 'react';
function calcNumber(count) {
console.log("calcNumber重新计算");
let total = 0;
for (let i = 1; i <= count; i++) {
total += i;
}
return total;
}
export default function MemoHookDemo01() {
const [count, setCount] = useState(10);
const [show, setShow] = useState(true);
// const total = calcNumber(count);
//不就类似于vue的计算属性吗
const total = useMemo(() => {
return calcNumber(count);
}, [count]);
return (
<div>
<h2>计算数字的和: {total}</h2>
<button onClick={e => setCount(count + 1)}>+1</button>
<button onClick={e => setShow(!show)}>show切换</button>
</div>
)
}
场景二:传入给子组件的应用类型(当然也可以使用其他方法来解决下面的性能问题)
import React, { useState, memo, useMemo } from 'react';
const HYInfo = memo((props) => {
console.log("HYInfo重新渲染");
return <h2>名字: {props.info.name} 年龄: {props.info.age}</h2>
});
export default function MemoHookDemo02() {
console.log("MemoHookDemo02重新渲染");
const [show, setShow] = useState(true);
// const info = { name: "why", age: 18 };
const info = useMemo(() => {
return { name: "why", age: 18 };
}, []);
return (
<div>
<HYInfo info={info} />
<button onClick={e => setShow(!show)}>show切换</button>
</div>
)
}
useRef
useRef返回一个ref对象,返回的ref对象在组件的整个生命周期中保持不变。
最常用的ref有两种用法:
用法一:引入DOM元素或者组件。
import React, { useRef } from 'react';
class TestCpn extends React.Component {
render() {
return <h2>TestCpn</h2>
}
}
function TestCpn2(props) {
return <h2>TestCpn2</h2>
}
export default function RefHookDemo01() {
const titleRef = useRef();
const inputRef = useRef();
const testRef = useRef();
const testRef2 = useRef();
function changeDOM() {
titleRef.current.innerHTML = "Hello World";
inputRef.current.focus();
console.log(testRef.current);
console.log(testRef2.current);
}
return (
<div>
<h2 ref={titleRef}>RefHookDemo01</h2>
<input ref={inputRef} type="text"/>
<TestCpn ref={testRef}/>
<TestCpn2 ref={testRef2}/>
<button onClick={e => changeDOM()}>修改DOM</button>
</div>
)
}
用法二:保存一个数据,这个对象在整个生命周期中可以保持不变。
import React, { useRef, useState, useEffect } from 'react'
export default function RefHookDemo02() {
const [count, setCount] = useState(0);
const numRef = useRef(count);
//这里会在重新render之后再去执行的,因此会拿到上一次的count值。
useEffect(() => {
numRef.current = count;
}, [count])
return (
<div>
{/* <h2>numRef中的值: {numRef.current}</h2>
<h2>count中的值: {count}</h2> */}
<h2>count上一次的值: {numRef.current}</h2>
<h2>count这一次的值: {count}</h2>
<button onClick={e => setCount(count + 10)}>+10</button>
</div>
)
}
useImpertiveHandle
这个挺难理解的,需要一点点来学习。
我们先来回顾一下ref和forwardRef的使用
通过forwardRef可以将ref转发到函数式子组件。
子组件便可以拿到父组件创建的ref,绑定到自己的某一个元素中。
import React, { useRef, forwardRef } from 'react';
const HYInput = forwardRef((props, ref) => {
return <input ref={ref} type="text"/>
})
export default function ForwardRefDemo() {
const inputRef = useRef();
return (
<div>
<HYInput ref={inputRef}/>
<button onClick={e => inputRef.current.focus()}>聚焦</button>
</div>
)
}
forwardRef的做法本身没有什么问题,但是我们是将子组件的DOM直接暴露给了父组件:直接暴露给父组件带来的问题是某些情况的不可控;因为父组件可以拿到DOM后进行任意的操作; 但是,事实上在上面的案例中,我们只是希望父组件可以操作的focus,其他并不希望它随意操作;而通过useImperativeHandle可以只暴露固定的操作: 通过useImperativeHandle的Hook,将传入的ref和useImperativeHandle第二个参数返回的对象绑定到了一起; 所以在父组件中,使用 inputRef.current时,实际上使用的是返回的对象; 比如我调用了 focus函数,甚至可以调用 printHello函数;
import React, { useRef, forwardRef, useImperativeHandle } from 'react';
const HYInput = forwardRef((props, ref) => {
const inputRef = useRef();
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus();
}
}), [inputRef])
return <input ref={inputRef} type="text"/>
})
export default function UseImperativeHandleHookDemo() {
const inputRef = useRef();
return (
<div>
<HYInput ref={inputRef}/>
<button onClick={e => inputRef.current.focus()}>聚焦</button>
</div>
)
}
useLayoutEffect
其和useEffect也仅有一点区别:
useEffect会在渲染的内容更新到DOM之后再执行,并不会阻碍DOM的更新。
而useLayoutEffect会在渲染的内容更新到DOm之前执行,会阻碍DOM的更新。
如果我们希望在更新DOM之前做一些额外操作的话,可以使用它。
import React, { useState, useEffect, useLayoutEffect } from 'react'
export default function LayoutEffectCounterDemo() {
const [count, setCount] = useState(10);
useLayoutEffect(() => {
if (count === 0) {
setCount(Math.random() + 200)
}
}, [count]);
return (
<div>
<h2>数字: {count}</h2>
<button onClick={e => setCount(0)}>修改数字</button>
</div>
)
}
自定义hook
自定义hook本质上只是一种函数代码逻辑的抽取,严格意义来说,它本身并不算react的特性。(vue3的自定义hook相同)
需求:所有的组件在被创建和销毁时都进行打印
组件被创建:打印组件被创建了。
组件被销毁:打印组件被销毁了。
我们以一个案例来进行引入:需求:所有的组件在创建和销毁时都进行打印。(因为只允许在自定义hooks里面使用我们的hooks函数)普通函数加上use。
import React, { useEffect } from 'react';
const Home = (props) => {
useLoggingLife("Home");
return <h2>Home</h2>
}
const Profile = (props) => {
useLoggingLife("Profile");
return <h2>Profile</h2>
}
export default function CustomLifeHookDemo01() {
useLoggingLife("CustomLifeHookDemo01");
return (
<div>
<h2>CustomLifeHookDemo01</h2>
<Home/>
<Profile/>
</div>
)
}
function useLoggingLife(name) {
useEffect(() => {
console.log(`${name}组件被创建出来了`);
return () => {
console.log(`${name}组件被销毁掉了`);
}
}, []);
}