hoistStatic.ts 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. import {
  2. RootNode,
  3. NodeTypes,
  4. TemplateChildNode,
  5. SimpleExpressionNode,
  6. ElementTypes,
  7. PlainElementNode,
  8. ComponentNode,
  9. TemplateNode,
  10. ElementNode,
  11. VNodeCall
  12. } from '../ast'
  13. import { TransformContext } from '../transform'
  14. import { PatchFlags, isString, isSymbol } from '@vue/shared'
  15. import { isSlotOutlet, findProp } from '../utils'
  16. export function hoistStatic(root: RootNode, context: TransformContext) {
  17. walk(
  18. root.children,
  19. context,
  20. new Map(),
  21. // Root node is unfortunately non-hoistable due to potential parent
  22. // fallthrough attributes.
  23. isSingleElementRoot(root, root.children[0])
  24. )
  25. }
  26. export function isSingleElementRoot(
  27. root: RootNode,
  28. child: TemplateChildNode
  29. ): child is PlainElementNode | ComponentNode | TemplateNode {
  30. const { children } = root
  31. return (
  32. children.length === 1 &&
  33. child.type === NodeTypes.ELEMENT &&
  34. !isSlotOutlet(child)
  35. )
  36. }
  37. function walk(
  38. children: TemplateChildNode[],
  39. context: TransformContext,
  40. resultCache: Map<TemplateChildNode, boolean>,
  41. doNotHoistNode: boolean = false
  42. ) {
  43. let hasHoistedNode = false
  44. for (let i = 0; i < children.length; i++) {
  45. const child = children[i]
  46. // only plain elements & text calls are eligible for hoisting.
  47. if (
  48. child.type === NodeTypes.ELEMENT &&
  49. child.tagType === ElementTypes.ELEMENT
  50. ) {
  51. if (!doNotHoistNode && isStaticNode(child, resultCache)) {
  52. // whole tree is static
  53. ;(child.codegenNode as VNodeCall).patchFlag =
  54. PatchFlags.HOISTED + (__DEV__ ? ` /* HOISTED */` : ``)
  55. child.codegenNode = context.hoist(child.codegenNode!)
  56. hasHoistedNode = true
  57. continue
  58. } else {
  59. // node may contain dynamic children, but its props may be eligible for
  60. // hoisting.
  61. const codegenNode = child.codegenNode!
  62. if (codegenNode.type === NodeTypes.VNODE_CALL) {
  63. const flag = getPatchFlag(codegenNode)
  64. if (
  65. (!flag ||
  66. flag === PatchFlags.NEED_PATCH ||
  67. flag === PatchFlags.TEXT) &&
  68. !hasDynamicKeyOrRef(child) &&
  69. !hasCachedProps(child)
  70. ) {
  71. const props = getNodeProps(child)
  72. if (props) {
  73. codegenNode.props = context.hoist(props)
  74. }
  75. }
  76. }
  77. }
  78. } else if (
  79. child.type === NodeTypes.TEXT_CALL &&
  80. isStaticNode(child.content, resultCache)
  81. ) {
  82. child.codegenNode = context.hoist(child.codegenNode)
  83. hasHoistedNode = true
  84. }
  85. // walk further
  86. if (child.type === NodeTypes.ELEMENT) {
  87. walk(child.children, context, resultCache)
  88. } else if (child.type === NodeTypes.FOR) {
  89. // Do not hoist v-for single child because it has to be a block
  90. walk(child.children, context, resultCache, child.children.length === 1)
  91. } else if (child.type === NodeTypes.IF) {
  92. for (let i = 0; i < child.branches.length; i++) {
  93. const branchChildren = child.branches[i].children
  94. // Do not hoist v-if single child because it has to be a block
  95. walk(branchChildren, context, resultCache, branchChildren.length === 1)
  96. }
  97. }
  98. }
  99. if (hasHoistedNode && context.transformHoist) {
  100. context.transformHoist(children, context)
  101. }
  102. }
  103. export function isStaticNode(
  104. node: TemplateChildNode | SimpleExpressionNode,
  105. resultCache: Map<TemplateChildNode, boolean> = new Map()
  106. ): boolean {
  107. switch (node.type) {
  108. case NodeTypes.ELEMENT:
  109. if (node.tagType !== ElementTypes.ELEMENT) {
  110. return false
  111. }
  112. const cached = resultCache.get(node)
  113. if (cached !== undefined) {
  114. return cached
  115. }
  116. const codegenNode = node.codegenNode!
  117. if (codegenNode.type !== NodeTypes.VNODE_CALL) {
  118. return false
  119. }
  120. const flag = getPatchFlag(codegenNode)
  121. if (!flag && !hasDynamicKeyOrRef(node) && !hasCachedProps(node)) {
  122. // element self is static. check its children.
  123. for (let i = 0; i < node.children.length; i++) {
  124. if (!isStaticNode(node.children[i], resultCache)) {
  125. resultCache.set(node, false)
  126. return false
  127. }
  128. }
  129. // only svg/foreignObject could be block here, however if they are static
  130. // then they don't need to be blocks since there will be no nested
  131. // updates.
  132. if (codegenNode.isBlock) {
  133. codegenNode.isBlock = false
  134. }
  135. resultCache.set(node, true)
  136. return true
  137. } else {
  138. resultCache.set(node, false)
  139. return false
  140. }
  141. case NodeTypes.TEXT:
  142. case NodeTypes.COMMENT:
  143. return true
  144. case NodeTypes.IF:
  145. case NodeTypes.FOR:
  146. case NodeTypes.IF_BRANCH:
  147. return false
  148. case NodeTypes.INTERPOLATION:
  149. case NodeTypes.TEXT_CALL:
  150. return isStaticNode(node.content, resultCache)
  151. case NodeTypes.SIMPLE_EXPRESSION:
  152. return node.isConstant
  153. case NodeTypes.COMPOUND_EXPRESSION:
  154. return node.children.every(child => {
  155. return (
  156. isString(child) || isSymbol(child) || isStaticNode(child, resultCache)
  157. )
  158. })
  159. default:
  160. if (__DEV__) {
  161. const exhaustiveCheck: never = node
  162. exhaustiveCheck
  163. }
  164. return false
  165. }
  166. }
  167. function hasDynamicKeyOrRef(node: ElementNode): boolean {
  168. return !!(findProp(node, 'key', true) || findProp(node, 'ref', true))
  169. }
  170. function hasCachedProps(node: PlainElementNode): boolean {
  171. if (__BROWSER__) {
  172. return false
  173. }
  174. const props = getNodeProps(node)
  175. if (props && props.type === NodeTypes.JS_OBJECT_EXPRESSION) {
  176. const { properties } = props
  177. for (let i = 0; i < properties.length; i++) {
  178. const val = properties[i].value
  179. if (val.type === NodeTypes.JS_CACHE_EXPRESSION) {
  180. return true
  181. }
  182. // merged event handlers
  183. if (
  184. val.type === NodeTypes.JS_ARRAY_EXPRESSION &&
  185. val.elements.some(
  186. e => !isString(e) && e.type === NodeTypes.JS_CACHE_EXPRESSION
  187. )
  188. ) {
  189. return true
  190. }
  191. }
  192. }
  193. return false
  194. }
  195. function getNodeProps(node: PlainElementNode) {
  196. const codegenNode = node.codegenNode!
  197. if (codegenNode.type === NodeTypes.VNODE_CALL) {
  198. return codegenNode.props
  199. }
  200. }
  201. function getPatchFlag(node: VNodeCall): number | undefined {
  202. const flag = node.patchFlag
  203. return flag ? parseInt(flag, 10) : undefined
  204. }