componentRenderUtils.ts 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353
  1. import {
  2. ComponentInternalInstance,
  3. FunctionalComponent,
  4. Data
  5. } from './component'
  6. import {
  7. VNode,
  8. normalizeVNode,
  9. createVNode,
  10. Comment,
  11. cloneVNode,
  12. Fragment,
  13. VNodeArrayChildren,
  14. isVNode
  15. } from './vnode'
  16. import { handleError, ErrorCodes } from './errorHandling'
  17. import { PatchFlags, ShapeFlags, isOn } from '@vue/shared'
  18. import { warn } from './warning'
  19. import { isHmrUpdating } from './hmr'
  20. // mark the current rendering instance for asset resolution (e.g.
  21. // resolveComponent, resolveDirective) during render
  22. export let currentRenderingInstance: ComponentInternalInstance | null = null
  23. export function setCurrentRenderingInstance(
  24. instance: ComponentInternalInstance | null
  25. ) {
  26. currentRenderingInstance = instance
  27. }
  28. // dev only flag to track whether $attrs was used during render.
  29. // If $attrs was used during render then the warning for failed attrs
  30. // fallthrough can be suppressed.
  31. let accessedAttrs: boolean = false
  32. export function markAttrsAccessed() {
  33. accessedAttrs = true
  34. }
  35. export function renderComponentRoot(
  36. instance: ComponentInternalInstance
  37. ): VNode {
  38. const {
  39. type: Component,
  40. parent,
  41. vnode,
  42. proxy,
  43. withProxy,
  44. props,
  45. slots,
  46. attrs,
  47. emit,
  48. render,
  49. renderCache,
  50. data,
  51. setupState,
  52. ctx
  53. } = instance
  54. let result
  55. currentRenderingInstance = instance
  56. if (__DEV__) {
  57. accessedAttrs = false
  58. }
  59. try {
  60. let fallthroughAttrs
  61. if (vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {
  62. // withProxy is a proxy with a different `has` trap only for
  63. // runtime-compiled render functions using `with` block.
  64. const proxyToUse = withProxy || proxy
  65. result = normalizeVNode(
  66. render!.call(
  67. proxyToUse,
  68. proxyToUse!,
  69. renderCache,
  70. props,
  71. setupState,
  72. data,
  73. ctx
  74. )
  75. )
  76. fallthroughAttrs = attrs
  77. } else {
  78. // functional
  79. const render = Component as FunctionalComponent
  80. // in dev, mark attrs accessed if optional props (attrs === props)
  81. if (__DEV__ && attrs === props) {
  82. markAttrsAccessed()
  83. }
  84. result = normalizeVNode(
  85. render.length > 1
  86. ? render(
  87. props,
  88. __DEV__
  89. ? {
  90. get attrs() {
  91. markAttrsAccessed()
  92. return attrs
  93. },
  94. slots,
  95. emit
  96. }
  97. : { attrs, slots, emit }
  98. )
  99. : render(props, null as any /* we know it doesn't need it */)
  100. )
  101. fallthroughAttrs = Component.props ? attrs : getFallthroughAttrs(attrs)
  102. }
  103. // attr merging
  104. // in dev mode, comments are preserved, and it's possible for a template
  105. // to have comments along side the root element which makes it a fragment
  106. let root = result
  107. let setRoot: ((root: VNode) => void) | undefined = undefined
  108. if (__DEV__) {
  109. ;[root, setRoot] = getChildRoot(result)
  110. }
  111. if (
  112. Component.inheritAttrs !== false &&
  113. fallthroughAttrs &&
  114. Object.keys(fallthroughAttrs).length
  115. ) {
  116. if (
  117. root.shapeFlag & ShapeFlags.ELEMENT ||
  118. root.shapeFlag & ShapeFlags.COMPONENT
  119. ) {
  120. root = cloneVNode(root, fallthroughAttrs)
  121. } else if (__DEV__ && !accessedAttrs && root.type !== Comment) {
  122. const allAttrs = Object.keys(attrs)
  123. const eventAttrs: string[] = []
  124. const extraAttrs: string[] = []
  125. for (let i = 0, l = allAttrs.length; i < l; i++) {
  126. const key = allAttrs[i]
  127. if (isOn(key)) {
  128. // ignore v-model handlers when they fail to fallthrough
  129. if (!key.startsWith('onUpdate:')) {
  130. // remove `on`, lowercase first letter to reflect event casing
  131. // accurately
  132. eventAttrs.push(key[2].toLowerCase() + key.slice(3))
  133. }
  134. } else {
  135. extraAttrs.push(key)
  136. }
  137. }
  138. if (extraAttrs.length) {
  139. warn(
  140. `Extraneous non-props attributes (` +
  141. `${extraAttrs.join(', ')}) ` +
  142. `were passed to component but could not be automatically inherited ` +
  143. `because component renders fragment or text root nodes.`
  144. )
  145. }
  146. if (eventAttrs.length) {
  147. warn(
  148. `Extraneous non-emits event listeners (` +
  149. `${eventAttrs.join(', ')}) ` +
  150. `were passed to component but could not be automatically inherited ` +
  151. `because component renders fragment or text root nodes. ` +
  152. `If the listener is intended to be a component custom event listener only, ` +
  153. `declare it using the "emits" option.`
  154. )
  155. }
  156. }
  157. }
  158. // inherit scopeId
  159. const scopeId = vnode.scopeId
  160. // vite#536: if subtree root is created from parent slot if would already
  161. // have the correct scopeId, in this case adding the scopeId will cause
  162. // it to be removed if the original slot vnode is reused.
  163. const needScopeId = scopeId && root.scopeId !== scopeId
  164. const treeOwnerId = parent && parent.type.__scopeId
  165. const slotScopeId =
  166. treeOwnerId && treeOwnerId !== scopeId ? treeOwnerId + '-s' : null
  167. if (needScopeId || slotScopeId) {
  168. const extras: Data = {}
  169. if (needScopeId) extras[scopeId!] = ''
  170. if (slotScopeId) extras[slotScopeId] = ''
  171. root = cloneVNode(root, extras)
  172. }
  173. // inherit directives
  174. if (vnode.dirs) {
  175. if (__DEV__ && !isElementRoot(root)) {
  176. warn(
  177. `Runtime directive used on component with non-element root node. ` +
  178. `The directives will not function as intended.`
  179. )
  180. }
  181. root.dirs = vnode.dirs
  182. }
  183. // inherit transition data
  184. if (vnode.transition) {
  185. if (__DEV__ && !isElementRoot(root)) {
  186. warn(
  187. `Component inside <Transition> renders non-element root node ` +
  188. `that cannot be animated.`
  189. )
  190. }
  191. root.transition = vnode.transition
  192. }
  193. if (__DEV__ && setRoot) {
  194. setRoot(root)
  195. } else {
  196. result = root
  197. }
  198. } catch (err) {
  199. handleError(err, instance, ErrorCodes.RENDER_FUNCTION)
  200. result = createVNode(Comment)
  201. }
  202. currentRenderingInstance = null
  203. return result
  204. }
  205. /**
  206. * dev only
  207. */
  208. const getChildRoot = (
  209. vnode: VNode
  210. ): [VNode, ((root: VNode) => void) | undefined] => {
  211. if (vnode.type !== Fragment) {
  212. return [vnode, undefined]
  213. }
  214. const rawChildren = vnode.children as VNodeArrayChildren
  215. const dynamicChildren = vnode.dynamicChildren as VNodeArrayChildren
  216. const children = rawChildren.filter(child => {
  217. return !(isVNode(child) && child.type === Comment)
  218. })
  219. if (children.length !== 1) {
  220. return [vnode, undefined]
  221. }
  222. const childRoot = children[0]
  223. const index = rawChildren.indexOf(childRoot)
  224. const dynamicIndex = dynamicChildren ? dynamicChildren.indexOf(childRoot) : -1
  225. const setRoot = (updatedRoot: VNode) => {
  226. rawChildren[index] = updatedRoot
  227. if (dynamicIndex > -1) {
  228. dynamicChildren[dynamicIndex] = updatedRoot
  229. } else if (dynamicChildren && updatedRoot.patchFlag > 0) {
  230. dynamicChildren.push(updatedRoot)
  231. }
  232. }
  233. return [normalizeVNode(childRoot), setRoot]
  234. }
  235. const getFallthroughAttrs = (attrs: Data): Data | undefined => {
  236. let res: Data | undefined
  237. for (const key in attrs) {
  238. if (key === 'class' || key === 'style' || isOn(key)) {
  239. ;(res || (res = {}))[key] = attrs[key]
  240. }
  241. }
  242. return res
  243. }
  244. const isElementRoot = (vnode: VNode) => {
  245. return (
  246. vnode.shapeFlag & ShapeFlags.COMPONENT ||
  247. vnode.shapeFlag & ShapeFlags.ELEMENT ||
  248. vnode.type === Comment // potential v-if branch switch
  249. )
  250. }
  251. export function shouldUpdateComponent(
  252. prevVNode: VNode,
  253. nextVNode: VNode,
  254. optimized?: boolean
  255. ): boolean {
  256. const { props: prevProps, children: prevChildren } = prevVNode
  257. const { props: nextProps, children: nextChildren, patchFlag } = nextVNode
  258. // Parent component's render function was hot-updated. Since this may have
  259. // caused the child component's slots content to have changed, we need to
  260. // force the child to update as well.
  261. if (__DEV__ && (prevChildren || nextChildren) && isHmrUpdating) {
  262. return true
  263. }
  264. // force child update for runtime directive or transition on component vnode.
  265. if (nextVNode.dirs || nextVNode.transition) {
  266. return true
  267. }
  268. if (optimized && patchFlag > 0) {
  269. if (patchFlag & PatchFlags.DYNAMIC_SLOTS) {
  270. // slot content that references values that might have changed,
  271. // e.g. in a v-for
  272. return true
  273. }
  274. if (patchFlag & PatchFlags.FULL_PROPS) {
  275. if (!prevProps) {
  276. return !!nextProps
  277. }
  278. // presence of this flag indicates props are always non-null
  279. return hasPropsChanged(prevProps, nextProps!)
  280. } else if (patchFlag & PatchFlags.PROPS) {
  281. const dynamicProps = nextVNode.dynamicProps!
  282. for (let i = 0; i < dynamicProps.length; i++) {
  283. const key = dynamicProps[i]
  284. if (nextProps![key] !== prevProps![key]) {
  285. return true
  286. }
  287. }
  288. }
  289. } else {
  290. // this path is only taken by manually written render functions
  291. // so presence of any children leads to a forced update
  292. if (prevChildren || nextChildren) {
  293. if (!nextChildren || !(nextChildren as any).$stable) {
  294. return true
  295. }
  296. }
  297. if (prevProps === nextProps) {
  298. return false
  299. }
  300. if (!prevProps) {
  301. return !!nextProps
  302. }
  303. if (!nextProps) {
  304. return true
  305. }
  306. return hasPropsChanged(prevProps, nextProps)
  307. }
  308. return false
  309. }
  310. function hasPropsChanged(prevProps: Data, nextProps: Data): boolean {
  311. const nextKeys = Object.keys(nextProps)
  312. if (nextKeys.length !== Object.keys(prevProps).length) {
  313. return true
  314. }
  315. for (let i = 0; i < nextKeys.length; i++) {
  316. const key = nextKeys[i]
  317. if (nextProps[key] !== prevProps[key]) {
  318. return true
  319. }
  320. }
  321. return false
  322. }
  323. export function updateHOCHostEl(
  324. { vnode, parent }: ComponentInternalInstance,
  325. el: typeof vnode.el // HostNode
  326. ) {
  327. while (parent && parent.subTree === vnode) {
  328. ;(vnode = parent.vnode).el = el
  329. parent = parent.parent
  330. }
  331. }