componentRenderUtils.ts 13 KB

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