Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。
前言:hooks
出了已有大半年了,关注的公众号也大都推了关于hooks
的文章,可是因为工作中一直用的是class
,所以一直没有用,也没有学,趁着这段时间项目不那么干,将hooks
系统性的学习一下,并做笔记记录一下。
目录
- useState
- userEffect
- userEffect实现componentWillUnmont
- 父子组件传值
- userContext
- userReducer
- useReducer替代Redux案例
- useMemo
- useRef
- useCallBack
- 自定义函数
一:useState
在组件中,我们难免使用state
来进行数据的实时响应,这是react
框架的一大特性,只需更改state
,组件就会重新渲染,试图也会响应更新。
不同于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
中是定义变量的同时定义一个改变变量的函数。
userState
是一个方法,方法返回值为当前state以及更新state的函数,所以,在上面的例子中,我们用const [count, setCount] = useState(0);
将count和setCount解构出来,而userState
方法的参数就是state的初始值。当然count和与之对应的改变函数名称并不一定非得是setCount,名称可以随便起,只要是一块解构出来的即可。
在class
组件中,我们可以用setState
一次更改多个state
值而只渲染一次,同样的,在hooks
中,我们调用多个改变state
的方法,也只是渲染一次。
二:userEffect 回目录
在class
组件中,有生命周期的概念,最常用的,我们通常会在componentDidMount
这个生命周期中做数据请求,偶尔,我们也会用一些其它的生命周期,像是componentDidUpdata
,componentWillReceiveProps
等。在hooks
中,没有生命周期的概念,但是,有副作用函数useEffect。
使用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函数会在浏览器完成画面渲染之后延迟调用
在一个hooks函数中,可以同时存在多个effect函数,所以,当有需求每次更新都执行useEffect中的代码时,可以用一个useEffect请求数据,用其他的useEffect做另外的事情。只需根据第二个参数即可区别不同作用。
//官方示例性能优化
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]); // 仅在 count 更改时更新
三:userEffect实现componentWillUnmount 回目录
部分情况下,需要在组件卸载是做一些事情,例如移除监听事件等,在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
函数的触发以及返回函数的调用。
四:父子组件传值 回目录
父子组件传值在实际开发中是必不可少的,在class
组件中,我们可以直接给子组件添加属性,然后在子组件通过props
即可获取到父组件的值。但是在hooks
中,组件都是函数,没有props
,所以不能用相同的方式传值
在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函数组件中以解构参数的方式获取父组件的值。这种传值方式和类组件本质上还是一样的。
五:userContext 回目录
使用userContext
,不仅可以实现父子组件传值,还可以跨越多个层级进行传值,例如父组件可以给孙子组件甚至重孙子组件进行直接传值等,redux
全局状态管理本质上也是对content
的一种应用。
在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
。
content
提供了一种树状结构,被Context.Provider
所包裹的所有组件,都可以直接取数据。redux
就是利用了context
的这种特性实现全局状态管理。在下面的几小节中,我们会讲hooks
中context
搭配useReducer
来实现redux
的功能。
六:userReducer 回目录
userReducer
是useState
的替代方案,它接收一个形如(state,action) => newState
的reducer
,并返回当前的state
以及其配套的dispatch
方法。
总的来说呢,userReducer
可以接受两个参数,第一个参数就是和redux
中的reducer
一样的纯函数,第二个参数是state
的初始值,并返回当前state
以及dispatch
。
还是以官方示例的计数器为例
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。
七:useReducer替代Redux案例 回目录
在本小节中,我们会用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;
通过reduce
r和context
实现计数器的功能,我们共用了四个文件,当然count.js
这个文件本应该拆分成三个文件,常量单独定义一个文件,reducer
纯函数也应该单独定一个文件,不过代码不多,就暂时合一块了。
在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
。
在Button
组件中,我们通过countContext
取到dispatch
及常量,改变count
这个数值,在Show
组件中,只是展示count
,在ReducerToRedux
文件中,是做一个连接器,用CountWrap
包裹Button
和Show
组件。
可能说的有些啰嗦,看上去有些复杂,其实稍一整理,原理很简单,自己写一遍整理清楚逻辑使用上就很简单了。
讲到这里,其实hooks
已经能应对绝大部分场景了,下面两小节,我们会讲一下useMemo
和useRef
,用于优化渲染以及处理特殊情况。
八:useMemo 回目录
把“创建”函数和依赖项数组作为参数传入 useMemo,它仅会在某个依赖项改变时才重新计算 memoized 值。这种优化有助于避免在每次渲染时都进行高开销的计算。
useMemo是函数式组件官方提供的性能优化的一个方法,接受两个参数,第一个参数是要执行的函数,第二个参数是state中的值或者父组件传下来的值,代表只有当第二个参数的值发生变化时,才执行函数。其中,第二个参数是数组,可以同时优化多个state或者父组件传下来的参数,首次渲染组件是,如果页面用到要优化的值,函数会执行。
我们还是以计数器以及年龄为例
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
函数,返回我的
年龄永远比小女子
年龄大两岁。
但是如果没有useMemo,当父组件的计数器count
值发生变化时,子组件的ageChange
函数也会执行,这不是我们想要的结果,我们只想当小女子
的年龄发生变化时,再执行ageChange
函数。所以,用useMemo可以实现我们想要的效果。如上面代码所示const myAge = useMemo(() => ageChange(age), [age]);
,使用useMemo,第二个参数是age
,这样,只有当age
发生变化时,才执行其中的函数。
在类组件中,有shouldComponentDidUpdata
生命周期,我们可以在其中做监测,当检测到state
值没发生变化时,直接不渲染组件,而useMemo
和这个生命周期还有些许不同。它是当检测的state
发生变化时而执行某些函数,避免额外的开销,节省性能。
九: useRef 回目录
在项目开发中,我们比较少用到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
元素。
十: useCallBack 回目录
把内联回调函数及依赖项数组作为参数传入 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
的效果。
十一: 自定义函数 回目录
这一小节,我们做一个监听浏览器窗口的自定义函数,废话不多说,直接上例子:
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
等以后开发中如果有用到,再来补充。
在下一节中,我们将会把TS
从基础到项目应用整个的梳理出来,分两篇来完成,待续。。。