Fragment,Portal和Suspense

6 篇文章 0 订阅
3 篇文章 0 订阅

Fragment:

<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8" />
  <title>test</title>
</head>

<body>
  <div id="container"></div>
</body>
<script src="./vue.global.js"></script>
<script>
  let app = Vue.createApp()
  const App = {
    setup() {
      let message = Vue.reactive({
        name: '张三',
        age: 18
      })
      return {
        message
      }
    },
    render() {
      return Vue.h(Vue.Fragment, {}, [Vue.h('p', {
          style: {
            'font-size': '30px',
            'color': 'red'
          },
          onclick: () => {
            this.message.age++
          },
        }, this.message.age), Vue.h('p', null,this.message.name),
      ])
    }
  }

  app.mount(App, container)
</script>

</html>

浏览器生成的节点为:

<div id="container">
  <!--fragment-0-start-->
    <p style="font-size: 30px; color: red;">18</p>
    <p>张三</p>
  <!--fragment-0-end-->
</div>

通过代码和结果其实就能猜出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同样也没有黑科技,使用方法如下:

<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8" />
  <title>test</title>
</head>

<body>
  <div id="container"></div>
  <div id="test"></div>
</body>
<script src="./vue.global.js"></script>
<script>
  let app = Vue.createApp()
  const App = {
    setup() {
      let message = Vue.reactive({
        name: '张三',
        age: 18
      })
      return {
        message
      }
    },
    render() {
      return Vue.h(Vue.Fragment, {}, [Vue.h('p', {
          style: {
            'font-size': '30px',
            'color': 'red'
          },
          onclick: () => {
            this.message.age++
          },
        }, this.message.age), Vue.h('p', null,this.message.name),
        Vue.h(Vue.Portal, {
            target:'#test'
        },this.message.age),
      ])
    }
  }

  app.mount(App, container)
</script>

</html>

浏览器生成的节点为:

<div id="container">
    <!---->
    <p style="font-size: 30px; color: red;">18</p>
    <p>张三</p>
    <!--18-->
    <!---->
</div>
<div id="test">18</div>

惊讶的发现,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实现原理其实同样也很简单,先看应用场景:

<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8" />
  <title>test</title>
</head>

<body>
  <div id="container"></div>
</body>
<script src="./vue.global.js"></script>
<script>
  function createAsyncComponent(component) {
    return {
      setup() {
        const p = new Promise(resolve => {
          setTimeout(() => {
            resolve(() => Vue.h('div', '加载完成'))
          }, 1000)
        })
        return p
      }
    }
  }
  const Async = createAsyncComponent()
  let app = Vue.createApp()
  const App = {
    setup() {
      let message = Vue.reactive({
        name: '张三',
        age: 18
      })
      return {
        message
      }
    },
    render() {
      return Vue.h(Vue.Suspense, null, {
        default: Vue.h(Async),
        fallback: Vue.h('div', '加载中')
      })
    }
  }

  app.mount(App, container)
</script>

</html>

现象为先展示加载中,一秒后结果变成加载完成。其实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。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值