React Hooks
在学习React副作用Hook之前,我们需要知道
在React 16 之后,采用了Fiber架构,从此以后,React功能被分为了三个部分:
Scheduler(调度器)—— 调度任务的优先级,高优任务优先进入ReconcilerFiber Reconciler(协调器)—— 负责找出变化的组件Renderer(渲染器)—— 负责将变化的组件渲染到页面上
为什么改变架构呢?
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 特性。
hook汉译为“钩子”,我们可以理解为 React Hooks 是React为了能够在函数组件中使用React生命周期钩子、副作用钩子、状态管理 所提供的一种语法
使用环境
React 16.8 +,同时需要升级 React DOM100%向后兼容,使用Hooks不会造成任何破坏性的改动,不影响继续使用class组件,可以选择使用或不使用hooks无任何破坏性的改动,只要是函数组件,就可以使用HooKs(原来的类组件不需要重构),并且 没有计划从 React 中移除 class
Hooks主要想解决的问题(React为什么推出Hooks)
1、复用状态逻辑
使用hooks之前,我们使用Class组件,我们将状态保存在 this.state, 这些状态只能在class组件内部使用,或者通过props传递给子组件,很难讲逻辑抽离并在其他地方复用。 组件之间要 复用状态逻辑,需要用到 Context、render 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中并未暴露给使用者):
我们可以将这一些hooks做归类:
状态类:
useStateuseReduceruseContext
副作用类:
useEffectuseLayoutEffect
性能优化类
useMemouseCallback
功能性类:
useRefuseImperativeHandleuseDebugValue
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
- 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
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(处理函数),,除了组件初始化时 useState和useReducer有不同处理逻辑,在更新的时候,useState是直接调用的useReducer,并自动传入了参数
react在执行hooks时,会根据组件是否是第一次挂载,运行不同的方法
可以发现,在组件更新时,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
可以在Context对象中看到_currentValue和_currentValue2
2、副作用类 Hook
什么是副作用Hook?
我们知道药物的主要作用是治病,而有些药服用了却让病人昏昏欲睡,这是在使用这些药物发挥主要作用的同时,产生了额外的作用效果,这就是药物的副作用;这些副作用是这些药物天生的属性
在React中也一样,我们开发的一个React函数组件被执行后,主要作用就是渲染页面,而在渲染页面之外产生的作用效果也被React称之为副作用 跟药物天生的副作用效果不同的是,React给我们提供了定义这些副作用的方法,我们可以为我们的组件赋予不同的副作用表现,并且可以控制这些副作用用“发作”的时机。
useEffect
useEffect用来定义函数组件在渲染(render)后产生的副作用
函数组件没有生命周期的概念,只有副作用,通过不同的副作用触发条件,可以模拟class组件的大部分生命周期
// 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阶段
3、性能优化类 Hook
不要滥用性能优化Hooks,当我们明确需要使用时才使用它们,因为使用它们本身也是一种性能消耗
useMemo和useCallback
useMemo和useCallback功能一样,用做某段逻辑代码是否重复执行的优化
在使用传参上都跟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属性赋值的行为发生在组件挂载时,componentDidMount和componentDidUpdate之前。
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
用户点击按钮,在程序中记录一次,界面刷新,组件重新render时,对count2重新定义,并赋予初始值0,所以count2不能记录用户点击次数。使用useRef可以保存一个值(示例中的count1),这个值不会随组件更新被重新初始化。
函数组件如何提供ref?(如何让父组件能拿到子组件的ref并主动调用子组件的方法)
React class组件,自带ref props, 我们不需要在class组件的props中定义ref,React会帮我们做这些事。
如何让我们自定义的函数组件也可以接受ref,并且能把组件内部的方法和属性通过ref传递给父组件使用呢?
我们需要用到另外一个hook useImperativeHandle和forwardRef
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定义
我们可以看到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属性上
useDebugValue
useDebugValue是一个辅助调试hooks,它结合浏览器React调试工具插件React Developer Tools使用,可以在调试工具上对应组件节点上,看到使用useDebugValue提供的信息
如上图,元素重复的提示,来自于程序中使用useDebugValue对自定义hook的内部状态分析输出
useDebugValue主要用于自定义Hook的内部状态调试,useDebugValue中的代码在不打开React Developer Tools调试工具时不会执行,这是相比于console.log的优势,对用户来说是无感的。
不建议所有的自定义hooks都使用,只有在自定义hook内部状态不能直观的表现,并且当内部状态值为某个状态时需要给出提示时才使用它,因为就算不使用
useDebugValue,在React调试工具中也能看到自定义hook的内部状态
或许它有更多的用途,当明确需要它时才使用它
总结
React hooks在定义时,会创建hook对象存储在FiberNode的memoizedState属性上,memoizedState是一个链表结构,由next属性指向下一个hook,所以一个函数组件中的hooks是有序的,不能在条件语句或回调函数中创建hooks。
当调用组件状态更新时,react在FiberNode上创建更新函数update,再将FiberNode交给调度器调度执行(diff计算更新内容),将执行的结果放在对应的FiberNode的updateQueue上,再将这些需要更新的FiberNode通过链表形式串联起来存放到FiberRoot的finishedWork上,当所有的update调度执行完成后协,最后同步调用commitRoot方法,执行finishedWork上的内更新内容更新页面,所以多次同步更新state,最终只会触发一次更新