componentRenderUtils.ts 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503
  1. import {
  2. type ComponentInternalInstance,
  3. type Data,
  4. type FunctionalComponent,
  5. getComponentName,
  6. } from './component'
  7. import {
  8. Comment,
  9. type VNode,
  10. type VNodeArrayChildren,
  11. blockStack,
  12. cloneVNode,
  13. createVNode,
  14. isVNode,
  15. normalizeVNode,
  16. } from './vnode'
  17. import { ErrorCodes, handleError } from './errorHandling'
  18. import {
  19. PatchFlags,
  20. ShapeFlags,
  21. isModelListener,
  22. isObject,
  23. isOn,
  24. looseEqual,
  25. } from '@vue/shared'
  26. import { warn } from './warning'
  27. import { isHmrUpdating } from './hmr'
  28. import type { NormalizedProps } from './componentProps'
  29. import { isEmitListener } from './componentEmits'
  30. import { setCurrentRenderingInstance } from './componentRenderContext'
  31. import {
  32. DeprecationTypes,
  33. isCompatEnabled,
  34. warnDeprecation,
  35. } from './compat/compatConfig'
  36. import { shallowReadonly } from '@vue/reactivity'
  37. import { setTransitionHooks } from './components/BaseTransition'
  38. /**
  39. * dev only flag to track whether $attrs was used during render.
  40. * If $attrs was used during render then the warning for failed attrs
  41. * fallthrough can be suppressed.
  42. */
  43. let accessedAttrs: boolean = false
  44. export function markAttrsAccessed(): void {
  45. accessedAttrs = true
  46. }
  47. type SetRootFn = ((root: VNode) => void) | undefined
  48. export function renderComponentRoot(
  49. instance: ComponentInternalInstance,
  50. ): VNode {
  51. const {
  52. type: Component,
  53. vnode,
  54. proxy,
  55. withProxy,
  56. propsOptions: [propsOptions],
  57. slots,
  58. attrs,
  59. emit,
  60. render,
  61. renderCache,
  62. props,
  63. data,
  64. setupState,
  65. ctx,
  66. inheritAttrs,
  67. } = instance
  68. const prev = setCurrentRenderingInstance(instance)
  69. let result
  70. let fallthroughAttrs
  71. if (__DEV__) {
  72. accessedAttrs = false
  73. }
  74. try {
  75. if (vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {
  76. // withProxy is a proxy with a different `has` trap only for
  77. // runtime-compiled render functions using `with` block.
  78. const proxyToUse = withProxy || proxy
  79. // 'this' isn't available in production builds with `<script setup>`,
  80. // so warn if it's used in dev.
  81. const thisProxy =
  82. __DEV__ && setupState.__isScriptSetup
  83. ? new Proxy(proxyToUse!, {
  84. get(target, key, receiver) {
  85. warn(
  86. `Property '${String(
  87. key,
  88. )}' was accessed via 'this'. Avoid using 'this' in templates.`,
  89. )
  90. return Reflect.get(target, key, receiver)
  91. },
  92. })
  93. : proxyToUse
  94. result = normalizeVNode(
  95. render!.call(
  96. thisProxy,
  97. proxyToUse!,
  98. renderCache,
  99. __DEV__ ? shallowReadonly(props) : props,
  100. setupState,
  101. data,
  102. ctx,
  103. ),
  104. )
  105. fallthroughAttrs = attrs
  106. } else {
  107. // functional
  108. const render = Component as FunctionalComponent
  109. // in dev, mark attrs accessed if optional props (attrs === props)
  110. if (__DEV__ && attrs === props) {
  111. markAttrsAccessed()
  112. }
  113. result = normalizeVNode(
  114. render.length > 1
  115. ? render(
  116. __DEV__ ? shallowReadonly(props) : props,
  117. __DEV__
  118. ? {
  119. get attrs() {
  120. markAttrsAccessed()
  121. return shallowReadonly(attrs)
  122. },
  123. slots,
  124. emit,
  125. }
  126. : { attrs, slots, emit },
  127. )
  128. : render(
  129. __DEV__ ? shallowReadonly(props) : props,
  130. null as any /* we know it doesn't need it */,
  131. ),
  132. )
  133. fallthroughAttrs = Component.props
  134. ? attrs
  135. : getFunctionalFallthrough(attrs)
  136. }
  137. } catch (err) {
  138. blockStack.length = 0
  139. handleError(err, instance, ErrorCodes.RENDER_FUNCTION)
  140. result = createVNode(Comment)
  141. }
  142. // attr merging
  143. // in dev mode, comments are preserved, and it's possible for a template
  144. // to have comments along side the root element which makes it a fragment
  145. let root = result
  146. let setRoot: SetRootFn = undefined
  147. if (
  148. __DEV__ &&
  149. result.patchFlag > 0 &&
  150. result.patchFlag & PatchFlags.DEV_ROOT_FRAGMENT
  151. ) {
  152. ;[root, setRoot] = getChildRoot(result)
  153. }
  154. if (fallthroughAttrs && inheritAttrs !== false) {
  155. const keys = Object.keys(fallthroughAttrs)
  156. const { shapeFlag } = root
  157. if (keys.length) {
  158. if (shapeFlag & (ShapeFlags.ELEMENT | ShapeFlags.COMPONENT)) {
  159. if (propsOptions && keys.some(isModelListener)) {
  160. // If a v-model listener (onUpdate:xxx) has a corresponding declared
  161. // prop, it indicates this component expects to handle v-model and
  162. // it should not fallthrough.
  163. // related: #1543, #1643, #1989
  164. fallthroughAttrs = filterModelListeners(
  165. fallthroughAttrs,
  166. propsOptions,
  167. )
  168. }
  169. root = cloneVNode(root, fallthroughAttrs, false, true)
  170. } else if (__DEV__ && !accessedAttrs && root.type !== Comment) {
  171. warnExtraneousAttributes(attrs)
  172. }
  173. }
  174. }
  175. if (
  176. __COMPAT__ &&
  177. isCompatEnabled(DeprecationTypes.INSTANCE_ATTRS_CLASS_STYLE, instance) &&
  178. vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT &&
  179. root.shapeFlag & (ShapeFlags.ELEMENT | ShapeFlags.COMPONENT)
  180. ) {
  181. const { class: cls, style } = vnode.props || {}
  182. if (cls || style) {
  183. if (__DEV__ && inheritAttrs === false) {
  184. warnDeprecation(
  185. DeprecationTypes.INSTANCE_ATTRS_CLASS_STYLE,
  186. instance,
  187. getComponentName(instance.type),
  188. )
  189. }
  190. root = cloneVNode(
  191. root,
  192. {
  193. class: cls,
  194. style: style,
  195. },
  196. false,
  197. true,
  198. )
  199. }
  200. }
  201. // inherit directives
  202. if (vnode.dirs) {
  203. if (__DEV__ && !isElementRoot(root)) {
  204. warn(
  205. `Runtime directive used on component with non-element root node. ` +
  206. `The directives will not function as intended.`,
  207. )
  208. }
  209. // clone before mutating since the root may be a hoisted vnode
  210. root = cloneVNode(root, null, false, true)
  211. root.dirs = root.dirs ? root.dirs.concat(vnode.dirs) : vnode.dirs
  212. }
  213. // inherit transition data
  214. if (vnode.transition) {
  215. if (__DEV__ && !isElementRoot(root)) {
  216. warn(
  217. `Component inside <Transition> renders non-element root node ` +
  218. `that cannot be animated.`,
  219. )
  220. }
  221. setTransitionHooks(root, vnode.transition)
  222. }
  223. if (__DEV__ && setRoot) {
  224. setRoot(root)
  225. } else {
  226. result = root
  227. }
  228. setCurrentRenderingInstance(prev)
  229. return result
  230. }
  231. /**
  232. * dev only
  233. * In dev mode, template root level comments are rendered, which turns the
  234. * template into a fragment root, but we need to locate the single element
  235. * root for attrs and scope id processing.
  236. */
  237. const getChildRoot = (vnode: VNode): [VNode, SetRootFn] => {
  238. const rawChildren = vnode.children as VNodeArrayChildren
  239. const dynamicChildren = vnode.dynamicChildren
  240. const childRoot = filterSingleRoot(rawChildren, false)
  241. if (!childRoot) {
  242. return [vnode, undefined]
  243. } else if (
  244. __DEV__ &&
  245. childRoot.patchFlag > 0 &&
  246. childRoot.patchFlag & PatchFlags.DEV_ROOT_FRAGMENT
  247. ) {
  248. return getChildRoot(childRoot)
  249. }
  250. const index = rawChildren.indexOf(childRoot)
  251. const dynamicIndex = dynamicChildren ? dynamicChildren.indexOf(childRoot) : -1
  252. const setRoot: SetRootFn = (updatedRoot: VNode) => {
  253. rawChildren[index] = updatedRoot
  254. if (dynamicChildren) {
  255. if (dynamicIndex > -1) {
  256. dynamicChildren[dynamicIndex] = updatedRoot
  257. } else if (updatedRoot.patchFlag > 0) {
  258. vnode.dynamicChildren = [...dynamicChildren, updatedRoot]
  259. }
  260. }
  261. }
  262. return [normalizeVNode(childRoot), setRoot]
  263. }
  264. /**
  265. * Dev only
  266. */
  267. export function warnExtraneousAttributes(attrs: Record<string, any>): void {
  268. const allAttrs = Object.keys(attrs)
  269. const eventAttrs: string[] = []
  270. const extraAttrs: string[] = []
  271. for (let i = 0, l = allAttrs.length; i < l; i++) {
  272. const key = allAttrs[i]
  273. if (isOn(key)) {
  274. // ignore v-model handlers when they fail to fallthrough
  275. if (!isModelListener(key)) {
  276. // remove `on`, lowercase first letter to reflect event casing
  277. // accurately
  278. eventAttrs.push(key[2].toLowerCase() + key.slice(3))
  279. }
  280. } else {
  281. extraAttrs.push(key)
  282. }
  283. }
  284. if (extraAttrs.length) {
  285. warn(
  286. `Extraneous non-props attributes (` +
  287. `${extraAttrs.join(', ')}) ` +
  288. `were passed to component but could not be automatically inherited ` +
  289. `because component renders fragment or text or teleport root nodes.`,
  290. )
  291. }
  292. if (eventAttrs.length) {
  293. warn(
  294. `Extraneous non-emits event listeners (` +
  295. `${eventAttrs.join(', ')}) ` +
  296. `were passed to component but could not be automatically inherited ` +
  297. `because component renders fragment or text root nodes. ` +
  298. `If the listener is intended to be a component custom event listener only, ` +
  299. `declare it using the "emits" option.`,
  300. )
  301. }
  302. }
  303. export function filterSingleRoot(
  304. children: VNodeArrayChildren,
  305. recurse = true,
  306. ): VNode | undefined {
  307. let singleRoot
  308. for (let i = 0; i < children.length; i++) {
  309. const child = children[i]
  310. if (isVNode(child)) {
  311. // ignore user comment
  312. if (child.type !== Comment || child.children === 'v-if') {
  313. if (singleRoot) {
  314. // has more than 1 non-comment child, return now
  315. return
  316. } else {
  317. singleRoot = child
  318. if (
  319. __DEV__ &&
  320. recurse &&
  321. singleRoot.patchFlag > 0 &&
  322. singleRoot.patchFlag & PatchFlags.DEV_ROOT_FRAGMENT
  323. ) {
  324. return filterSingleRoot(singleRoot.children as VNodeArrayChildren)
  325. }
  326. }
  327. }
  328. } else {
  329. return
  330. }
  331. }
  332. return singleRoot
  333. }
  334. export const getFunctionalFallthrough = (attrs: Data): Data | undefined => {
  335. let res: Data | undefined
  336. for (const key in attrs) {
  337. if (key === 'class' || key === 'style' || isOn(key)) {
  338. ;(res || (res = {}))[key] = attrs[key]
  339. }
  340. }
  341. return res
  342. }
  343. const filterModelListeners = (attrs: Data, props: NormalizedProps): Data => {
  344. const res: Data = {}
  345. for (const key in attrs) {
  346. if (!isModelListener(key) || !(key.slice(9) in props)) {
  347. res[key] = attrs[key]
  348. }
  349. }
  350. return res
  351. }
  352. const isElementRoot = (vnode: VNode) => {
  353. return (
  354. vnode.shapeFlag & (ShapeFlags.COMPONENT | ShapeFlags.ELEMENT) ||
  355. vnode.type === Comment // potential v-if branch switch
  356. )
  357. }
  358. export function shouldUpdateComponent(
  359. prevVNode: VNode,
  360. nextVNode: VNode,
  361. optimized?: boolean,
  362. ): boolean {
  363. const { props: prevProps, children: prevChildren, component } = prevVNode
  364. const { props: nextProps, children: nextChildren, patchFlag } = nextVNode
  365. const emits = component!.emitsOptions
  366. // Parent component's render function was hot-updated. Since this may have
  367. // caused the child component's slots content to have changed, we need to
  368. // force the child to update as well.
  369. if (__DEV__ && (prevChildren || nextChildren) && isHmrUpdating) {
  370. return true
  371. }
  372. // force child update for runtime directive or transition on component vnode.
  373. if (nextVNode.dirs || nextVNode.transition) {
  374. return true
  375. }
  376. if (optimized && patchFlag >= 0) {
  377. if (patchFlag & PatchFlags.DYNAMIC_SLOTS) {
  378. // slot content that references values that might have changed,
  379. // e.g. in a v-for
  380. return true
  381. }
  382. if (patchFlag & PatchFlags.FULL_PROPS) {
  383. if (!prevProps) {
  384. return !!nextProps
  385. }
  386. // presence of this flag indicates props are always non-null
  387. return hasPropsChanged(prevProps, nextProps!, emits)
  388. } else if (patchFlag & PatchFlags.PROPS) {
  389. const dynamicProps = nextVNode.dynamicProps!
  390. for (let i = 0; i < dynamicProps.length; i++) {
  391. const key = dynamicProps[i]
  392. if (
  393. hasPropValueChanged(nextProps!, prevProps!, key) &&
  394. !isEmitListener(emits, key)
  395. ) {
  396. return true
  397. }
  398. }
  399. }
  400. } else {
  401. // this path is only taken by manually written render functions
  402. // so presence of any children leads to a forced update
  403. if (prevChildren || nextChildren) {
  404. if (!nextChildren || !(nextChildren as any).$stable) {
  405. return true
  406. }
  407. }
  408. if (prevProps === nextProps) {
  409. return false
  410. }
  411. if (!prevProps) {
  412. return !!nextProps
  413. }
  414. if (!nextProps) {
  415. return true
  416. }
  417. return hasPropsChanged(prevProps, nextProps, emits)
  418. }
  419. return false
  420. }
  421. function hasPropsChanged(
  422. prevProps: Data,
  423. nextProps: Data,
  424. emitsOptions: ComponentInternalInstance['emitsOptions'],
  425. ): boolean {
  426. const nextKeys = Object.keys(nextProps)
  427. if (nextKeys.length !== Object.keys(prevProps).length) {
  428. return true
  429. }
  430. for (let i = 0; i < nextKeys.length; i++) {
  431. const key = nextKeys[i]
  432. if (
  433. hasPropValueChanged(nextProps, prevProps, key) &&
  434. !isEmitListener(emitsOptions, key)
  435. ) {
  436. return true
  437. }
  438. }
  439. return false
  440. }
  441. function hasPropValueChanged(
  442. nextProps: Data,
  443. prevProps: Data,
  444. key: string,
  445. ): boolean {
  446. const nextProp = nextProps[key]
  447. const prevProp = prevProps[key]
  448. if (key === 'style' && isObject(nextProp) && isObject(prevProp)) {
  449. return !looseEqual(nextProp, prevProp)
  450. }
  451. return nextProp !== prevProp
  452. }
  453. export function updateHOCHostEl(
  454. { vnode, parent, suspense }: ComponentInternalInstance,
  455. el: typeof vnode.el, // HostNode
  456. ): void {
  457. while (parent && !parent.vapor) {
  458. const root = parent.subTree!
  459. if (root.suspense && root.suspense.activeBranch === vnode) {
  460. // Suspense proxies its active branch host node, so keep propagating from
  461. // the boundary vnode to any wrapper components above it.
  462. root.suspense.vnode.el = root.el = el
  463. vnode = root
  464. }
  465. if (root === vnode) {
  466. ;(vnode = parent.vnode!).el = el
  467. parent = parent.parent
  468. } else {
  469. break
  470. }
  471. }
  472. // also update suspense vnode el
  473. if (suspense && suspense.activeBranch === vnode) {
  474. suspense.vnode.el = el
  475. }
  476. }