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。