react源码解析(三)useRef

useRef返回一个current属性的对象,current属性存储的值不会随着重新渲染而重置,且修改该对象不会引起组件的重新渲染。我们可以选择用ref存储一个值,或者存储dom。

假设存在以下示例

import { useState, useRef, useEffect } from "react";

export default function App() {
  const [state, setState] = useState(0);
  const cbRef = useRef(() => {});
  const btnRef = useRef(null);
  const cb = () => console.log(state);
  cbRef.current = cb;
  function handleCb() {
    console.log("cb():");
    cb();
  }
  function handleCbRef() {
    console.log("cbRef.current():");
    cbRef.current();
  }
  useEffect(() => {
    btnRef.current.addEventListener("click", handleCbRef);
    btnRef.current.addEventListener("click", handleCb);
    return () => {
      btnRef.current.removeEventListener("click", handleCbRef);
      btnRef.current.removeEventListener("click", handleCb);
    };
  }, []);
  return (
    <>
      <button ref={btnRef} onClick={() => setState(state + 1)}>
        state+1
      </button>
    </>
  );
}

点击三次按钮,打印台输出如下。

这个例子印证了ref在多次渲染中保持着唯一实例,而普通的变量每次渲染都会新增一个实例。

使用ref引用一个值

mount阶段

和之前分析useEffect一样,当我们通过renderWithHooks调用到Component方法时,我们就会执行StopWatch函数,由此调用到useRef。在mount阶段我们使用mount hook,所以就调用到了mountRef。

export function useRef<T>(initialValue: T): {|current: T|} {
  const dispatcher = resolveDispatcher();
  return dispatcher.useRef(initialValue);
}
function mountRef<T>(initialValue: T): {|current: T|} {
  const hook = mountWorkInProgressHook();
  const ref = {current: initialValue};
  hook.memoizedState = ref;
  return ref;
}

mountWorkInProgressHook我们在学习useEffect也分析过,就是形成hook的单向链表。

function mountWorkInProgressHook(): Hook {
  const hook: Hook = {
    memoizedState: null,

    baseState: null,
    baseQueue: null,
    queue: null,

    next: null,
  };

  if (workInProgressHook === null) {
    // This is the first hook in the list
    currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
  } else {
    // Append to the end of the list
    workInProgressHook = workInProgressHook.next = hook;
  }
  return workInProgressHook;
}

所以如果我们的代码中只含有useRef这一种Hook,那么形成的结构就应该如下。

由于useRef会返回ref对象,所以我们可以直接通过ref.current读取/修改存储值。

要保证ref.current在多次渲染中保持唯一实例,我们就需要查看更新阶段react做了什么处理。

update阶段

function updateRef<T>(initialValue: T): {|current: T|} {
  const hook = updateWorkInProgressHook();
  return hook.memoizedState;
}
function updateWorkInProgressHook(): Hook {
  let nextCurrentHook: null | Hook;
  if (currentHook === null) {
    const current = currentlyRenderingFiber.alternate;
    if (current !== null) {
      nextCurrentHook = current.memoizedState;
    } else {
      nextCurrentHook = null;
    }
  } else {
    nextCurrentHook = currentHook.next;
  }

  let nextWorkInProgressHook: null | Hook;
  if (workInProgressHook === null) {
    nextWorkInProgressHook = currentlyRenderingFiber.memoizedState;
  } else {
    nextWorkInProgressHook = workInProgressHook.next;
  }

  if (nextWorkInProgressHook !== null) {
    workInProgressHook = nextWorkInProgressHook;
    nextWorkInProgressHook = workInProgressHook.next;

    currentHook = nextCurrentHook;
  } else {


    currentHook = nextCurrentHook;

    const newHook: Hook = {
      memoizedState: currentHook.memoizedState,

      baseState: currentHook.baseState,
      baseQueue: currentHook.baseQueue,
      queue: currentHook.queue,

      next: null,
    };

    if (workInProgressHook === null) {
      currentlyRenderingFiber.memoizedState = workInProgressHook = newHook;
    } else {
      workInProgressHook = workInProgressHook.next = newHook;
    }
  }
  return workInProgressHook;
}

update阶段所做的操作其实很简单,就是拷贝hook,复用ref。我们在多次渲染期间对于一个useRef,hook实例会有多个,但是ref实例是唯一的,所以当我们修改ref.current时,无论是在哪次渲染绑定的回调函数,ref.current的值都会被同步修改。

我们每重新渲染一次,都会形成一个新的cb复用旧的Ref并修改这个Ref的current指针指向最新的cb,只有在新的cb中才能获取到最新的state。

然而我们只在初始化时绑定了事件监听器,所以调用到的handleCb和handleCbRef都是初始化时形成的。初始化时的handleCb直接调用cb函数,自然调用到的就是初始化时形成的cb,打印出的值就是初始化时的state。初始化时的handleCbRef调用ref.current,调用的也是初始化时形成的ref,但是ref.current已经被修改指向最新的cb,所以打印出的值就是最新的state。

 

使用ref操作dom

当我们在tsx标签上写入ref属性时,在renderWithHooks中调用Component方法返回的element元素的button子节点元素上ref指针就会指向{current: null},于是在diff算法阶段就会将相应的button fiber的ref指针赋值为{current: null}并给fiber.flag 打上Ref标签。

  function reconcileSingleElement(
    returnFiber: Fiber,
    currentFirstChild: Fiber | null,
    element: ReactElement,
    lanes: Lanes,
  ): Fiber {
    while (child !== null) {
      if (child.key === key) {
        const elementType = element.type;
        if (elementType === REACT_FRAGMENT_TYPE) {
          // ...
        } else {
          if (
            child.elementType === elementType
          ) {
            // ...
            existing.ref = coerceRef(returnFiber, child, element);
            // ...
            return existing;
          }
        }
        // ...
      }
    }

    if (element.type === REACT_FRAGMENT_TYPE) {
      // ...
    } else {
      const created = createFiberFromElement(element, returnFiber.mode, lanes);
      created.ref = coerceRef(returnFiber, currentFirstChild, element);
      return created;
    }
  }
function coerceRef(
  returnFiber: Fiber,
  current: Fiber | null,
  element: ReactElement,
) {
  const mixedRef = element.ref;
  // ...
  return mixedRef;
}
function updateHostComponent(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
) {
  // ...
  markRef(current, workInProgress);
  // ...
}
function markRef(current: Fiber | null, workInProgress: Fiber) {
  const ref = workInProgress.ref;
  if (
    (current === null && ref !== null) ||
    (current !== null && current.ref !== ref)
  ) {
    // Schedule a Ref effect
    workInProgress.flags |= Ref;
    if (enableSuspenseLayoutEffectSemantics) {
      workInProgress.flags |= RefStatic;
    }
  }
}

 那么ref.current什么时候被修改为真实的dom节点的呢?其实是在commit阶段执行的。在commiyRootImpl中,我们会调用到commitLayoutEffects,执行跟Layout相关的副作用,最终在commitLayoutEffectOnFiber中调用commitAttachRef方法。

function commitLayoutEffectOnFiber(
  finishedRoot: FiberRoot,
  current: Fiber | null,
  finishedWork: Fiber,
  committedLanes: Lanes,
): void {
  // ...
  if (!enableSuspenseLayoutEffectSemantics || !offscreenSubtreeWasHidden) {
    if (finishedWork.flags & Ref) {
      commitAttachRef(finishedWork);
    }
  }
}

 ref === function就对应以下情况,相当于把dom节点手动收集在外部变量中。

<button ref={c => this.btn = c} />
function commitAttachRef(finishedWork: Fiber) {
  const ref = finishedWork.ref;
  if (ref !== null) {
    const instance = finishedWork.stateNode;
    let instanceToUse;
    instanceToUse = instance;
    if (typeof ref === 'function') {
      let retVal;
      retVal = ref(instanceToUse);
    } else {
      ref.current = instanceToUse;
    }
  }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值