1. 首页

React 16.8 之 React Hook

Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性

这是react官网对react hook 的一句话简介。简而言之,在我看来就是让函数式组件(无状态组件)可以拥有自己的状态(state)以及改变状态的方法(setState)

React Hook 为何而存在

  1. 在组件之间复用状态逻辑很难

虽然我们可以用render props 和高阶组件来解决这一问题,但是!但是! 这不仅会容易形成“嵌套地狱”,还会面临一个很尴尬的问题(相对于我这种新手菜鸟来说) 那就是复杂逻辑难写!简单逻辑我宁可重写一遍都觉得比用高阶组件要快要方便(毕竟可以复制粘贴嘛)。

  1. 复杂组件难以理解

各种生命周期函数内充斥着各种状态逻辑处理和副作用,且难以复用、零散,比如一个调用列表数据的接口方法getList分别要写到componentDidMount 和componentDidUpdate中等等。

  1. 难以理解的class

this的指向问题(经常在某一处忘记bind(this)然后bug找半天)、组件预编译技术(组件折叠)会在class中遇到优化失效的case(还并不了解,先写上~)、class不能很好的压缩、class在热重载时会出现不稳定的情况。

所以!有了 react hook 就不用写class了,组件都可以用function来写,不用再写生命周期钩子,最关键的,不用再面对this了!

State Hook

首先我们先看下官方给出的简单例子

这是一个简单的累加计数器,我们先看class声明的组件


class Example extends React.Component { constructor(props) { super(props); this.state = { count: 0 }; } render() { return ( <div> <p>你点击了{this.state.count}次</p> <button onClick={() => this.setState({ count: this.state.count + 1 })}> 点我 </button> </div> ); } }

这是一个非常非常简单的组件了,相信了解过react的人都能够看懂

那么我们再看一下使用hook的版本


import React, { useState } from 'react'; function Example() { // 声明一个叫 "count" 的 state 变量 const [count, setCount] = useState(0); return ( <div> <p>你点击了{count}次</p> <button onClick={() => setCount(count + 1)}> 点我 </button> </div> ); }

是不是hook要简单了一点?可以看到,这是一个函数,与以往不同的是它拥有了自己的状态(count),就像class中的this.state.count;同时它还可以通过setCount()来更新自己的状态,就像class中的this.setState()。

为什么会这样呢? 仔细看第一行我们引入了useState这个hook,就是这个hook让我们的无状态组件成为了一个有状态的组件。

状态值的声明、读取、更新

那么我们分解一下看看到底这个hook都为我们做了什么


const [count, setCount] = useState(0);

首先useState的作用就是来声明状态变量,这个函数接收的参数是我们要为变量赋予的初始值,它返回了一个数组,索引[0]是当前的状态值,[1]是用来更新这个状态值的方法

所以这一句其实就是声明了一个count变量,就好比


this.state = { count: 0 };

并且通过useState(0)传入参数0来为count赋了一个初始值0,同时提供了一个像this.setState一样可以更新它的方法setCount

然后我们引用读取这个变量的时候直接 {count}就好了,不用再this.state.count 这么长了!


<p>你点击了{count}次</p>

当我们想更新这个值的时候,直接调用setCount(新的值)就可以了。

怎么样,是不是很简单很容易理解?

但是,但是! 诶? 不对啊,这个函数是怎么记住之前的状态的? 通常来说我们在函数中声明一个变量,函数运行完也就跟着销毁了,重复调用的时候会重新声明,那这个Example函数是怎么做到记住之前声明的状态变量的?

State Hook 解决存储持久化的方案

我所知道的可以通过js存储持久化状态的方法有: class类、全局变量、DOM、闭包

通过学习react hook源码解析了解到是用闭包实现的,深入的我也看不懂!简单来说首先useState就是这样实现的


function useState(initialState){ let state = initialState; function dispatch = (newState, action)=>{ state = newState; } return [state, dispatch] }

像不像redux? 给定一个初始state,然后通过dispatch一个action,经由reducer改变state,再返回新的state,触发组件的重新渲染。

但是仅仅这样还满足不了要求,我们需要一个新的数据结构来保存上一次的state和这一次的state,以便可以在初始化流程调用useState和更新流程调用useState时可以取到对应的正确值。假定这个数据结构叫Hook:


type Hook = { memoizedState: any, // 上一次完整更新之后的最终状态值 queue: UpdateQueue<any, any> | null, // 更新队列 };

考虑到第一次组件mounting和后续的updating逻辑差异,定义两个不同的useState函数来实现,分别叫做mountState和updateState


function useState(initialState){ if(isMounting){ return mountState(initialState); } if(isUpdateing){ return updateState(initialState); } } // 第一次调用组件的 useState 时实际调用的方法 function mountState(initialState){ let hook = createNewHook(); hook.memoizedState = initalState; return [hook.memoizedState, dispatchAction] } function dispatchAction(action){ // 使用数据结构存储所有的更新行为,以便在 rerender 流程中计算最新的状态值 storeUpdateActions(action); // 执行 fiber 的渲染 scheduleWork(); } // 第一次之后每一次执行 useState 时实际调用的方法 function updateState(initialState){ // 根据 dispatchAction 中存储的更新行为计算出新的状态值,并返回给组件 doReducerWork(); return [hook.memoizedState, dispatchAction]; } function createNewHook(){ return { memoizedState: null, baseUpdate: null } }

以上就是基本的实现思路,内容参考自源码解析React Hook构建过程

想深入了解源码原理的请自行跳转~

声明多个state变量

useState是可以多次调用的


function ExampleWithManyStates() { // 声明多个 state 变量 const [age, setAge] = useState(42); const [fruit, setFruit] = useState('banana'); const [todos, setTodos] = useState([{ text: '学习 Hook' }]); // ... }

useState也支持接收对象或者数组作为参数。与this.setState不同的是,this.setState是合并状态后返回一个新的状态,而useState只是直接替换老状态后返回新状态。

Hook 规则

hook本质也是javaScript函数,但是他在使用的时候需要遵循两条规则,并且react要求强制执行这两条规则,不然就会出现奇怪的bug。

1.只能在最顶层使用hook

不要在循环,条件或者嵌套函数中调用hook,确保总是在react函数的最顶层调用,目的是为了确保hook在每一次渲染中都按照同样的顺序被调用,这让react能够在多次的useState和useEffect(另一种hook,下面会说)调用之间保持状态的正确,来保证多个state的相互独立和一一对应的关系。

2.只在react函数中调用hook

不要在普通js函数中调用hook

这里针对第一条举个栗子:


function Form() { const [name1, setName1] = useState('zhangsan'); const [name2, setName2] = useState('lisi'); const [name3, setName3] = useState('wangwu'); // ... }

这里连续使用了三次useState来声明了name1,name2,name3三个状态并且都赋予了初始值,那么在渲染时是这样的


//首次渲染(赋初始值) useState('zhangsan') // 1. 使用 'zhangsan' 初始化变量名为 name1 的 state useState('lisi') // 2. 使用 'lisi' 初始化变量名为 name2 的 state useState('wangwu') // 3. 使用 'wangwu' 初始化变量名为 name3 的 state //第二次渲染 useState('zhangsan') // 1. 读取name1的值 useState('lisi') // 2. 读取name2的值 useState('wangwu') // 3. 读取name3的值

如果我们这么写!


let tag = true; function Form() { const [name1, setName1] = useState('zhangsan'); if (tag) { const [name2, setName2] = useState('lisi'); tag = false; } const [name3, setName3] = useState('wangwu'); }

这个过程就会变为


//首次渲染(赋初始值,和上面一样) useState('zhangsan') // 1. 使用 'zhangsan' 初始化变量名为 name1 的 state useState('lisi') // 2. 使用 'lisi' 初始化变量名为 name2 的 state useState('wangwu') // 3. 使用 'wangwu' 初始化变量名为 name3 的 state //第二次渲染 useState('zhangsan') // 1. 读取name1的值 //useState('lisi') // 2. 通过条件判断并没有走读取name2这一步 useState('wangwu') // 3. 读取name3的值时通过顺序判断读取到的却是name2,导致报错

所以hook规则其实就是确保hook的执行顺序,因为它是通过顺序来完成一一对应关系以及互相独立的!

Effect Hook

官方文档中一句话说明 Effect Hook 可以让你在函数组件中执行副作用操作,可能一句话并不能让人很好的理解它是干嘛用的,我们可以接着看上面那个最简单的计数器的栗子,在它的基础上加一个小功能

function Example() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    //设置浏览器标题内容
    document.title = `你点击了${count} 次`;
  });

  return (
    <div>
      <p>你点击了{count}次</p>
      <button onClick={() => setCount(count + 1)}>
        点我
      </button>
    </div>
  );
}

这段代码增加了一个将document的title设置为包含了点击次数的消息,如果通过class组件,应该怎么写呢?我们对比一下:


class Example extends React.Component { constructor(props) { super(props); this.state = { count: 0 }; } componentDidMount() { document.title = `你点击了 ${this.state.count} 次`; } componentDidUpdate() { document.title = `你点击了 ${this.state.count} 次`; } render() { return ( <div> <p>你点击了{this.state.count}次</p> <button onClick={() => this.setState({ count: this.state.count + 1 })}> 点我 </button> </div> ); } }

通过对比,可以看出好像effect hook就相当于生命周期的componentDidMount,componentDidUpdate; 其实,我们写的有状态组件,通常都会产生副作用,比如ajax请求,浏览器事件的绑定和解绑,手动修改dom,记录日志等等;副作用还分为需要清除的和不需要清除的,所以effect hook 还相当于一个componentWillUnmount(比如我们有个需求是需要论询向服务器请求最新数据,那么我们就需要在组件卸载的时候来清理掉这个轮询操作)

清除副作用


componentDidMount(){ //轮询获取数据 this.getNewData() } componentWillUnmount(){ //组件卸载前清除轮询操作 this.unGetNewData() }

我们完全可以在函数式组件中使用effect hook 来清除这个副作用,用法是在effect函数中return一个函数(清除操作)


useEffect(()=>{ getNewData() return function cleanup() { unGetNewData() } })

effect中返回一个函数,这是effect可选的清除机制。每个effect都可以返回一个清除函数,看你的需要。

react会在组件卸载的时候执行清除操作。effect在每次渲染的时候都会执行,并且是每次渲染之前都会去执行cleanup来清除上一个effect副作用。

需要注意的是!!!这种解绑模式同componentWillUnmount不一样。componentWillUnmount只会在组件被销毁前执行一次,而effect hook里的函数,每次组件渲染都会执行一遍,包括副作用函数和它return的清理操作

effect的性能优化

大家一看到“每一次”都会执行,首先就会想到跟性能有关的问题,那么其实effect是可以跳过的,同样通过对比class组件来理解effect hook,class组件中使用componentDidUpdate来进行前后逻辑的比较


componentDidUpdate(prevProps, prevState) { if (prevState.count !== this.state.count) { //判断状态值count改变了,再触发副作用操作 document.title = `你点击了${this.state.count}次`; } }

同样的在effect中,有第二个参数,起同样的作用


useEffect(() => { document.title = `你点击了${count}次`; }, [count]); // 仅在 count 更改时更新

第二个参数为一个数组,如果数组中有多个元素,即使只有一个元素发生了改变,react也会执行effect(即只有数组中所有元素都未变化,react才会跳过这次effect);若为空数组[],则表示该effect只会执行一次(包括副作用和return的清除副作用操作)

自定义Hook

也就是我们最初的目的:逻辑复用! 试想一下,加入我们现在要实现这样一个功能组件,点击button随机切换背景颜色,如图

React 16.8 之 React Hook

用我们所熟悉的class组件是这样实现的


import React, { Component } from "react"; export default class ChangeColor extends Component { constructor() { super(); this.state = { color: "red" }; this.colors = ["red", "bule", "green", "yellow", "black"]; } changeColor() { const index = Math.floor(Math.random() * this.colors.length); this.setState({ color: this.colors[index] }); } render() { return ( <div> <div style={{ width: 400, height: 100, border: "1px solid #ccc", background: this.state.color }} ></div> <button onClick={() => this.changeColor()}>随机切换</button> </div> ); } }

那么用hook是怎么实现的呢


import React, { useState } from "react"; export default function ChangeColor() { const colors = ["red", "bule", "green", "yellow", "black"]; const [color, setColor] = useState("red"); function changeColor() { const index = Math.floor(Math.random() * colors.length); setColor(colors[index]); } return ( <div> <div style={{ width: 400, height: 100, border: "1px solid #ccc", background: color }} ></div> <button onClick={changeColor}>随机切换</button> </div> ); }

这样看起来两种方法并没有什么大的差别,只是写法上的不同而已。那么问题来了,现在我们同时又有了一个需求,和它很像,但是不是点击切换了,而是定时器自动切换(滑动切换,反正各种切换方法~)。这下class组件直接就傻掉了,没办法复用啊! 里面有点击事件的逻辑和dom啊,我们能够复用的只是切换颜色这个逻辑而已!没办法那就复制粘贴再写一个新的吧,反正也很短。那那那要是很复杂的逻辑呢?怎么办?这时候我们看一哈自定义hook,它可以帮我们解决这个问题,抽离出我们需要复用的逻辑,实现优雅的复用。

首先我们考虑,能够复用的只是切换颜色的逻辑而已,所以我们抽离出的也一定是纯逻辑,也就是说这个自定义hook中不应包含dom


import { useState } from "react"; export default function useRandomColor() { const colors = ["red", "bule", "green", "yellow", "black"]; const [color, setColor] = useState("red"); function changeColor() { const index = Math.floor(Math.random() * colors.length); setColor(colors[index]); } return [color, changeColor]; }

这里我们自定义了一个叫做useRandomColor的hook,返回值为[颜色,改变颜色的方法] 是不是很像useState?这就是一个简单的由我们自定义的一个hook。我们在父组件中调用的时候就像这样


import React from "react"; import useRandomColor from "./changeColor"; export default function ChangeColor() { const [color, setColor] = useRandomColor(); return ( <div> <div style={{ width: 400, height: 100, border: "1px solid #ccc", background: color }} ></div> <button onClick={setColor}>随机切换</button> </div> ); }

简直可以说是和useState的使用方法一模一样是不是!

需要注意的一点是,自定义hook是一个函数,其名称以 “use” 开头,它的内部可以调用其他的hook(无论是api提供的还是我们自定义的)

自定义 Hook 必须以 “use” 开头吗?必须如此。这个约定非常重要。不遵循的话,由于无法判断某个函数是否包含对其内部 Hook 的调用,React 将无法自动检查你的 Hook 是否违反了 Hook 的规则。

在两个组件中使用相同的 Hook 会共享 state 吗?不会。自定义 Hook 是一种重用状态逻辑的机制(例如设置为订阅并存储当前值),所以每次使用自定义 Hook 时,其中的所有 state 和副作用都是完全隔离的。

还有哪些react提供的hook

这里我只讲述了useState 和 useEffect两个最重要也是最最常用的hook(对于目前阶段的我来说) 其实react还提供了很多hook:

  • useContext
  • useReducer
  • useCallback
  • useMemo
  • useRef
  • useImperativeMethods
  • useMutationEffect
  • useLayoutEffect

深入了解使用方法可跳转:react-1251415695.cos-website.ap-chengdu.myqcloud.com/docs/hooks-… 鉴于我个人的能力有限和不足,这些hook会在日后陆续学习和研究使用方法和场景等等。

Js中文网 – 前端进阶资源教程 www.javascriptC.com,typescript 中文手册
专注分享前端知识,你想要的,在这里都能找到

参考

作者:张不喝.
链接:https://juejin.im/post/5d9168aa6fb9a04e0855a8a5

看完两件小事

如果你觉得这篇文章对你挺有启发,我想请你帮我两个小忙:

  1. 关注我们的 GitHub 博客,让我们成为长期关系
  2. 把这篇文章分享给你的朋友 / 交流群,让更多的人看到,一起进步,一起成长!
  3. 关注公众号 「画漫画的程序员」,公众号后台回复「资源」 免费领取我精心整理的前端进阶资源教程

JS中文网是中国领先的新一代开发者社区和专业的技术媒体,一个帮助开发者成长的社区,目前已经覆盖和服务了超过 300 万开发者,你每天都可以在这里找到技术世界的头条内容。欢迎热爱技术的你一起加入交流与学习,JS中文网的使命是帮助开发者用代码改变世界

本文著作权归作者所有,如若转载,请注明出处

转载请注明:文章转载自「 Js中文网 · 前端进阶资源教程 」https://www.javascriptc.com

标题:React 16.8 之 React Hook

链接:https://www.javascriptc.com/3825.html

« 我在阿里做中后台开发
闲鱼 Flutter 实践与思考»
Flutter 中文教程资源

相关推荐

QR code