useRef的实现

lxf2023-05-05 00:46:18

本系列是讲述从0开始实现一个react18的基本版本。通过实现一个基本版本,让大家深入了解React内部机制。 由于React源码通过Mono-repo 管理仓库,我们也是用pnpm提供的workspaces来管理我们的代码仓库,打包我们使用rollup进行打包。

仓库地址

本节对应的代码

系列文章:

  1. React实现系列一 - jsx
  2. 剖析React系列二-reconciler
  3. 剖析React系列三-打标记
  4. 剖析React系列四-commit
  5. 剖析React系列五-update流程
  6. 剖析React系列六-dispatch update流程
  7. 剖析React系列七-事件系统
  8. 剖析React系列八-同级节点diff
  9. 剖析React系列九-Fragment的部分实现逻辑
  10. 剖析React系列十- 调度<合并更新、优先级>
  11. 剖析React系列十一- useEffect的实现原理
  12. 剖析React系列十二-调度器的实现
  13. 剖析React系列十三-react调度
  14. useTransition的实现

本章我们来讲解useRef的使用和实现原理。

useRef的使用

useRef的使用非常简单,我们可以通过useRef来获取一个可变的ref对象ref对象的.current属性在重复渲染的过程中,会保持引用不变,我们可以通过ref对象来获取dom节点,或者在重复渲染中不变的值。

useRef 接受一个初始值, 返回一个对象,包含.current属性。

let ref = useRef(null)

通常我们用它去获取渲染的dom节点, 有一下2种方式

// 第一种:直接赋值给ReactElement的ref属性
let ref = useRef(null)
function App() {
  return <div ref={ref}>hello world</div>
}

// 第二种:通过函数的形式, 将dom传递给函数的参数
let ref = useRef(null)
function App() {
  return <div ref={(dom) => {ref.current = dom}}>hello world</div>
}

接下来我们来看看useRef的实现原理。

获取ref值

当我们传递给reactElementref属性的时候,首先我们将其ref的属性值赋值到对应的fiber节点上。

所以我们要修改fiber.ts中的createFiberFromElement以及createWorkInProgress方法

export function createFiberFromElement(element: ReactElementType): FiberNode {
+ const { type, key, props, ref } = element;
  let fiberTag: WorkTag = FunctionComponent;
  ...
+ fiber.ref = ref;
  return fiber;
}

export const createWorkInProgress = (
  current: FiberNode,
  pendingProps: Props
): FiberNode => {
  let wip = current.alternate;
  ...
+ wip.ref = current.ref;
  return wip;
};

这样我们在进行调和reconciler的时候,就可以获取到ref的值了。

reconciler阶段

之前的章节我们晓得调和阶段分为2个阶段:

  1. beginWork阶段
  2. completeWork阶段

对于ref的处理,这2个阶段主要是针对有ref属性的fiber进行打标记处理。

正常情况下,我们只需要在beginWork中打好标记,这里的completeWork中是兜底操作。

新增一个flag类型

我们在fiberFlags中新增一个ref类型,用来表示ref的打标记。同时新增一个LayoutMask,用来表示layout阶段要执行的标志。

export const Ref = 0b0010000; // ref
export const LayoutMask = Ref; // layout阶段要执行的标志

新增了一个Ref的标志后,我们接下来就需要根据条件判断是否要打上这个标记。

beginWork阶段

beginWork阶段,我们需要对2种情况进行处理:

  1. 刚刚初始化的时候,wip.alternatenull,这个时候我们需要判断fiber节点是否有ref属性,如果有,就打上Ref标记。
  2. wip.alternate不为null,这个时候我们需要判断fiber节点的ref属性是否发生变化,如果发生变化,就打上Ref标记。

在调和的过程中,如果遇到wip.tagupdateHostComponent的时候,标识这是一个dom类型的fiber,我们就可以判断是否有ref属性了。

function updateHostComponent(wip: FiberNode) {
  // ....
  markRef(wip.alternate, wip);
  // ....
  return wip.child;
}

/**
 * 标记Ref
 */
function markRef(current: FiberNode | null, workInProgress: FiberNode) {
  const ref = workInProgress.ref;
  // mount时 有ref 或者 update时 ref变化
  if (
    (current === null && ref !== null) ||
    (current !== null && current.ref !== ref)
  ) {
    workInProgress.flags |= Ref;
  }
}

completeWork阶段

completeWork阶段,主要是进行兜底操作。

由于本身已经区分了初始化和更新的情况,所以我们只需要在不同的情况下判断即可, 所以针对HostComponent类型进行如下判断:

// 更新阶段
// 标记ref
if (current.ref !== wip.ref) {
  markRef(wip);
}

// 初始化阶段
// 标记Ref
if (wip.ref !== null) {
  markRef(wip);
}

function markRef(fiber: FiberNode) {
  fiber.flags |= Ref;
}

commit阶段

commit阶段,我们需要对Ref标记进行处理,在之前的commit阶段文章,我们了解到commit分为三个子阶段:

  • beforeMutation阶段
  • mutation阶段
  • layout阶段 针对ref的处理,主要是在mutation子阶段进行原有的ref值的解绑, layout阶段需要绑定新的值。

解绑和绑定

对于mutation阶段的解绑操作, 获取到ref的值,然后判断是否是函数,如果是函数的话就传递null,否者,就将current设置为null

  // 解绑之前的Ref
  if ((flags & Ref) !== NoFlags && tag === HostComponent) {
    safelyDetachRef(finishedWork);
  }
/**
 *  解绑当前的ref
 */
function safelyDetachRef(current: FiberNode) {
  const ref = current.ref;
  if (ref !== null) {
    if (typeof ref === "function") {
      ref(null);
    } else {
      ref.current = null;
    }
  }
}

layout阶段进行重新的绑定。

  if ((flags & Ref) !== NoFlags && tag === HostComponent) {
    // 绑定新的ref
    safelyAttachRef(finishedWork);
  }

/**
 *  绑定新的ref
 */
function safelyAttachRef(fiber: FiberNode) {
  const ref = fiber.ref;
  if (ref !== null) {
    const instance = fiber.stateNode;
    if (typeof ref === "function") {
      ref(instance);
    } else {
      ref.current = instance;
    }
  }
}

useRef的实现

卸载

当被绑定的fiber节点被卸载的时候,我们需要对ref进行解绑操作。 我们知道在卸载的时候会执行commitDeletion, 所以针对hostComponent类型的fiber节点,我们需要进行解绑操作。

function commitDeletion(childToDelete: FiberNode, root: FiberRootNode) {
  const rootChildrenToDelete: FiberNode[] = [];
  // 递归子树
  commitNestedComponent(childToDelete, (unmountFiber) => {
    switch (unmountFiber.tag) {
      case HostComponent:
        // xxxxxx
        safelyDetachRef(unmountFiber);
        return;
    }
    // xxxxx
  })
}

ref重复渲染值不变

我们知道在重复渲染的时候ref.current的值是保持不变的,那么它是如何实现的呢? 我们来看看useRef的2个阶段的实现代码。

/**
 * useRef使用 ref = useRef(null)
 * @param initialValue
 */
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;
}

在初始化的时候调用mountRef函数。我们创建一个{current: initialValue}的对象,然后将这个对象赋值给hook.memoizedState

对于不同的hook,都是通过对于的hook.memoizedState来保存数据的。

在更新的时候,我们调用updateRef函数,这个时候我们直接返回hook.memoizedState,也就是说,我们返回的是同一个对象,所以ref.current的值是不会变化的。

总结

至此,函数组件针对ref的2种处理就都完成了。

通过上面的讲解,我们应该知道了,当我们通过函数接受dom的时候,当触发更新的时候,会触发2次函数的执行,第一次是解绑操作,第二次是绑定操作

例如点击count的时候,会输出2次

function App() {
  const ref = useRef(null);
  const [count, setCount] = useState(1);
  return (
    <div className="App">
      <div
        ref={(dom) => {
          console.log("dom: --", dom);
        }}
      >
        hcc
      </div>
      <div
        onClick={() => {
          setCount(count + 1);
        }}
      >
        {count}
      </div>
    </div>
  );
}

useRef的实现

本网站是一个以CSS、JavaScript、Vue、HTML为核心的前端开发技术网站。我们致力于为广大前端开发者提供专业、全面、实用的前端开发知识和技术支持。 在本网站中,您可以学习到最新的前端开发技术,了解前端开发的最新趋势和最佳实践。我们提供丰富的教程和案例,让您可以快速掌握前端开发的核心技术和流程。 本网站还提供一系列实用的工具和插件,帮助您更加高效地进行前端开发工作。我们提供的工具和插件都经过精心设计和优化,可以帮助您节省时间和精力,提升开发效率。 除此之外,本网站还拥有一个活跃的社区,您可以在社区中与其他前端开发者交流技术、分享经验、解决问题。我们相信,社区的力量可以帮助您更好地成长和进步。 在本网站中,您可以找到您需要的一切前端开发资源,让您成为一名更加优秀的前端开发者。欢迎您加入我们的大家庭,一起探索前端开发的无限可能!