一、Hooks
1. Hooks 简介
Hooks,可以翻译成钩子。
在类组件中钩子函数就是生命周期函数,Hooks 主要用在函数组件。
在 react 中定义组件有2种方式:class 定义的类组件和 function 定义的函数组件。
在类组件中,钩子函数可以给组件增加额外的功能。
类组件的不足:
- 在一个钩子里可能有很多业务代码。
- 一个业务很可能出现在多个钩子里。
- class 组件中的 this 指向问题
在函数组件中,函数在执行完毕之后,会自动销毁内存,存储在函数中的状态无法保留。为了增加函数组件的功能,我们需要引入 Hooks。
类组件通过在构造器中直接使用 this.state = {} 给组件设置状态值,而函数组件不行。因为函数执行完后会销毁内容,在函数内声明的变量就会自定销毁,所以函数组件无法设置状态,于是 react 提供了 Hooks。
2. Hooks 的分类
Hook 分为2种,基础 Hook 和额外的 Hook。
基础 Hook:
- useState
- useEffect
- useContext
额外的 Hook:
- useReducer
- useCallback
- useMemo
- useRef
所有 Hook 都以 use 开头。
3. Hook 的使用
所有 Hook 都在 react 模块下,使用时需进行引入。
import { useState } from 'react';
二、useState
1. 使用 useState 的注意事项
(1) useState 向组件引入新的状态,这个状态会被保留在 react 中。
(2) useState 只有一个参数,这个参数是初始状态值,它可以是任意类型。
(3) useState 可以多次调用,意味着可以为组件传入多个状态。
(4) useState 返回值是一个数组,第一个元素就是我们定义的 state,第二个元素就是修改这个 state 的方法。接收 useState 的返回值使用数组结构语法,我们可以随意为 state 起名字。修改 state 的方法必须是 set + 状态名首字母大写构成,不按照约定写就会报错。
(5) useState 的参数也可以是一个函数,这个函数的返回值就是我们的初始状态。
2. 计数器案例
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<h3>{count}</h3>
<button onClick={() => setCount(count + 1)}>+</button>
</div>
);
}
在这个案例中,我们定义了一个状态 count,传入的参数为函数 () => 1,则 count 的初始值为函数的返回值1。
当点击按钮时,调用修改方法 setCount 将 count 的值自增1,页面上计数器也会同步自增1。
3. 修改状态方法
修改状态的方法 set + 状态变量名:
- 这个方法的参数可以是值(替换方法),也可以是一个函数。如果是函数,那么这个函数的参数就是初始状态值,这个方法就是更新方法。
- 这个方法是异步的。
看如下案例:
function Counter() {
const [count, setCount] = useState(() => 1);
function handleCount(a) {
setCount(a + 1);
document.title = a;
}
return (
<div>
<h3>{count}</h3>
<button onClick={() => handleCount(count)}>+</button>
</div>
);
}
在点击按钮后,计数器显示数字变为了2,但页面的标题还是1。每次点击按钮后,页面的标题都比计数器显示数字少1。因此执行代码先执行了第5行的设置页面标题,才执行第4行的设置 count 值,看过上篇文章就很容易理解。
三、useEffect
1. useEffect 简介
useEffect 这个 Hook 函数的主要作用就是将副作用代码添加到函数组件内。所谓的副作用代码就是 dom 更改、计时器、数据请求等。
使用场景:
- useEffect(() => {}) 这种写法代表两个生命周期函数 componentDidMount 和 componentDidUpdate。
- useEffect(() => {}, []) 这种写法代表生命周期函数 componentDidMount。
- useEffect(() => () => {}) 这种写法代表组件卸载之前 componentWillUnmount 和 componentDidUpdate 两个生命周期函数。
2. useEffect(() => {})
useEffect 钩子函数传入一个函数作为参数,代表组件在加载完成后和数据更新时都会调用传入的函数。
function Counter() {
const [count, setCount] = useState(() => 1);
function handleCount(a) {
setCount(a + 1);
document.title = a;
}
useEffect(() => {
console.log('hahaha');
});
return (
<div>
<h3>{count}</h3>
<button onClick={() => handleCount(count)}>+</button>
</div>
);
}
在页面加载完成和每次点击按钮让计数器自增后,控制台都会打印 hahaha。即 useEffect Hook 实现了生命周期函数 componentDidMount 和 componentDidUpdate 的功能。
3. useEffect(() => {}, [])
useEffect 钩子函数传入一个函数和一个数组作为参数,代表组件在加载完成后会调用传入的函数。
function Counter() {
const [count, setCount] = useState(() => 1);
function handleCount(a) {
setCount(a + 1);
document.title = a;
}
useEffect(() => {
console.log('hahaha');
}, []);
return (
<div>
<h3>{count}</h3>
<button onClick={() => handleCount(count)}>+</button>
</div>
);
}
这个案例和上一个案例不同之处在于 useEffect 钩子函数传入了一个空数组作为第2个参数,当页面加载完成时控制台会打印 hahaha,点击按钮更新计数器后不再打印,即实现了生命周期函数 componentDidMount 的功能。
4. useEffect(() => () => {})
useEffect 钩子函数传入一个返回函数的函数作为参数,代表组件在数据更新时和卸载时会调用返回的函数。
function Counter(props) {
const [count, setCount] = useState(() => 1);
function handleCount(a) {
setCount(a + 1);
document.title = a;
}
useEffect(() => () => {
console.log('hahaha');
}, []);
return (
<div>
<h3>{count}</h3>
<button onClick={() => handleCount(count)}>+</button>
<button onClick={() => props.root.unmount()}>卸载组件</button>
</div>
);
}
这个案例中,点击按钮更新计数器和点击卸载组件时控制台都会打印 hahaha,即实现了生命周期函数 componentWillUnmount 和 componentDidUpdate 的功能。
5. useEffect 第二个参数的使用
useEffect 钩子函数的第二个参数,正常添加空数组,代表的生命周期是 componentDidMount。即使我们修改了 state,useEffect 也只会调用一次。
如果我们想让某个 state 发生改变的时候,继续调用 useEffect,就需要把这个状态添加到第二个参数的数组中。
看下面的案例:
function Counter() {
const [count, setCount] = useState(() => 1);
const [person, setPerson] = useState({ name: 'zhangsan' });
function handleCount(a) {
setCount(a + 1);
document.title = a;
}
useEffect(() => {
console.log('计数器改变');
}, [count]); // count 一旦发生改变,就会执行 useEffect
return (
<div>
<h3>{count}</h3>
<h3>{person.name}</h3>
<button onClick={() => handleCount(count)}>+</button>
<button onClick={() => setPerson({ name: 'lisi' })}>更改person</button>
</div>
);
}
useEffect 第二个参数传递了 count,那么将会在 count 状态更新时才会执行传入的函数。
6. useEffect 的异步处理
看下面的案例:
function Counter() {
function asyncFn() {
setTimeout(() => {
console.log('hahaha');
}, 1000);
}
useEffect(() => {
asyncFn();
});
return (
<div>
</div>
);
}
我们声明了一个异步函数,然后在 useEffect 中调用,预览正常,在页面加载完成1秒后打印 hahaha。
当我们使用 async/await 时,像下面这样:
function Counter() {
function asyncFn() {
setTimeout(() => {
console.log('hahaha');
}, 1000);
}
useEffect(async () => {
await asyncFn();
});
return (
<div>
</div>
);
}
这时控制台就会报一个警告:
在 useEffect 中如果使用了异步函数,那就需要定义一个自调用函数。如:
function Counter() {
function asyncFn() {
setTimeout(() => {
console.log('hahaha');
}, 1000);
}
useEffect(() => {
(async () => {
await asyncFn();
})();
});
return (
<div>
</div>
);
}
这时控制台就不报警告了。
遇到异步函数,我们需要在 useEffect 中添加一个自调用函数。
四、useContext
useContext 用于父组件向子孙组件传递数据,不需要再使用通过 props 从父组件向子组件逐级传递数据。
如果组件多层嵌套,使用 props 来传值显得极其复杂,这时就需要使用 useContext。
1. 引入 useContext
要使用 useContext,需要引入 useContext 和 createContext 两个函数。
import { useContext, createContext } from 'react';
2. 使用方法
首先定义一个 context 变量,用于存放当前上下文对象。将上下文对象的 Provider 作为父组件,通过 value 属性将要传递的值传给子孙组件。在子孙组件中就可以通过 useContext 获取到要传递的值。
const myContext = createContext(); // 当前上下文对象
function App() {
const value = useContext(myContext);
return (
<div>{value}</div>
);
}
export default function () {
return (
<myContext.Provider value={100}>
<div>hello world</div>
<App />
</myContext.Provider>
);
}
在 App 组件中我们获取到了父组件通过 context 传递来的 value 值。
五、useReducer
1. useReducer 简介
useReducer 是 useState 的替代方案。
对于拥有许多状态更新逻辑的组件来说,过于分散的事件处理程序可能会令人不知所措。对于这种情况,我们可以将组件的所有状态更新逻辑整合到一个外部函数中,这个函数叫作 reducer。
Reducer 是处理状态的另一种方式。可以通过三个步骤将 useState 迁移到 useReducer:
(1) 将设置状态的逻辑修改成 dispatch 的一个 action;
(2) 编写 一个 reducer 函数;
(3) 在组件中 使用 reducer。
使用 reducers 管理状态与直接设置状态略有不同。它不是通过设置状态来告诉 react “要做什么”,而是通过事件处理程序 dispatch 一个 “action” 来指明 “用户刚刚做了什么”。(而状态更新逻辑则保存在其他地方!)因此,我们不再通过事件处理器直接 “设置 task”,而是 dispatch 一个 “添加/修改/删除任务” 的 action。这更加符合用户的思维。
action 对象可以有多种结构。按照惯例,我们通常会添加一个字符串类型的 type 字段来描述发生了什么,并通过其它字段传递额外的信息。type 是特定于组件的,选一个能描述清楚发生的事件的名字!
2. 计数器案例
在这个案例中,要实现一个可以增加和减少数字的计数器,效果如下:
点击+号按钮自增,点击-号按钮自减。
function Counter() {
const initState = { count: 0 };
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
default:
return { count: state };
}
}
const [state, dispatch] = useReducer(reducer, initState);
return (
<div>
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
<span>{state.count}</span>
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
</div>
);
}
在这个案例中,我们定义了一个 reducer 函数处理不同操作需要更新的状态。在点击按钮时,调用 useReducer 返回的 dispatch 方法,传递操作类型给 reducer 函数,然后按对应方法更新状态。
reducer 函数就是我们放置状态逻辑的地方。它接受两个参数,分别为当前 state 和 action 对象,并且返回的是更新后的 state,react 会将状态设置为从 reducer 返回的状态。
在 reducers 中使用 switch 语句是一种惯例,和使用 if/else 的结果是相同的,但 switch 语句读起来一目了然。
六、useMemo
1. useMemo 简介
useMemo 是一种性能优化的手段,主要用途就是对状态 state 的记忆,并且还具有缓存功能,类似于 vue 中的计算属性。还有一个作用,避免在代码中参与大量运算。
useMemo 接收2个参数,第1个参数为执行运算的函数,第2个参数为要监控的状态。
2. 使用 useMemo
还是计数器案例,使用 useMemo 通过计数器当前值计算出一个新的值展示在页面上。
function Counter() {
const [count, setCount] = useState(0);
const value = useMemo(function () {
return count * 10;
}, [count]); // 数组中的元素就是 useMemo 监控的状态
return (
<div>
<h3>{count}</h3>
<h3>{value}</h3>
<button onClick={() => setCount(count + 1)}>按钮</button>
</div>
);
}
在这个案例中,我们使用 useState 定义了一个计数器。使用 useMemo 定义了一个 value 状态,代表 useMemo 中计算结果的值。
useMemo 会监控第2个参数数组中的状态,当对应状态更新时,才会执行 useMemo。
七、useRef
1. useRef 简介
useRef 用于获取组件中的 dom 对象。
2. useRef 使用
在组件的属性中加入 ref 属性为存储 useRef 的返回值的变量,就可以获取到这个组件的 dom 对象。
function App() {
const refObj = useRef();
console.log(refOjb);
function getRef() {
console.log(refObj);
}
return (
<div>
<div ref={refObj}>hello</div>
<button onClick={getRef}>按钮</button>
</div>
);
}
current 属性为获取到的 dom 对象。
第一次打印出的 refObj 的 current 属性为 undefined,因为这时页面元素还未挂载。当点击按钮时,就会获取到 div 的 dom 对象了。
八、memo
1. memo 简介
memo 是一个高阶组件(参数又是一个组件),功能是对组件进行记忆。
2. memo 使用
先来看一个案例:
function App() {
const [count, setCount] = useState(0);
const fn = function () {
console.log('hahaha');
};
return (
<div>
{count}
<button onClick={() => setCount(count + 1)}>增加</button>
<Heads fn={fn}></Heads>
</div>
);
}
function Heads(props) {
console.log('我被渲染了');
return <button>按钮</button>;
}
在这个案例中,我们定义了一个父组件 App 和一个子组件 Heads。接下来查看在 count 变化时,子组件是否重新被渲染,答案是肯定的,子组件被渲染了多次。
但这个过程中子组件没有改变,重新渲染多次显然不好。
当前组件内的视图没有发生改变,但被重渲染了,这时就需要借助 memo。
function App() {
const [count, setCount] = useState(0);
const fn = function () {
console.log('hahaha');
};
return (
<div>
{count}
<button onClick={() => setCount(count + 1)}>增加</button>
<Heads fn={fn}></Heads>
</div>
);
}
const Heads = memo(function (props) {
console.log('我被渲染了');
return <button>按钮</button>;
});
memo 的参数是一个组件,返回值为一个高阶组件,可以对传入的组件进行记忆,不修改就不会重新渲染。
但上述案例中,点击按钮时子组件还是会重新渲染。原因在于父组件给子组件传递了一个 fn,当点击按钮时,父组件重新渲染导致 fn 被赋值,fn 修改就会导致子组件重新渲染。
修改代码如下:
function App() {
const [count, setCount] = useState(0);
const fn = function () {
console.log('hahaha');
};
return (
<div>
{count}
<button onClick={() => setCount(count + 1)}>增加</button>
<Heads></Heads>
</div>
);
}
const Heads = memo(function (props) {
console.log('我被渲染了');
return <button>按钮</button>;
});
删除给子组件传入的 fn,这时再点击按钮,子组件就不会重新渲染了。
要实现前面的案例传入 fn 不让子组件重新渲染,需要使用 useCallback Hook。
九、useCallback
1. useCallback 简介
useCallback 是一个允许我们在多次渲染中缓存函数的 React Hook,它返回一个 memoized 回调函数。
useMemo 是对数据的记忆,useCallback 是对函数的记忆。
useCallback 有2个参数,第1个参数为要缓存的函数,第2个参数是一个数组,表示在哪些响应值(包括 props 、state 和所有在组件内部直接声明的变量和函数)变化时更新函数。
2. useCallback 使用
将上面的案例用 useCallback 改写:
function App() {
const [count, setCount] = useState(1);
const fn = useCallback(function () {
return count;
}, []);
return (
<div>
{count}
<button onClick={() => setCount(count + 1)}>增加</button>
<Heads fn={fn}></Heads>
</div>
);
}
const Heads = memo(function (props) {
return <button onClick={() => console.log(`我被渲染了${props.fn()}次`)}>按钮</button>;
});
这里我们使用 useCallback 将函数 fn 进行缓存,这时再去点击增加按钮,将不会再重新渲染子组件。
接下来看 useCallback 第2个参数如何使用:
function App() {
const [count, setCount] = useState(1);
const fn = useCallback(function () {
return count;
}, [count]);
return (
<div>
{count}
<button onClick={() => setCount(count + 1)}>增加</button>
<Heads fn={fn}></Heads>
</div>
);
}
const Heads = memo(function (props) {
return <button onClick={() => console.log(`我被渲染了${props.fn()}次`)}>按钮</button>;
});
这里我们在 useCallback 中加入了第二个参数,数组中有一个元素 count,表示在 count 变化时更新函数 fn。这时在点击增加按钮,就会重新渲染子组件。
十、自定义 Hook
自定义 Hook 就是自己封装的函数功能和 react 中内置的 Hook 进行结合,用于组件间共享逻辑
自定义 Hook 必须以 use 开头。
下面来封装一个 axios get 请求的自定义 Hook:
const useGet = function ({ path, params }) {
const [data, setData] = useState({});
useEffect(() => {
axios.get(path, params).then(res => {
setData(res.data);
});
}, []);
return data;
};
我们自定义了一个 Hook useGet,作用是使用 axios 的get 请求方式请求接口数据。
function App() {
const data = useGet({ path: 'https://conduit.productionready.io/api/articles', params: {}});
if (!data.articles) {
return <div>
请求中...
</div>;
}
return (
<div>
{data.articles.map(item => <p>{item.title}</p>)}
</div>
);
}
使用时和使用内置 Hook 一样的形式使用自定义 Hook。这里我们通过 useGet 获取到接口中的文章数据,并将它们的标题展示在页面上。