vue3-core/packages/runtime-core/src/componentRenderUtils.ts

472 lines
13 KiB
TypeScript

import {
type ComponentInternalInstance,
type Data,
type FunctionalComponent,
getComponentName,
} from './component'
import {
Comment,
type VNode,
type VNodeArrayChildren,
blockStack,
cloneVNode,
createVNode,
isVNode,
normalizeVNode,
} from './vnode'
import { ErrorCodes, handleError } from './errorHandling'
import { PatchFlags, ShapeFlags, isModelListener, isOn } from '@vue/shared'
import { warn } from './warning'
import { isHmrUpdating } from './hmr'
import type { NormalizedProps } from './componentProps'
import { isEmitListener } from './componentEmits'
import { setCurrentRenderingInstance } from './componentRenderContext'
import {
DeprecationTypes,
isCompatEnabled,
warnDeprecation,
} from './compat/compatConfig'
import { shallowReadonly } from '@vue/reactivity'
/**
* dev only flag to track whether $attrs was used during render.
* If $attrs was used during render then the warning for failed attrs
* fallthrough can be suppressed.
*/
let accessedAttrs: boolean = false
export function markAttrsAccessed(): void {
accessedAttrs = true
}
type SetRootFn = ((root: VNode) => void) | undefined
export function renderComponentRoot(
instance: ComponentInternalInstance,
): VNode {
const {
type: Component,
vnode,
proxy,
withProxy,
propsOptions: [propsOptions],
slots,
attrs,
emit,
render,
renderCache,
props,
data,
setupState,
ctx,
inheritAttrs,
isMounted,
} = instance
const prev = setCurrentRenderingInstance(instance)
let result
let fallthroughAttrs
if (__DEV__) {
accessedAttrs = false
}
try {
if (vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {
// withProxy is a proxy with a different `has` trap only for
// runtime-compiled render functions using `with` block.
const proxyToUse = withProxy || proxy
// 'this' isn't available in production builds with `<script setup>`,
// so warn if it's used in dev.
const thisProxy =
__DEV__ && setupState.__isScriptSetup
? new Proxy(proxyToUse!, {
get(target, key, receiver) {
warn(
`Property '${String(
key,
)}' was accessed via 'this'. Avoid using 'this' in templates.`,
)
return Reflect.get(target, key, receiver)
},
})
: proxyToUse
result = normalizeVNode(
render!.call(
thisProxy,
proxyToUse!,
renderCache,
__DEV__ ? shallowReadonly(props) : props,
setupState,
data,
ctx,
),
)
fallthroughAttrs = attrs
} else {
// functional
const render = Component as FunctionalComponent
// in dev, mark attrs accessed if optional props (attrs === props)
if (__DEV__ && attrs === props) {
markAttrsAccessed()
}
result = normalizeVNode(
render.length > 1
? render(
__DEV__ ? shallowReadonly(props) : props,
__DEV__
? {
get attrs() {
markAttrsAccessed()
return shallowReadonly(attrs)
},
slots,
emit,
}
: { attrs, slots, emit },
)
: render(
__DEV__ ? shallowReadonly(props) : props,
null as any /* we know it doesn't need it */,
),
)
fallthroughAttrs = Component.props
? attrs
: getFunctionalFallthrough(attrs)
}
} catch (err) {
blockStack.length = 0
handleError(err, instance, ErrorCodes.RENDER_FUNCTION)
result = createVNode(Comment)
}
// attr merging
// in dev mode, comments are preserved, and it's possible for a template
// to have comments along side the root element which makes it a fragment
let root = result
let setRoot: SetRootFn = undefined
if (
__DEV__ &&
result.patchFlag > 0 &&
result.patchFlag & PatchFlags.DEV_ROOT_FRAGMENT
) {
;[root, setRoot] = getChildRoot(result)
}
if (fallthroughAttrs && inheritAttrs !== false) {
const keys = Object.keys(fallthroughAttrs)
const { shapeFlag } = root
if (keys.length) {
if (shapeFlag & (ShapeFlags.ELEMENT | ShapeFlags.COMPONENT)) {
if (propsOptions && keys.some(isModelListener)) {
// If a v-model listener (onUpdate:xxx) has a corresponding declared
// prop, it indicates this component expects to handle v-model and
// it should not fallthrough.
// related: #1543, #1643, #1989
fallthroughAttrs = filterModelListeners(
fallthroughAttrs,
propsOptions,
)
}
root = cloneVNode(root, fallthroughAttrs, false, true)
} else if (__DEV__ && !accessedAttrs && root.type !== Comment) {
const allAttrs = Object.keys(attrs)
const eventAttrs: string[] = []
const extraAttrs: string[] = []
for (let i = 0, l = allAttrs.length; i < l; i++) {
const key = allAttrs[i]
if (isOn(key)) {
// ignore v-model handlers when they fail to fallthrough
if (!isModelListener(key)) {
// remove `on`, lowercase first letter to reflect event casing
// accurately
eventAttrs.push(key[2].toLowerCase() + key.slice(3))
}
} else {
extraAttrs.push(key)
}
}
if (extraAttrs.length) {
warn(
`Extraneous non-props attributes (` +
`${extraAttrs.join(', ')}) ` +
`were passed to component but could not be automatically inherited ` +
`because component renders fragment or text root nodes.`,
)
}
if (eventAttrs.length) {
warn(
`Extraneous non-emits event listeners (` +
`${eventAttrs.join(', ')}) ` +
`were passed to component but could not be automatically inherited ` +
`because component renders fragment or text root nodes. ` +
`If the listener is intended to be a component custom event listener only, ` +
`declare it using the "emits" option.`,
)
}
}
}
}
if (
__COMPAT__ &&
isCompatEnabled(DeprecationTypes.INSTANCE_ATTRS_CLASS_STYLE, instance) &&
vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT &&
root.shapeFlag & (ShapeFlags.ELEMENT | ShapeFlags.COMPONENT)
) {
const { class: cls, style } = vnode.props || {}
if (cls || style) {
if (__DEV__ && inheritAttrs === false) {
warnDeprecation(
DeprecationTypes.INSTANCE_ATTRS_CLASS_STYLE,
instance,
getComponentName(instance.type),
)
}
root = cloneVNode(
root,
{
class: cls,
style: style,
},
false,
true,
)
}
}
// inherit directives
if (vnode.dirs) {
if (__DEV__ && !isElementRoot(root)) {
warn(
`Runtime directive used on component with non-element root node. ` +
`The directives will not function as intended.`,
)
}
// clone before mutating since the root may be a hoisted vnode
root = cloneVNode(root, null, false, true)
root.dirs = root.dirs ? root.dirs.concat(vnode.dirs) : vnode.dirs
}
// inherit transition data
if (vnode.transition) {
if (__DEV__ && !isElementRoot(root)) {
warn(
`Component inside <Transition> renders non-element root node ` +
`that cannot be animated.`,
)
}
root.transition = isMounted
? vnode.component!.subTree.transition!
: vnode.transition
}
if (__DEV__ && setRoot) {
setRoot(root)
} else {
result = root
}
setCurrentRenderingInstance(prev)
return result
}
/**
* dev only
* In dev mode, template root level comments are rendered, which turns the
* template into a fragment root, but we need to locate the single element
* root for attrs and scope id processing.
*/
const getChildRoot = (vnode: VNode): [VNode, SetRootFn] => {
const rawChildren = vnode.children as VNodeArrayChildren
const dynamicChildren = vnode.dynamicChildren
const childRoot = filterSingleRoot(rawChildren, false)
if (!childRoot) {
return [vnode, undefined]
} else if (
__DEV__ &&
childRoot.patchFlag > 0 &&
childRoot.patchFlag & PatchFlags.DEV_ROOT_FRAGMENT
) {
return getChildRoot(childRoot)
}
const index = rawChildren.indexOf(childRoot)
const dynamicIndex = dynamicChildren ? dynamicChildren.indexOf(childRoot) : -1
const setRoot: SetRootFn = (updatedRoot: VNode) => {
rawChildren[index] = updatedRoot
if (dynamicChildren) {
if (dynamicIndex > -1) {
dynamicChildren[dynamicIndex] = updatedRoot
} else if (updatedRoot.patchFlag > 0) {
vnode.dynamicChildren = [...dynamicChildren, updatedRoot]
}
}
}
return [normalizeVNode(childRoot), setRoot]
}
export function filterSingleRoot(
children: VNodeArrayChildren,
recurse = true,
): VNode | undefined {
let singleRoot
for (let i = 0; i < children.length; i++) {
const child = children[i]
if (isVNode(child)) {
// ignore user comment
if (child.type !== Comment || child.children === 'v-if') {
if (singleRoot) {
// has more than 1 non-comment child, return now
return
} else {
singleRoot = child
if (
__DEV__ &&
recurse &&
singleRoot.patchFlag > 0 &&
singleRoot.patchFlag & PatchFlags.DEV_ROOT_FRAGMENT
) {
return filterSingleRoot(singleRoot.children as VNodeArrayChildren)
}
}
}
} else {
return
}
}
return singleRoot
}
const getFunctionalFallthrough = (attrs: Data): Data | undefined => {
let res: Data | undefined
for (const key in attrs) {
if (key === 'class' || key === 'style' || isOn(key)) {
;(res || (res = {}))[key] = attrs[key]
}
}
return res
}
const filterModelListeners = (attrs: Data, props: NormalizedProps): Data => {
const res: Data = {}
for (const key in attrs) {
if (!isModelListener(key) || !(key.slice(9) in props)) {
res[key] = attrs[key]
}
}
return res
}
const isElementRoot = (vnode: VNode) => {
return (
vnode.shapeFlag & (ShapeFlags.COMPONENT | ShapeFlags.ELEMENT) ||
vnode.type === Comment // potential v-if branch switch
)
}
export function shouldUpdateComponent(
prevVNode: VNode,
nextVNode: VNode,
optimized?: boolean,
): boolean {
const { props: prevProps, children: prevChildren, component } = prevVNode
const { props: nextProps, children: nextChildren, patchFlag } = nextVNode
const emits = component!.emitsOptions
// Parent component's render function was hot-updated. Since this may have
// caused the child component's slots content to have changed, we need to
// force the child to update as well.
if (__DEV__ && (prevChildren || nextChildren) && isHmrUpdating) {
return true
}
// force child update for runtime directive or transition on component vnode.
if (nextVNode.dirs || nextVNode.transition) {
return true
}
if (optimized && patchFlag >= 0) {
if (patchFlag & PatchFlags.DYNAMIC_SLOTS) {
// slot content that references values that might have changed,
// e.g. in a v-for
return true
}
if (patchFlag & PatchFlags.FULL_PROPS) {
if (!prevProps) {
return !!nextProps
}
// presence of this flag indicates props are always non-null
return hasPropsChanged(prevProps, nextProps!, emits)
} else if (patchFlag & PatchFlags.PROPS) {
const dynamicProps = nextVNode.dynamicProps!
for (let i = 0; i < dynamicProps.length; i++) {
const key = dynamicProps[i]
if (
nextProps![key] !== prevProps![key] &&
!isEmitListener(emits, key)
) {
return true
}
}
}
} else {
// this path is only taken by manually written render functions
// so presence of any children leads to a forced update
if (prevChildren || nextChildren) {
if (!nextChildren || !(nextChildren as any).$stable) {
return true
}
}
if (prevProps === nextProps) {
return false
}
if (!prevProps) {
return !!nextProps
}
if (!nextProps) {
return true
}
return hasPropsChanged(prevProps, nextProps, emits)
}
return false
}
function hasPropsChanged(
prevProps: Data,
nextProps: Data,
emitsOptions: ComponentInternalInstance['emitsOptions'],
): boolean {
const nextKeys = Object.keys(nextProps)
if (nextKeys.length !== Object.keys(prevProps).length) {
return true
}
for (let i = 0; i < nextKeys.length; i++) {
const key = nextKeys[i]
if (
nextProps[key] !== prevProps[key] &&
!isEmitListener(emitsOptions, key)
) {
return true
}
}
return false
}
export function updateHOCHostEl(
{ vnode, parent }: ComponentInternalInstance,
el: typeof vnode.el, // HostNode
): void {
while (parent) {
const root = parent.subTree
if (root.suspense && root.suspense.activeBranch === vnode) {
root.el = vnode.el
}
if (root === vnode) {
;(vnode = parent.vnode).el = el
parent = parent.parent
} else {
break
}
}
}