原理

在 render 阶段标记 ref 标记,commit 阶段挂载 dom 节点时通过标记获取 dom 节点。主要实现流程如下:

  1. 标记 Ref​ 标记 Ref​ 需要满足:

​mount​ 时:存在 ref​ ​update​ 时:ref​ 引用变化

标记的时机包括:

​beginWork

  1. 执行 Ref​ 操作

包括两类操作:

  1. 对于正常的绑定操作:

    • 解绑之前的 ref​(mutation​ 阶段)

    • 绑定新的 ref​(layout​ 阶段)

  2. 对于组件卸载:

    • 解绑之前的 re

Mount

​mount​ 阶段主要是构建 hook 对象,并将 hook 对象添加到 fiber​ 节点的 hook 链表中。这也是所有 hook 在 mount​ 阶段的基本操作。

最后创建一个 current​ 对象,用来保存数据,这也就是 useRef​ 函数的返回值。

function mountRef(initialValue) {
  // 构建hook对象并添加到链表中
  const hook = mountWorkInProgressHook();
  // 创建current对象
  const ref = { current: initialValue };
  // 保存current对象,这也是useRef函数返回的对象结果
  hook.memoizedState = ref;
  return ref;
}

在构建 hook 对象时会有两种情况,如果是第一个 hook 函数,需要将其保存在 fiber​ 节点的 memoizedState​ 属性中,如果是后续的 hook 函数,直接通过 next​ 指针连接

function mountWorkInProgressHook() {
  const hook = {
    memoizedState: null,
    updateQueue: null,
    next: null,
    baseQueue: null,
    baseState: null,
  };
  if (workInProgressHook === null) {
    // mount时 第一个hook
    if (currentlyRenderingFiber === null) {
      throw new Error("请在函数组件内调用hook");
    } else {
      workInProgressHook = hook;
      currentlyRenderingFiber.memoizedState = workInProgressHook;
    }
  } else {
    // 后续的hook
    // 通过next指针连接
    workInProgressHook.next = hook;
    workInProgressHook = hook;
  }
  return workInProgressHook;
}

Update

更新阶段由于并没有其他操作,所以只需要通过双缓存树的机制更新 hook 对象。

function updateRef(initialValue) {
  // 更新hook对象
  const hook = updateWorkInProgressHook();
  return hook.memoizedState;
}

更新 hook 对象的本质是复用,通过 current​ 树中对应 fiber​ 节点的 hook 链表,来创建本次更新新的 hook 对象并更新链表

​currentHook​ 属性保存 current​ 树中上一个 hook 对应的对象,所以本次更新如果有值,通过 next​ 指针获取本次处理的 hook 对应的旧 hook 对象。如果没有值,说明当前 hook 函数为函数组件的第一个。

如果这两种情况都没有取到值,说明存在动态 hook 函数(本次更新比上一次更新 hooks 数量对应不上)。这种情况是 react 不被允许的,所以报错“本次执行时的 Hook 比上次执行时多”。

function updateWorkInProgressHook() {
  let nextCurrentHook;

  if (currentHook === null) {
    // 这是这个函数组件update时的第一个hook
    const current = currentlyRenderingFiber?.alternate;
    if (current !== null) {
      // 获取对应的memoizedState属性
      nextCurrentHook = current?.memoizedState;
    } else {
      nextCurrentHook = null;
    }
  } else {
    // 这个函数组件update时 后续的hook
    nextCurrentHook = currentHook.next;
  }

  if (nextCurrentHook === null) {
    throw new Error(
      `组件${currentlyRenderingFiber?.type}本次执行时的Hook比上次执行时多`
    );
  }

  currentHook = nextCurrentHook;
  // 复用current树的旧hook对象属性
  const newHook = {
    memoizedState: currentHook.memoizedState,
    updateQueue: currentHook.updateQueue,
    next: null,
    baseQueue: currentHook.baseQueue,
    baseState: currentHook.baseState,
  };
  if (workInProgressHook === null) {
    // mount时 第一个hook
    if (currentlyRenderingFiber === null) {
      throw new Error("请在函数组件内调用hook");
    } else {
      workInProgressHook = newHook;
      currentlyRenderingFiber.memoizedState = workInProgressHook;
    }
  } else {
    // 后续的hook
    workInProgressHook.next = newHook;
    workInProgressHook = newHook;
  }
  return workInProgressHook;
}

类组件才能实例化 才能使用 refs

  1. 关于把 ref 对象作为 useCallback 依赖的做法

正常的话,ref 是不会变的,一直指向同一个对象。依然把它传递给 useCallback 的原因是,让 useCallback 的逻辑与变量 counterRef 的逻辑解耦。

意思就是对于 useCallback 的意义是:我不管 counterRef 会不会变,反正我依赖了它,为了我能正常运行,我必须把它声明出来。

你也可以这样理解,万一 counterRef 会变化,那么 useCallback 是不会受到影响的,这就是解耦合的意义所在,比如

const oneRef = useRef(); const twoRef = useRef(); const counterRef = ? oneRef : twoRef;

  1. useRef 使用注意初始化

useRef使用注意初始化

ref 想象成类的属性变量,它的值在渲染周期之间可以保持,并且,和 state 的一个重要区别是,它的变化不会触发组件重新渲染。

useRef 可以传入初始值,你这个代码没有传入,因此直接当作函数来调用肯定报错的。

  1. 同步不同渲染周期需要共用的数据, 只能设置在 useRef 的 current 属性上吗 ?

取决于这个数据的变更要不要触发重渲染,如果不重渲染,那么放 ref 是比较建议的做法;否则,就只能用 state 了。

export default function App() {
  const [count, setCount] = useState(0);
  const preCountRef = useRef(count);
  useEffect(() => {
    preCountRef.current = count;
  });
  const preCount = preCountRef.current;

  return (
    <div className="App">
      <div
        onClick={() => {
          setCount((count) => count + 1);
        }}
      >
        <h1>
          Now:{count} preCount: {preCount}
        </h1>
      </div>
    </div>
  );
}

useEffect 它没有依赖参数,说明每一次组件的渲染,都会运行一次这个副作用。而 useEffect 是在每次的渲染后来执行的。因而我们又把本次渲染所使用的 count 保存在了 preCountRef 中 现在我们执行下一次渲染,先运行的代码是:

const [count, setCount] = useState(0);
const prevCountRef = useRef();
const prevCount = prevCountRef.current;

这个就是典型的先渲染 jsx, 在执行 useEffect

ref 有个限制,那就是 ref 的 current 值改变的时候,并不会重新触发渲染,所以我们不能完全抛弃 useState 而只用 useRef。

const [count, setCount] = useState(3);
const countRef = useRef(3);
const countDown = () => {
  var timer = setInterval(() => {
    // setCount((pre) => {
    //   if (pre === 0) {
    //     clearInterval(timer);
    //   }
    //   console.log("pre", pre);

    // });
    countRef.current -= 1;
    console.log("ref", countRef.current);
    if (countRef.current === 0) {
      clearInterval(timer);
    }
  }, 1000);
};

最后更新于

这有帮助吗?