422 lines
12 KiB
TypeScript
422 lines
12 KiB
TypeScript
import {
|
|
ComponentInternalInstance,
|
|
FunctionalComponent,
|
|
Data,
|
|
getComponentName
|
|
} from './component'
|
|
import {
|
|
VNode,
|
|
normalizeVNode,
|
|
createVNode,
|
|
Comment,
|
|
cloneVNode,
|
|
VNodeArrayChildren,
|
|
isVNode,
|
|
blockStack
|
|
} from './vnode'
|
|
import { handleError, ErrorCodes } from './errorHandling'
|
|
import { PatchFlags, ShapeFlags, isOn, isModelListener } from '@vue/shared'
|
|
import { warn } from './warning'
|
|
import { isHmrUpdating } from './hmr'
|
|
import { NormalizedProps } from './componentProps'
|
|
import { isEmitListener } from './componentEmits'
|
|
import { setCurrentRenderingInstance } from './componentRenderContext'
|
|
import {
|
|
DeprecationTypes,
|
|
isCompatEnabled,
|
|
warnDeprecation
|
|
} from './compat/compatConfig'
|
|
|
|
/**
|
|
* 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() {
|
|
accessedAttrs = true
|
|
}
|
|
|
|
export function renderComponentRoot(
|
|
instance: ComponentInternalInstance
|
|
): VNode {
|
|
const {
|
|
type: Component,
|
|
vnode,
|
|
proxy,
|
|
withProxy,
|
|
props,
|
|
propsOptions: [propsOptions],
|
|
slots,
|
|
attrs,
|
|
emit,
|
|
render,
|
|
renderCache,
|
|
data,
|
|
setupState,
|
|
ctx
|
|
} = instance
|
|
|
|
let result
|
|
const prev = setCurrentRenderingInstance(instance)
|
|
if (__DEV__) {
|
|
accessedAttrs = false
|
|
}
|
|
try {
|
|
let fallthroughAttrs
|
|
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
|
|
result = normalizeVNode(
|
|
render!.call(
|
|
proxyToUse,
|
|
proxyToUse!,
|
|
renderCache,
|
|
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(
|
|
props,
|
|
__DEV__
|
|
? {
|
|
get attrs() {
|
|
markAttrsAccessed()
|
|
return attrs
|
|
},
|
|
slots,
|
|
emit
|
|
}
|
|
: { attrs, slots, emit }
|
|
)
|
|
: render(props, null as any /* we know it doesn't need it */)
|
|
)
|
|
fallthroughAttrs = Component.props
|
|
? attrs
|
|
: getFunctionalFallthrough(attrs)
|
|
}
|
|
|
|
// 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: ((root: VNode) => void) | undefined = undefined
|
|
if (
|
|
__DEV__ &&
|
|
result.patchFlag > 0 &&
|
|
result.patchFlag & PatchFlags.DEV_ROOT_FRAGMENT
|
|
) {
|
|
;[root, setRoot] = getChildRoot(result)
|
|
}
|
|
|
|
if (fallthroughAttrs && Component.inheritAttrs !== false) {
|
|
const keys = Object.keys(fallthroughAttrs)
|
|
const { shapeFlag } = root
|
|
if (keys.length) {
|
|
if (
|
|
shapeFlag & ShapeFlags.ELEMENT ||
|
|
shapeFlag & 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)
|
|
} 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 ||
|
|
root.shapeFlag & ShapeFlags.COMPONENT)
|
|
) {
|
|
const { class: cls, style } = vnode.props || {}
|
|
if (cls || style) {
|
|
if (__DEV__ && Component.inheritAttrs === false) {
|
|
warnDeprecation(
|
|
DeprecationTypes.INSTANCE_ATTRS_CLASS_STYLE,
|
|
instance,
|
|
getComponentName(instance.type)
|
|
)
|
|
}
|
|
root = cloneVNode(root, {
|
|
class: cls,
|
|
style: style
|
|
})
|
|
}
|
|
}
|
|
|
|
// 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.`
|
|
)
|
|
}
|
|
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 = vnode.transition
|
|
}
|
|
|
|
if (__DEV__ && setRoot) {
|
|
setRoot(root)
|
|
} else {
|
|
result = root
|
|
}
|
|
} catch (err) {
|
|
blockStack.length = 0
|
|
handleError(err, instance, ErrorCodes.RENDER_FUNCTION)
|
|
result = createVNode(Comment)
|
|
}
|
|
|
|
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, ((root: VNode) => void) | undefined] => {
|
|
const rawChildren = vnode.children as VNodeArrayChildren
|
|
const dynamicChildren = vnode.dynamicChildren
|
|
const childRoot = filterSingleRoot(rawChildren)
|
|
if (!childRoot) {
|
|
return [vnode, undefined]
|
|
}
|
|
const index = rawChildren.indexOf(childRoot)
|
|
const dynamicIndex = dynamicChildren ? dynamicChildren.indexOf(childRoot) : -1
|
|
const setRoot = (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
|
|
): 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
|
|
}
|
|
}
|
|
} 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 ||
|
|
vnode.shapeFlag & 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
|
|
) {
|
|
while (parent && parent.subTree === vnode) {
|
|
;(vnode = parent.vnode).el = el
|
|
parent = parent.parent
|
|
}
|
|
}
|