useState原理以及其它react-hook的使用

前言

自react16.8发布了正式版hook用法以来,我们公司组件的写法逐渐由class向函数式组件+hook的方向转移,虽然用了这么久的hook,但是用得多的基本就useStateuseEffectuseMemo,其他的官方hook因为使用场景不明导致基本没用过,所以这两天特地去了解了一下其他hook的使用场景以及useState的原理,然后用这篇文章记录一下。

useState的使用及其原理

在hook版本出来之前,react函数组件无法拥有自身内部的状态,而useState赋予了函数组件拥有内部状态的能力,并且它的使用非常简单。

  • useState用法
    useState是一个函数,它接收一个初始值,并返回一个数组,该数组的第一位是一个state,第二位则是改变这个的state的函数,比如下面这个计数器的例子,当我点击按钮+的时候,数字就+1:

    image.png

    上例中的n就是这个函数组件的内部状态了。

  • useState原理
    在上面的例子中,我们每次点击按钮+的时候,数字都会增加1,也就是说App这个函数会被重新执行一次:

    image.png

    image.png

    既然App会被重新执行,那么useState(0)也会被重新执行一次,但是为什么n的值不会被重置为0呢?
    原因是,第二次useState执行后返回的n并非之前的n,setN改变的并不是之前返回出来的那个n,setN改变的数值存储于其它地方而非n,之后useState通过闭包的形式将这个新的数值返回了出来,并且执行dom的更新,我们可以在下面的实现一个useState看得更清楚。

  • 实现一个useState

    1. 首先通过上面的原理的解析,setN改变的并非n,而是另一个变量,所以我们创建这个变量_state,并创建myUseState函数:

      image.png

    2. 之后myUseState接收一个初始值,并返回一个数组,注意这个数组的第一项返回的并不是接收到的初始值而是第一步_state,而第二项则是改变_state的函数:

      image.png

      另外需要注意的是,在第一次执行myUseState的时候需要将初始值赋值给_state,而第二次执行的时候则是将之前的_state赋值回_state:
      image.png

    3. 接着useState在更新state的时候会重新渲染Dom,所以我们在setState函数中执行重新渲染的步骤(这里为了方便简化了更新步骤):

      image.png

    4. 这时候我们自己的useState就实现完成分了,用来测试一下:

      image.png

      结果可见是成功的:
      image.png

      但是此时我们的myUseState存在一个严重的bug,如果一个组件内存在多个state,而_state却只有一个,就会导致多个state都共用了一个状态,比如下面的组件:
      image.png

      image.png

  • 修复myUseState的bug

    1. 针对上面所说的bug,在组件拥有多个state的情况下,useState的执行存在由上到下的顺序,那么我们就可以将_state改造为一个数组,用于存储多个state,另外还需要新建一个变量index用于表明state在_state中的顺序:

      image.png

    2. 之后在myUseState中要做的第一步就是将接收到的state放入到_state中(注意这里和之前一样,第一次执行放入的是初始state,从第二次开始变成_state中对应的state):

      image.png

    3. 第三步我们在myUseState中创建一个能够修改_state中对应数据的函数setState并将其返回出来:

      image.png

    4. 之后需要考虑,setState执行的时候会重新渲染组件,所以在这一步中需要重置index:

      image.png

    5. 另外,为了保证每个_state中的state的顺序是一致的,所以在myUseState中将state放入到_state之后,将index + 1,这样我们就修复了之前多个state冲突的问题了:

      image.png

    6. 测试结果和代码总览:


      image.png
import React, { useState } from 'react'
import ReactDOM from 'react-dom'

const _state = []
let index = 0

const myUseState = initialValue => {
    const currentIndex = index
    _state[currentIndex] = _state[currentIndex] === undefined ? initialValue : _state[currentIndex]
    index = index + 1

    const setState = newValue => {
        _state[currentIndex] = newValue
        index = 0
        ReactDOM.render(<App />, document.getElementById('app'))
    }
    return [_state[currentIndex], setState]
}

const App = () => {
    const [n, setN] = myUseState(0)
    const [m, setM] = myUseState(0)

    const clickN = () => {
        setN(n + 1)
    }

    const clickM = () => {
        setM(m + 1)
    }

    return (
        <div>
            <div>{n}</div>
            <button onClick={clickN}>+</button>
            <div>{m}</div>
            <button onClick={clickM}>+</button>
        </div>
    )
}

ReactDOM.render(<App />, document.getElementById('app'))
  • useState的一些其他知识
    1. useState不能在条件语句后使用的原因: 原因根据上面自己实现的myUseState中就能看出来了,如果放在条件语句后使用,那么就有可能打破_state存放state的顺序导致state错乱。
    2. 看下面代码,虽然用了两次setN,但实际上点击按钮+后实际上数字只会加一:
      image.png

      image.png

      我们可以将clickN中的代码改成如下,使其变成每次都能+2:
      image.png

      image.png

useEffect和useLayoutEffect的使用及其异同

  • useEffect
    useEffect接收两个参数,第一个参数是函数,用于当组件内的state产生变化之后执行,而第二个参数(非必传)是一个数组,接收依赖的state,比如下面的例子,当n变化的时候将会打印出n的数值:

    image.png

    另外useEffetc接收的函数参数可以返回一个函数,这个函数将在该组件注销时执行,类似于class组件的componentWillUnmount
    例如下面的组件,在组件挂载后会设置一个定时器,每一秒钟打印一个1出来,当该组件被注销后,这个定时也会被注销:
    image.png

    另外还需要注意,usweEffect接收的函数是在组件渲染完毕之后才执行的。

  • useLayoutEffect
    useLayoutEffect用的非常少,这是一个有点像vue的v-cloak的功能,比如下面的代码,当组件挂载之后,把div里面的文字从value: 0改成value: 1000:

    image.png

    我们看到的效果确实也是这样的:
    image.png

    但是当你刷新多几次的时候,仔细观察就会发现,每次加载进来页面都会看到value: 0闪烁一下然后变成value: 1000,这是因为useEffect接收的函数是在组件被渲染之后才会执行的。
    这时候要解决这个问题,就需要将useEffect改成useLayoutEffect了,就不会存在这个闪烁的问题,而是直接显示value: 1000
    image.png

    原因在于,useLayoutEffect接收到的函数参数在组件渲染之前就会被执行,也就是说useEffectuseLayoutEffect功能其实是类似的,但是执行的时机不同,我们可以从下面的执行顺序看出来:
    image.png

    打印的顺序确实是1 2 3 4:
    image.png

  • 注意:
    虽然说useLayoutEffect能够在useEffect之前就执行,但是在不改变网页Dom文字样式的情况下,还是推荐使用useEffect的,在需要改变网页Dom文字样式的情况下再使用useLayoutEffect

useReducer以及useContext

  • useReducer
    useReducer的使用和redux的使用有些类似,useReducer接收两个参数,第一个是reducer(和redux中的一模一样),第二个参数是初始state,之后他会返回一个数组,数组第一项是state,第二项是改变state的函数dispatch,比如下面的例子:

    image.png

    测试结果:
    image.png

  • useContext
    useContext需要和createContext结合起来使用,实际上他们所要解决的问题和redux、mobx是类似的,都是夸组件间的数据传递,比如下面的例子,存在App组件,一个父亲组件,一个儿子组件,我们就通过创建一个Context,并用这个Context将App组件包裹起来,将App组件内的state传入到Context,使得父亲组件和儿子组件都能够通过useContext拿到App组件的state:

    image.png

  • useReducer和useContext结合搭建状态管理系统
    使用useContext可以在任意被对应Context包裹的组件中拿到传入的数据,将其和useReducer结合起来,
    就可以创建一个组件的状态管理系统,如何搭建可以参考我的这篇文章从零搭建项目(5) --- 前端: 搭建路由和状态管理

React.memo、useMemo和useCallback

这三个Api通常都在优化组件的时候使用,并且他们使用的都是记忆化函数的原理,关于记忆化函数可以参考我之前写的这篇文章: 再谈js中的函数

  • React.memo
    memo的功能其实之前class组件的pureComponent差不多,但是这个memo是用在函数式组件上的。
    首先我们来看下面的例子,Child组件引用了App组件的状态m,状态n和Child组件并无关系:

    image.png

    但实际上我点击按钮并执行setN的时候,Child组件也被更新了:
    image.png

    原因是Child组件被App组件所包裹,而执行setN的时候,App组件被重新渲染了,那么在其之中的Child组件自然也就被重新渲染了。
    所以这时候我们就需要用到memo来优化一下,使得我在执行setN的时候,Child组件不会跟着一起被渲染。
    memo的使用也非常简单,直接用它包裹需要被优化的组件即可,在本例中就是Child组件,所以代码可以修改为如下:
    image.png

    这时候我们执行setN的时候就不会使得Child组件跟着重新渲染了,只有执行setM的时候Child才会重新渲染:
    image.png

  • useCallback
    在上面使用memo的例子中,存在一个问题,当Child接收的props中存在函数的时候,之前使用memo做的优化就无效了,比如下面的代码:

    image.png

    结果:
    image.png

    原因和之前一样,由于App组件的重新渲染,所以const test = () => {}这段代码也被重新执行了,而test是一个函数,函数是引用类型,所以传入到Child中的test也和之前的test函数不一样,导致Child组件重新渲染。
    这时候我们就可以使用useCallback对其进行优化了。
    useCallback接收两个参数,首参是一个函数,在本例子中就是test函数,第二个参数是一个数组,这个数组接收的是改变这个函数引用的依赖,比如下面例子,m的值被改变的时候,test函数的引用才会被改变,Child组件才会被重新渲染:
    image.png

    优化结果:
    image.png

  • useMemo类似于vue中computed的功能,他接收两个参数,第一个参数是一个函数并通过计算得出一个state,第二个参数是计算这个state所需要的依赖,比如下面的例子,Child组件接收多一个props: num,这个num是n与m相加得出的:
    image.png

    结果:
    image.png

useRef和forwardRef

  • useRef
    useRef接收一个参数作为初始值,返回一个可变的 ref 对象,这个 ref 对象含有.current属性,该属性可以在整个组件色生命周期内不变。
    通常useRef被用作获取某个Dom,比如下面的例子:

    image.png

    image.png

  • forwardRef
    forwardRef这个函数用的场景相对较少,它主要用于在父组件获取子组件的Dom作为自己的ref的时候使用,比如下面的例子:

    image.png

    但实际上这样做是有问题的,会报错,并且buttonRef也没有获取到:
    image.png

    这时候我们就可以使用forwardRef对Button组件进行包裹,forwardRef会为Button组件注入一个新的参数ref:
    image.png

    这时候父组件就可以获取得到这个子组件的Dom了:
    image.png

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,491评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,856评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,745评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,196评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,073评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,112评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,531评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,215评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,485评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,578评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,356评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,215评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,583评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,898评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,497评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,697评论 2 335

推荐阅读更多精彩内容