componentRenderUtils.ts 12 KB

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