紫棋的一句想要我帮你唱 hook 吗?让 hook 红了一把!
做为一个开发,尤其是前端开发,在电视上听到这个词还是有点小兴奋的(虽然彼 hook 非此 hook)。玩够这个梗,是不是也要了解一下自己的 react hook?
一、why hook
使用 react 有一段时间了,组件化编程也早已成为习惯。常用的两种编写组件的方式就是就是 class 组件 和函数组件 。
class 组件: 通过继承 React.Component 来构建组件,虽然提供了 state 状态和完备的生命周期函数,但是也有很多不方便的地方。
- 很多事件需要在 挂载期 componentDidMount和 更新期 componentDidUpdate重复书写。有些副作用还需要在 卸载期 componentWillUnmount卸载。代码重复度很高,而且难以理解,一旦忘记了就会引起不少 bug。
componentDidMount() {document.title = `You clicked ${this.state.count} times`;
ChatAPI.subscribeToFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
componentDidUpdate() {document.title = `You clicked ${this.state.count} times`;
}
componentWillUnmount() {
ChatAPI.unsubscribeFromFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
- 通过 class 书写自然会碰到 this 的问题,要想使用 this 函数不能忘记绑定。代码十分冗余。
函数组件:通过函数直接书写的组件,看起来是简洁了许多,但是不能使用 state,也没有生命周期函数、更不能使用 react 的一些其他特性。只能通过父组件传递进来,任人鱼肉,只能惨淡的沦为 展示组件。
那么,函数组件就只能沦为 展示 的花瓶吗?能结合 class 组件能使用 react 特性 的优点和函数组件 简洁优雅 的特性吗?好在 react hook 来了,函数组件的春天来了。
React 16.8 新增 Hook 特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。
二、what’s hook
是不是感觉 hook 挺神奇的?hook 是什么?怎么使用?不用惊讶,hook 就是一个钩子 函数,钩着 react 的一些 api 供给函数组件使用。
先看一个官方的 useState 例子:
import React, {useState} from 'react';
function Example() {
// 声明一个叫“count”的 state 变量。const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
引入一个 hook(useState 函数),传入一个 初始值 0 ,然后函数返回了一个数组,通过数组的 结构赋值,取得 state,count = 0 和修改 count 值的函数 setCount。当 button 发生点击事件时,触发 setCount 函数,并且传入新的 count 值完成对 state 值的修改,并展示到页面上。代码转换成 class 组件如下:
class Example extends React.Component {constructor(props) {super(props);
this.state = {count: 0};
}
render() {
return (
<div>
<p>You clicked {this.state.count} times</p>
<button onClick={() => this.setState({ count: this.state.count + 1})}>
Click me
</button>
</div>
);
}
}
看明白了吗?
Hook 概念
Hook 是一些可以让你在 函数组件 里“钩入”React state 及 生命周期 等(context、refs)特性的函数。
React 内置了一些像 useState 这样的 Hook。你也可以创建你自己的 Hook 来复用不同组件之间的状态逻辑。
hook 使用规则
一个组件中可以使用多个 hook,每个 hook 会独立存在,内部状态不会共用。每次更新组件时,hook 会按顺序从上到下执行。
function Form() {
// 1. Use the name state variable
const [name, setName] = useState('Mary');
// 2. Use the age state variable
const [age, setAge] = useState('12');
// 3. Use the surname state variable
const [surname, setSurname] = useState('Poppins');
// ...
}
第一次执行结果:设置状态 name Mary;设置状态 age 12;设置状态 surname Poppins;第二次执行结果:设置状态 name Mary;设置状态 age 12;设置状态 surname Poppins;
那么,react 是如何知道哪个 state 对应 哪一个 useState?很简单,React 靠的是 Hook 调用的顺序。
react hook 使用,要遵循下面两个规则:
-
只在最顶层使用 Hook
react 依靠 hook 调用顺序来对应 state,所以调用顺序不能变。function Form() { // 1. Use the name state variable const [name, setName] = useState('Mary'); // 2. Use the age state variable if(!name){const [age, setAge] = useState('12'); } // 3. Use the surname state variable const [surname, setSurname] = useState('Poppins'); // ... } 第一次执行结果:设置状态 name Mary;设置状态 age 12;设置状态 surname Poppins;第二次执行结果:设置状态 name Mary;设置状态 age 12;// 判断后会被忽略 设置状态 surname 12;// 此时设置为 12,与需求已不符
也就是说只能在顶层使用 hook。
-
只在 React 函数中调用 Hook
在 class 组件中是无法使用 hook 函数的,只能在函数组件中使用。
在自定义 hook 中也可以使用。
内置 Hook
上面使用的 useState Hook 是一种可以让你在函数组件内使用 state 的内置 hook,除此之外 react 还提供了许多内置 hook:
基础 Hook
useState
useEffect
useContext
额外的 Hook
useReducer
useCallback
useMemo
useRef
useImperativeHandle
useLayoutEffect
useDebugValue
下面看一下 useState、useEffect 两个常用的 hook,其他的使用频率较低,使用方法也都基本一致其他 hook
三、state Hook
我们想在函数组件里加入 state,点击 +,完成数字 累加 功能:
import React , {useState} from 'react'
function Item(){const [count,setCount] = useState({name:'tom',age:11})
return <>
{`${count.name}已经 ${count.age}岁了 `}
<div onClick={()=>{setCount({name:'janny',age:count.age+1})}}>+</div>
</>
}
export default Item;
从上面代码可以看出来,useState hook 提供了在 函数组件 里使用 state 的能力。十分简洁。
首先,通过 react 引入 useState 钩子函数;
接着,在函数组件内调用 useState,并传入 state 的初始值,可以是个值,也可以是对象。useState 函数的返回值个数组,这里通过数组的结构赋值取得 count(变量名可以随意定义)和修改 count 的方法 setCount(函数名也可以随意定义)。
取值 state:可以通过变量 count 直接取值
修改 state:通过给 setCount 函数传递值来修改
是不是很简单?以后终于可以在函数组件里使用 state 了,拥有控制自己主权的能力了!除了使用 state,还有一种场景也是我们经常碰到的,那就是函数组件没有 生命周期,一些副作用的操作没法完成!别着急,react 又内置了极好的 effect hook!
四、effect Hook
当我们点击 + 时,不仅想完成数字累加,而且想要在 title 里显示我们的修改,如果在 class 组件里,我们可以在 componentDidMount 生命周期里通过 setState 完成,但是函数组件里没法完成,即使现在有 useState Hook 也没有生命周期函数,没法完成!那就在来一个 hook 吧,引入 effect Hook。
useEffect hook 不仅提供了生命周期,而且 useEffect Hook 可以看做componentDidMount,componentDidUpdate 和 componentWillUnmount 这三个函数的组合。也就是在这一个钩子函数里可以完成很多事!
1. 无需清除的副作用
import React , {useState,useEffect} from 'react'
function Item(){const [count,setCount] = useState({name:'tom',age:11})
useEffect(()=>{document.title = count.age})
return <>
{`${count.name}已经 ${count.age}岁了 `}
<div onClick={()=>{setCount({name:'tom',age:count.age+1})}}>+</div>
</>
}
export default Item;
好好感受一下!useEffect 就像是一个生命周期函数。这个功能在 class 组件中,需要在两个生命周期函数(componentDidMount、componentDidUpdate)中编写重复的代码。在函数 useEffect 内进行副作用操作就行了。因为每次挂载、更新后 useEffect 都会执行。
在 React 组件中有 两种常见副作用操作 : 需要清除 的和 不需要清除的。上面这种就是不需要清除的副作用。
2. 需要清除的副作用
假如,我们觉得只用手指点击太累了,想弄个计时器帮我们执行。
let timer = null;
function Item(){const [count,setCount] = useState({name:'tom',age:11})
useEffect(()=>{clearInterval(timer);// 清除
timer = setInterval(()=>{console.log(1)
setCount({name:'tom',age:count.age+1})
},1000)
document.title = count.age
})
return <>
{`${count.name}已经 ${count.age}岁了 `}
<div onClick={()=>{setCount({name:'tom',age:count.age+1})}}>+</div>
</>
}
这样就很棒,换上自动挡就很舒服!但是当我们切换路由,移除这个组件后,出去看看风景回来!发现定时器没有停,自己玩的很嗨 … 哎呦喂!
这就是 需要清除的副作用,class 组件里我们可以通过在 componentWillUnmount 生命周期函数里清除,所以这里我们也要清除这些需要清除的副作用!
function Item(){const [count,setCount] = useState({name:'tom',age:11})
useEffect(()=>{clearInterval(timer)
timer = setInterval(()=>{console.log(1)
setCount({name:'tom',age:count.age+1})
},1000)
document.title = count.age;
return function(){//add
clearInterval(timer)
}
})
return <>
{`${count.name}已经 ${count.age}岁了 `}
<div onClick={()=>{setCount({name:'tom',age:count.age+1})}}>+</div>
</>
}
useEffect清除副作用 的方式也很简单,return 一个函数,在函数里清除 就可以了。
3. 优化 useEffect
上面说了 useEffect hook 在每次挂载、更新后都会执行。这不就是跟 setState 在性能方面造成的问题一样严重吗?class 组件 通过在 componentDidUpdate 进行优化,useEffect 怎么优化!
可以通过 useEffect 函数的第二个参数 进行优化。
1. 传递空数组 ,表示函数只执行一次!这就告诉 React 你的 effect 不依赖于 props 或 state 中的任何值,所以它永远都不需要重复执行。effect 内部的 props 和 state 就会一直拥有 其初始值。更接近大家熟悉的 componentDidMount。
useEffect(()=>{...},[])
2. 传递包含 state 的数组
会通过 === 比较 count 的 前一次渲染的值 与这一次要渲染的值 ,如果全等则 React 会跳过这个 effect,这就实现了性能的优化。一定要将所有需要比较的 state 都放进数组 ,否则将不会进行比较, 也就会直接跳过!
useEffect(()=>{...},[count])
五、自定义 Hook
react 内置了多 hook,我们也可以对这些 hook 进行业务方面的封装,写出属于自己的 hook!我们将上面的叠加计时封装成一个自定义 hook:数字每秒叠加,并且修改显示在 title 上。
import React , {useState,useEffect} from 'react'
function useTitle(){// 自定义 hook
let timer = null;
const [count,setCount] = useState({name:'tom',age:11})
useEffect(()=>{clearInterval(timer)
timer = setInterval(()=>{setCount({name:'tom',age:count.age+1})
},1000)
document.title = count.age;
return function(){clearInterval(timer)
}
},[count])
return count;// 返回一个对象
}
function Item(){// 自定义 hook 使用
const count = useTitle()
return <>
{`${count.name}已经 ${count.age}岁了 `}
</>
}
将一些公共代码提取出来,最后返回需要在页面显示的数据就成了
六、总结
到这里,react hook 已经有所了解了吧!就是让 函数组件 能够像 class 组件一样自由自在的使用 react 的特性的 函数 。可以不影响之前任何业务,在项目中 完全可选 、100% 向后兼容 的 react 新特性!快在项目中用起来吧!
如有不妥!欢迎指正