Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。
前言:hooks出了已有大半年了,关注的公众号也大都推了关于hooks的文章,可是因为工作中一直用的是class,所以一直没有用,也没有学,趁着这段时间项目不那么干,将hooks系统性的学习一下,并做笔记记录一下。
<span id="目录">目录</span>
- useState
- userEffect
- userEffect实现componentWillUnmont
- 父子组件传值
- userContext
- userReducer
- useReducer替代Redux案例
- useMemo
- useRef
- useCallBack
- 自定义函数
一:<span id="useState">useState</span>
在组件中,我们难免使用state来进行数据的实时响应,这是react框架的一大特性,只需更改state,组件就会重新渲染,试图也会响应更新。<br />
不同于react在class可以直接定义state,或者是在constructor中使用this.state来直接定义state值,在hooks中使用state需要useState函数,如下:
import React, { useState, useEffect } from 'react';
function Hooks() {
const [count, setCount] = useState(0);
const [age] = useState(16);
useEffect(() => {
console.log(count);
});
return (
<div>
<p>小女子芳年{age}</p>
<p>计数器目前值为{count}</p>
<button type="button" onClick={() => { setCount(count + 1); }}>点击+1</button>
<button type="button" onClick={() => { setCount(count - 1); }}>点击-1</button>
</div>
);
}
export default Hooks;
在上面的例子中,我们使用了useState定义了两个state变量,count和age,其中定义count的时候还定义了setCount,就是用来改变count值的函数。在class类中,改变state是使用setState函数,而在hooks中是定义变量的同时定义一个改变变量的函数。<br />
userState是一个方法,方法返回值为当前state以及更新state的函数,所以,在上面的例子中,我们用const [count, setCount] = useState(0);将count和setCount解构出来,而userState方法的参数就是state的初始值。当然count和与之对应的改变函数名称并不一定非得是setCount,名称可以随便起,只要是一块解构出来的即可。<br />
在class组件中,我们可以用setState一次更改多个state值而只渲染一次,同样的,在hooks中,我们调用多个改变state的方法,也只是渲染一次。
二:<span id="userEffect">userEffect</span> 回目录
在class组件中,有生命周期的概念,最常用的,我们通常会在componentDidMount这个生命周期中做数据请求,偶尔,我们也会用一些其它的生命周期,像是componentDidUpdata,componentWillReceiveProps等。在hooks中,没有生命周期的概念,但是,有副作用函数useEffect。<br />
使用useEffect,和使用useState相同,必须得先引入import React, { useState, useEffect } from 'react';,默认情况下,useEffect会在第一次和每次更新之后都会执行,useEffect函数接受两个参数,第一个参数是一个函数,每次执行的就是函数中的内容,第二个函数是个数组,数组中可选择性写state中的数据,代表只有当数组中的state发生变化是才执行函数内的语句。如果是个空数组,代表只执行一次,类似于componentDidUpdata。所以,向后端请求可以写成下面这种方式:
// 页面进来只调用一次
useEffect(()=>{
axios.get('/getYearMonth').then(res=> {
console.log('getYearMonth',res);
setValues(oldValues => ({
...oldValues,
fileList:res.data.msg
}));
})
},[]);
effect函数会在浏览器完成画面渲染之后延迟调用<br />
在一个hooks函数中,可以同时存在多个effect函数,所以,当有需求每次更新都执行useEffect中的代码时,可以用一个useEffect请求数据,用其他的useEffect做另外的事情。只需根据第二个参数即可区别不同作用。
//官方示例性能优化
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]); // 仅在 count 更改时更新
三:<span id="userEffect实现componentWillUnmount">userEffect实现componentWillUnmount</span> 回目录
部分情况下,需要在组件卸载是做一些事情,例如移除监听事件等,在class组件中,我们可以在componentWillUNmount这个生命周期中做这些事情,而在hooks中,我们可以通过useEffect第一个函数参数中返回一个函数来实现相同效果。
// 官方示例
useEffect(() => {
// ...
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
个人示例:
import React, { useState, useEffect } from 'react';
import { Switch, Route, Link } from 'react-router-dom';
function Index() {
useEffect(() => {
console.log('useEffect:come-index');
return () => {
console.log('useEffect:leave-index');
};
}, []);
return <div>这是首页</div>;
}
function List() {
useEffect(() => {
console.log('useEffect:come-list');
return () => {
console.log('useEffect:leave-list');
};
}, []);
return <div>这是列表页</div>;
}
function HooksEffect() {
const [count, setCount] = useState(0);
useEffect(() => {
console.log(count);
return () => {
console.log('-------------------');
};
}, [count]);
return (
<div>
<p>你点击了{count}次</p>
<button
type="submit"
onClick={() => { setCount(count + 1); }}
>
点击+1
</button>
<ul>
<li><Link to="/index">首页</Link></li>
<li><Link to="/list">列表</Link></li>
</ul>
<Switch>
<Route path="/index" exact component={Index} />
<Route path="/list" component={List} />
</Switch>
</div>
);
}
export default HooksEffect;
在上面的例子中,全部用了清除副作用的return 函数,其中,hooksEffect组件为父组件,list和index为子组件,如果在子组件的useEffect中不使用第二个参数空数组,则父组件的每次更新都会引发子组件的useEffect的调用,在父组件的useEffect函数中,第二个参数数组中为count,代表每次count的变化都会引起useEffect函数的触发以及返回函数的调用。
四:<span id="父子组件传值">父子组件传值</span> 回目录
父子组件传值在实际开发中是必不可少的,在class组件中,我们可以直接给子组件添加属性,然后在子组件通过props即可获取到父组件的值。但是在hooks中,组件都是函数,没有props,所以不能用相同的方式传值<br />
在hooks中,组件都是函数,所以我们可以通过参数的方式进行传值,也可以通过content来进行传值,这一小节主要是讲通过参数方式进行传值,案例如下:
import React, { useState } from 'react';
function Show({ count, age, clear }) {
return (
<div>
数量:{count}
年龄:{age}
<button
type="button"
onClick={() => { clear(); }}
>
复原
</button>
</div>
);
}
function HooksContext() {
const [count, setCnt] = useState(0);
const [age, setAge] = useState(16);
function clear() {
setCnt(0);
setAge(16);
}
return (
<div>
<p>小女子芳年{age}</p>
<p>你点击了{count}次</p>
<button
type="button"
onClick={() => { setCnt(count + 1);
setAge(age + 1); }}
>
点击+1
</button>
<Show count={count} age={age} clear={clear} />
</div>
);
}
export default HooksContext;
在上面的案例中,通过给Show组件属性赋值,然后在Show函数组件中以解构参数的方式获取父组件的值。这种传值方式和类组件本质上还是一样的。
五:<span id="userContext">userContext</span> 回目录
使用userContext,不仅可以实现父子组件传值,还可以跨越多个层级进行传值,例如父组件可以给孙子组件甚至重孙子组件进行直接传值等,redux全局状态管理本质上也是对content的一种应用。<br />
在hooks中使用content,需要使用createContext,useContext,废话不多说,直接示例展示用法
// context.js 新建一个context
import { createContext } from 'react';
const ShowContext = createContext('aaa');
export default ShowContext;
// HooksContext.jsx 父组件,提供context
import React, { useState } from 'react';
import Show from './Show.jsx';
import ShowContext from './context';
function HooksContext() {
const [count, setCnt] = useState(0);
const [age, setAge] = useState(16);
function clear() {
setCnt(0);
setAge(16);
}
return (
<div>
<p>小女子芳年{age}</p>
<p>你点击了{count}次</p>
<button
type="button"
onClick={() => { setCnt(count + 1); setAge(age + 1); }}
>
点击+1
</button>
<ShowContext.Provider value={{ count, age, clear }}>
<Show />
</ShowContext.Provider>
</div>
);
}
export default HooksContext;
// Show.jsx 子组件,使用context
import React, { useContext } from 'react';
import ShowContext from './context';
function Show() {
const { count, age, clear } = useContext(ShowContext);
return (
<div>
数量:{count}
年龄:{age}
<button
type="button"
onClick={() => { clear(); }}
>
复原
</button>
</div>
);
}
export default Show;
上面是一个完整的使用content实现父子组件传值的过程,如果Show组件下还有子组件,无论多少层,都可以用useContext直接取到HooksContext父组件提供的值,而context.js文件是新建一个context,新建必须要单独列出来,否则子组件无法使用useContext。<br />
content提供了一种树状结构,被Context.Provider所包裹的所有组件,都可以直接取数据。redux就是利用了context的这种特性实现全局状态管理。在下面的几小节中,我们会讲hooks中context搭配useReducer来实现redux的功能。
六:<span id="userReducer">userReducer</span> 回目录
userReducer是useState的替代方案,它接收一个形如(state,action) => newState的reducer,并返回当前的state以及其配套的dispatch方法。
总的来说呢,userReducer可以接受两个参数,第一个参数就是和redux中的reducer一样的纯函数,第二个参数是state的初始值,并返回当前state以及dispatch。<br />
还是以官方示例的计数器为例
import React, { useReducer } from 'react';
function countReducer(state, action) {
switch (action.type) {
case 'add':
return state + 1;
case 'minus':
return state - 1;
default:
return state;
}
}
function HooksEffect() {
const [count, dispatch] = useReducer(countReducer, 0);
return (
<div>
<p>你点击了{count}次</p>
<button
type="button"
onClick={() => { dispatch({ type: 'add' }); }}
>
点击+1
</button>
<button
type="button"
onClick={() => { dispatch({ type: 'minus' }); }}
>
点击-1
</button>
</div>
);
}
export default HooksEffect;
相比起redux还需要connect高阶函数包裹一下才能将dispatch和state注入到props中,hooks中使用reducer更加简洁。在下面一小节中,我们会用案例来实现redux。
七:<span id="useReducer替代Redux案例">useReducer替代Redux案例</span> 回目录
在本小节中,我们会用context和useReducer来实现redux的效果。依然是使用计数器这个功能,先贴代码,后面会详细讲解:
// count.js 定义context和reducer,导出context和包含reducer的context包裹组件。
import React, { createContext, useReducer } from 'react';
function countReducer(state, action) {
switch (action.type) {
case 'add':
return state + 1;
case 'minus':
return state - 1;
default:
return state;
}
}
const ADDCOUNT = 'add';
const MINUSCOUNT = 'minus';
export const CountContext = createContext();
export const CountWrap = (props) => {
const [count, dispatch] = useReducer(countReducer, 0);
return (
<CountContext.Provider
value={{ count, dispatch, ADDCOUNT, MINUSCOUNT }}
>
{props.children}
</CountContext.Provider>
);
};
// ReducerToRedux.jsx,连接组件,
import React from 'react';
import Button from './Button';
import Show from './Show';
import { CountWrap } from './count';
function ReducerToRedux() {
return (
<div>
<CountWrap>
<Show />
<Button />
</CountWrap>
</div>
);
}
export default ReducerToRedux;
// Show.jsx 显示当前数值的组件
import React, { useContext } from 'react';
import { CountContext } from './count';
function ReducerToRedux() {
const { count } = useContext(CountContext);
return (
<div>现在的计数器值为:{count}</div>
);
}
export default ReducerToRedux;
// Button.jsx 按钮组件,可以实现计数器的增和减
import React, { useContext } from 'react';
import { CountContext } from './count';
function ReducerToRedux() {
const { dispatch, ADDCOUNT, MINUSCOUNT } = useContext(CountContext);
return (
<div>
<button
type="button"
onClick={() => { dispatch({ type: MINUSCOUNT }); }}
>点我-1</button>
<button
type="button"
onClick={() => { dispatch({ type: ADDCOUNT }); }}
>点我+1</button>
</div>
);
}
export default ReducerToRedux;
通过reducer和context实现计数器的功能,我们共用了四个文件,当然count.js这个文件本应该拆分成三个文件,常量单独定义一个文件,reducer纯函数也应该单独定一个文件,不过代码不多,就暂时合一块了。<br />
在count.js中,我们导出CountContext和CountWrap,其中,CoutWrap就是provider,也就是只要被CountWrap包裹过的组件,就可以使用userContent取到传递数据,而CountContext就是用createContext新建的一个content,使用useContext取传递数据的时候会用到。同时,在这个文件中,我们还将从useReducer解构出的count和dispatch,以及常量增减通过provider传递给包裹组件,使被包裹的组件可以通过useContext取到这些数据。函数countReducer就是和redux中的reducer一样的纯函数,子组件dispatch action,reducer则是接受当前state和action,通过判断action,返回新的state。<br />
在Button组件中,我们通过countContext取到dispatch及常量,改变count这个数值,在Show组件中,只是展示count,在ReducerToRedux文件中,是做一个连接器,用CountWrap包裹Button和Show组件。<br />
可能说的有些啰嗦,看上去有些复杂,其实稍一整理,原理很简单,自己写一遍整理清楚逻辑使用上就很简单了。<br />
讲到这里,其实hooks已经能应对绝大部分场景了,下面两小节,我们会讲一下useMemo和useRef,用于优化渲染以及处理特殊情况。
八:<span id="useMemo">useMemo</span> 回目录
把“创建”函数和依赖项数组作为参数传入 useMemo,它仅会在某个依赖项改变时才重新计算 memoized 值。这种优化有助于避免在每次渲染时都进行高开销的计算。<br />
useMemo是函数式组件官方提供的性能优化的一个方法,接受两个参数,第一个参数是要执行的函数,第二个参数是state中的值或者父组件传下来的值,代表只有当第二个参数的值发生变化时,才执行函数。其中,第二个参数是数组,可以同时优化多个state或者父组件传下来的参数,首次渲染组件是,如果页面用到要优化的值,函数会执行。<br />
我们还是以计数器以及年龄为例
import React, { useState, useMemo } from 'react';
function Show({ count, age, clear }) {
function ageChange(value) {
console.log(value);
return value + 2;
}
const myAge = useMemo(() => ageChange(age), [age]);
return (
<div>
数量:{count} 我的年龄:{myAge}
<button
type="button"
onClick={() => { clear(); }}
>复原</button>
</div>
);
}
function HooksUseMome() {
const [count, setCnt] = useState(0);
const [age, setAge] = useState(16);
function clear() {
setCnt(0);
setAge(16);
}
return (
<div>
<p>小女子芳年{age}</p>
<p>你点击了{count}次</p>
<button
type="button"
onClick={() => { setAge(age + 1); }}
>点击年龄+1</button>
<button
type="button"
onClick={() => { setCnt(count + 1); }}
>点击计数器+1</button>
<Show count={count} age={age} clear={clear} />
</div>
);
}
export default HooksUseMome;
在上面的例子中,父组件小女子初始年龄为16岁,而到子组件经过ageChange函数,返回我的年龄永远比小女子年龄大两岁。<br />
但是如果没有useMemo,当父组件的计数器count值发生变化时,子组件的ageChange函数也会执行,这不是我们想要的结果,我们只想当小女子的年龄发生变化时,再执行ageChange函数。所以,用useMemo可以实现我们想要的效果。如上面代码所示const myAge = useMemo(() => ageChange(age), [age]);,使用useMemo,第二个参数是age,这样,只有当age发生变化时,才执行其中的函数。<br />
在类组件中,有shouldComponentDidUpdata生命周期,我们可以在其中做监测,当检测到state值没发生变化时,直接不渲染组件,而useMemo和这个生命周期还有些许不同。它是当检测的state发生变化时而执行某些函数,避免额外的开销,节省性能。
九: <span id="useRef">useRef</span> 回目录
在项目开发中,我们比较少用到ref,一般我们不直接操作DOM,都是通过状态来控制DOM,不过在某些情况下,可能还是会用到ref,这一节我们通过对input输入框数据的双向绑定来认识useRef
import React, { useState, useRef } from 'react';
function HooksUseRef() {
const [inputValue, setInputValue] = useState();
const inputRef = useRef(null);
function inputChangeHandle(e) {
setInputValue(e.target.value);
}
function inputRefChangeHandle() {
console.log(inputRef.current.value);
}
return (
<div>
<div>
<input
value={inputValue}
onChange={inputChangeHandle}
type="text"
/>
<span>使用state绑定inputValue值</span>
</div>
<div>
<input
ref={inputRef}
onChange={inputRefChangeHandle}
type="text"
/>
<span>使用Ref绑定inputValue值</span>
</div>
</div>
);
}
export default HooksUseRef;
在上面的案例中,我们如果要取input的值,如果是state双向绑定,可以直接取inputValue,如果是用ref,则可以通过inputRef.current.value取到值
通过const inputRef = useRef(null);,我们获取到的是一个对象,而current属性就是其中的dom元素。
十: <span id="useCallBack">useCallBack</span> 回目录
把内联回调函数及依赖项数组作为参数传入 useCallback,它将返回该回调函数的 memoized 版本,该回调函数仅在某个依赖项改变时才会更新。
简而言之,useCallBack是用来缓存函数的,在class类中,我们通常在constructor中使用this.fn = this.fn.bind(this)来绑定this,是每次调用的fn都是之前的fn,而不用开辟新的函数。而useCallback同样有此功能,useCallBack和useMemo的不同点在于useMemo相当于缓存state,而useCallBack相当于缓存函数,官方给的解释是这样的useCallback(fn, deps) 相当于 useMemo(() => fn, deps)。.
我们下面还是用计数器和年龄做例子
import React, { useState, useEffect, useCallback } from 'react';
function Show({ countCallBack, ageCallBack }) {
const [count, setCount] = useState(() => { countCallBack(); });
const [age, setAge] = useState(() => { ageCallBack(); });
useEffect(() => {
setCount(countCallBack());
}, [countCallBack]);
useEffect(() => {
setAge(ageCallBack());
}, [ageCallBack]);
return (
<div>
数量:{count} 年龄:{age}
</div>
);
}
function HooksCallBack() {
const [count, setCnt] = useState(0);
const [age, setAge] = useState(16);
const countCallBack = useCallback(() => {
return count;
}, [count]);
const ageCallBack = useCallback(() => {
return age;
}, []);
return (
<div>
<p>小女子芳年{age}</p>
<p>你点击了{count}次</p>
<button
type="button"
onClick={() => { setAge(age + 1); }}
>点击年龄+1</button>
<button
type="button"
onClick={() => { setCnt(count + 1); }}
>点击计数器+1</button>
<Show countCallBack={countCallBack} ageCallBack={ageCallBack} />
</div>
);
}
export default HooksCallBack;
在上面的例子中,只有点击计数器按钮,子组件才会跟着更新,点击年龄按钮子组件则不跟着更新。使用useCallback如果没有依赖,则只会执行一次,只有依赖改变,才会返回新的函数,我们可以根据这个规则实现bind的效果。
十一: <span id="自定义函数">自定义函数</span> 回目录
这一小节,我们做一个监听浏览器窗口的自定义函数,废话不多说,直接上例子:
import React, { useState, useEffect, useCallback } from 'react';
function useWinSize() {
const [size, setSize] = useState({
width: document.documentElement.clientWidth,
height: document.documentElement.clientHeight
});
const resizeHandle = useCallback(() => {
setSize({
width: document.documentElement.clientWidth,
height: document.documentElement.clientHeight
});
}, []);
useEffect(() => {
window.addEventListener('resize', resizeHandle);
return () => {
window.removeEventListener('resize', resizeHandle);
};
}, []);
return size;
}
function HooksFunction() {
let size = useWinSize();
return (
<div>
浏览器窗口尺寸{`${size.width}*${size.height}`}
</div>
);
}
export default HooksFunction;
上面的代码就不多解释了,所需要注意的是自定义函数需要以use开头,且后面应该用大写字母与use分隔开。到此呢,hooks先写到这里,基本上也能面对绝大多数的业务场景,其它的hooksAPI等以后开发中如果有用到,再来补充。<br />
在下一节中,我们将会把TS从基础到项目应用整个的梳理出来,分两篇来完成,待续。。。