React Hooks使用基础

397 阅读24分钟

React Hooks

在学习React副作用Hook之前,我们需要知道

React 16 之后,采用了Fiber架构,从此以后,React功能被分为了三个部分:

  • Scheduler(调度器)—— 调度任务的优先级,高优任务优先进入Reconciler
  • Fiber Reconciler(协调器)—— 负责找出变化的组件
  • Renderer(渲染器)—— 负责将变化的组件渲染到页面上

为什么改变架构呢?

image.png

React16之前,React架构分为两部分

  • Stack Reconciler(协调器)
  • Renderer(渲染器)

可以看到,不同的是,新的架构增加了调度器和更改了协调器

原来的Stack Reconciler协调器工作时,是递归解析的,不可中断,会一直占用js线程

以60帧为例, 表示1s中内浏览器要渲染60次画面,这意味着,浏览器需要每16.6ms(1帧)为用户呈现一次新的内容,因为浏览器渲染线程和js线程互斥,如果浏览器需要呈现页面内容的时候,js正占用线程在计算哪些内容需要更新,就会导致内容呈现不及时,这个时候如果我们在进行页面滚动、按钮点击、input输入等“既视”操作时,就会感觉明显卡顿。

React为了解决这个问题,舍弃了原来的递归解析虚拟DOM,采用了异步可中断的更新,由于原来的虚拟DOM不满足异步可中断的功能,所以重新设计了架构,新的架构就叫做Fiber架构,Fiber架构中,我们的代码在内存中以一个一个的Fiber节点的形式存在,每个节点上有三个指针 return-指向父级Fiber节点child-指向子级Fiber节点sibling-指向兄弟Fiber节点,所有的Fiber节点,由这三个指针链接在一起,形成一个Fiber链表。所以, 在新的架构中,虚拟DOM已经不再是一棵树,而是链表

调度器 - Scheduler
React将不同的任务标记不同的优先级,并基于浏览器API requestIdleCallback进行封装,利用浏览器渲染页面每一帧剩余的时间,来优先调度优先级高的任务,交给Reconciler来处理。最大可能确保浏览器每一帧渲染顺利完成,减少页面卡顿。

协调器 - Reconciler
接受调度器指派的任务,在内存中计算组件的更新内容并标记在对应的Fiber节点上,并且这些更新也会以链表的方式保存(方便最终render commit的时候,直接使用,而不是重新遍历Fiber节点)

渲染器 - Renderer
将协调器计算得到的新内容commit 提交渲染到浏览器界面

不同的平台有不同的渲染器

function FiberNode(
  tag: WorkTag,
  pendingProps: mixed,
  key: null | string,
  mode: TypeOfMode,
) {
  // 作为静态数据结构的属性
  this.tag = tag;
  this.key = key;
  this.elementType = null;
  this.type = null;
  this.stateNode = null;

  // 用于连接其他Fiber节点形成Fiber树
  this.return = null;
  this.child = null;
  this.sibling = null;
  this.index = 0;

  this.ref = null;

  // 作为动态的工作单元的属性
  this.pendingProps = pendingProps;
  this.memoizedProps = null;
  this.updateQueue = null;
  this.memoizedState = null;
  this.dependencies = null;

  this.mode = mode;

  this.effectTag = NoEffect;
  this.nextEffect = null;

  this.firstEffect = null;
  this.lastEffect = null;

  // 调度优先级相关
  this.lanes = NoLanes;
  this.childLanes = NoLanes;

  // 指向该fiber在另一次更新时对应的fiber
  this.alternate = null;
}

新的架构,React经过协调器解析后,生成一颗由FiberNode组成的Fiber树,树的每个节点上有上面的属性

因为新的架构采用了可中断的渲染过程,中断后再继续时,会从头渲染这一个组件,也会导致原来渲染阶段中的生命周期钩子函数可能被重复调用(React class组件舍弃原来的componentWillMount和componentWillReceiveProps的原因)。

什么是React Hooks

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

image.png

hook汉译为“钩子”,我们可以理解为 React Hooks 是React为了能够在函数组件中使用React生命周期钩子、副作用钩子、状态管理 所提供的一种语法

使用环境

  • React 16.8 +,同时需要升级 React DOM
  • 100%向后兼容,使用Hooks不会造成任何破坏性的改动,不影响继续使用class组件,可以选择使用或不使用hooks
  • 无任何破坏性的改动,只要是函数组件,就可以使用HooKs(原来的类组件不需要重构),并且 没有计划从 React 中移除 class

Hooks主要想解决的问题(React为什么推出Hooks)

1、复用状态逻辑

使用hooks之前,我们使用Class组件,我们将状态保存在 this.state, 这些状态只能在class组件内部使用,或者通过props传递给子组件,很难讲逻辑抽离并在其他地方复用。 组件之间要 复用状态逻辑,需要用到 Contextrender props高阶组件(HOC)等,这些技术的确很有用,但通常情况下,需要我们重新去组织组件结构。

Hooks可以使你在无需修改组件结构的情况下复用状态逻辑,将能复用的状态逻辑抽离成一个hooks,可以很方便的在组件间共享

2、让组件变得容易理解、简洁

往往在使用class组件时,将很多功能按照生命周期划分,在componentDidMount时应该做什么,在 componentDidUpdate时应该做什么 .... ,有时候完全不相干的功能,在一个代码块里面出现。

这种按生命周期划分功能的默认行为,决定了一个组件里,某些逻辑不能拆分得很细;在组件中只有少量逻辑时,这不会有什么问题,如果组件中写了大量的逻辑,会让代码变得不易管理

3、更贴近React理念和底层原理

Hooks直接操作Fiber节点

React是一个用于构建用户界面的JavaScript库,React也更热衷于用一个函数返回一个视图 的这种编程方式

4、在不使用class的情况下,可以使用更多的React特性

往往有时候,我们只需要在组件中使用很少的(有时候甚至只需要一个)状态变量,以前我们需要写

class MyComponent extends React.Components{
    constructor(props) {
        super(props)
        this.state = { vb: 'hello' }
    }
    ...
}

本来不想用class组件,因为需要一个状态管理,不得已去使用了class。

现在我们只需要:

const MyComponent = ()=> {
    const [vb, setVb] = useState()
}

从概念上讲,React组件更像是函数,而Hooks则更深层次的拥抱这个概念

但 Class组件也并非一无是处, class的继承写法,hooks并没有替代的解决方案

我们用class组件,封装一个列表页基础行为,在使用时,我们只需要继承这个 “基础行为”。 如果我们要修改某一个函数逻辑(比如获取请求参数的逻辑),我们只需要重写这个函数,这类似于对象的原型链调用,我们可以选择用对象自己的方法,也可以选择借用对象原型链上的关联对象的方法

即便如此,我们依然可以使用自定义hooks来替代之前的class继承的功能

Hooks的使用

hooks不能在class组件中使用

在React的index.js文件中,可以看到, react 为使用者提供了以下内置hooks(useMutableSource被标记为不稳定的,在React16、React17中并未暴露给使用者):

image.png

我们可以将这一些hooks做归类:

状态类:

  • useState
  • useReducer
  • useContext

副作用类:

  • useEffect
  • useLayoutEffect

性能优化类

  • useMemo
  • useCallback

功能性类:

  • useRef
  • useImperativeHandle
  • useDebugValue

1、状态类 Hooks

useState

useState为函数组件提供状态管理

    import React, { useState } from 'react';
    
    const Example:React.FC<IPropsTypes> = (props)=> {  // Hooks组件的TS类型为 React.FC
        const [count, setCount] = useState<number>(0)
        return (
            <div>
                <p>You clicked {count} times</p>
                <button onClick={() => setCount(count + 1)}>
                    Click me
                </button
            <div>
        )
    }
    
    export default Example

image.png

  • useState()返回一个数组,数组第一个元素为state状态值,数组第二个元素为更新state值的函数(Dispatch,会触发dispatchAction)
  • 通常把设置状态值的函数已 set + 状态值名称 来命名
  • useState接受一个类型参数,写法为 useState<S>() 用来定义state的类型

我们可以看到 useState 的类型定义

function useState<S = undefined>(): [S | undefined, Dispatch<SetStateAction<S | undefined>>];

// Unlike the class component setState, the updates are not allowed to be partial  *不允许部分更新
type SetStateAction<S> = S | ((prevState: S) => S);

注意事项

1、每次调用更新函数都会重新计算state值进行覆盖更新,如果state为一个对象,在调用更新函数后,将会更新整个对象,而不是像class组件的setState一样,对state对象进行合并

2、useState()返回的更新函数可以直接传入一个新的值,也可以传入一个函数,这个传入的函数的入参是当前(本次更新前)的state的值,并返回新的state值

(prevState: S) => S 

3、和class的setState一样,在一次渲染中,多次调用state的更新函数,最终也只会执行一次dom更新

这两种更新state的方式有区别,例如:

import { Button } from "antd"
import { useState } from "react"

const Demo1:React.FC = ()=> {
  const [n, setN] = useState<number>(0)

  console.log('render-------', n)
  return <div>
    <p>{n}</p>
    <Button onClick={()=> {
      // setN(n+1)
      // setN(n+1)
      // setN(n+1)
      // setN(n+1)   console.log(n) ==> 1 2 3 4 ....
      setN(n=>n+1)
      setN(n=>n+1)
      setN(n=>n+1)
      setN(n=>n+1)   console.log(n) ==> 4 8 12 16 ....

    }}>setN</Button>
  </div>
}

export default Demo1

useReducer

image.png

import { Reducer, useReducer } from "react"

const Demo: React.FC = () => {
  const [count, dispatchCount] = useReducer<Reducer<number, string>, number>((state, action) => {
    switch (action) {
      case 'add':
        return state + 1
      case 'minus':
        return state - 1;
      default:
        return state
    }
  }, 10, (a) => {
    return a
  })

  console.log('render')
  return (
    <div>
      <p>{count}</p>
      <button onClick={() => {
        dispatchCount('add')
      }}>点我+1</button>

      <button onClick={() => {
        dispatchCount('minus')
      }}>点我-1</button>
    </div>
  )
}

export default Demo

useReducer 跟useState有很大关联useState就是被包装过的useReducer, react给useReducer默认提供了reducer(处理函数),,除了组件初始化时 useStateuseReducer有不同处理逻辑,在更新的时候,useState是直接调用的useReducer,并自动传入了参数

react在执行hooks时,会根据组件是否是第一次挂载,运行不同的方法

image.png

image.png

image.png

可以发现,在组件更新时,updateState直接返回了updateReducer的调用

如同上面的demo一样,我们可以根据不同的dispatch类型来触发不同的更新逻辑


useContext

使用useContext之前,我们应该知道React Context是什么:

context被翻译成上下文 它可以让我们不需要一层一层的传递props 就可以把上层的数据直接传递给层级很深的子组件,主要用于 很多不同层级的组件需要访问同样的数据 的情况

// 定义一个context
const DemoContext = React.createContext(inititalValue)

React.createContext会返回一个对象,这个对象有两个属性: 提供者Provider 和 消费者Consumer, 他们都是React组件

我们需要在提供数据的地方,用Provider将我们的组件包裹,并且传入需要提供的数据,并且在使用的地方,将我们要使用数据的组件用Consumer组件包裹,可以从Consumer中取得Provider提供的数据,具体用法如下:

// 使用Context
const Demo1: React.FC = () => {
  const [str, setStr] = useState<string>('Hello useContext')

  return <div>
    <p>最上层数据{str}</p>
    <DemoContext.Provider value={{ value: str, onChange: (value) => setStr(value) }}>
      <div>
        <div>
          <ChildDemo />
        </div>
      </div>
    </DemoContext.Provider>
  </div>
}
export default Demo1


const ChildDemo: React.FC = () => {
  return (
    <DemoContext.Consumer>
      {({ value, onChange }) => {
        return (
          <>
            <p>
              {value}
            </p>
            <button onClick={() => { onChange && onChange('Hello useContext ' + Math.random().toString()) }}>向上传递</button>
          </>
        )
      }}
    </DemoContext.Consumer>
  )
}

useContext则不需要我们在需要使用context数据的地方用消费者组件Consumer再进行包裹,直接使用:

 const data = useContext( MyContext )
import React, { useContext, useState } from "react"
import DemoContext from "./contextDemo/base"

const UseContextDemo: React.FC = () => {
  const [str, setStr] = useState<string>('Hello useContext')

  return <div>
    <p>最上层数据{str}</p>
    <DemoContext.Provider value={{ value: str, onChange: (value) => setStr(value) }}>
      <div>
        <div>
          <div>
            <ChildDemo />
          </div>
        </div>
      </div>
    </DemoContext.Provider>
  </div>
}
export default UseContextDemo


const ChildDemo: React.FC = () => {
  console.log('子组件 render ---')
  const ctx = useContext(DemoContext)
  return (
    <>
      <p>{ctx.value}</p>
      <button onClick={() => { ctx.onChange && ctx.onChange('Hello useContext ' + Math.random().toString()) }}>向上传递</button>
    </>
  )
}

当前,上面的例子我们需要在父组件使用useState来定义状态,并使用onChange函数来改变状态,这会显得有些冗余,我们可以结合useContext和useReducer来封装一个全局状态

// 用Context和useReducer封装:

import React, { ReactNode, Reducer, useReducer, useState } from 'react'

type IActionType = 'ADD' | 'MINUS'

export const GlobalCountContext = React.createContext<{ count: number, dispatch: (action: IActionType) => void }>({ count: 10, dispatch: ()=>{} })

const reducer: Reducer<number, IActionType> = (state, action) => {
  switch (action) {
    case 'ADD':
      return state + 1
    case 'MINUS':
      return state - 1
    default:
      return state
  }
}

export const GlobalCount = (props: { children: ReactNode }) => {

  const [count, setCount] = useReducer(reducer, 10)

  return (
    <GlobalCountContext.Provider value={{ count, dispatch: setCount }}>
      {props.children}
    </GlobalCountContext.Provider>
  )
}

// 使用封装好的全局状态,通过调用dispatch来触发更新

import React, { useContext } from "react"
import { GlobalCount, GlobalCountContext } from "./contextDemo/GlobalCountContext"

const WithUseReducer: React.FC = () => {

  return <div>
    <GlobalCount>
      <div>
        <div>
          <div>
            <ChildDemo />
          </div>
        </div>
      </div>
    </GlobalCount>
  </div>
}
export default WithUseReducer


const ChildDemo: React.FC = () => {
  console.log('子组件 render ---')
  const ctx = useContext(GlobalCountContext)
  return (
    <div>
      <p>{ctx.count}</p>
      <button onClick={() => { ctx.dispatch('ADD') }}>加一</button>
      <button onClick={() => { ctx.dispatch('MINUS') }}>减一</button>
    </div>
  )
}

总结: useContext其实就是一个帮我们读取Context值的hooks

image.png

image.png

可以在Context对象中看到_currentValue和_currentValue2

image.png



2、副作用类 Hook

什么是副作用Hook?
我们知道药物的主要作用是治病,而有些药服用了却让病人昏昏欲睡,这是在使用这些药物发挥主要作用的同时,产生了额外的作用效果,这就是药物的副作用;这些副作用是这些药物天生的属性

在React中也一样,我们开发的一个React函数组件被执行后,主要作用就是渲染页面,而在渲染页面之外产生的作用效果也被React称之为副作用 跟药物天生的副作用效果不同的是,React给我们提供了定义这些副作用的方法,我们可以为我们的组件赋予不同的副作用表现,并且可以控制这些副作用用“发作”的时机。

useEffect

useEffect用来定义函数组件在渲染(render)后产生的副作用

函数组件没有生命周期的概念,只有副作用,通过不同的副作用触发条件,可以模拟class组件的大部分生命周期

image.png

// TS定义

function useEffect(effect: EffectCallback, deps?: DependencyList): void;

effectCallback: 副作用的执行函数

deps控制副作用触发的时机,可以选择不传或传入数组

  • null: 不传-在组件每次render后都会触发
  • [ ] : 空数组-只有组件在第一次render后触发, 我们可以当做componentDidMount 使用
  • [ v1, v2, v3, .... ] : 在第一次render后和v1、v2、v3、... 任意一个依赖发生改变后触发,可以模拟实现componentWillReceiveProps

以下是useEffect的基本写法Demo:

// 可以定义多个
// 按顺序执行
// 每个effect副作用函数,可以再返回一个清除函数,再次跟新effect函数会执行上一次副作用函数返回的清除函数。在组件销毁时,会执行所有的清除函数

import { useEffect, useState } from "react";

const Demo: React.FC = () => {
  const [count, setCount] = useState<number>(0)
  const [age, setAge] = useState<number>(0)

  useEffect(() => {
    console.log('不传入依赖关系-执行副作用函数')
  })

  useEffect(() => {
    console.log('传入空的依赖关系-执行副作用函数')
  }, [])

  useEffect(() => {
    console.log('传入依赖关系 - count  执行副作用函数')
  }, [count])

  useEffect(() => {
    console.log('传入依赖关系 - age  执行副作用函数')
  }, [age])


  console.log('render --- ')
  return (
    <div>
      <p>{count}</p>
      <button onClick={() => { setCount(pre => pre + 1) }}>count + 1</button>
      <button onClick={() => { setAge(pre => pre + 1) }}>age + 1</button>
    </div>
  )
}

export default Demo


在一个函数组件中定义多个副作用,会按顺序执行

需要清除的副作用
有些副作用产生的结果是持续性的,在一次执行effectCallback后,会一直产生影响,并且不会随着组件的销毁而销毁
例如:我们在副作用中使用了定时器, 在组件销毁时,定时器不会随组件销毁而清除,我们需要手动调用 clearTimeout 或 clearInterval 来清除它们。

所以: React提供了一种写法,让我们可以在定义副作用的同时,也可以定义副作用清除函数:
effectCallback的返回值如果是一个函数, 这个函数就是当前副作用的清除函数

useEffect( ()=> { 
   // do something
   return function() {   }  // 清除函数
} , [ ])

副作用清除函数被调用的时机

  • 组件销毁时,当前组件上所有的清除函数都会被执行
  • effectCallback 再次执行前,会先执行上一次保存的清除函数,并且被执行的清除函数中,如果使用了状态变量,状态变量也是上一次的值

注意事项

  • useEffect注册的effectCallback,会在render之后(画面呈现之后)被调用。
  • React在执行任意一个组件的任意一个effectCallback函数之前,会遍历所有组件,查找是否有需要执行的清除函数,会先执行所有的清除函数

useLayoutEffect

使用方法跟useEffect一致

在React协调器工作完成后,会将所有的更改进行commit,生成真实的DOM树,然后交给浏览器进行渲染

useEffect和useLayoutEffect都是在生成真实DOM树后,浏览器渲染前被触发,不同的是,useEffect交给调度器异步调度执行,useLayoutEffect则是同步执行 useLayoutEffect的回调函数如果修改了dom,则会进行同步渲染,不会出现页面闪动的现象

Class组件的componentDidMout也是同步渲染,所以,useLayoutEffect才是等价于componentDidMout, 为了页面流畅,推荐使用useEffect

useEffect和useLayoutEffect真正执行的时机是在commit阶段

image.png


3、性能优化类 Hook

不要滥用性能优化Hooks,当我们明确需要使用时才使用它们,因为使用它们本身也是一种性能消耗

useMemo和useCallback

useMemouseCallback功能一样,用做某段逻辑代码是否重复执行的优化

在使用传参上都跟useEffect一样,接受两个参数,第一个参数是一个函数,第二个参数是deps依赖项

useMemo

由于函数式组件代码每一次执行都是从上至下全部执行,一些计算量很大的代码,如果每次都执行会影响性能, useMemo用于缓存某一段计算逻辑,在不必要时不重复执行

// TS定义

// useMemo
function useMemo<T>(factory: () => T, deps: DependencyList | undefined): T;
// useCallback
function useCallback<T extends (...args: any[]) => any>(callback: T, deps: DependencyList): T;


// 用法

// 一般情况不需要使用useMemo做性能优化,因为使用useMemo也会产生性能消耗,可能最终比不做优化消耗的还多
// useMemo对一个组件是没有记忆功能的,如果把一个组件用useMemo包裹,无论useMemo的依赖如何,组件都会执行。
import React from "react"
import { useMemo, useState } from "react"

const Demo: React.FC = () => {
  const [a, setA] = useState(1)
  const [b, setB] = useState(10)

  const value1 = useMemo(() => {
    // 假设这里面是一个很复杂的计算 例如一个层级很深的递归运算
    console.log('计算')
    return a * b
  }, [a, b])

  return (
    <div>
      <p>{value1}</p>
      <button onClick={() => { setA(pre => pre + 1) }}>setA</button>
      <button onClick={() => { setB(pre => pre + 1) }}>setB</button>
    </div>
  )
}

export default Demo

从useMemo实现上我们可以发现,在组件更新时,React对比了当前组件上保存的依赖数组和新的依赖数组,如果没有变化则直接返回上一次保存 的值,不会在执行useMemo传入的函数。

// 为什么是updateMemo?  React所有的Hooks,初始化都调用 mountHook方法,更新时都调用updateHook

function updateMemo<T>(
  nextCreate: () => T,
  deps: Array<mixed> | void | null,
): T {
  const hook = updateWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  const prevState = hook.memoizedState;
  if (prevState !== null) {
    // Assume these are defined. If they're not, areHookInputsEqual will warn.
    if (nextDeps !== null) {
      const prevDeps: Array<mixed> | null = prevState[1];
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        return prevState[0];
      }
    }
  }
  const nextValue = nextCreate();
  hook.memoizedState = [nextValue, nextDeps];
  return nextValue;
}

我们可以把useMemo当初一个计算属性来用,只有当我们的deps依赖发生改变时,才会重新进行计算,可以节约计算成本
虽然我们的函数组件也是一个函数,并且有返回值(JSX),但是useMemo不适合用来缓存组件,因为useMemo不会帮我们传递props给返回的值,useCallBack也一样,如果我们需要对一个组件实现shouldComponentUpdate这样的功能,我们可以使用React.memo

React.memo

这不是一个Hook, 却可以用来弥补React hooks相对于class组件缺失的地方。在class组件中,我们可以使用shouldComponentUpdate,通过返回boolean来控制组件是否需要渲染来做一些性能优化,在函数组件中,我们可以使用React.memo

React.memo在 React 16.6+之后 可以使用

const Demo = React.memo((props: { count: number }) => {
  console.log('子组件1 --- render')
  return (
    <div>
      子组件countA :  {props.count}
    </div>
  )
}, (preProps, nextProps) => {
  return nextProps.count === preProps.count
})

React.memo接受两个参数

  • component :使用memo的组件
  • propsAreEqual: 判断新的props和旧的props是否相等的函数,这个函数接受两个参数,第一个参数是旧的props,第二个参数是新的props,通过函数返回的boolean,来控制组件是否更新。

useCallback

useCallback的用法和useMemo基本一样,不同的地方在于,useMemo会将传入的函数执行,并保存函数执行后返回的值,而useCallback则直接将传入的函数保存, 不会将传入的函数执行

function updateCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
  const hook = updateWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  const prevState = hook.memoizedState;
  if (prevState !== null) {
    if (nextDeps !== null) {
      const prevDeps: Array<mixed> | null = prevState[1];
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        return prevState[0];
      }
    }
  }
  hook.memoizedState = [callback, nextDeps];
  return callback;
}

注意: 使用useCallBack返回回调函数的同时,回调函数中的闭包引用会一同被缓存,如果在回调函数中使用了变量,记得将变量设置为useCallback的deps依赖项,否则往往在执行useCallback返回的函数时,拿到的都是最开始的状态


4.功能性类

useRef

使用useRef之前,我们需要知道refs

什么是refs?

Refs 提供了一种方式,允许我们访问 DOM 节点或在 render 方法中创建的 React 元素

当我们遇到下面几种情况时:

  • 管理焦点,文本选择或媒体播放。
  • 触发强制动画。
  • 集成第三方 DOM 库。
  • 迫切的需要主动调用子组件的api

我们用React.createRef()创建一个ref,把这个ref传入class组件或HTML元素的props中

  • 当 ref 属性用于 HTML 元素时,ref对象接收底层 DOM 元素作为其 current 属性。
  • 当 ref 属性用于自定义 class 组件时,ref 对象接收组件的挂载实例作为其 current 属性。

React为ref对象的current属性赋值的行为发生在组件挂载时,componentDidMountcomponentDidUpdate之前。

import React, { RefObject } from "react";

class Demo extends React.Component {
  private inputRef = React.createRef<HTMLInputElement>()
  private childRef = React.createRef<Child>()

  render() {
    return (
      <>
        <input ref={this.inputRef} /> // ref使用在HTML元素
        <Child ref={this.childRef} /> // ref使用在class组件


        <button onClick={() => {
          this.inputRef.current && (this.inputRef.current.value = 'hello word')
        }}>设置原生input值</button>
        <button onClick={() => {
          this.childRef.current?.setValue('hello word')
        }}>设置自定义class组件值</button>
      </>
    )
  }
}

export default Demo

class Child extends React.Component<any, { value: string }> {
  constructor(props: any) {
    super(props)
    this.state = {
      value: ''
    }
  }
  // 标记为public,组件外才能使用ref拿到
  public setValue = (value: string) => {
    this.setState({
      value
    })
  }
  render() {
    return (
      <div>子组件 {this.state.value}</div>
    )
  }
}

我们也可以为ref属性传入一个函数,组件在挂载时,会调用这个回调函数,并且将组件实例传递出来,我们开可以自己保存这个class组件的实例

// 不通过createRef, 自己保存组件的实例
<input ref={(current)=>{ this.inputRef = current }} />

上面的使用方式,只适用于HTML元素和class组件,因为他们都有自己的实例,react可以将实例给到ref.current,但是函数组件没有实例, 在函数组件中,该如何使用和提供ref呢,?

函数组件使用ref
如果我们在函数组件中依然使用React.createRef,虽然基本功能可以实现,但是在组件更新时,createRef会重复创建,这不是react想要的

const Demo = ()=> {
    const inputRef = Rect.createRef() // 重复调用React.createRef()
    return (
        <input ref={inputRef} />
    )
}

所以react提供了useRef hook, 让我们可以在函数组件中优雅的使用ref

useRef

function useRef<T>(initialValue: T): MutableRefObject<T>;

interface MutableRefObject<T> {
    current: T;
}

在实现上,useRef应该是hooks中最简单的一个了

  • 在初始化时,创建ref对象,挂载到当前组件对应的hook的memoizedState
  • 在组件更新时,直接取当前组件对应hook的memoizedState返回
function updateRef<T>(initialValue: T): {|current: T|} {
  const hook = updateWorkInProgressHook();
  return hook.memoizedState;
}

Demo:

import React, { useRef } from "react"

const BaseDemo: React.FC = (props) => {
  const inputRef = useRef<HTMLInputElement | null>(null)

  return <div>
    <input ref={inputRef} />

    <button onClick={() => {
      inputRef.current && (inputRef.current.value = 'hello word')
    }}>设置值</button>

    <button onClick={() => {
      inputRef.current && (inputRef.current.value = '')
    }}>重置</button>

    <button onClick={() => {
      inputRef.current?.focus()
    }}>聚焦</button>
  </div>
}

export default BaseDemo

useRef用到函数组件上时,还有另外的一个用途:申明一个变量这个变量不随组件更新而初始化

class组件可以直接挂载到this上,一样可以实现,所以说这是ref在函数组件的另外一个用途

如果我们函数组件中,有一个变量会记录用户对某个按钮的操作次数,并且这个变量不在页面上展示(变量发生变化时不需要更新页面:

import { useRef, useState } from "react"

const Demo = () => {
  const [str, setStr] = useState<string>('')
  const count1 = useRef<number>(0)
  let count2: number = 0
  console.log(count1)
  console.log(count2)
  return (
    <div>
      <p>{str}</p>
      <button onClick={() => {
        setStr(pre => pre + ' hello')
        count1.current += 1
        count2 += 1
      }}>用户点击</button>
    </div>
  )
}

export default Demo

image.png

用户点击按钮,在程序中记录一次,界面刷新,组件重新render时,对count2重新定义,并赋予初始值0,所以count2不能记录用户点击次数。使用useRef可以保存一个值(示例中的count1),这个值不会随组件更新被重新初始化。

函数组件如何提供ref?(如何让父组件能拿到子组件的ref并主动调用子组件的方法)

React class组件,自带ref props, 我们不需要在class组件的props中定义ref,React会帮我们做这些事。
如何让我们自定义的函数组件也可以接受ref,并且能把组件内部的方法和属性通过ref传递给父组件使用呢?
我们需要用到另外一个hook useImperativeHandleforwardRef

forwardRef

forwardRef被称为ref转发,使用forwardRef包裹的函数组件,React让这个组件除了接受props外,自动注入ref作为函数组件的第二个参数

import { forwardRef, ForwardRefRenderFunction, useState } from "react"

const Demo: ForwardRefRenderFunction<IChildRefType, IChildProps> = (props, ref)=> {
  return (
    <div>
    </div>
  )
}

interface IChildRefType {}
interface IChildProps {}

export default forwardRef(Demo)

使用了forwardRef的函数组件,这个函数组件的类型变成了ForwardRefRenderFunction<T, P>。 第一个类型参数T是ref的类型定义,第二个参数p是组件的props定义

image.png 我们可以看到forward为生成的组件设置了类型为REACT_FORWARD_REF_TYPE,React在渲染时,会为这个类型的组件传递ref作为函数组件的第二个参数

上面的例子我们只是让函数组件可以接受ref,但是函数组件内部并没有对ref进行定义,为了能在函数组件中定义ref提供给外部使用,React hooks提供了一个useImperativeHandlehook

useImperativeHandle

useImperativeHandle 可以让函数组件在使用 ref 时自定义给ref的实例值。

function useImperativeHandle<T, R extends T>(ref: Ref<T>|undefined, init: () => R, deps?: DependencyList): void;
  • 第一个参数,是组件接受的ref
  • 第二个参数,是ref实例的初始化函数,这个函数的返回值,作为组件ref实例的值
  • 第三个参数,是deps依赖项,当依赖发生变化时,会更新ref实例

看下面这个例子:

import { forwardRef, ForwardRefRenderFunction, useImperativeHandle, useState } from "react"

const Child: ForwardRefRenderFunction<IChildRefType, IChildProsp> = (props, ref) => {
  const [n, setN] = useState<number>(0)

  useImperativeHandle(ref, () => {
    return {
      currentValue: n,
      reset: () => {
        setN(0)
      },
    }
  }, [n]) // 如果不添加依赖, 在getValue获取n的值时,将会是子组件初始化的时候的值

  return <div>
    <p>{n}</p>
    <button onClick={() => { setN(n=>n + 1) }}>setN</button>
  </div>
}

export interface IChildRefType {
  currentValue: number,
  reset: ()=> void
}

interface IChildProsp {
  data?: string
}

export default forwardRef(Child)

我们使用useImperativeHandle为这个组件的ref实例提供了一个属性和一个方法
现在我们的函数组件可以接受ref了,并且将需要暴露的方法和属性挂载到ref实例上,我们可以在父组件中通过ref调用子组件提供的实例方法

import React, {  useRef } from "react"
import Child1, { IChildRefType } from "./Child1"

const UseRefDemo: React.FC<IUseRefDemoProsp> = (props) => {
  const ref1 = useRef<IChildRefType>(null)

  return <div>
    <div style={{ padding: '20px', border: '1px solid #ccc' }}>
      <p>子组件</p>
      <Child1 ref={ref1} />
    </div>

    <button onClick={() => {
      ref1.current?.reset()
    }}>重置子组件</button>

      <button onClick={() => {
      const value = ref1.current?.currentValue
      alert(value)
      
    }}>获取子组件值值</button>
  </div>
}

interface IUseRefDemoProsp {}
export default UseRefDemo

useImperativeHandle将创建的ref实例挂载到当前组件的FiberNode的ref属性上

image.png


useDebugValue

useDebugValue是一个辅助调试hooks,它结合浏览器React调试工具插件React Developer Tools使用,可以在调试工具上对应组件节点上,看到使用useDebugValue提供的信息

image.png

如上图,元素重复的提示,来自于程序中使用useDebugValue对自定义hook的内部状态分析输出

image.png

useDebugValue主要用于自定义Hook的内部状态调试,useDebugValue中的代码在不打开React Developer Tools调试工具时不会执行,这是相比于console.log的优势,对用户来说是无感的。

不建议所有的自定义hooks都使用,只有在自定义hook内部状态不能直观的表现,并且当内部状态值为某个状态时需要给出提示时才使用它,因为就算不使用useDebugValue,在React调试工具中也能看到自定义hook的内部状态

或许它有更多的用途,当明确需要它时才使用它


总结

React hooks在定义时,会创建hook对象存储在FiberNodememoizedState属性上,memoizedState是一个链表结构,由next属性指向下一个hook,所以一个函数组件中的hooks是有序的,不能在条件语句或回调函数中创建hooks
当调用组件状态更新时,react在FiberNode上创建更新函数update,再将FiberNode交给调度器调度执行(diff计算更新内容),将执行的结果放在对应的FiberNode的updateQueue上,再将这些需要更新的FiberNode通过链表形式串联起来存放到FiberRootfinishedWork上,当所有的update调度执行完成后协,最后同步调用commitRoot方法,执行finishedWork上的内更新内容更新页面,所以多次同步更新state,最终只会触发一次更新