React学习:理解和使用Hooks

Hooks 是React的一次革命性升级,本文将对其优势和API进行比较全面的解析

为什么要有hooks

在没有hooks之前,除了对于一些无状态组件可以使用函数来声明组件以外,大家都会使用class来声明组件。作为一个主要工作内容为Android开发的我,早已习惯万物皆class,而Android中的Activity(可以理解为每一个交互界面)就是class,所以也欣然接受,并且在ES6中class带有的constructor、super以及react的生命周期函数对使用Java的Android开发者来讲很容易理解接受。

但是使用class来声明组件却存在以下问题:

  1. 状态逻辑难以复用
    假如有一段逻辑代码需要在多个组件中使用,那么在以前,可以通过以下几个方式来实现:
  • copy代码,显然不符合代码设计的原则
  • 继承,一方面,js只支持单继承(Java中可以通过接口实现对多个类的实现),想要对复用多个组件的逻辑就无能为力;另一方面,只为了复用部- 分逻辑而滥用继承,显然是违背oop原则的
  • HOC高阶组件,使用HOC复用的原理很简单,就是包裹封装,比如我们想实现对onScroll方法的复用:
import React,{Component} from 'react'

function scrollable(Child) {
    return class ScrollWrapper extends Component {
        ref = React.createRef()
        onScroll = (...args) => {
            console.log('onscroll')
            this.ref.current.onScroll(...args)
        }
        componentDidMount() {
            document.addEventListener('scroll', this.onScroll, false);
        }

        componentWillUnmount() {
            document.removeEventListener('scroll', this.onScroll, false);
        }

        render() {
            return <Child ref={this.ref} />
        }
    }
}

class ScrollableApp extends Component {
    onScroll(){
        console.log('child onscroll')
    }
    render() { 
        return <div style={{color:'red',height:10000,width:800}}>
            
        </div>
    }
}

export default scrollable(ScrollableApp);

ScrollableApp通过高阶组件即函数scrollable()封装后,复用了ScrollWrapper中的onScroll()方法,并且还能再ScrollWrapper的onScroll使子组件ScrollableApp中的onScroll也能得到调用;使用渲染属性也可以实现复用:

export class Scrollable extends Component {
    onScroll = (...args) => {
        console.log('onscroll')
        console.log(this.props.children)        
    }
    componentDidMount() {
        document.addEventListener('scroll', this.onScroll, false);
    }

    componentWillUnmount() {
        document.removeEventListener('scroll', this.onScroll, false);
    }

    render() {
        return this.props.render()
    }
}

export class ScrollableApp extends Component {
    onScroll() {
        console.log('child onscroll')
    }
    render() {
        return <div style={{ color: 'red', height: 10000, width: 800 }}>

        </div>
    }
}

使用时:

function App() {
  return (
    <div>
      <Scrollable render={() => <ScrollableApp></ScrollableApp>}></Scrollable>
    </div>
  );
}

export default App;

这个方法和高阶组件方式差不多,不再赘述
使用这两者虽然能实现逻辑复用,但无疑对代码的简洁和运行性能都有不少的损耗

  • 另外,“组合优于继承”,或许可以使用策略模式,抽取出不同的类来封装这些逻辑,在使用时引入相关的类来实现,但这无疑有点过度设计,也大大增加了代码结构的复杂度
  1. 类组件复杂,难以维护,主要指生命周期函数混乱,比如上面的onScroll监听,在componentDidMount和componentWillUnmount分别要注册反注册,相关的逻辑分散在不同地方,而在componentDidMount往往还需要处理类似网络请求等各种初始化的动作,也导致不相关的逻辑混杂在一起,使得代码难以维护(这个在Android开发中其实也是再正常不过做法...)

  2. this指向等问题
    上面有一段代码:

onScroll = (...args) => {
   ...
}

这里使用类属性的方式定义onScroll,才能通过this.onScroll访问到该方法,而如果声明为类成员函数,则在向下一级组件传递回调函数时无法正确访问到该方法

而hooks则很好的解决了以上的问题

使用hooks

useState

  1. 使用

在没有react hooks之前,组件可以分为有状态组件和无状态组件,如:

class Counter extends Component {
    state = {
        count: 0
    }
    render() {
        return (
            <div>
                {this.state.count}
            </div>
        )
    }
}
function Counter(props) {
    return (
        <div>
            {props.count}
        </div>
    )
}

第一种写法Counter中存储了状态state,而函数组件写法中只能通过props来获取状态

使用useState:

import React, { useState } from 'react'

export default function Counter(props) {
    const [count, setCount] = useState(0)
    return (
        <div>
            <button
                onClick={() => {
                    setCount(count=>count + 1)
                }}>
            </button>
            {count}
        </div>
    )
}

这里const [count, setCount] = useState(0)中,相当于定义了一个count变量作为该组件state的一个属性,而useState中传入的值为count的初始默认值(也可以不传入,则为undefined),setCount为改变count值的方法;以上代码等价于:

export class Counter extends React.Component {
    state = {
        count: 0
    }
    render() {
        return (
            <div>
                <button
                    onClick={() => {
                        this.setState(
                            {
                                count: this.state.count + 1
                            }
                        )
                    }}>
                </button>
                {this.state.count}
            </div>
        )
    }
}

可见,使用useState使得代码大大简化,可以不使用class声明组件,也不用担心this指向的问题;并且从此我们不用再以有无状态来区分组件了,因为函数组件也可以拥有状态

2.原理
这里有几个值得探讨的问题:

  • useState()如何确定应该返回的是哪一个component的state
    这个很简单,因为js运行在单线程环境中,所以在运行到某一个useState函数时,可以获取到对应的运行上下文处在哪一个component中

  • 如何确定useState对应于哪一个返回值
    思考以下的伪代码:

function Counter(props) {
  if (someCondition) {
    useState();
  }
  useState();
}

在实际执行中会报错,而且如果eslint配置了react-hooks/rules-of-hooks,会直接编译报错
实际上为了代码尽可能简洁,useState是通过记录第一次运行时的顺序来确定之后的每次运行分别返回对应哪个state的,所以Hooks函数必须始终以相同的次序和数量被调用

  • setState相同值的时候会否重新渲染
    改写之前的代码,setCount时每次都为0,发现并不会执行render函数
function Counter(props) {
    const [count, setCount] = useState(0)
    console.log('render')
    return (
        <div>
            <button
                onClick={() => {
                    setCount(0)
                }}>
            </button>
            {count}
        </div>
    )
}

假如我们的state中存储的是对象呢?

function Counter(props) {
    const [countObj, setCountObj] = useState({ count: 0 })
    console.log('render')
    return (
        <div>
            <button
                onClick={() => {
                    countObj.count = countObj.count + 1
                    setCountObj(countObj)
                }}>
            </button>
            {countObj.count}
        </div>
    )
}

点击button,发现也未重新渲染。因此在setState时,如果为对象,对比的地址值未改变,并不会重新render,这和PureComponent类似

useEffect

effec被翻译过来为副作用,但是这个确很容易产生语义误解;实际上副作用实际上指的是视图组件与视图组件之外系统进行交互的行为u,比如与DOM交互,网络请求,数据持久化操作等
假如我们需要在componentDidMount之后设置onScroll监听,使用class的写法为:

class Scrollable extends React.Component {
    onScroll = () => {
        console.log('onscroll')
    }
    componentDidMount() {
        document.addEventListener('scroll', this.onScroll, false);
    }
}

通过useEffect改写则为:

function Scrollable(props) {
    useEffect(() => {
        document.addEventListener('scroll', this.onScroll, false);
    })
    return <div></div>
}

useEffect()中的函数会在每次componentDidMount、componentDidUpdate的时候执行,如果要在componentWillUnmount中取消监听也很简单,只需在useEffect()传入的函数中return相关处理函数即可:

function Scrollable(props) {
    useEffect(() => {
        document.addEventListener('scroll', this.onScroll, false);
        return () => {
            document.removeEventListener('scroll', this.onScroll, false);
        };
    })
    return <div></div>
}

除此之外,useEffect还可以传入第二个参数,该参数类型为数组;这里分为三种情况:

1.不传入数组参数:在不传入该数组的情况下(参考上面的代码),每次componentDidMount、componentDidUpdate或componentWillUnmount时都会执行对应的副作用函数

2.传入空数组:该副作用会在组件整个生命周期中只执行一次、清理一次;这很适用于对网络请求、事件监听等操作

3.传入非空数组:该副作用会在数组中的各个参数发生变化时(对象比较地址值),才会在对应的生命周期中重新执行

通过useEffect,可以使得我们的代码更简洁,逻辑更清晰易维护,对数组参数的控制也能帮助我们更轻松的写出高性能的代码

useContext

在没有hooks之前Context就已经存在,用以实现跨层级数据传递,一般使用Consumer和ContextType实现

但是Context的似乎使用得不是很多,多数还是通过redux的store存储全局数据;useContext使得Context的使用更容易,可以在函数组件中使用Context,并且不用依赖ContextType,避免了每一个组件只能对应一个ContextType的缺点,当然也不需要Consumer

使用很简单(这里只是演示,代码结构可以根据实际做优化;这里顺便列出以往使用Consumer和ContextType实现Context数据传递的写法作对比):

import React, { Component, createContext, useContext, useState } from 'react'
const CountContext = createContext(0)

function App() {
  const [count, setCount] = useState(0)
  return (
    <div>
      <button
        onClick={() => {
          setCount(count + 1)
        }}>
      </button>
      <CountContext.Provider value={count}>
        <CounterByConsummer></CounterByConsummer>
        <Counter></Counter>
        <CounterByContextType></CounterByContextType>
      </CountContext.Provider>
    </div>
  );
}

//useContext写法
function Counter(props) {
  const count = useContext(CountContext)
  return (
    <div>
      {count}
    </div>
  )
}
//Consummer写法
class CounterByConsummer extends Component {
  render() {
    return (
      <CountContext.Consumer>
        {count => <div>{count}</div>}
      </CountContext.Consumer>
    )
  }
}

//ContextType写法
class CounterByContextType extends Component {
  static contextType = CountContext
  render() {
    const count = this.context
    return (
      <div>
        {count}
      </div>
    )
  }
}

export default App;

需要注意的一点是,不要滥用context,因为会破坏组件的独立性

useMemo&useCallback

理解memo

为了提高react的运行效率,避免无用的重渲染,我们常使用继承PureComponent的方式;而在函数组件则可以使用React.memo(Component)来达到同样的效果;

使用useMemo

React.memo()针对组件,而useMemo则是针对组件的方法,思考如下代码:

import React, { useMemo, useState } from 'react'

function App() {
  const [name, setName] = useState('smartzheng')
  const [age, setAge] = useState(18)
  return (
    <>
      <button
        onClick={() => {
          setName(name + 'changed')
        }}>
        changeName
      </button>
      <button
        onClick={() => {
          setAge(age + 1)
        }}>
        changeAge
      </button>
      <Description age={age} name={name}></Description>
    </>
  );
}


function Description({ name, age }) {
  function getAge(age) {
    console.log('changeAge')
    return age + '岁'
  }
  const newAge = useMemo(() => getAge(age), [age])

  return <>
    <div>
      {name}
    </div>
    <div>
      {newAge}
    </div></>
}
export default App;

这段代码主要做的是显示两个button,一个用来改变name,一个改变age,而子组件中对name和age进行显示,并且通过getAge()在age后加上“岁”字。测试发现,点击changeName和changeAge都会导致子组件重新执行getAge(),这并不是我们想要的结果;这里就可以通过useMemo来实现只有在age发生变化时才执行getAge(),使用很简单,只需将 const newAge = useMemo(() => getAge(age), [age]) 改为const newAge = useMemo(() => getAge(age), [age])即可

useMemo(() => getAge(age), [age])中,传入的是一个函数,可以理解为一个回调,数组[age]代表该回调只有在age变化时才会执行,而执行的内容为getAge(age)

使用useCallback

useCallback和useMemo很类似,不过他返回的是缓存的函数:const fnA = useCallback(fnB, [a]),代表useCallback会将fnB函数返回,返回值是否改变依赖于a值是否改变。举例如下:

import React, { useState, useCallback } from 'react';
const set = new Set();

export default function Callback() {
  const [count, setCount] = useState(0);
  const [val, setVal] = useState('');

  const callback = useCallback(() => {
    console.log(count);
  }, [count]);
  set.add(callback);
  return <div>
    <h1>{count}</h1>
    <h1>{set.size}</h1>
    <div>
      <button onClick={() => setCount(count + 1)}>changeCount</button>
      <input value={val} onChange={event => setVal(event.target.value)} />
    </div>
  </div>;
}

这里的callback就是对() => {console.log(count)的缓存,通过一个set来存放它,只有当点击changeCount的按钮是set的size才会发生变化,说明只有在count变化时,才会返回新的callback方法,这对减少重复创建相同的方法对象很有帮助

useRef

在hooks之前,常用createRef来创建ref来获取DOM元素的引用,React Hooks中则提供了useRef

  1. 使用useRef获取DOM元素的引用
import React, { PureComponent, useRef } from 'react';

function App(props) {
  const countRef = useRef();
  return (
    <>
      <Counter ref={countRef}></Counter>
      <button onClick={() => { console.log(countRef.current) }}></button>
    </>
  )
}

class Counter extends PureComponent {
  render() {
    return (
      <div>

      </div>
    )
  }
}
export default App

在上述代码中,通过useRef()创建了countRef并在Counter组件上进行赋值,点击button,每次都会正确打印出Counter组件
值得注意的是,这里的Counter如果使用函数组件则会报错,提示function components can not be given refs,原因是函数组件会被React底层处理,被class wrap,所以直接给该函数组件进行ref赋值没有意义;从这里也可以发现函数组件还不能完全替代类组件

  1. 使用useRef存储对象
    正常情况下,如果在函数组件中声明一个变量,那么该变量会在每次渲染时重新创建,而使用useRef可以实现跨越声明周期存储数据,思考如下代码:
import React, { useRef,useEffect,useState } from 'react'

function App(props) {
  const [count, setCount] = useState(0)
  let interval;
  useEffect(()=>{
    interval = setInterval(()=>{
      setCount(count+1)
    },1000)
  },[])
  if(count>5){
    clearInterval(interval)
  }
  return (
    <>
      <div>{count}</div>
    </>
  )
}

export default App

当count大于5时,清除定时器,这样写肯定是无效的,因为每次都会创建一个新的interval,clearInterval中的interval并不是最开始的interval,通过useRef改写即可实现:

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

function App(props) {
  const [count, setCount] = useState(0)
  let interval = useRef();
  useEffect(()=>{
    interval.current = setInterval(()=>{
      setCount(count+1)
    },1000)
  },[])
  if(count>5){
    clearInterval(interval.current)
  }
  return (
    <>
      <div>{count}</div>
    </>
  )
}

export default App

自定义Hooks

前面提到类组件有三个缺点,首当其冲的是逻辑复用问题,我们可以通过自定义Hooks来解决该问题,例如我们通过自定义useCount来复用一个定时器:

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

function App(props) {
  const [count] = useCount(0)
  return (
    <>
      <div>{count}</div>
    </>
  )
}

function useCount(defaultCount){
  const [count, setCount] = useState(0)
  let interval = useRef();
  useEffect(() => {
    interval.current = setInterval(() => {
      setCount(count => count + 1)
    }, 1000)
  }, [])
  useEffect(()=>{
    if (count >= 5) {
      clearInterval(interval.current)
    }
  })
  return [count, setCount]
}
export default App

关于React Hooks的优势和常用API使用先写到这,后面的文章再对Hooks的深层实现原理和自定义Hooks学习和解析

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

推荐阅读更多精彩内容