React ref 从原理到应用

开发 前端
Fiber是React更新时的最小单元,是一种包含指针的数据结构,从数据结构上看Fiber架构 ≈ 树 + 链表。Fiber单元是从 jsx createElement之后根据ReactElement生成的,相比 ReactElement,Fiber单元具备动态工作能力。

[[398682]]

提到 ref或者 refs 如果你用过React 16以前的版本 第一印象都是用来访问DOM或者修改组件实例的,

正如官网所介绍的这样:

 

然后到了React 16.3出现的 createRef 以及16.8 hooks中的 useRef出现时,发现这里的ref好像不仅仅只有之前的绑定到DOM/组件实例的 作用?本文将带你逐一梳理这些知识点,并尝试分析相关源码。

前置知识

这部分知识点不是本文重点,每个点展开都非常庞大,了方便本文理解先在这里简单提及。

Fiber架构

Fiber是React更新时的最小单元,是一种包含指针的数据结构,从数据结构上看Fiber架构 ≈ 树 + 链表。

Fiber单元是从 jsx createElement之后根据ReactElement生成的,相比 ReactElement,Fiber单元具备动态工作能力。

React 的工作流程

使用chrome perfomance录制一个react应用渲染看函数调用栈会看到下面这张图

这三块内容分别代表: 1.生成react root节点 2.reconciler 协调生成需要更新的子节点 3.将节点更新commit 到视图

Hooks基础知识

在函数组件中每执行一次use开头的hook函数都会生成一个hook对象。

type Hook = { 
  memoizedState: any,   // 上次更新之后的最终状态值 
  queue: UpdateQueue, //更新队列 
  next, // 下一个 hook 对象 
}; 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

其中memoizedState会保存该hook上次更新之后的最终状态,比如当我们使用一次useState之后就会在memoizedState中保存初始值。

React 中大部分 hook 分为两个阶段:第一次初始化时`mount`阶段和更新`update`时阶段

hooks函数的执行分两个阶段 mount和 update,比如 useState只会在初始化时执行一次,下文中将提到的

useImperativeHandle 和 useRef也包括在内。

调试源码

本文已梳理摘取了源码相关的函数,但你如果配合源码调试一起食用效果会更加。

本文基于React v17.0.2。

拉取React代码并安装依赖

将react,scheduler以及react-dom打包为commonjs

yarn build react/index,react-dom/index,scheduler --type NODE

3.进入build/node_modules/react/cjs 执行yarn link 同理 react-dom

4.在 build/node_modules/react/cjs/react.development.js中加入link标记console以确保检查link状态

5.使用create-react-app创建一个测试应用 并link react,react-dom

ref prop

组件上的ref属性是一个保留属性,你不能把ref当成一个普通的prop属性在一个组件中获取,比如:

const Parent = () => { 
    return <Child ref={{test:1}}> 

const Child = (props) => { 
  console.log(props); 
  // 这里获取不到ref属性 
    return <div></div> 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.

 这个ref去哪里了呢, React本身又对它做了什么呢?

我们知道React的解析是从createElement开始的,找到了下面创建ReactElement的地方,确实有对ref保留属性的处理。

export function createElement(type, config, children) { 
let propName; 
  // Reserved names are extracted 
  const props = {}; 
  let ref = null
  if (config != null) { 
    if (hasValidRef(config)) { 
      ref = config.ref; 
    } 
    for (propName in config) { 
      if ( 
        hasOwnProperty.call(config, propName) && 
        !RESERVED_PROPS.hasOwnProperty(propName) 
      ) { 
        props[propName] = config[propName]; 
      } 
    } 
  } 
  return ReactElement( 
    type, 
    key
    ref, 
    props, 
    ... 
  ); 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.

从createElement开始就已经创建了对ref属性的引用。

createElement之后我们需要构建Fiber工作树,接下来主要讲对ref相关的处理。

React对于不同的组件有不通的处理

先主要关注 FunctionComponent/ClassComponent/HostComponent(原生html标签)

FunctionComponent

function updateFunctionComponent(current, workInProgress, Component, nextProps, renderLanes) { 
      try { 
        nextChildren = renderWithHooks(current, workInProgress, Component, nextProps, context, renderLanes); 
      } finally { 
        reenableLogs(); 
      } 
      reconcileChildren(current, workInProgress, nextChildren, renderLanes); 
      return workInProgress.child; 

functin renderWithHooks(current, workInProgress, Component, props, secondArg, nextRenderLanes){ 
            children = Component(props, secondArg); // 这里的Component就是指我们的函数组件 
                return children; 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.

我们可以看到函数组件在渲染的时候就是直接执行。

Class组件和原生标签的ref prop

ClassComponent

function updateClassComponent(current, workInProgress, Component, nextProps, renderLanes) { 
  ... 
  { 
    ... 
    constructClassInstance(workInProgress, Component, nextProps); 
        .... 
  } 
  var nextUnitOfWork = finishClassComponent(current, workInProgress, Component, shouldUpdate, hasContext, renderLanes); 
    ... 
  return nextUnitOfWork; 

function constructClassInstance(workInProgress, ctor, props) { 
    .... 
  var instance = new ctor(props, context); 
  // 把instance实例挂载到workInProgress stateNode属性上 
  adoptClassInstance(workInProgress, instance); 
    ..... 
  return instance; 

function finishClassComponent(current, workInProgress, Component, shouldUpdate, hasContext, renderLanes) { 
  // 标记是否有ref更新 
  markRef(current, workInProgress); 

function markRef(current, workInProgress) { 
  var ref = workInProgress.ref; 
  if (current === null && ref !== null || current !== null && current.ref !== ref) { 
    // Schedule a Ref effect 
    workInProgress.flags |= Ref; 
  } 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.

ClassComponent则是通过构造函数生成实例并标记了ref属性。

回顾一下之前提到的React工作流程,既然是要将组件实例或者真实DOM赋值给ref那肯定不能在一开始就处理这个ref,而是根据标记到commit阶段再给ref赋值。

function commitLayoutEffectOnFiber(finishedRoot, current, finishedWork, committedLanes) { 
    .... 
  { 
    if (finishedWork.flags & Ref) { 
      commitAttachRef(finishedWork); 
    } 
  } 
  .... 

function commitAttachRef(finishedWork) { 
  var ref = finishedWork.ref; 
  if (ref !== null) { 
    var instance = finishedWork.stateNode; 
    var instanceToUse; 
    switch (finishedWork.tag) { 
      case HostComponent: 
        // getPublicInstance 这里调用了DOM API 返回了DOM对象 
        instanceToUse = getPublicInstance(instance); 
        break; 
      default
        instanceToUse = instance; 
    }  
    // 对函数回调形式设置ref的处理 
    if (typeof ref === 'function') { 
      { 
        ref(instanceToUse); 
      } 
    } else { 
      ref.current = instanceToUse; 
    } 
  } 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.

在commit阶段,如果是原生标签则将真实DOM赋值给ref对象的current属性, 如果是class componnet 则是组件instance。

函数组件的ref prop

如果你对function组件未做处理直接加上ref,react会直接忽略并在开发环境给出警告

函数组件没有实例可以赋值给ref对象,而且组件上的ref prop会被当作保留属性无法在组件中获取,那该怎么办呢?

forwardRef

React提供了一个forwardRef函数 来处理函数组件的 ref prop,用起来就像下面这个示例:

const Parent = () => { 
    const childRef = useRef(null
  return <Child ref={childRef}/> 

const Child = forWardRef((props,ref) => { 
    return <div>Child</div> 
}} 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.

 这个方法的源码主体也非常简单,返回了一个新的elementType对象,这个对象的render属性包含了原本的这个函数组件,而$$typeof则标记了这个特殊组件类型。

function forwardRef(render) { 
  .... 
  var elementType = { 
    $$typeof: REACT_FORWARD_REF_TYPE, 
    render: render 
  } 
  .... 
  return elementType; 
 } 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.

那么React对forwardRef这个特殊的组件是怎么处理的呢

function beginWork(current, workInProgress, renderLanes) { 
    ... 
  switch (workInProgress.tag) { 
    case FunctionComponent: 
      { 
       ... 
        return updateFunctionComponent(current, workInProgress, _Component, resolvedProps, renderLanes); 
      } 
    case ClassComponent: 
      { 
                .... 
        return updateClassComponent(current, workInProgress, _Component2, _resolvedProps, renderLanes); 
      } 
    case HostComponent: 
      return updateHostComponent(current, workInProgress, renderLanes); 
    case ForwardRef: 
      { 
                .... 
        // 第三个参数type就是forwardRef创建的elementType 
        return updateForwardRef(current, workInProgress, type, _resolvedProps2, renderLanes); 
      } 

   
function updateForwardRef(current, workInProgress, Component, nextProps, renderLanes) { 
    .... 
  var render = Component.render; 
  var ref = workInProgress.ref; // The rest is a fork of updateFunctionComponent 
  var nextChildren; 
  { 
        ... 
    //  将ref引用传入renderWithHooks 
    nextChildren = renderWithHooks(current, workInProgress, render, nextProps, ref, renderLanes); 
    ... 
  } 
  workInProgress.flags |= PerformedWork; 
  reconcileChildren(current, workInProgress, nextChildren, renderLanes); 
  return workInProgress.child; 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.

可以看到和上面 FunctionComponent的主要区别仅仅是把ref保留属性当成普通属性传入 renderWithHooks方法!

那么又有一个问题出现了,如果只是传了一个ref引用,而没有像Class组件那样可以attach的实例,岂不是没有办法操作子函数组件的行为?

用上面的例子验证一下

const Parent = () => {   
  const childRef = useRef(null
  useEffect(()=>{ 
    console.log(childref) // { current:null } 
  }) 
  return <Child ref={childRef}/> 

const Child = forwardRef((props,ref) => { 
    return <div>Child</div> 
}} 
                          
 const Parent = () => {  
  const childRef = useRef(null
  useEffect(()=>{ 
    console.log(childref) // { current: div } 
  }) 
  return <Child ref={childRef}/> 

const Child = forwardRef((props,ref) => { 
    return <div ref={ref}>Child</div> 
}} 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.

 结合输出可以看出如果单独使用forwardRef仅仅只能转发ref属性。如果ref最终没有绑定到一个ClassCompnent或者原生DOM上那么这个ref将不会改变。

假设一个业务场景,你封装了一个表单组件,想对外暴露一些接口比如说提交的action以及校验等操作,这样应该如何处理呢?

useImperativeHandle

react为我们提供了这个hook来帮助函数组件向外部暴露属性

先看下效果

const Parent = () => {   
  const childRef = useRef(null
  useEffect(()=>{ 
    chilRef.current.sayName();// child 
  }) 
  return <Child ref={childRef}/> 

const Child = forwardRef((props,ref) => { 
  useImperativeHandle(ref,()=>({ 
    sayName:()=>{ 
        console.log('child'
    } 
  })) 
    return <div>Child</div> 
}} 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.

 看一下该hook的源码部分(以hook mount阶段为例):

useImperativeHandle: function (ref, create, deps) { 
      currentHookNameInDev = 'useImperativeHandle'
      mountHookTypesDev(); 
      checkDepsAreArrayDev(deps); 
      return mountImperativeHandle(ref, create, deps); 
 } 
function mountImperativeHandle(ref, create, deps) { 
  { 
    if (typeof create !== 'function') { 
      error('Expected useImperativeHandle() second argument to be a function ' + 'that creates a handle. Instead received: %s.'create !== null ? typeof create : 'null'); 
    } 
  } // TODO: If deps are provided, should we skip comparing the ref itself? 
  var effectDeps = deps !== null && deps !== undefined ? deps.concat([ref]) : null
  var fiberFlags = Update
  return mountEffectImpl(fiberFlags, Layout, imperativeHandleEffect.bind(nullcreate, ref), effectDeps); 

function imperativeHandleEffect(create, ref) { 
  if (typeof ref === 'function') { 
    var refCallback = ref; 
    var _inst = create(); 
    refCallback(_inst); 
    return function () { 
      refCallback(null); 
    }; 
  } else if (ref !== null && ref !== undefined) { 
    var refObject = ref; 
    { 
      if (!refObject.hasOwnProperty('current')) { 
        error('Expected useImperativeHandle() first argument to either be a ' + 'ref callback or React.createRef() object. Instead received: %s.''an object with keys {' + Object.keys(refObject).join(', ') + '}'); 
      } 
    } 
        // 这里执行了传给hook的第二个参数 
    var _inst2 = create(); 
    refObject.current = _inst2; 
    return function () { 
      refObject.current = null
    }; 
  } 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.

其实就是将我们需要暴露的对象及传给useImperativeHandle的第二个函数参数执行结果赋值给了ref的current对象。

同一份引用

到此为止我们大致梳理了组件上ref prop 的工作流程,以及如何在函数组件中使用ref prop,貌似比想象中简单。

上面的过程我们注意到从createElement再到构建WorkInProgess Fiber树到最后commit的过程,ref似乎是一直在被传递。

中间过程的代码过于庞大复杂,但是我们可以通过一个简单的测试来验证一下。

const isEqualRefDemo = () => { 
    const isEqualRef = useRef(1) 
  return <input key="test" ref={isEqualRef}> 

  • 1.
  • 2.
  • 3.
  • 4.

对于 class component 和 原生标签来说 就是 createElement 到 commitAttachRef之前:

在createElement里将ref挂载给window对象,然后在commitAttachRef里判断一下这两次的ref是否全等。

对于函数组件来说就是 createElement 到 hook执行 imperativeHandleEffect 之前:

const Parent = () => {   
  const childRef = useRef(1) 
  useEffect(()=>{ 
    chilRef.current.sayName();// child 
  }) 
  return <Child ref={childRef}/> 

const Child = forwardRef((props,ref) => { 
  useImperativeHandle(ref,()=>({ 
    sayName:()=>{ 
        console.log('child'
    } 
  })) 
    return <div>Child</div> 
}} 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.

 

从createElement添加ref到React整个渲染过程的末尾(commit阶段)被赋值前,这个ref都是同一份引用。

这也正如 ref单词的本意 reference引用一样。

小节总结

1.ref出现在组件上时是一个保留属性

2.ref在组件存在的生命周期内维护了同一个引用(可变对象 MutableObject)

3.当ref挂载的对象是原生html标签时会ref对象的current属性会被赋值为真实DOM 而如果是React组件会被赋值为React"组件实例"

4.ref挂载都在commit阶段处理

创建ref的方式

ref prop相当于在组件上挖了一个“坑” 来承接 ref对象,但是这样还不够我们还需要先创建ref对象

字符串ref & callback ref

这两种创建ref的方式不再赘述,官网以及社区优秀文章可供参考。

https://zh-hans.reactjs.org/docs/refs-and-the-dom.html

https://blog.logrocket.com/how-to-use-react-createref-ea014ad09dba/

createRef & useRef

createRef

16.3引入了createRef这个api

createRef的源码就是一个闭包,对外暴露了 一个具有 current属性的对象。

我们一般会这样在class component中使用createRef

class CreateRefComponent extends React.Component { 
  constructor(props) { 
    super(props); 
    this.myRef = React.createRef() 
  } 
  componentDidMount() { 
    this.myRef.current.focus() 
    console.log(this.myRef.current
    // dom input 
  } 
  render() { 
    return <input ref={this.myRef} /> 
  } 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.

为什么不能在函数组件中使用createRef

结合第一节的内容以及 createRef的源码,我们发现,这不过就是在类组件内部挂载了一个可变对象。因为类组件构造函数不会被反复执行,因此这个createRef自然保持同一份引用。但是到了函数组件就不一样了,每一次组件更新, 因为没有特殊处理createRef会被反复重新创建执行,因此在函数组件中使用createRef将不能达到只有同一份引用的效果。

const CreateRefInFC = () => { 
  const valRef = React.createRef();  // 如果在函数组件中使用createRef 在这个例子中点击后ref就会被重新创建因此将始终显示为null 
  const [, update] = React.useState(); 
  return <div> 
    value: {valRef.current
    <button onClick={() => { 
      valRef.current = 80; 
      update({}); 
    }}>+ 
    </button> 
  </div> 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.

 useRef

React 16.8中出现了hooks,使得我们可以在函数组件中定义状态,同时也带来了 useRef

再来看moutRef和updateRef所做的事:

function mountRef(initialValue) { 
  var hook = mountWorkInProgressHook(); 
  { 
    var _ref2 = { 
      current: initialValue 
    }; 
    hook.memoizedState = _ref2; 
    return _ref2; 
  } 

function updateRef(initialValue) { 
  var hook = updateWorkInProgressHook(); 
  return hook.memoizedState; 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.

借助hook数据结构,第一次useRef时将创建的值保存在memoizedState中,之后每次更新阶段则直接返回。

这样在函数组件更新时重复执行useRef仍返回同一份引用。

因此实际上和 createRef一样本质上只是创建了一个 Mutable Object,只是因为渲染方式的不同,在函数组件中做了一些处理。而挂载和卸载的行为全部交由组件本身来维护。

被扩展的ref

从 createRef开始我们可以看到,ref对象的消费不再和DOM以及组件属性所绑定了,这意味着你可以在任何地方消费他们,这也回答了本文一开始的那个问题。

useRef的应用

解决闭包问题

由于函数组件每次执行形成的闭包,下面这段代码会始终打印1

export const ClosureDemo =  () => { 
    const [ count,setCount ] = useState(0); 
    useEffect(()=> { 
        const interval = setInterval(()=>{ 
          setCount(count+1) 
        }, 1000) 
        return () => clearInterval(interval) 
      }, []) 
    // count显示始终是1 
    return <div>{ count }</div> 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.

 将 count 作为依赖传入useEffect可以解决上面这个问题

export const ClosureDemo =  () => { 
    const [ count,setCount ] = useState(0); 
    useEffect(()=> { 
        const interval = setInterval(()=>{ 
          setCount(count+1) 
        }, 1000) 
        return () => clearInterval(interval) 
      }, [count]) 
    return <div>{ count }</div> 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.

 但是这样定时器也会随着count值的更新而被不断创建,一方面会带来性能问题(这个例子中没有那么明显),更重要的一个方面是它不符合我们的开发语义,因为很明显我们希望定时器本身是不变的。

另外一个方式也可以处理这个问题

export const ClosureDemo =  () => { 
    const [ count,setCount ] = useState(0); 
    useEffect(()=> { 
        const interval = setInterval(()=>{ 
          setCount(count=> count + 1) // 使用setSate函数式更新可以确保每次都取到新的值 
        }, 1000) 
        return () => clearInterval(interval) 
      }, []) 
    return <div>{ count }</div> 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.

 这样做确实可以处理闭包带来的影响,但是仅限于需要使用setState的场景,对数据的修改和触发setState是需要绑定的,这可能会造成不必要的刷新。

使用useRef创建引用

export const ClosureDemo =  () => { 
    const [ count,setCount ] = useState(0); 
    const countRef = useRef(0); 
    countRef.current = count 
    useEffect(()=> { 
        const interval = setInterval(()=>{ 
          // 这里将更新count的逻辑和触发更新的逻辑解耦了 
          if(countRef.current < 5){ 
            countRef.current++ 
          } else { 
            setCount(countRef.current
          } 
        }, 1000) 
        return () => clearInterval(interval) 
      }, []) 
    return <div>{ count }</div> 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.

 封装自定义hooks

useCreation

通过factory函数来避免类似于 useRef(new Construcotr)中构造函数的重复执行

import { useRef } from 'react'
export default function useCreation<T>(factory: () => T, deps: any[]) { 
  const { current } = useRef({ 
    deps, 
    obj: undefined as undefined | T, 
    initialized: false
  }); 
  if (current.initialized === false || !depsAreSame(current.deps, deps)) { 
    current.deps = deps; 
    current.obj = factory(); 
    current.initialized = true
  } 
  return current.obj as T; 

function depsAreSame(oldDeps: any[], deps: any[]): boolean { 
  if (oldDeps === deps) return true
  for (const i in oldDeps) { 
    if (oldDeps[i] !== deps[i]) return false
  } 
  return true

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.

usePrevious

通过创建两个ref来保存前一次的state

import { useRef } from 'react'
export type compareFunction<T> = (prev: T | undefined, next: T) => boolean; 
function usePrevious<T>(state: T, compare?: compareFunction<T>): T | undefined { 
  const prevRef = useRef<T>(); 
  const curRef = useRef<T>(); 
  const needUpdate = typeof compare === 'function' ? compare(curRef.current, state) : true
  if (needUpdate) { 
    prevRef.current = curRef.current
    curRef.current = state; 
  } 
  return prevRef.current

export default usePrevious; 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.

useClickAway

自定义的元素失焦响应hook

import { useEffect, useRef } from 'react'
export type BasicTarget<T = HTMLElement> = 
  | (() => T | null
  | T 
  | null 
  | MutableRefObject<T | null | undefined>; 
   
 export function getTargetElement( 
  target?: BasicTarget<TargetElement>, 
  defaultElement?: TargetElement, 
): TargetElement | undefined | null { 
  if (!target) { 
    return defaultElement; 
  } 
  let targetElement: TargetElement | undefined | null
  if (typeof target === 'function') { 
    targetElement = target(); 
  } else if ('current' in target) { 
    targetElement = target.current
  } else { 
    targetElement = target; 
  } 
  return targetElement; 

// 鼠标点击事件,click 不会监听右键 
const defaultEvent = 'click'
type EventType = MouseEvent | TouchEvent; 
export default function useClickAway( 
  onClickAway: (event: EventType) => void, 
  target: BasicTarget | BasicTarget[], 
  eventName: string = defaultEvent, 
) { 
  // 使用useRef保存回调函数 
  const onClickAwayRef = useRef(onClickAway); 
  onClickAwayRef.current = onClickAway; 
  useEffect(() => { 
    const handler = (event: any) => { 
      const targets = Array.isArray(target) ? target : [target]; 
      if ( 
        targets.some((targetItem) => { 
          const targetElement = getTargetElement(targetItem) as HTMLElement; 
          return !targetElement || targetElement?.contains(event.target); 
        }) 
) { 
        return
      } 
      onClickAwayRef.current(event); 
    }; 
    document.addEventListener(eventName, handler); 
    return () => { 
      document.removeEventListener(eventName, handler); 
    }; 
  }, [target, eventName]); 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.

以上自定义hooks均出自ahooks

还有许多好用的自定义hook以及仓库比如react-use都基于useRef自定义了很多好用的hook。

参考资料

  • React Fiber https://juejin.cn/post/6844903975112671239#heading-10
  • React 官网ref使用 https://zh-hans.reactjs.org/docs/refs-and-the-dom.html#gatsby-focus-wrapper
  • React 前生今世 https://zhuanlan.zhihu.com/p/40462264
  • React ref源码分析 https://blog.csdn.net/qq_32281471/article/details/98473846

 

责任编辑:姜华 来源: Eval Studio
相关推荐

2018-05-17 15:18:48

Logistic回归算法机器学习

2024-07-07 21:49:22

2010-06-29 14:20:52

2024-03-27 10:14:48

2020-04-28 22:12:30

Nginx正向代理反向代理

2022-02-28 10:05:12

组件化架构设计从原组件化模块化

2023-08-03 08:03:05

2019-11-23 17:27:54

IO开源

2023-06-15 10:53:57

2023-02-07 08:55:04

进程栈内存底层

2011-06-22 16:13:29

2011-06-22 16:35:59

2017-06-16 16:58:54

机器学习神经形态架构

2025-03-03 00:00:00

Chrome工具前端

2017-07-06 11:34:17

神经形态计算人工智能突触

2022-06-15 22:33:07

React逃生舱

2022-02-14 21:17:21

RPC框架协议

2024-12-17 08:04:04

2010-07-30 12:19:04

无线路由连接局域网

2023-05-15 08:32:45

点赞
收藏

51CTO技术栈公众号