hydrate

hydrate是 React 中提供在初次渲染的时候,去复用原本已经存在的 DOM 节点,减少重新生成节点以及删除原本 DOM 节点的开销,来加速初次渲染的功能。主要使用场景是服务端渲染或者像prerender等情况

决定在初次渲染时是否需要执行hydrate的标准取决于以下条件

是否调用方法时就指定使用hydrate

初次渲染可以调用两种方法:ReactDOM.renderReactDOM.hydrate。后者就是直接告诉ReactDOM需要hydrate目前来说如果你调用的是render,但是 React 会检测是否可以hydrate,如果可以他会提醒你应该使用hydrate

shouldHydrateDueToLegacyHeuristic

即便你不指定需要hydrate,React 也会调用该方法进行检测

const ROOT_ATTRIBUTE_NAME = 'data-reactroot'

function shouldHydrateDueToLegacyHeuristic(container) {
  const rootElement = getReactRootElementInContainer(container)
  return !!(
    rootElement &&
    rootElement.nodeType === ELEMENT_NODE &&
    rootElement.hasAttribute(ROOT_ATTRIBUTE_NAME)
  )
}

function getReactRootElementInContainer(container: any) {
  if (!container) {
    return null
  }

  if (container.nodeType === DOCUMENT_NODE) {
    return container.documentElement
  } else {
    return container.firstChild
  }
}

如果containerdocumentrootElementhtml,否则是他的第一个子节点。看到这里就需要注意我们为什么不推荐使用document来作为container了,因为他会直接把html覆盖。

然后判断条件是rootElement是普通的element并且具有data-reactroot属性,这是 React 在服务端渲染的时候加上去的。


符合以上两个条件之一的,就会在初次渲染的时候进行hydrate,通过在FiberRoot上标记hydrate属性。

开始hydrate

在更新到HostRoot的时候,就正式开始了hydrate的流程,hydrate是一个整体的模块设计,会存在着一系列的公共变量

变量名 作用
nextHydratableInstance 下一个可以被hydrate的节点
isHydrating 是否正在hydrate
hydrationParentFiber 下一个被hydrate的节点的父节点

supportsHydration来自于HostConfig.js,在ReactDOM中,他是固定值true

enterHydrationState

在更新HostRoot的时候,会调用这个方法标志着开始进入hydrate流程,设置isHydratingtrue

getFirstHydratableChild用于获取子节点中第一个普通Element或者Text节点

function enterHydrationState(fiber: Fiber): boolean {
  if (!supportsHydration) {
    return false
  }

  const parentInstance = fiber.stateNode.containerInfo
  nextHydratableInstance = getFirstHydratableChild(parentInstance)
  hydrationParentFiber = fiber
  isHydrating = true
  return true
}

export function getFirstHydratableChild(
  parentInstance: Container | Instance,
): null | Instance | TextInstance {
  let next = parentInstance.firstChild
  while (
    next &&
    next.nodeType !== ELEMENT_NODE &&
    next.nodeType !== TEXT_NODE
  ) {
    next = next.nextSibling
  }
  return (next: any)
}

tryToClaimNextHydratableInstance

在更新流程中,还需要做的就是在更新HostComponentHostText节点的时候,调用该方法

function tryToClaimNextHydratableInstance(fiber: Fiber): void {
  if (!isHydrating) {
    return
  }
  let nextInstance = nextHydratableInstance
  if (!nextInstance) {
    // Nothing to hydrate. Make it an insertion.
    insertNonHydratedInstance((hydrationParentFiber: any), fiber)
    isHydrating = false
    hydrationParentFiber = fiber
    return
  }
  const firstAttemptedInstance = nextInstance
  if (!tryHydrate(fiber, nextInstance)) {
    nextInstance = getNextHydratableSibling(firstAttemptedInstance)
    if (!nextInstance || !tryHydrate(fiber, nextInstance)) {
      // Nothing to hydrate. Make it an insertion.
      insertNonHydratedInstance((hydrationParentFiber: any), fiber)
      isHydrating = false
      hydrationParentFiber = fiber
      return
    }
    deleteHydratableInstance(
      (hydrationParentFiber: any),
      firstAttemptedInstance,
    )
  }
  hydrationParentFiber = fiber
  nextHydratableInstance = getFirstHydratableChild((nextInstance: any))
}

该方法做的事情是检查nextInstance是否可以和当前节点进行复用,通过tryHydrate进行判断

判断条件其实很简单,主要对比节点类型是否相同。

注意这里如果第一次tryHydrate不成功,会找他的兄弟节点再次尝试他的下一个兄弟节点,如果两次都不成功,就直接关闭整个hydrate流程,设置isHydratingfalse

function tryHydrate(fiber, nextInstance) {
  switch (fiber.tag) {
    case HostComponent: {
      const type = fiber.type
      const props = fiber.pendingProps
      const instance = canHydrateInstance(nextInstance, type, props)
      if (instance !== null) {
        fiber.stateNode = (instance: Instance)
        return true
      }
      return false
    }
    case HostText: {
      const text = fiber.pendingProps
      const textInstance = canHydrateTextInstance(nextInstance, text)
      if (textInstance !== null) {
        fiber.stateNode = (textInstance: TextInstance)
        return true
      }
      return false
    }
    default:
      return false
  }
}

export function canHydrateInstance(
  instance: Instance | TextInstance,
  type: string,
  props: Props,
): null | Instance {
  if (
    instance.nodeType !== ELEMENT_NODE ||
    type.toLowerCase() !== instance.nodeName.toLowerCase()
  ) {
    return null
  }
  // This has now been refined to an element node.
  return ((instance: any): Instance)
}

export function canHydrateTextInstance(
  instance: Instance | TextInstance,
  text: string,
): null | TextInstance {
  if (text === '' || instance.nodeType !== TEXT_NODE) {
    // Empty strings are not parsed by HTML so there won't be a correct match here.
    return null
  }
  // This has now been refined to a text node.
  return ((instance: any): TextInstance)
}

真正的hydrate

之前是在更新节点的过程中通过判断来确定是否可以hydrate的过程,真正的合并要在completeWork中进行,因为 DOM 节点是在这里被创建的。

let wasHydrated = popHydrationState(workInProgress);
if (wasHydrated) {
  // TODO: Move this and createInstance step into the beginPhase
  // to consolidate.
  if (
    prepareToHydrateHostInstance(
      workInProgress,
      rootContainerInstance,
      currentHostContext,
    )
  ) {
    // If changes to the hydrated node needs to be applied at the
    // commit-phase we mark this as such.
    markUpdate(workInProgress);
  }

popHydrationState

completeUnitOfWork的时候,对于HostComponentHostText会调用该方法。

这里需要注意一个逻辑,就是在update节点,我们沿着一侧子树向下遍历到叶子节点,那么hydrationParentFiber会等于叶子节点对应的 DOM 节点,而nextHydratableInstance会等于他的子节点。而completeUnitOfWork是从这个叶子节点开始的,所以呢这边的判断fiber !== hydrationParentFiber正常来讲是不成立的。

而在下一个判断中,对于当前fiber是文字节点的情况,会清除nextHydratableInstance和他的所有兄弟节点,因为文字节点不会有子节点。

popToNextHostParent这就是找父链上的第一个HostComponent节点了。

最后设置nextHydratableInstance为兄弟节点,因为按照 React 遍历Fiber树的流程,如果有兄弟节点,接下去会更新兄弟节点。

function popToNextHostParent(fiber: Fiber): void {
  let parent = fiber.return
  while (
    parent !== null &&
    parent.tag !== HostComponent &&
    parent.tag !== HostRoot
  ) {
    parent = parent.return
  }
  hydrationParentFiber = parent
}
function popHydrationState(fiber: Fiber): boolean {
  if (!supportsHydration) {
    return false
  }
  if (fiber !== hydrationParentFiber) {
    return false
  }
  if (!isHydrating) {
    popToNextHostParent(fiber)
    isHydrating = true
    return false
  }

  const type = fiber.type

  if (
    fiber.tag !== HostComponent ||
    (type !== 'head' &&
      type !== 'body' &&
      !shouldSetTextContent(type, fiber.memoizedProps))
  ) {
    let nextInstance = nextHydratableInstance
    while (nextInstance) {
      deleteHydratableInstance(fiber, nextInstance)
      nextInstance = getNextHydratableSibling(nextInstance)
    }
  }

  popToNextHostParent(fiber)
  nextHydratableInstance = hydrationParentFiber
    ? getNextHydratableSibling(fiber.stateNode)
    : null
  return true
}

prepareToHydrateHostInstance

这个方法很像completeWork中对于HostComponentupdateHostComponent,调用hydrateInstance并返回payload,这个方法类似于finalizeInitialChildrendiffProperties的结合,执行了事件的初始化绑定,并对比属性变化并返回updatePayload。具体内容就不深究了,有兴趣的同学可以自己去看。

function prepareToHydrateHostInstance(
  fiber: Fiber,
  rootContainerInstance: Container,
  hostContext: HostContext,
): boolean {
  const instance: Instance = fiber.stateNode
  const updatePayload = hydrateInstance(
    instance,
    fiber.type,
    fiber.memoizedProps,
    rootContainerInstance,
    hostContext,
    fiber,
  )
  fiber.updateQueue = (updatePayload: any)
  if (updatePayload !== null) {
    return true
  }
  return false
}

export function hydrateInstance(
  instance: Instance,
  type: string,
  props: Props,
  rootContainerInstance: Container,
  hostContext: HostContext,
  internalInstanceHandle: Object,
): null | Array<mixed> {
  precacheFiberNode(internalInstanceHandle, instance)
  updateFiberProps(instance, props)
  let parentNamespace: string
  parentNamespace = ((hostContext: any): HostContextProd)
  return diffHydratedProperties(
    instance,
    type,
    props,
    parentNamespace,
    rootContainerInstance,
  )
}

prepareToHydrateHostTextInstance

对于文字节点判断是否需要更新

function prepareToHydrateHostTextInstance(fiber: Fiber): boolean {
  const textInstance: TextInstance = fiber.stateNode
  const textContent: string = fiber.memoizedProps
  const shouldUpdate = hydrateTextInstance(textInstance, textContent, fiber)
  return shouldUpdate
}

export function hydrateTextInstance(
  textInstance: TextInstance,
  text: string,
  internalInstanceHandle: Object,
): boolean {
  precacheFiberNode(internalInstanceHandle, textInstance)
  return diffHydratedText(textInstance, text)
}

export function diffHydratedText(textNode: Text, text: string): boolean {
  const isDifferent = textNode.nodeValue !== text
  return isDifferent
}

删除多余的 DOM 节点

deleteHydratableInstance

最终的删除要放到commit阶段去做,所以这里专门为了记录删除行为创建了Fiber对象,用于记录side effect,并加入到了effect链上。

function deleteHydratableInstance(
  returnFiber: Fiber,
  instance: HydratableInstance,
) {
  const childToDelete = createFiberFromHostInstanceForDeletion()
  childToDelete.stateNode = instance
  childToDelete.return = returnFiber
  childToDelete.effectTag = Deletion

  if (returnFiber.lastEffect !== null) {
    returnFiber.lastEffect.nextEffect = childToDelete
    returnFiber.lastEffect = childToDelete
  } else {
    returnFiber.firstEffect = returnFiber.lastEffect = childToDelete
  }
}

export function createFiberFromHostInstanceForDeletion(): Fiber {
  const fiber = createFiber(HostComponent, null, null, NoContext)
  // TODO: These should not need a type.
  fiber.elementType = 'DELETED'
  fiber.type = 'DELETED'
  return fiber
}

results matching ""

    No results matching ""

    Jokcy的二维码

    扫码添加Jokcy,更多更新更优质的前端学习内容不断更新中,期待与你一起成长!

    Jokcy的二维码