资讯专栏INFORMATION COLUMN

React Hooks入门: 基础

mrli2016 / 3195人阅读

摘要:当组件安装和更新时,回调函数都会被调用。好在为我们提供了第二个参数,如果第二个参数传入一个数组,仅当重新渲染时数组中的值发生改变时,中的回调函数才会执行。

前言

  首先欢迎大家关注我的Github博客,也算是对我的一点鼓励,毕竟写东西没法获得变现,能坚持下去也是靠的是自己的热情和大家的鼓励,希望大家多多关注呀!React 16.8中新增了Hooks特性,并且在React官方文档中新增加了Hooks模块介绍新特性,可见React对Hooks的重视程度,如果你还不清楚Hooks是什么,强烈建议你了解一下,毕竟这可能真的是React未来的发展方向。
  

起源

  React一直以来有两种创建组件的方式: Function Components(函数组件)与Class Components(类组件)。函数组件只是一个普通的JavaScript函数,接受props对象并返回React Element。在我看来,函数组件更符合React的思想,数据驱动视图,不含有任何的副作用和状态。在应用程序中,一般只有非常基础的组件才会使用函数组件,并且你会发现随着业务的增长和变化,组件内部可能必须要包含状态和其他副作用,因此你不得不将之前的函数组件改写为类组件。但事情往往并没有这么简单,类组件也没有我们想象的那么美好,除了徒增工作量之外,还存在其他种种的问题。

  首先类组件共用状态逻辑非常麻烦。比如我们借用官方文档中的一个场景,FriendStatus组件用来显示朋友列表中该用户是否在线。

</>复制代码

  1. class FriendStatus extends React.Component {
  2. constructor(props) {
  3. super(props);
  4. this.state = { isOnline: null };
  5. this.handleStatusChange = this.handleStatusChange.bind(this);
  6. }
  7. componentDidMount() {
  8. ChatAPI.subscribeToFriendStatus(
  9. this.props.friend.id,
  10. this.handleStatusChange
  11. );
  12. }
  13. componentWillUnmount() {
  14. ChatAPI.unsubscribeFromFriendStatus(
  15. this.props.friend.id,
  16. this.handleStatusChange
  17. );
  18. }
  19. handleStatusChange(status) {
  20. this.setState({
  21. isOnline: status.isOnline
  22. });
  23. }
  24. render() {
  25. if (this.state.isOnline === null) {
  26. return "Loading...";
  27. }
  28. return this.state.isOnline ? "Online" : "Offline";
  29. }
  30. }

  上面FriendStatus组件会在创建时主动订阅用户状态,并在卸载时会退订状态防止造成内存泄露。假设又出现了一个组件也需要去订阅用户在线状态,如果想用复用该逻辑,我们一般会使用render props和高阶组件来实现状态逻辑的复用。

</>复制代码

  1. // 采用render props的方式复用状态逻辑
  2. class OnlineStatus extends React.Component {
  3. constructor(props) {
  4. super(props);
  5. this.state = { isOnline: null };
  6. this.handleStatusChange = this.handleStatusChange.bind(this);
  7. }
  8. componentDidMount() {
  9. ChatAPI.subscribeToFriendStatus(
  10. this.props.friend.id,
  11. this.handleStatusChange
  12. );
  13. }
  14. componentWillUnmount() {
  15. ChatAPI.unsubscribeFromFriendStatus(
  16. this.props.friend.id,
  17. this.handleStatusChange
  18. );
  19. }
  20. handleStatusChange(status) {
  21. this.setState({
  22. isOnline: status.isOnline
  23. });
  24. }
  25. render() {
  26. const {isOnline } = this.state;
  27. return this.props.children({isOnline})
  28. }
  29. }
  30. class FriendStatus extends React.Component{
  31. render(){
  32. return (
  33. {
  34. ({isOnline}) => {
  35. if (isOnline === null) {
  36. return "Loading...";
  37. }
  38. return isOnline ? "Online" : "Offline";
  39. }
  40. }
  41. );
  42. }
  43. }

</>复制代码

  1. // 采用高阶组件的方式复用状态逻辑
  2. function withSubscription(WrappedComponent) {
  3. return class extends React.Component {
  4. constructor(props) {
  5. super(props);
  6. this.state = { isOnline: null };
  7. this.handleStatusChange = this.handleStatusChange.bind(this);
  8. }
  9. componentDidMount() {
  10. ChatAPI.subscribeToFriendStatus(
  11. this.props.friend.id,
  12. this.handleStatusChange
  13. );
  14. }
  15. componentWillUnmount() {
  16. ChatAPI.unsubscribeFromFriendStatus(
  17. this.props.friend.id,
  18. this.handleStatusChange
  19. );
  20. }
  21. handleStatusChange(status) {
  22. this.setState({
  23. isOnline: status.isOnline
  24. });
  25. }
  26. render() {
  27. return
  28. }
  29. }
  30. }
  31. const FriendStatus = withSubscription(({isOnline}) => {
  32. if (isOnline === null) {
  33. return "Loading...";
  34. }
  35. return isOnline ? "Online" : "Offline";
  36. })

  上面两种复用状态逻辑的方式不仅需要费时费力地重构组件,而且Devtools查看组件的层次结构时,会发现组件层级结构变深,当复用的状态逻辑过多时,也会陷入组件嵌套地狱(wrapper hell)的情况。可见上述两种方式并不能完美解决状态逻辑复用的问题。

  不仅如此,随着类组件中业务逻辑逐渐复杂,维护难度也会逐步提升,因为状态逻辑会被分割到不同的生命周期函数中,例如订阅状态逻辑位于componentDidMount,取消订阅逻辑位于componentWillUnmount中,相关逻辑的代码相互割裂,而逻辑不相关的代码反而有可能集中在一起,整体都是不利于维护的。并且相比如函数式组件,类组件学习更为复杂,你需要时刻提防this在组件中的陷阱,永远不能忘了为事件处理程序绑定this。如此种种,看来函数组件还是有特有的优势的。

Hooks

  函数式组件一直以来都缺乏类组件诸如状态、生命周期等种种特性,而Hooks的出现就是让函数式组件拥有类组件的特性。官方定义:

</>复制代码

  1. Hooks are functions that let you “hook into” React state and lifecycle features from function components.

  要让函数组件拥有类组件的特性,首先就要实现状态state的逻辑。

State: useState useReducer

  useState就是React提供最基础、最常用的Hook,主要用来定义本地状态,我们以一个最简单的计数器为例:

</>复制代码

  1. import React, { useState } from "react"
  2. function Example() {
  3. const [count, setCount] = useState(0);
  4. return (
  5. {count}
  6. );
  7. }

  useState可以用来定义一个状态,与state不同的是,状态不仅仅可以是对象,而且可以是基础类型值,例如上面的Number类型的变量。useState返回的是一个数组,第一个是当前状态的实际值,第二个用于更改该状态的函数,类似于setState。更新函数与setState相同的是都可以接受值和函数两种类型的参数,与useState不同的是,更新函数会将状态替换(replace)而不是合并(merge)。

  函数组件中如果存在多个状态,既可以通过一个useState声明对象类型的状态,也可以通过useState多次声明状态。

</>复制代码

  1. // 声明对象类型的状态
  2. const [count, setCount] = useState({
  3. count1: 0,
  4. count2: 0
  5. });
  6. // 多次声明
  7. const [count1, setCount1] = useState(0);
  8. const [count2, setCount2] = useState(0);

  相比于声明对象类型的状态,明显多次声明状态的方式更加方便,主要是因为更新函数是采用的替换的方式,因此你必须给参数中添加未变化的属性,非常的麻烦。需要注意的是,React是通过Hook调用的次序来记录各个内部状态的,因此Hook不能在条件语句(如if)或者循环语句中调用,并在需要注意的是,我们仅可以在函数组件中调用Hook,不能在组件和普通函数中(除自定义Hook)调用Hook。

  当我们要在函数组件中处理复杂多层数据逻辑时,使用useState就开始力不从心,值得庆幸的是,React为我们提供了useReducer来处理函数组件中复杂状态逻辑。如果你使用过Redux,那么useReducer可谓是非常的亲切,让我们用useReducer重写之前的计数器例子:

</>复制代码

  1. import React, { useReducer } from "react"
  2. const reducer = function (state, action) {
  3. switch (action.type) {
  4. case "increment":
  5. return { count : state.count + 1};
  6. case "decrement":
  7. return { count: state.count - 1};
  8. default:
  9. return { count: state.count }
  10. }
  11. }
  12. function Example() {
  13. const [state, dispatch] = useReducer(reducer, { count: 0 });
  14. const {count} = state;
  15. return (
  16. {count}
  17. );
  18. }

  useReducer接受两个参数: reducer函数和默认值,并返回当前状态state和dispatch函数的数组,其逻辑与Redux基本一致。useReducer和Redux的区别在于默认值,Redux的默认值是通过给reducer函数赋值默认参数的方式给定,例如:

</>复制代码

  1. // Redux的默认值逻辑
  2. const reducer = function (state = { count: 0 }, action) {
  3. switch (action.type) {
  4. case "increment":
  5. return { count : state.count + 1};
  6. case "decrement":
  7. return { count: state.count - 1};
  8. default:
  9. return { count: state.count }
  10. }
  11. }

  useReducer之所以没有采用Redux的逻辑是因为React认为state的默认值可能是来自于函数组件的props,例如:

</>复制代码

  1. function Example({initialState = 0}) {
  2. const [state, dispatch] = useReducer(reducer, { count: initialState });
  3. // 省略...
  4. }

  这样就能实现通过传递props来决定state的默认值,当然React虽然不推荐Redux的默认值方式,但也允许你类似Redux的方式去赋值默认值。这就要接触useReducer的第三个参数: initialization。

  顾名思义,第三个参数initialization是用来初始化状态,当useReducer初始化状态时,会将第二个参数initialState传递initialization函数,initialState函数返回的值就是state的初始状态,这也就允许在reducer外抽象出一个函数专门负责计算state的初始状态。例如:

</>复制代码

  1. const initialization = (initialState) => ({ count: initialState })
  2. function Example({initialState = 0}) {
  3. const [state, dispatch] = useReducer(reducer, initialState, initialization);
  4. // 省略...
  5. }

  所以借助于initialization函数,我们就可以模拟Redux的初始值方式:

</>复制代码

  1. import React, { useReducer } from "react"
  2. const reducer = function (state = {count: 0}, action) {
  3. // 省略...
  4. }
  5. function Example({initialState = 0}) {
  6. const [state, dispatch] = useReducer(reducer, undefined, reducer());
  7. // 省略...
  8. }
Side Effects: useEffect useLayoutEffect

  解决了函数组件中内部状态的定义,接下来亟待解决的函数组件中生命周期函数的问题。在函数式思想的React中,生命周期函数是沟通函数式和命令式的桥梁,你可以在生命周期中执行相关的副作用(Side Effects),例如: 请求数据、操作DOM等。React提供了useEffect来处理副作用。例如:

</>复制代码

  1. import React, { useState, useEffect } from "react";
  2. function Example() {
  3. const [count, setCount] = useState(0);
  4. useEffect(() => {
  5. document.title = `You clicked ${count} times`
  6. return () => {
  7. console.log("clean up!")
  8. }
  9. });
  10. return (
  11. You clicked {count} times

  12. Click me
  13. );
  14. }

  在上面的例子中我们给useEffect传入了一个函数,并在函数内根据count值更新网页标题。我们会发现每次组件更新时,useEffect中的回调函数都会被调用。因此我们可以认为useEffect是componentDidMount和componentDidUpdate结合体。当组件安装(Mounted)和更新(Updated)时,回调函数都会被调用。观察上面的例中,回调函数返回了一个函数,这个函数就是专门用来清除副作用,我们知道类似监听事件的副作用在组件卸载时应该及时被清除,否则会造成内存泄露。清除函数会在每次组件重新渲染前调用,因此执行顺序是:

</>复制代码

  1. render -> effect callback -> re-render -> clean callback -> effect callback

  因此我们可以使用useEffect模拟componentDidMount、componentDidUpdate、componentWillUnmount行为。之前我们提到过,正是因为生命周期函数,我们迫不得已将相关的代码拆分到不同的生命周期函数,反而将不相关的代码放置在同一个生命周期函数,之所以会出现这个情况,主要问题在于我们并不是依据于业务逻辑书写代码,而是通过执行时间编码。为了解决这个问题,我们可以通过创建多个Hook,将相关逻辑代码放置在同一个Hook来解决上述问题:

</>复制代码

  1. import React, { useState, useEffect } from "react";
  2. function Example() {
  3. useEffect(() => {
  4. ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
  5. return function cleanup() {
  6. ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
  7. };
  8. });
  9. useEffect(() => {
  10. otherAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
  11. return function cleanup() {
  12. otherAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
  13. };
  14. });
  15. // 省略...
  16. }

  我们通过多个Hook来集中逻辑关注点,避免不相关的代码糅杂而出现的逻辑混乱。但是随之而来就遇到一个问题,假设我们的某个行为确定是要在区分componentDidUpdate或者componentDidMount时才执行,useEffect是否能区分。好在useEffect为我们提供了第二个参数,如果第二个参数传入一个数组,仅当重新渲染时数组中的值发生改变时,useEffect中的回调函数才会执行。因此如果我们向其传入一个空数组,则可以模拟生命周期componentDidMount。但是如果你想仅模拟componentDidUpdate,目前暂时未发现什么好的方法。

  useEffect与类组件生命周期不同的是,componentDidUpdatecomponentDidMount都是在DOM更新后同步执行的,但useEffect并不会在DOM更新后同步执行,也不会阻塞更新界面。如果需要模拟生命周期同步效果,则需要使用useLayoutEffect,其使用方法和useEffect相同,区域只在于执行时间上。

Context:useContext

  借助Hook:useContext,我们也可以在函数组件中使用context。相比于在类组件中需要通过render props的方式使用,useContext的使用则相当方便。

</>复制代码

  1. import { createContext } from "react"
  2. const ThemeContext = createContext({ color: "color", background: "black"});
  3. function Example() {
  4. const theme = useContext(Conext);
  5. return (
  6. Hello World!

  7. );
  8. }
  9. class App extends Component {
  10. state = {
  11. color: "red",
  12. background: "black"
  13. };
  14. render() {
  15. return (
  16. );
  17. }
  18. }

  useContext接受函数React.createContext返回的context对象作为参数,返回当前context中值。每当Provider中的值发生改变时,函数组件就会重新渲染,需要注意的是,即使的context的未使用的值发生改变时,函数组件也会重新渲染,正如上面的例子,Example组件中即使没有使用过background,但background发生改变时,Example也会重新渲染。因此必要时,如果Example组件还含有子组件,你可能需要添加shouldComponentUpdate防止不必要的渲染浪费性能。

Ref: useRef useImperativeHandle

  useRef常用在访问子元素的实例:

</>复制代码

  1. function Example() {
  2. const inputEl = useRef();
  3. const onButtonClick = () => {
  4. inputEl.current.focus();
  5. };
  6. return (
  7. <>
  8. );
  9. }

  上面我们说了useRef常用在ref属性上,实际上useRef的作用不止于此

</>复制代码

  1. const refContainer = useRef(initialValue)

  useRef可以接受一个默认值,并返回一个含有current属性的可变对象,该可变对象会将持续整个组件的生命周期。因此可以将其当做类组件的属性一样使用。

  useImperativeHandle用于自定义暴露给父组件的ref属性。需要配合forwardRef一起使用。

</>复制代码

  1. function Example(props, ref) {
  2. const inputRef = useRef();
  3. useImperativeHandle(ref, () => ({
  4. focus: () => {
  5. inputRef.current.focus();
  6. }
  7. }));
  8. return ;
  9. }
  10. export default forwardRef(Example);

</>复制代码

  1. class App extends Component {
  2. constructor(props){
  3. super(props);
  4. this.inputRef = createRef()
  5. }
  6. render() {
  7. return (
  8. <>
  9. );
  10. }
  11. }
New Feature: useCallback useMemo

  熟悉React的同学见过类似的场景:
  

</>复制代码

  1. class Example extends React.PureComponent{
  2. render(){
  3. // ......
  4. }
  5. }
  6. class App extends Component{
  7. render(){
  8. return this.setState()}/>
  9. }
  10. }

  其实在这种场景下,虽然Example继承了PureComponent,但实际上并不能够优化性能,原因在于每次App组件传入的onChange属性都是一个新的函数实例,因此每次Example都会重新渲染。一般我们为了解决这个情况,一般会采用下面的方法:

</>复制代码

  1. class App extends Component{
  2. constructor(props){
  3. super(props);
  4. this.onChange = this.onChange.bind(this);
  5. }
  6. render(){
  7. return
  8. }
  9. onChange(){
  10. // ...
  11. }
  12. }

  通过上面的方法一并解决了两个问题,首先保证了每次渲染时传给Example组件的onChange属性都是同一个函数实例,并且解决了回调函数this的绑定。那么如何解决函数组件中存在的该问题呢?React提供useCallback函数,对事件句柄进行缓存。

</>复制代码

  1. const memoizedCallback = useCallback(
  2. () => {
  3. doSomething(a, b);
  4. },
  5. [a, b],
  6. );

  useCallback接受函数和一个数组输入,并返回的一个缓存版本的回调函数,仅当重新渲染时数组中的值发生改变时,才会返回新的函数实例,这也就解决我们上面提到的优化子组件性能的问题,并且也不会有上面繁琐的步骤。

useCallback类似的是,useMemo返回的是一个缓存的值。

</>复制代码

  1. const memoizedValue = useMemo(
  2. () => complexComputed(),
  3. [a, b],
  4. );

  也就是仅当重新渲染时数组中的值发生改变时,回调函数才会重新计算缓存数据,这可以使得我们避免在每次重新渲染时都进行复杂的数据计算。因此我们可以认为:

</>复制代码

  1. useCallback(fn, input) 等同于 useMemo(() => fn, input)

  如果没有给useMemo传入第二个参数,则useMemo仅会在收到新的函数实例时,才重新计算,需要注意的是,React官方文档提示我们,useMemo仅可以作为一种优化性能的手段,不能当做语义上的保证,这就是说,也会React在某些情况下,即使数组中的数据未发生改变,也会重新执行。

自定义Hook

  我们前面讲过,Hook只能在函数组件的顶部调用,不能再循环、条件、普通函数中使用。我们前面讲过,类组件想要共享状态逻辑非常麻烦,必须要借助于render props和HOC,非常的繁琐。相比于次,React允许我们创建自定义Hook来封装共享状态逻辑。所谓的自定义Hook是指以函数名以use开头并调用其他Hook的函数。我们用自定义Hook来重写刚开始的订阅用户状态的例子:

</>复制代码

  1. function useFriendStatus(friendID) {
  2. const [isOnline, setIsOnline] = useState(null);
  3. function handleStatusChange(isOnline) {
  4. setIsOnline(isOnline);
  5. }
  6. useEffect(() => {
  7. ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
  8. return () => {
  9. ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
  10. };
  11. });
  12. return isOnline;
  13. }
  14. function FriendStatus() {
  15. const isOnline = useFriendStatus();
  16. if (isOnline === null) {
  17. return "Loading...";
  18. }
  19. return isOnline ? "Online" : "Offline";
  20. }

  我们用自定义Hook重写了之前的订阅用户在线状态的例子,相比于render prop和HOC复杂的逻辑,自定义Hook更加的简洁,不仅于此,自定义Hook也不会引起之前我们说提到过的组件嵌套地狱(wrapper hell)的情况。优雅的解决了之前类组件复用状态逻辑困难的情况。

总结

  借助于Hooks,函数组件已经能基本实现绝大部分的类组件的功能,不仅于此,Hooks在共享状态逻辑、提高组件可维护性上有具有一定的优势。可以预见的是,Hooks很有可能是React可预见未来大的方向。React官方对Hook采用的是逐步采用策略(Gradual Adoption Strategy),并表示目前没有计划会将class从React中剔除,可见Hooks会很长时间内和我们的现有代码并行工作,React并不建议我们全部用Hooks重写之前的类组件,而是建议我们在新的组件或者非关键性组件中使用Hooks。
  
 如有表述不周之处,虚心接受批评指教。愿大家一同进步!

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/102305.html

相关文章

  • React Hooks入门

    摘要:组件的职责增长并变得不可分割。是架构的重要组成部分。有许多好处,但它们为初学者创造了入门的障碍。方法使用状态钩子的最好方法是对其进行解构并设置原始值。第一个参数将用于存储状态,第二个参数用于更新状态。 学习目标 在本文结束时,您将能够回答以下问题: 什么是 hooks? 如何使用hooks? 使用hooks的一些规则? 什么是custom hook(自定义钩子)? 什么时候应该使用 ...

    zhangke3016 评论0 收藏0
  • React Hooks入门到上手

    摘要:前言楼主最近在整理的一些资料,为项目重构作准备,下午整理成了这篇文章。给传入的是一个初始值,比如,这个按钮的最初要显示的是。取代了提供了一个统一的。 showImg(https://segmentfault.com/img/bVbpUle?w=900&h=550); Hooks are a new addition in React 16.8. They let you use sta...

    XFLY 评论0 收藏0
  • React Hooks入门到上手

    摘要:前言楼主最近在整理的一些资料,为项目重构作准备,下午整理成了这篇文章。给传入的是一个初始值,比如,这个按钮的最初要显示的是。取代了提供了一个统一的。 showImg(https://segmentfault.com/img/bVbpUle?w=900&h=550); Hooks are a new addition in React 16.8. They let you use sta...

    zhouzhou 评论0 收藏0
  • 新上课程推荐:《React Hooks 案例详解(React 进阶必备)》

    摘要:课程制作和案例制作都经过精心编排。对于开发者意义重大,希望对有需要的开发者有所帮助。是从提案转为正式加入的新特性。并不需要用继承,而是推荐用嵌套。大型项目中模块化与功能解耦困难。从而更加易于复用和独立测试。但使用会减少这种几率。 showImg(https://segmentfault.com/img/bVbpNRZ?w=1920&h=1080); 讲师简介 曾任职中软军队事业部,参与...

    Lin_YT 评论0 收藏0
  • React Hooks 入门(2019)

    摘要:到目前为止,表达这种流程的基本形式是课程。按钮依次响应并更改获取更新的文本。事实证明不能从返回一个。可以在组件中使用本地状态,而无需使用类。替换了提供统一,和。另一方面,跟踪中的状态变化确实很难。 备注:为了保证的可读性,本文采用意译而非直译。 在这个 React钩子 教程中,你将学习如何使用 React钩子,它们是什么,以及我们为什么这样做! showImg(https://segm...

    GitCafe 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<