componentRenderUtils.ts 12 KB

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