componentRenderUtils.ts 13 KB

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