likes
comments
collection
share

10分钟掌握 react 中的 hooks

作者站长头像
站长
· 阅读数 4

前言

本系列的文章适合 vuereact 的同志,建议接上文来看,目前反馈均不错

react 函数组件写法才是官方推荐的,官方推荐我们使用函数组件就是因为官方封装了很多 hooks 函数给我们,hooks 函数就是钩子函数,生命周期函数也可以称之为钩子函数,但是钩子函数并不是生命周期函数,一个大一个小的包含关系。

vue 的 hooks 就是比如 useRouteuseRouteruseStore 这样带 use 开头的函数,这就是 yyx 借鉴 react 的体现之处

react 中的 hooks 本质就是为了让函数组件更加强大,官方给我们提供的 hooks 就 5,6 个的样子,接下来就详细学习下这几个 hooks

useState

之前类组件的时候有个 constructor 用于存放组件用到的状态(数据源),在函数组件中,数据源可以直接写在函数中,下面写一个效果,就是点击按钮可以实现累加,先看下类组件的写法

import React, { Component } from 'react'

export default class State extends Component {
  constructor () {
    super()
    this.state = {
        count: 0
    }
  }

  add = () => {
    this.setState({
        count: this.state.count + 1
    })
  }

  render() {
    return (
      <div>
        <button onClick={() => this.add()}>{this.state.count}</button>
      </div>
    )
  }
}

现在用函数组件来写

import React from 'react';

const State = () => {

    let count = 0

    const add = () => {
        count++
    }

    return (
        <div>
            <button onClick={() => add()}>{count}</button>
        </div>
    );
};

export default State;

你会发现这样实现不了,其实 count 确实实现了 ++ 的效果,相比较类组件的写法,你会发现目前的函数写法没有一个 setState,之前的类组件中因为可以利用 class 的继承特性拿到 Component 中的 setState ,但是现在的函数写法拿不到 setState ,那如何实现试图更新呢

函数写法中,用得就是 useState 这个钩子函数

现在我想要更改 count 的值

const [count, setCount] = useState(0)

引入 useState 后,调用返回一个数组,这个数组里面你写上自己的数据源,以及一个函数用于修改数据,这个函数前缀是 setuseState 里面的参数就是数据源

其实第二个参数你可以任意取名,写 abc 都可以,这只是约定俗成的语义化

这个 setCount 就相当于类组件中的 setState ,现在的 “State” 可以更改了,其实这个时候我们自己也知道, setCount 无非就是干了两件事,一个是更改数据,另一个是触发 render

setCount 里面写的值一定是返回给 count ,最终带上 useState 写法如下

import React, { useState } from 'react';

const State = () => {

    const [count, setCount] = useState(10) // useState()的执行结果一定是个数组

    const add = () => {
        setCount(count + 1)
    }

    return (
        <div>
            <button onClick={() => add()}>{count}</button>
        </div>
    );
};

export default State;

因此 useState 的作用就是为函数组件提供状态

何谓组件的状态?光是一个普通的数据是不能称之为状态的,状态需要组件的值改了可以重新渲染页面,或者保持 vue 的思想,称之为响应式也可以

执行过程

读取代码的时候执行一遍 useStatesetCountcount 更改,并执行 render ,当我们 render 完后又会再一次读取 count 值, useState 依旧会重新调用,其实 render 一遍,这个函数组件就会重新执行一遍

或许你会疑惑:为何 render 了一次,整个函数都要执行一次,只更改响应式数据不可吗,响应式数据依靠函数中的代码,执行函数的代码是由 v8 负责的, v8 性能是很强大的,因此无需担心这个问题

这个时候你肯定会疑惑,我 count 后面加到了 11 ,重新 render 时就意味着再次执行 useState ,那 count 岂不是又会初始化到 10 ?这就是 useState 的奥义所在,它会帮你把上一次的数据源拿到,也就是 setCount 中的值

不能放在 if 或者 for 循环语句中

我们看看下面这种写法

10分钟掌握 react 中的 hooks

flag 甚至都是暗的,其实完全可以理解, flag 在外面拿不到就是以为 const{ }形成了块级作用域,作用域链只能从里往外,而不能从外往里。当然你用 var 可以拿到,但是现在基本上不会去写 var 了,其实这里就算你用 var 也看不到内容,因为 html 中展示只能是字符串或者数字等,布尔是看不到的

为什么不建议用 var,我们都知道这东西会出现声明提升,声明提升就会导致可能与其他语句相冲突

接受回调函数

有时候你也不知道 useState 的初始值是多少,你可能需要一个函数计算出来,这个时候可以传递一个回调进去

应用场景:比如购物车,初始时就计算好价格

依旧拿这个点击按钮实现累加的效果

import React, { useState } from 'react';

const State2 = () => {
    const [count, setCount] = useState(0)

    return (
        <div>
            <button onClick={() => setCount(count+1)}>{count}</button>
        </div>
    );
};

export default State2;

比如这个值是根据父组件的值来决定的,父组件<State2 num={10} />进行传参,子组件的 count 初始值为其一倍,写法就是如下,记得给函数加上 props 形参

const [count, setCount] = useState(() => {
    return props.num * 2
})

useEffect

看到这个 effect 你就会想起 vueeffect 函数了,就是副作用函数,所谓副作用并不是指的不好的影响,而是你在干这件事,附带产生了其他效果

react 中,我们可以把作用分为主作用和副作用,主作用就是根据数据来渲染页面,其他的作用全是副作用,常见的副作用有 useEffect

vue 中的副作用函数常见的就是 computedwatch ,只要状态值变了, computed 就会监听执行

在类组件中我们有生命周期钩子用于发接口请求啥的,但是函数组件我们就用 useEffect ,比如依旧是 button 那个栗子, count 值变了,去发接口请求

import React, { useEffect, useState} from 'react';

function Effect(props) {
    const [count, setCount] = useState(0)

    useEffect(() => {
        console.log(`当前点击了${count}次`);
    })
    
    return (
        <div>
            <button onClick={() => setCount(count + 1)}>{count}</button>
        </div>
    );
}

export default Effect;

你去控制台你就会发现页面初次加载就打印了两次,这就有点像是 vue 中的 watch 可以设置 immediate ,然后点击按钮,打印一次

若我们打印中不用上 count 呢, count 值变了依旧会触发 useEffect 的执行

其实也很好接受,刚才说了只要是 render 了,那么函数组件代码就会重新执行一遍,因此 useEffect 无论是否用上响应式数据,都会顺带执行一次

因此 useEffect 在页面初次加载时就会执行一次,并且组件中任意状态变更导致需要重新 render 就会让 useEffect 重新执行一次

第二个参数

useEffect 中的第二个参数是个数组,数组中有东西变更就会执行一次,若是什么都不放,就等于不会变更,因此执行了页面初次加载后,后续不会执行

useEffect(() => {
    console.log(`当前点击了10次`);
}, [])

从这个效果上来看, useEffect 很像是一个生命周期,没错!这种写法就可以充当 componentDidMount

另外,当你有多个状态时,比如我这里再加上一个 name,再来一个按钮,点击可以更改 name,我在第二个参数中只写上 count,那么带来的效果是只有改变了 count 才会执行 useEffect

useEffect(() => {
    console.log(`当前点击了10次`);
}, [count])
// ……
<button onClick={() => setCount(count + 1)}>{count}</button>
<button onClick={() => setName('海豚')}>{name}</button>

这么来看,这个效果就可以充当 componentDidUpdate ,并且这个效果很像 vue 中的 computed 或者 watch

因此这个钩子很强大啊!

刚刚有说到只要是 render 完毕后,就会触发组件(函数)的重新执行,其实重新执行之前就会卸载上一次的组件,因此这又可以看成是卸载 componentWillUnmount 这个钩子

比如这里我写一个定时器间隔 1s 执行一次

useEffect(() => {
    const timer = setInterval(() => {
        setCount(count + 1)
    }, 1000)
}, [count])

10分钟掌握 react 中的 hooks

居然出现了卡顿?!这是为什么? useEffect 在页面初次加载会执行一次,执行一次就代表会创建一个 timer ,这个 timer 又会更改 count ,更改了 count 又会导致 renderrender 又会导致 useEffect 的重新执行,这样下去会创建很多个定时器,浏览器的定时器线程支持不了很多定时器,只要变多了就会出现卡顿,因此需要对组件进行卸载

useEffect 中,卸载写在回调的 return

useEffect(() => {
    const timer = setInterval(() => {
        setCount(count + 1)
    }, 1000)
    return () => {  // 会在组件卸载时触发
        // 清除副作用
        clearInterval(timer)
    } 
}, [count])

这样就不会出现卡顿

10分钟掌握 react 中的 hooks

每次卸载之前都把当前的 timer 给清除掉了,另外 count 会被 setCount 保存下来,实现累加的效果

因此函数组件中没有生命周期这一说法就是因为 useEffect 太强大了都可以充当,恰恰充当了最常用的三个生命周期函数,它还能充当vue 中的 computedwatch

useRef

在类组件中,我们拿到 dom 结构需要用 createRef 创建一个 dom 容器。在函数组件中就需要用 useRef ,跟 vue 写法几乎一致, vue 直接写 ref ,没有 use

写法如下,我们可以试着打印下这个 dom

import React, { useRef } from 'react';

const Ref = () => {
    const h2Ref = useRef(null) // 得到一个可以存放dom结构的对象
    console.log(h2Ref);

    return (
        <div>
            <h2 ref={h2Ref}>Ref</h2>
        </div>
    );
};

export default Ref;

打印效果一看,居然都是 null

10分钟掌握 react 中的 hooks

打印两次是因为:有一次是 react 源码中编译执行的,另一次才是浏览器执行的

这其实是因为打印代码的位置不对,函数组件和类组件其实是一样的,在类组件中, return 那里多了个 render,其实函数组件中是一样的,也就是说打印代码在 render 之前先执行,此时还没渲染,必然拿不到 dom

因此想要拿到 dom ,需要用上钩子 componentDidMount ,在函数组件中就是 useEffect 并且第二个参数放一个空数组

import React, { useRef, useEffect } from 'react';

const Ref = () => {
    const h2Ref = useRef(null) // 得到一个可以存放dom结构的对象
    
    useEffect(() => {
        console.log(h2Ref);
    }, [])

    return (
        <div>
            <h2 ref={h2Ref}>Ref</h2>
        </div>
    );
};

export default Ref;

拿到 dom

10分钟掌握 react 中的 hooks

useContext

我们清楚父组件向后代组件跨组件通讯用的是 ProviderConsumer ,这是类组件的写法,在函数组件中也能用,不过需要借助 useContext

比如这里的情景,让 App.jsx 当爷爷组件,子组件 Context.jsx ,孙子组件 ContextChild.jsx

和类组件同样的写法, ProviderConsumer 需要来自同一个地方,因此需要额外有个文件提供 ProviderConsumer ,其实就是用的 useContext

这种写法很像是 vue 中的 eventBus ,写到全局中去,提供给别的组件使用

src/_context.js

import { createContext } from 'react'

const Con = createContext()

export default Con

子组件src/components/Context.jsx

import React, { useContext } from 'react';
import ContextChild from './ContextChild';
import Con from '../_context'

const Context = () => {
    const msg = useContext(Con)

    return (
        <div>
            <h3>子组件 -- {msg}</h3>

            <ContextChild />
        </div>
    );
};

export default Context;

孙子组件src/components/ContextChild.jsx

import React from 'react';
import { useContext } from 'react'
import Con from '../_context'

const ContextChild = () => {
    const msg = useContext(Con)

    return (
        <div>
            孙子组件 - {msg}
        </div>
    );
};

export default ContextChild;

语法就是 src 下新建一个总线,通过 createContext 提供 Context,这个 Context 让父组件去拿到 Provider 提供 value 给到后代组件,后代组件需要引入 Context ,在借助引入的 useContext 传入 Context 拿到 value。相比较类组件,子组件就不需要 consumer 了,直接借助 useContext 来拿值

写法会复杂点,这点你要承认 vueprovideinject 封装得更好

打造一个简单的 hooks

获取页面滚动距离

现在我们来尝试自己打造一个 hooks 函数,来获取页面滚动的距离

先看看直接用 js 如何拿到,如下

import React, { useState } from 'react';

const Myhooks = () => {
    const [y, setY] = useState(0)

    window.addEventListener('scroll', (e) => {
        console.log(document.documentElement.scrollTop);
    })
    
    return (
        <div style={{height: '200vh'}}>
            <h2>当前页面的滚动距离:{y}</h2>
        </div>
    );
};

export default Myhooks;

所有的 hooks 函数都是 use 开头的,接下来打造一个 useScroll 函数,让它拿到滚动距离,给他默认距离 10

import React, { useState } from 'react';
import { useScroll } from '../_hooks/useScroll'

const Myhooks = () => {
    const [pageY] = useScroll(10)
    
    return (
        <div style={{height: '200vh'}}>
            <h2>当前页面的滚动距离:{pageY}</h2>
        </div>
    );
};

export default Myhooks;

好,src 下新建一个文件夹 _hooks ,新建一个文件放自己打造的 hooks

src/_hooks/useScroll.js

这个函数待会儿要抛出去,然后 hooks 一定会返回一个数组,数组里面就是想要的 y 值,这个 y 值可以有初始值,就是 useScroll 传入的参数

export function useScroll (instance) {
    let y = instance

    return [y]
}

现在再把 y 改了,y 就是 document.documentElement.scrollTop

export function useScroll (instance) {
    let y = instance

    const handleScroll = () => {
        y = document.documentElement.scrollTop
    }

    window.addEventListener('scroll', handleScroll)

    return [y]
}

但是目前来看, y 虽然是改了,但是页面没有更新。这是因为赋值完给 pageY 后,组件加载完毕后,不会重新 render,这样也就不会让 y 二次 return

既然要驱动 render,那么我就在 y 中用 useState ,打造 hooks 时是可以用官方提供的 hooks

import { useState } from 'react'

export function useScroll (instance) {
    const [y, setY] = useState(instance)

    const handleScroll = () => {
        setY(document.documentElement.scrollTop)
        console.log(y);
    }

    window.addEventListener('scroll', handleScroll)

    return [y]
}

目前这个效果其实已经打造完成了,但是当我们打印 y 的时候你会发现一个问题,假设页面已经滚到中间了,此时重新刷新,屏幕上的值不会跟着刷新变成 10 ,这个效果是正确的,因为 useState 将上次数据缓存起来了

这里可以进行优化,当组件卸载时,这个事件监听器可以移除掉,因此整个事件我都可以写入 useEffect 中,这样方便我们移除掉事件,如下

import { useState, useEffect } from 'react'

export function useScroll (instance) {
    const [y, setY] = useState(instance)


    useEffect(() => {
        const handleScroll = () => {
            setY(document.documentElement.scrollTop)
            console.log(y);
        }
    
        window.addEventListener('scroll', handleScroll)

        return () => {
            window.removeEventListener('scroll', handleScroll)
        }
    })

    return [y]
}

这么来看,其实封装 hooks 很像是自行封装 utils 工具函数

再打造一个 hooks ,将按钮点击次数存到浏览器本地存储中

按钮点击次数存储到浏览器本地存储中

实现效果:可以修改 count 值,并将其存储到 LocalStorage

import React, { useState } from 'react';
import { useLocal } from '../_hooks/useLocal';

const Myhooks = () => {
    const [count, setCount] = useLocal('count', 0)
    
    return (
        <div>
            <button onClick={() => setCount(count + 1)}>{count}</button>
        </div>
    );
};

export default Myhooks;

这里我待会儿打造的 useLocal 我传入两个参数,第一个是 key ,存到 LocalStorage 中的 key ,第二个是初始值

先保证这个 useLocal 具有 useState 的能力,可以修改 count

import { useState } from 'react'

export function useLocal (key, value) {
    const [n, setN] = useState(value)
    
    return [n, setN]
}

接下来就是使用 useEffect 来往 localStorage 中存 key-value

import { useState, useEffect } from 'react'

export function useLocal (key, value) {
    const [n, setN] = useState(value)
    useEffect(() => {
        window.localStorage.setItem(key, n)
    })
    return [n, setN]
}

目前这样来看是没有问题的,但是,这个组件被拿到根组件中调用,根组件中若有其他的数据源变更到了 render ,那么这个 useLocal 就会重新调用一次,比如上面写的滚动距离,我们没点击按钮,仅仅滚动页面,这个往 localStorage 存值的行为依旧会发生,这就显得非常没有必要,因此,这就要动用 useEffect 的第二个参数,往里面放 n 即可,这样别人即使 render 了,自己的 useEffect 没有看到 count 值变,就不会执行

useEffect(() => {
    window.localStorage.setItem(key, n)
}, [n])

好了,目前可以存值又不受其他钩子的影响,或者说是不受其他数据源导致的 render 的影响

其实封装 hooks 就是封装一个函数

另外封装 hooks 时,命名文件无需命名为 jsx 后缀,这不是个组件,否则又会被 react 源码进行一个编译解析

TodoList: antd

现在我们可以试着用函数组件实现一个 todolist ,为了让 todolist 更美观,这里用上了 antd 这个 ui 组件库,这个 ui 库原本打造就是为了提供给 react 用的,但是现在也有个 vue 版本的 antd

安装

10分钟掌握 react 中的 hooks

这里我用的包管理工具是 npm ,就是npm i antd --save

bun 也是个包管理工具,国外用的多

实现

可以按需引入,就是引入到需要用上的那个组件中,也可以直接引入到 main.js 中, antd 的引入就有一个,不想国内的组件库还需要引入 cssantd 天生就是按需加载,用到哪个组件才加载哪个组件的 css ,如果想要用 antd 中的 icon 得话还需要额外安装一个 icon

这里实现,我依旧把 todoList 分成两个组件来写,父组件是上面的输入框和确认按钮,子组件是列表,列表的数据源我又拿到父组件。输入框,按钮和列表我都用 antd 提供的

src/todo/TodoList.jsx

import React, { useRef } from 'react';
import TodoItem from './TodoItem';
import { Input, Button } from 'antd'

const data = [
    'Racing car sprays burning fuel into crowd.',
    'Japanese princess to wed commoner.',
    'Australian walks 100km after outback crash.',
    'Man charged over missing wedding girl.',
    'Los Angeles battles huge wildfires.',
];

// 父组件
const TodoList = () => {

    const handleClick = () => {}

    return (
        <div style={{width: '400px'}}>
            <header style={{display: 'flex'}}>
                <Input placeholder="Basic usage" />
                <Button onClick={handleClick}>提交</Button>
            </header>

            <section>
                <TodoItem data={data}/>
            </section>            
        </div>
    );
};

export default TodoList;

src/todo/Item.jsx

import React from 'react';
import { List, Tag } from 'antd'

// 子组件
const TodoItem = (props) => {
    return (
        <div>
            <List
                bordered
                dataSource={props.data}
                renderItem={(item) => (
                    <List.Item>
                        {item}
                        <Tag closeIcon onClose={() => {}}></Tag>
                    </List.Item>
                )}
            />
        </div>
    );
};

export default TodoItem;

子组件用的 tagtag 就是带会儿用来做删除用的

对于这个 button ,当我们用了它的组件的时候,就最好检查一下事件名是否改写,这里的 button 依旧是 onClick

拿到 input 框中的值有两种方法,一个受控组件,一个非受控组件,这里我就采用非受控写法,因为有个 useRef 钩子

import React, { useRef } from 'react';
// ……
const inputRef = useRef(null)
// ……
<Input placeholder="Basic usage" ref={inputRef} />

拿到 input 值后,需要塞入到 data 数据源中,塞之前将 data 响应式处理,也就是 useState

可以通过 button 点击事件将 input 值塞入到 data 中, data 就是 useState 的初始值,然后再通过这个点击事件更改这个 data , 用解构的写法,如下

import React, { useRef, useState } from 'react';
import TodoItem from './TodoItem';
import { Input, Button } from 'antd'

// 父组件
const TodoList = () => {
    const data = [
        'Racing car sprays burning fuel into crowd.',
        'Japanese princess to wed commoner.',
        'Australian walks 100km after outback crash.',
        'Man charged over missing wedding girl.',
        'Los Angeles battles huge wildfires.',
    ];

    const [newData, setNewData] = useState(data)

    const inputRef = useRef(null)

    const handleClick = () => {
        console.log(inputRef.current.input.value);
        setNewData([...newData, inputRef.current.input.value])
    }

    return (
        <div style={{width: '400px'}}>
            <header style={{display: 'flex'}}>
                <Input placeholder="Basic usage" ref={inputRef} />
                <Button onClick={handleClick}>提交</Button>
            </header>

            <section>
                <TodoItem data={newData}/>
            </section>            
        </div>
    );
};

export default TodoList;

这个更改值用解构很优雅,此前用类组件得话,浅拷贝一个新的数组,对这个新的数组进行赋值,然后放入 setState 中,将会是下面这种写法

const handleClick = () => {
    let arr = newData
    arr.push(inputRef.current.input.value)
    setNewData(arr)
}

其实这么写是不行的,当我们对 arr 修改的时候,其实就是对 newData 修改,这个修改并没有用上 setNewData ,因此不会生效,因为你自始至终都是对原来的 newData 进行操作

你肯定想问,最后不是还 setNewData 了吗,在执行 setNewData 时,此时的 newData 已经被添加了 input 值,想要用 usestate 修改值,必须与原来的值不同,这里已经相同了,因此不会生效

想要借助另外一个数组,就必须是深拷贝,得是另一份地址,如下写法,但是很没必要,直接用解构,解构本身就是深拷贝

const handleClick = () => {
    let arr = structuredClone(newData)
    arr.push(inputRef.current.input.value)
    setNewData(arr)
}

目前效果如下

10分钟掌握 react 中的 hooks

好了,现在去实现子组件的删除功能

那就需要给删除按钮绑定一个点击事件,然后将当前项下标进行传参,子组件删掉父组件的数据就是子父传参,需要父组件传递一个函数进来,最终如下

父组件TodoList.jsx

import React, { useRef, useState } from 'react';
import TodoItem from './TodoItem';
import { Input, Button } from 'antd'

// 父组件
const TodoList = () => {
    const data = [];

    const [newData, setNewData] = useState(data)

    const inputRef = useRef(null)

    const handleClick = () => {
        console.log(inputRef.current.input.value);
        setNewData([...newData, inputRef.current.input.value])
    }

    const onDelete = (i) => {
        let arr = newData.filter((str, index) => index !== i)
        setNewData(arr)
    }

    return (
        <div style={{width: '400px'}}>
            <header style={{display: 'flex'}}>
                <Input placeholder="Basic usage" ref={inputRef} />
                <Button onClick={handleClick}>提交</Button>
            </header>

            <section>
                <TodoItem data={newData} cb={onDelete}/>
            </section>            
        </div>
    );
};

export default TodoList;

子组件TodoItem.jsx

import React from 'react';
import { List, Tag } from 'antd'

// 子组件
const TodoItem = (props) => {

    const onDel = (e, i) => {
        e.preventDefault() // 文档上写的:阻止一个默认行为
        props.cb(i)
    }

    return (
        <div>
            <List
                bordered
                dataSource={props.data}
                renderItem={(item, i) => (
                    <List.Item>
                        {item}
                        <Tag closeIcon onClose={(e) => onDel(e, i)}>
                            删除
                        </Tag>
                    </List.Item>
                )}
            />
        </div>
    );
};

export default TodoItem;

最终效果如下

10分钟掌握 react 中的 hooks

总结

hooks 的目的就是让函数组件更强大,因此所有的 hooks 只能在函数组件中使用

  1. useState:为函数组件提供状态
  2. useEffect:不依赖指定数据源,默认执行一次,当组件中有状态变更导致组件重新 render ,该函数会重新执行;
    1. 第二个参数传入空数组可以充当 componentDidMount
    2. 第二个参数传入指定数据源作数组元素可以充当 componentDidUpdate
    3. 回调中返回的函数可以充当 componentWillUnmount
  3. useRef:在函数组件中获取 dom 结构
  4. useContext:在函数组件中实现祖先组件向后代组件跨组件传值

最后

函数式组件因为没有 class ,不方便继承状态和生命周期,就借助了 hooks 弥补这一问题,并且更加优雅。其实面试中,面试官很喜欢让你手写一个 hookshooks 聊完了,接下来准备出一期文章来聊 react 中的 router ,感兴趣的小伙伴可以关注我或者这一专栏,皆是方便各位 vuereact,一步一个脚印

另外有不懂之处欢迎在评论区留言,如果觉得文章对你学习有所帮助,还请 ”点赞+评论+收藏“ 一键三连,感谢支持!

转载自:https://juejin.cn/post/7360928627632357402
评论
请登录