-
-
Notifications
You must be signed in to change notification settings - Fork 8.3k
/
componentRenderUtils.ts
469 lines (444 loc) · 13.2 KB
/
componentRenderUtils.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
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'
import { setTransitionHooks } from './components/BaseTransition'
/**
* 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,
} = 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 or teleport 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.`,
)
}
setTransitionHooks(root, 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
}
}
}