Interpretation of React's source code and principles (thirteen): Hooks Interpretation 2 useRef

Written at the beginning of the column (stack armor)

  1. The author is not an expert in front-end technology, but just a novice in front-end technology who likes to learn new things. He wants to learn source code just to cope with the rapidly declining front-end market and the need to find a job. This column is the author's own thinking in the process of learning And experience, there are also many parts that refer to other tutorials. If there are errors or problems, please point them out to the author. The author guarantees that the content is 100% correct. Please do not use this column as a reference answer.

  2. The reading of this column requires you to have a certain foundation of React, JavaScript and front-end engineering. The author will not explain many basic knowledge points, such as: what is babel, what is the syntax of jsx, please refer to it when necessary material.

  3. Many parts of this column refer to a large number of other tutorials. If there are similarities, the author plagiarized them. Therefore, this tutorial is completely open source. You can integrate, summarize and add your own understanding to various tutorials as the author.

Contents of this section

This chapter mainly explains the useRef api of React. We will talk about it from the perspective of use and source code, and introduce its use and source code mounting in two scenarios of global variables and DOM references as components, as well as one related to references in React. api - forwardRef

Definition of useRef

Let's first look at the usage useRef of : it returns a mutable ref object whose .currentproperty is initialized to the passed in parameter ( initialValue). The returned ref object persists throughout the lifetime of the component.

const refContainer = useRef(initialValue);

useRef()The only difference from creating a self-built {current: ...}object is that useRefthe same ref object will be returned every time it is rendered. useRefYou will not be notified when the contents of the ref object change . Changing .currentthe property doesn't cause the component to re-render either:

function TextInputWithFocusButton() {
    
    
  const inputEl = useRef(null);
  const onButtonClick = () => {
    
    
    // `current` 指向已挂载到 DOM 上的文本输入元素
    inputEl.current.focus();
  };
  return (
    <>
      <input ref={
    
    inputEl} type="text" />
      <button onClick={
    
    onButtonClick}>Focus the input</button>
    </>
  );
}

UseRef usage scenarios

Here are useRef several usage scenarios for :

Manipulate the DOM

This is useRef the most basic function, because in business requirements, operation DOMscenarios are often encountered, such as: gaining focus on a form, implementing animation through DOMoperations and so on. In this case, you can use useRefto save DOMa reference to the object. That is to say: when you need to call React's external system or some APInative , useRefit is very useful to implement:

function Form() {
    
    
  const inputRef = useRef(null);
  function handleClick() {
    
    
    inputRef.current.focus();
  }

  return (
    <div>
      <input ref={
    
    inputRef} />
      <button onClick={
    
    handleClick}>
        Focus
      </button>
    <div/>
  );
}

But what you need to pay attention to is that your rendering cannot depend on the property of this ref, otherwise it will not be updated in time. We can look at this problem through the next usage scenario:

Use it as a global variable in the component lifecycle

According to the above definition, we can understand that useRef the stored data is valid throughout the life cycle of the component, that is, we maintain its state before the component is destroyed, and we can use it in the logic of the component at any time. At the same time, when its content is modified, it will not cause the component to re-render, and the modified content will take effect immediately.

As we said before, setStateit is not synchronous, and it will select a batch of updates to process together according to React's scheduling. But sometimes we need to get the latest state of a state right away. Although setStatethere are callback functions that can help us process the logic, if the data needs to be updated multiple times, the lengthy callback function will make our code poorly readable.

At this time, some readers will definitely say, it’s fine if I open a variable. At this time, we will take a look at this sentence. The useRef stored data is valid throughout the life cycle of the component. Before the component is destroyed , we maintain it. status. Redrawing is not destroying the component, but redrawing will refresh the ordinary variables we defined in it, and the useRef stored data will not be affected.

Let's look at this example:

  • We defined a countRefvariable as 0 and used handleClickthe method to operate it. When we click the button to trigger this method, countRef.currentthe value rendered on the page will not change, because countRefour change will not cause redrawing, but our consolestatement prints The content that comes out will be + 1 every time
  • We useStatedefined a countvariable and a handleClick2method to make it + 1. When we call this method, our component will be redrawn, and randomFlagthe data we set will be updated (indicating that we have called this randomFlag again), and we will show The count on the page will also be +1
  • When we click the button of handleClickthe method , and handleClick2then click the button of the method again, and then click handleClickthe method consoleagain, the value will continue to be +1 on the basis of the last time, instead of being reset to start from 0
  • But every time we redraw our component, our randomFlagwill be reassigned, which triggers initialization
export default function Counter() {
    
    
  let countRef = useRef(0);
  const [count, setCount] = useState(0);

  function handleClick2() {
    
    
    setCount(count + 1);
  }
  function handleClick() {
    
    
    countRef.current = countRef.current + 1;
    console.log(countRef.current)
  }
  const randomFlag = Math.floor(Math.random()* 10000000000000)
  return (
	   <div>
			<h1 style={
    
    {
    
     color: 'pink' }}>{
    
    randomFlag}</h1>
			<button onClick={
    
    handleClick}>
		       useRef {
    
     countRef.current }
		    </button>
            <button onClick={
    
    handleClick2}>
        	   useState {
    
     count }
		    </button>
	   </div>
  );
}

source code of useRef

useRef The source code of is not concise, let's look at it directly:

  • mountRefGet the initial value we passed in, and then store it memoizedStatein , because we returned our ref, which is an object, that is, a reference type, so we can directly modify the ref value, and it will also act memoizedStateon

  • updateRefis returned to ourmemoizedState

function mountRef<T>(initialValue: T): {
    
    | current: T |} {
    
    
  const hook = mountWorkInProgressHook();

  // 存储数据,并返回这个数据
  const ref = {
    
     current: initialValue };
  hook.memoizedState = ref;
  return ref;
}

function updateRef<T>(initialValue: T): {
    
    | current: T |} {
    
    
  const hook = updateWorkInProgressHook();
  return hook.memoizedState;
}

Mounting of Ref on the component

Of course, our ref does not only have such a simple definition. According to the above definition, we have realized that we can manipulate its dom by binding ref to the component, so how is this dom bound? Let's look at it step by step:

First of all, we need to go back to our element structure. We can see that in our React Element structure: Many readers may have wondered why our key and ref attributes should be operated separately. Aren’t they also props? Here we understand that the key mentioned before is used in the diff algorithm and does not participate in the final generation, and the ref solves the problem today. It is defined useRefor createRefDOM operation, and this ref attribute will be synchronized to our Fiber when generating our Fiber:

const element = {
    
    
    $$typeof: REACT_ELEMENT_TYPEtype: type,                    
    key: key,                      
    ref: ref,                      //对组件实例的引用
    props: props,                  
    _owner: owner                  
}

Then at beginWorkthis stage, some components call a function:

function markRef(current: Fiber | null, workInProgress: Fiber) {
    
    
  const ref = workInProgress.ref;
  if (
    (current === null && ref !== null) ||
    (current !== null && current.ref !== ref)
  ) {
    
    
    workInProgress.effectTag |= Ref;
  }
}

And at completeWorkthis stage, there is another function with the same name:

function markRef(workInProgress: Fiber) {
    
    
  workInProgress.effectTag |= Ref;
}

Their logic is the same, tell our React that the DOM corresponding to this fiber node has been refed, and we use a flag effectTagin identify whether our fiber has a ref, which is used for our subsequent logic to determine whether or not Perform ref-related operations.

Then we enter our submission stage, we go directly to commitMutationEffectsOnFiberthe function , as we said before, different types of components will call recursivelyTraverseMutationEffectsthe function to perform the first step of processing before executing the logic, which is responsible for the deletion operation, and we will go deeper step by step , seeing that it calls safelyDetachRefthe function, here is the logic we need:

  • It first gets the ref attribute of our current Fiber node, which mounts ouruseRef
  • If it is a function-type ref, the ref function will be executed, and the parameter is null. This function-type ref is createRefcreated function can set its internal value to null
  • If it's not a function, that is, a DOM with current, we set its useRef property to null, thus clearing our mount
function safelyDetachRef(current: Fiber, nearestMountedAncestor: Fiber | null) {
    
    
  const ref = current.ref;
  if (ref !== null) {
    
    
    if (typeof ref === 'function') {
    
    
      let retVal;
      try {
    
    
        if (
          enableProfilerTimer &&
          enableProfilerCommitHooks &&
          current.mode & ProfileMode
        ) {
    
    
          try {
    
    
            startLayoutEffectTimer();
            retVal = ref(null);
          } finally {
    
    
            recordLayoutEffectDuration(current);
          }
        } else {
    
    
          retVal = ref(null);
        }
      } catch (error) {
    
    
        captureCommitPhaseError(current, nearestMountedAncestor, error);
      }
    } else {
    
    
      ref.current = null;
    }
  }
}

Then we continue to look at commitLayoutEffectthis stage. We said that this stage will operate our real DOM. Let's go directly to this function. We skip the part that has been explained. We see that there is such an operation at the end of the function. If There is a Ref tag in the Fiber tag we are dealing with. We will enter commitAttachRefthis function. Its function is to useRef bind ref to our . Of course, if it is in a class component or some other components, we will safelyAttachRefcall it through this function commitAttachRef, in fact the operation is the same:

 function commitLayoutEffectOnFiber(
  finishedRoot: FiberRoot,
  current: Fiber | null,
  finishedWork: Fiber,
  committedLanes: Lanes,
): void {
    
    
   //.......省略
   // 确保能拿到我们的 dom
   if (!enableSuspenseLayoutEffectSemantics || !offscreenSubtreeWasHidden) {
    
    
    if (enableScopeAPI) {
    
    
      // 排除 ScopeComponent 是因为之前已经处理了
      if (finishedWork.flags & Ref && finishedWork.tag !== ScopeComponent) {
    
    
        commitAttachRef(finishedWork);
      }
    } else {
    
    
      if (finishedWork.flags & Ref) {
    
    
        commitAttachRef(finishedWork);
      }
    }
  }
}

function commitMutationEffectsOnFiber(
  finishedWork: Fiber,
  root: FiberRoot,
  lanes: Lanes,
) {
    
    
  switch (finishedWork.tag) {
    
    
    //....
    // 对于 ScopeComponent ,这里已经处理了 safelyDetachRef 和 safelyAttachRef
    case ScopeComponent: {
    
    
      if (enableScopeAPI) {
    
    
        recursivelyTraverseMutationEffects(root, finishedWork, lanes);
        commitReconciliationEffects(finishedWork);
        if (flags & Ref) {
    
    
          if (current !== null) {
    
    
            safelyDetachRef(finishedWork, finishedWork.return);
          }
          safelyAttachRef(finishedWork, finishedWork.return);
        }
        if (flags & Update) {
    
    
          const scopeInstance = finishedWork.stateNode;
          prepareScopeUpdate(scopeInstance, finishedWork);
        }
      }
      return;
    }
  }
}

Let's take a look at this function, its logic is:

  • Obtain the corresponding instance through the tag of the current fiber: for HostComponent, the instance is the obtained DOM node, and in other cases it is fiber.stateNode
  • Determine the type of ref, if it is a function type, call the ref function and pass the instance; if not, assign ref.current to the instance
function commitAttachRef(finishedWork: Fiber) {
    
    
  const ref = finishedWork.ref;
  if (ref !== null) {
    
    
    const instance = finishedWork.stateNode;
    let instanceToUse;
    switch (finishedWork.tag) {
    
    
      case HostComponent:
        instanceToUse = getPublicInstance(instance);
        break;
      default:
        instanceToUse = instance;
    }
    if (enableScopeAPI && finishedWork.tag === ScopeComponent) {
    
    
      instanceToUse = instance;
    }
    if (typeof ref === 'function') {
    
    
      let retVal;
      if (
        enableProfilerTimer &&
        enableProfilerCommitHooks &&
        finishedWork.mode & ProfileMode
      ) {
    
    
        try {
    
    
          startLayoutEffectTimer();
          retVal = ref(instanceToUse);
        } finally {
    
    
          recordLayoutEffectDuration(finishedWork);
        }
      } else {
    
    
        retVal = ref(instanceToUse);
      }
    } else {
    
    
      ref.current = instanceToUse;
    }
  }
}

Extension: forwardRef

In React, there is another one used with ref forwardRef, which creates a React component that can forward the ref attribute it accepts to another component under its component tree. It is useful in two scenarios:

  • Pass the ref of the parent component to the child component

The following example: forwardRefwhen , the parent component passes in the subcomponent refproperty, which refpoints to the subcomponent itself , so if you want to get the DOM of the subcomponent, you need to add buttonRefthe field

interface IProps {
    
    
  buttonRef: any;
}

class Child extends React.Component<IProps> {
    
    
  render() {
    
    
    return (
      <div>
        <button ref={
    
    this.props.buttonRef}> click </button>
      </div>
    );
  }
}

function App() {
    
    
  const child = useRef<any>();
  return (
    <div styleName="container">
      <Child buttonRef={
    
    child} />
    </div>
  );
}

When we use forwardRefto forward ref. In this way, the subcomponent does not need to expand the extra ref field when providing the internal dom :

const Child = forwardRef((props: any, ref: any) => {
    
    
  return (
    <div>
      <button ref={
    
    ref}>click</button>
    </div>
  );
});
function App() {
    
    
  const child = useRef<any>();
  return (
    <div styleName="container">
      <Child ref={
    
    child} />
    </div>
  );
}
  • Use forwardRef in high-order components to forward component Ref

Let's go straight to a scenario:

There is a higher-order component that prints the props of the component:

function logProps(WrappedComponent) {
    
    
  class LogProps extends React.Component {
    
    
    componentDidUpdate(prevProps) {
    
    
      console.log('old props:', prevProps);
      console.log('new props:', this.props);
    }
    render() {
    
    
      return <WrappedComponent {
    
    ...this.props} />;
    }
  }
  return LogProps;
}

When it acts on a child component, because there is a layer in the middle, the ref will fail to pass, which will eventually cause the parent component to fail when calling ref.current.xxx():

import Button from './Button';
const LoggedButton = logProps(Button);

const ref = React.createRef();

// LoggedButton 组件是高阶组件 LogProps。
// 我们的 ref 将指向 LogProps 而不是内部的 Button 组件!
// 这意味着我们不能调用例如 ref.current.xxx() 这样的方法
<LoggedButton label="Click Me" handleClick={
    
    handleClick} ref={
    
    ref} />;

At this time, we need to use forwardRef in hoc to wrap another layer and forward ref:

function logProps(WrappedComponent) {
    
    
  class LogProps extends React.Component {
    
    
    componentDidUpdate(prevProps) {
    
    
      console.log('old props:', prevProps);
      console.log('new props:', this.props);
    }
    render() {
    
    
      const {
    
     componentRef, ...rest } = this.props;
      return <WrappedComponent {
    
    ...rest} ref={
    
    this.props.componentRef} />;
    }
  }
  return forwardRef((props, ref) => {
    
    
    return <LogProps {
    
    ...props} componentRef={
    
    ref} />;
  });
}

Summarize

In this issue, we talked about useRefthis hooks. It can be used as a global variable that we don’t use for rendering in the function life cycle, and it can also help us get the DOM. The former only uses its caching function, and the latter needs to be used during rendering. , initialized through the ref attribute of React Element. At the same time, we also understand the difference between useRef and useState. The former will not cause the component to redraw, while the latter will, but the latter can maintain the state consistently before the component is destroyed, and the global variable will be reset every time it is drawn. In order to solve the trouble of operating ref in high-level components and parent-child components, React has entered forwardRef to facilitate our operation. The above is the content related to ref.

Now that useState is mentioned, in the next section I am going to talk about the hooks we use the most, and the useReducer similar to him, so stay tuned!

Guess you like

Origin blog.csdn.net/weixin_46463785/article/details/130648498