【React Hooks原理 - useRef】

概述

在Function Component项目中当我们需要操作dom的时候,第一时间想到的就是使用useRef这个Hook来绑定dom。但是这个仅仅是使用这个Hook而已,为了更好的学习React Hooks内部实现原理,知其所以然。所以本文根据源码从useRef的基础使用场景一步一步到内部实现来对其进行介绍。

基本使用

在React中useRef是这样定义的:useRef保存一个可变的持久化引用,重新渲染时不会重值,更新值也不会渲染页面

javascript 复制代码
export function useRef<T>(initialValue: T): { current: T } {
  const dispatcher = resolveDispatcher();
  return dispatcher.useRef(initialValue);
}

由代码能看出useRef接收任意类型的值,包含普通值、函数、dom,然后经过dispather进行派发处理,返回一个包含current属性的对象引用,该对象和普通Js对象一致,更新不收React约束。

一般在项目中useRef常用的有两个使用场景:

  • 通过useRef保持持久化的值,且不需要重新渲染
  • 通过useRef绑定dom,以便直接进行dom操作

比如在项目中常用的定时器,我们都会在组件销毁时通过clear函数进行定时器的清除避免内存泄露等问题,这时候就可以通过useRef来绑定timerId

javascript 复制代码
import { useRef } from 'react';

let timerId = useRef(null);

useEffect(() => {
	timerId.current = setInterval(() => {
		console.log('setInterval');
	}, 1000);
	return () => {
		clearInterval(timerId.current);
	}
}, [])

export default function Counter() {
  return <></>
}

当我们需要进行dom操作时,比如获取焦点、自动滚动等,就可以通过useRef来绑定dom进行操作

javascript 复制代码
import { useRef } from 'react';

let inputRef = useRef(null);

useEffect(() => {
	// 在组件挂载后聚焦输入框
    inputRef.current.focus();
}, [])

export default function Counter() {
  return <input ref={inputRef} type='text' />
}

源码解析

由于这里的mount、update逻辑很简单,并当useRef传递值/函数和传递dom时的处理是不一样的,所以我们以此来分开介绍。

传递普通值时

当传递普通值时(包含任意类型值、函数),主要执行mountRef、updateRef两个函数。在mount挂载时创建一个包含current属性的对象,然后在更新时返回相同的引用memoizedState保存的,所以这里就在一起写了。

javascript 复制代码
function mountRef<T>(initialValue: T): { current: T } {
  // 创建hook链表
  const hook = mountWorkInProgressHook();
  // ref初始化
  const ref = { current: initialValue };
  hook.memoizedState = ref;
  // 返回ref
  return ref;
}

function updateRef<T>(initialValue: T): { current: T } {
    // 复用hook
  const hook = updateWorkInProgressHook();
  // 返回相同引用
  return hook.memoizedState;
}

从源码能看出,useRef接收一个初始化参数,可以为值/返回值的函数,然后在mountRef中创建了一个包含current的对象,在updateRef中仍然返回的该对象引用。

如果初始值是函数,因为React内部不会做判断,直接将初始值赋予current,如何是函数,则需要手动显式调用

由于不管在mount挂载时,还是在update更新时都是返回的对象引用,以此来保持持久化,当我们通过ref.current修改值时本质修改的是同一个引用对象,所以也不会触发重新渲染(object.is对比一直都是true)。

传递DOM时

当传递DOM时,在mount、update阶段也和传值一样,不会做任何处理会返回相应的对象引用,但是如果传递的是DOM时,在Reconciler协调器中通过React.createElement将JSX转换为React元素后进行fiber构造,在构造完成生产fiber树之后会进入到commit阶段,在该阶段会遍历节点对副作用和ref进行处理,其中在layout阶段会判断当前节点类型(tag)如何是dom(tag === HostComponent)时,如果该dom有ref,则会对ref进行处理commitAttachRef函数

在commit阶段,即renderer阶段,针对dom的不同状态和处理分为了三个阶段: Before Mutation、Mutation、Layout。有兴趣的可以查看这篇文章【React源码 - Fiber架构之Renderer

以下commitAttachRef代码(省略了部分代码):

javascript 复制代码
function commitAttachRef(finishedWork: Fiber) {
  // 获取节点的ref属性
  const ref = finishedWork.ref;
  if (ref !== null) {
    // 获取dom实例,fiber.stateNode就是绑定的dom,在completeWork中会创建dom然后绑定到fiber.stateNode上
    const instance = finishedWork.stateNode;
    let instanceToUse;
    switch (finishedWork.tag) {
      case HostHoistable:
      case HostSingleton:
      case HostComponent:
        // 获取dom实例
        instanceToUse = getPublicInstance(instance);
        break;
      default:
        instanceToUse = instance;
    }
    //
    if (typeof ref === "function") {
        // 将dom实例回传给传递的ref函数
      finishedWork.refCleanup = ref(instanceToUse);
    } else {
        // 普通对象赋值到current
      ref.current = instanceToUse;
    }
  }
}

从代码能看出该函数主要就是获取ref绑定的dom实例,然后根据传入ref的不同进行处理,如果是函数则将dom实例传递给函数由开发者显式调用,否则则绑定到current属性上进行返回。

传递函数,显式处理ref的demo:

javascript 复制代码
import React, { useEffect, useRef } from 'react';

function App() {
  const divRef = useRef(null);

  useEffect(() => {
    if (divRef.current) {
      console.log('Element mounted:', divRef.current);
    }
    return () => {
      console.log('Element unmounted:', divRef.current);
    };
  }, []);

  return <div ref={divRef}>Hello, World!</div>;
}

export default App;

总结

基于以上了解,我们知道了useRef的基础使用和场景以及背后的代码处理,简要总结一下就是:useRef用于持久化引用,返回普通Js引用,修改其值不会导致组件重新渲染。当传递普通值时,不会进行特殊处理,只是返回相同的对象引用。当绑定dom时,在mount、update阶段初始化对象,然后在commit阶段进行ref处理,函数显式处理则会将dom实例作为参数回传,普通值则会绑定到ref.current中

相关推荐
GISer_Jing12 分钟前
HTML前端面试基础(一)
前端·html
鱼仰泳32 分钟前
JS+CSS案例:用CSS+JS做漂亮的拟真时钟
javascript·css·tensorflow
wfbl1234561 小时前
用Python打造精彩动画与视频,7.2 使用OpenCV增强视觉效果
前端·python·音视频
GISer_Jing1 小时前
Javascript常见算法(二)【学习】
javascript·算法·动态规划
营赢盈英4 小时前
Azure openai connection with javascript
javascript·python·ai·openai·azure
镜中的女孩-potato5 小时前
关于uniapp的vue2.x版本的路由守卫拦截方案
前端·vue.js·uni-app
恰小面包5 小时前
uniapp获取地理位置的API
前端·小程序·uni-app
天上掉下来个程小白6 小时前
HTML-04.新浪新闻-标题-超链接
前端·html
杯倾川·苶陽8 小时前
树状菜单分析
javascript
stonefisher9 小时前
推荐Nodejs下高效存储树到数据库工具库-FlexTree
javascript·数据库·nodejs·tree·prisma