Fragment,Portal和Suspense

 

Fragment:





  
  test



  

浏览器生成的节点为:

18

张三

通过代码和结果其实就能猜出Fragment的作用了。vue渲染组件需要有一个根节点,写过vue2的应该都知道一个组件只能有一个根节点,通常我们会在组件套一层div,然而这个div其实是无用的节点,Fragment其实也是一个根节点,只不过他声称的是一段注释。

解析Fragment主要源码如下:

function processFragment(
    n1: HostVNode | null,
    n2: HostVNode,
    container: HostElement,
    anchor: HostNode | null,
    parentComponent: ComponentInternalInstance | null,
    parentSuspense: HostSuspenseBoundary | null,
    isSVG: boolean,
    optimized: boolean
  ) {
    const showID = __DEV__ && !__TEST__
    const fragmentStartAnchor = (n2.el = n1
      ? n1.el
      : hostCreateComment(showID ? `fragment-${devFragmentID}-start` : ''))!
    const fragmentEndAnchor = (n2.anchor = n1
      ? n1.anchor
      : hostCreateComment(showID ? `fragment-${devFragmentID}-end` : ''))!
    if (showID) {
      devFragmentID++
    }
    if (n1 == null) {
      hostInsert(fragmentStartAnchor, container, anchor)
      hostInsert(fragmentEndAnchor, container, anchor)
      // a fragment can only have array children
      // since they are either generated by the compiler, or implicitly created
      // from arrays.
      mountChildren(
        n2.children as HostVNodeChildren,
        container,
        fragmentEndAnchor,
        parentComponent,
        parentSuspense,
        isSVG,
        optimized
      )
    } else {
      patchChildren(
        n1,
        n2,
        container,
        fragmentEndAnchor,
        parentComponent,
        parentSuspense,
        isSVG,
        optimized
      )
    }
  }

和vue2渲染原理一样,根据render渲染函数将其生成一个个vnode节点(如果是vue模版则生成ast树转换成render函数),然后从根节点根据不同的节点type进行渲染,当type为symbol(Fragment)时,执行processFragment函数。函数主要逻辑其实很简单,通过hostInsert函数插入两行注释代码,然后将children节点插入到第二行注释节点锚点之前。

Fragment并没有黑科技,只是将组件的根vnode变为2行注释代码。

Portal:

Portal同样也没有黑科技,使用方法如下:





  
  test



  

浏览器生成的节点为:

18

张三

18

惊讶的发现,vue实例挂载的是container节点,但是test居然也渲染出了数据,并且当点击container的年龄时,test中的年龄也随之改变。这个功能对于全剧弹框简直太棒了。其远离其实就几行代码:

function processPortal(
    n1: HostVNode | null,
    n2: HostVNode,
    container: HostElement,
    anchor: HostNode | null,
    parentComponent: ComponentInternalInstance | null,
    parentSuspense: HostSuspenseBoundary | null,
    isSVG: boolean,
    optimized: boolean
  ) {
    const targetSelector = n2.props && n2.props.target
    const { patchFlag, shapeFlag, children } = n2
    if (n1 == null) {
      const target = (n2.target = isString(targetSelector)
        ? hostQuerySelector(targetSelector)
        : null)
      if (target != null) {
        if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
          hostSetElementText(target, children as string)
        } else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
          mountChildren(
            children as HostVNodeChildren,
            target,
            null,
            parentComponent,
            parentSuspense,
            isSVG
          )
        }
      } else if (__DEV__) {
        warn('Invalid Portal target on mount:', target, `(${typeof target})`)
      }
    } else {
      // update content
      const target = (n2.target = n1.target)!
      if (patchFlag === PatchFlags.TEXT) {
        hostSetElementText(target, children as string)
      } else if (!optimized) {
        patchChildren(
          n1,
          n2,
          target,
          null,
          parentComponent,
          parentSuspense,
          isSVG
        )
      }
      // target changed
      if (targetSelector !== (n1.props && n1.props.target)) {
        const nextTarget = (n2.target = isString(targetSelector)
          ? hostQuerySelector(targetSelector)
          : null)
        if (nextTarget != null) {
          // move content
          if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
            hostSetElementText(target, '')
            hostSetElementText(nextTarget, children as string)
          } else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
            for (let i = 0; i < (children as HostVNode[]).length; i++) {
              move((children as HostVNode[])[i], nextTarget, null)
            }
          }
        } else if (__DEV__) {
          warn('Invalid Portal target on update:', target, `(${typeof target})`)
        }
      }
    }
    // insert an empty node as the placeholder for the portal
    processCommentNode(n1, n2, container, anchor)
  }

代码主要做的就是一件事,通过target属性找到对应的dom节点,然后将Portal vnode节点下的children渲染在对应的dom节点中。

Suspense:

Suspense实现原理其实同样也很简单,先看应用场景:





  
  test



  

现象为先展示加载中,一秒后结果变成加载完成。其实Suspense的实现原理采用的是slot,Suspense核心源码如下:

function mountSuspense(
  n2: VNode,
  container: object,
  anchor: object | null,
  parentComponent: ComponentInternalInstance | null,
  parentSuspense: SuspenseBoundary | null,
  isSVG: boolean,
  optimized: boolean,
  rendererInternals: RendererInternals
) {
  const {
    patch,
    options: { createElement }
  } = rendererInternals
  const hiddenContainer = createElement('div')
  const suspense = (n2.suspense = createSuspenseBoundary(
    n2,
    parentSuspense,
    parentComponent,
    container,
    hiddenContainer,
    anchor,
    isSVG,
    optimized,
    rendererInternals
  ))

  const { content, fallback } = normalizeSuspenseChildren(n2)
  suspense.subTree = content
  suspense.fallbackTree = fallback
  // start mounting the content subtree in an off-dom container
  patch(
    null,
    content,
    hiddenContainer,
    null,
    parentComponent,
    suspense,
    isSVG,
    optimized
  )
  // now check if we have encountered any async deps
  if (suspense.deps > 0) {
    // mount the fallback tree
    patch(
      null,
      fallback,
      container,
      anchor,
      parentComponent,
      null, // fallback tree will not have suspense context
      isSVG,
      optimized
    )
    n2.el = fallback.el
  } else {
    // Suspense has no async deps. Just resolve.
    suspense.resolve()
  }
}

主要就是干2件事,首先会patch default插槽(组件a,挂载节点为为一个隐藏的hiddenContainer,渲染加载完成),然后判断有没有异步deps,如果没有则将组件a从一个隐藏的hiddenContainer移动到container上(suspense.resolve()),如果是异步的那么patch fallback插槽(组件b,渲染加载中),很明显如果组件a为一个异步组件,那么会先渲染组件b,那么我们什么时候知道组件a是否渲染完成并且替换组件b的内容呢,在第一次patch default插槽时,会判断是否为异步,如果是异步会执行如下代码:

registerDep(instance, setupRenderEffect) {
      // suspense is already resolved, need to recede.
      // use queueJob so it's handled synchronously after patching the current
      // suspense tree
      if (suspense.isResolved) {
        queueJob(() => {
          suspense.recede()
        })
      }

      suspense.deps++
      instance
        .asyncDep!.catch(err => {
          handleError(err, instance, ErrorCodes.SETUP_FUNCTION)
        })
        .then(asyncSetupResult => {
          // retry when the setup() promise resolves.
          // component may have been unmounted before resolve.
          if (instance.isUnmounted || suspense.isUnmounted) {
            return
          }
          suspense.deps--
          // retry from this component
          instance.asyncResolved = true
          const { vnode } = instance
          if (__DEV__) {
            pushWarningContext(vnode)
          }
          handleSetupResult(instance, asyncSetupResult, suspense)
          setupRenderEffect(
            instance,
            parentComponent,
            suspense,
            vnode,
            // component may have been moved before resolve
            parentNode(instance.subTree.el)!,
            next(instance.subTree),
            isSVG
          )
          updateHOCHostEl(instance, vnode.el)
          if (__DEV__) {
            popWarningContext()
          }
          if (suspense.deps === 0) {
            suspense.resolve()
          }
        })
    },

当完成异步后,执行then里的函数,然后执行组件a的生命周期,然后通过suspense的resolve方法将组件a从一个隐藏的hiddenContainer移动到container上,即完成了替换组件b。

你可能感兴趣的:(vue3,前端)