vnode.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448
  1. import {
  2. isArray,
  3. isFunction,
  4. isString,
  5. isObject,
  6. EMPTY_ARR,
  7. extend,
  8. normalizeClass,
  9. normalizeStyle,
  10. PatchFlags,
  11. ShapeFlags
  12. } from '@vue/shared'
  13. import {
  14. ComponentInternalInstance,
  15. Data,
  16. SetupProxySymbol,
  17. Component,
  18. ClassComponent
  19. } from './component'
  20. import { RawSlots } from './componentSlots'
  21. import { isReactive, Ref } from '@vue/reactivity'
  22. import { AppContext } from './apiCreateApp'
  23. import {
  24. SuspenseImpl,
  25. isSuspense,
  26. SuspenseBoundary
  27. } from './components/Suspense'
  28. import { DirectiveBinding } from './directives'
  29. import { TransitionHooks } from './components/BaseTransition'
  30. import { warn } from './warning'
  31. import { currentScopeId } from './helpers/scopeId'
  32. import { PortalImpl, isPortal } from './components/Portal'
  33. import { currentRenderingInstance } from './componentRenderUtils'
  34. export const Fragment = (Symbol(__DEV__ ? 'Fragment' : undefined) as any) as {
  35. __isFragment: true
  36. new (): {
  37. $props: VNodeProps
  38. }
  39. }
  40. export const Text = Symbol(__DEV__ ? 'Text' : undefined)
  41. export const Comment = Symbol(__DEV__ ? 'Comment' : undefined)
  42. export const Static = Symbol(__DEV__ ? 'Static' : undefined)
  43. export type VNodeTypes =
  44. | string
  45. | Component
  46. | typeof Text
  47. | typeof Static
  48. | typeof Comment
  49. | typeof Fragment
  50. | typeof PortalImpl
  51. | typeof SuspenseImpl
  52. export type VNodeRef =
  53. | string
  54. | Ref
  55. | ((ref: object | null, refs: Record<string, any>) => void)
  56. export type VNodeNormalizedRef = [ComponentInternalInstance, VNodeRef]
  57. type VNodeMountHook = (vnode: VNode) => void
  58. type VNodeUpdateHook = (vnode: VNode, oldVNode: VNode) => void
  59. export type VNodeHook = VNodeMountHook | VNodeUpdateHook
  60. export interface VNodeProps {
  61. [key: string]: any
  62. key?: string | number
  63. ref?: VNodeRef
  64. // vnode hooks
  65. onVnodeBeforeMount?: VNodeMountHook
  66. onVnodeMounted?: VNodeMountHook
  67. onVnodeBeforeUpdate?: VNodeUpdateHook
  68. onVnodeUpdated?: VNodeUpdateHook
  69. onVnodeBeforeUnmount?: VNodeMountHook
  70. onVnodeUnmounted?: VNodeMountHook
  71. }
  72. type VNodeChildAtom<HostNode, HostElement> =
  73. | VNode<HostNode, HostElement>
  74. | string
  75. | number
  76. | boolean
  77. | null
  78. | void
  79. export interface VNodeArrayChildren<HostNode = any, HostElement = any>
  80. extends Array<
  81. | VNodeArrayChildren<HostNode, HostElement>
  82. | VNodeChildAtom<HostNode, HostElement>
  83. > {}
  84. export type VNodeChild<HostNode = any, HostElement = any> =
  85. | VNodeChildAtom<HostNode, HostElement>
  86. | VNodeArrayChildren<HostNode, HostElement>
  87. export type VNodeNormalizedChildren<HostNode = any, HostElement = any> =
  88. | string
  89. | VNodeArrayChildren<HostNode, HostElement>
  90. | RawSlots
  91. | null
  92. export interface VNode<HostNode = any, HostElement = any> {
  93. _isVNode: true
  94. type: VNodeTypes
  95. props: VNodeProps | null
  96. key: string | number | null
  97. ref: VNodeNormalizedRef | null
  98. scopeId: string | null // SFC only
  99. children: VNodeNormalizedChildren<HostNode, HostElement>
  100. component: ComponentInternalInstance | null
  101. suspense: SuspenseBoundary<HostNode, HostElement> | null
  102. dirs: DirectiveBinding[] | null
  103. transition: TransitionHooks | null
  104. // DOM
  105. el: HostNode | null
  106. anchor: HostNode | null // fragment anchor
  107. target: HostElement | null // portal target
  108. // optimization only
  109. shapeFlag: number
  110. patchFlag: number
  111. dynamicProps: string[] | null
  112. dynamicChildren: VNode[] | null
  113. // application root node only
  114. appContext: AppContext | null
  115. }
  116. // Since v-if and v-for are the two possible ways node structure can dynamically
  117. // change, once we consider v-if branches and each v-for fragment a block, we
  118. // can divide a template into nested blocks, and within each block the node
  119. // structure would be stable. This allows us to skip most children diffing
  120. // and only worry about the dynamic nodes (indicated by patch flags).
  121. const blockStack: (VNode[] | null)[] = []
  122. let currentBlock: VNode[] | null = null
  123. // Open a block.
  124. // This must be called before `createBlock`. It cannot be part of `createBlock`
  125. // because the children of the block are evaluated before `createBlock` itself
  126. // is called. The generated code typically looks like this:
  127. //
  128. // function render() {
  129. // return (openBlock(),createBlock('div', null, [...]))
  130. // }
  131. //
  132. // disableTracking is true when creating a fragment block, since a fragment
  133. // always diffs its children.
  134. export function openBlock(disableTracking = false) {
  135. blockStack.push((currentBlock = disableTracking ? null : []))
  136. }
  137. // Whether we should be tracking dynamic child nodes inside a block.
  138. // Only tracks when this value is > 0
  139. // We are not using a simple boolean because this value may need to be
  140. // incremented/decremented by nested usage of v-once (see below)
  141. let shouldTrack = 1
  142. // Block tracking sometimes needs to be disabled, for example during the
  143. // creation of a tree that needs to be cached by v-once. The compiler generates
  144. // code like this:
  145. // _cache[1] || (
  146. // setBlockTracking(-1),
  147. // _cache[1] = createVNode(...),
  148. // setBlockTracking(1),
  149. // _cache[1]
  150. // )
  151. export function setBlockTracking(value: number) {
  152. shouldTrack += value
  153. }
  154. // Create a block root vnode. Takes the same exact arguments as `createVNode`.
  155. // A block root keeps track of dynamic nodes within the block in the
  156. // `dynamicChildren` array.
  157. export function createBlock(
  158. type: VNodeTypes | ClassComponent,
  159. props?: { [key: string]: any } | null,
  160. children?: any,
  161. patchFlag?: number,
  162. dynamicProps?: string[]
  163. ): VNode {
  164. // avoid a block with patchFlag tracking itself
  165. shouldTrack--
  166. const vnode = createVNode(type, props, children, patchFlag, dynamicProps)
  167. shouldTrack++
  168. // save current block children on the block vnode
  169. vnode.dynamicChildren = currentBlock || EMPTY_ARR
  170. // close block
  171. blockStack.pop()
  172. currentBlock = blockStack[blockStack.length - 1] || null
  173. // a block is always going to be patched, so track it as a child of its
  174. // parent block
  175. if (currentBlock !== null) {
  176. currentBlock.push(vnode)
  177. }
  178. return vnode
  179. }
  180. export function isVNode(value: any): value is VNode {
  181. return value ? value._isVNode === true : false
  182. }
  183. export function isSameVNodeType(n1: VNode, n2: VNode): boolean {
  184. if (
  185. __BUNDLER__ &&
  186. __DEV__ &&
  187. n2.shapeFlag & ShapeFlags.COMPONENT &&
  188. (n2.type as Component).__hmrUpdated
  189. ) {
  190. // HMR only: if the component has been hot-updated, force a reload.
  191. return false
  192. }
  193. return n1.type === n2.type && n1.key === n2.key
  194. }
  195. export function createVNode(
  196. type: VNodeTypes | ClassComponent,
  197. props: (Data & VNodeProps) | null = null,
  198. children: unknown = null,
  199. patchFlag: number = 0,
  200. dynamicProps: string[] | null = null
  201. ): VNode {
  202. if (!type) {
  203. if (__DEV__) {
  204. warn(`Invalid vnode type when creating vnode: ${type}.`)
  205. }
  206. type = Comment
  207. }
  208. // class component normalization.
  209. if (isFunction(type) && '__vccOpts' in type) {
  210. type = type.__vccOpts
  211. }
  212. // class & style normalization.
  213. if (props !== null) {
  214. // for reactive or proxy objects, we need to clone it to enable mutation.
  215. if (isReactive(props) || SetupProxySymbol in props) {
  216. props = extend({}, props)
  217. }
  218. let { class: klass, style } = props
  219. if (klass != null && !isString(klass)) {
  220. props.class = normalizeClass(klass)
  221. }
  222. if (isObject(style)) {
  223. // reactive state objects need to be cloned since they are likely to be
  224. // mutated
  225. if (isReactive(style) && !isArray(style)) {
  226. style = extend({}, style)
  227. }
  228. props.style = normalizeStyle(style)
  229. }
  230. }
  231. // encode the vnode type information into a bitmap
  232. const shapeFlag = isString(type)
  233. ? ShapeFlags.ELEMENT
  234. : __FEATURE_SUSPENSE__ && isSuspense(type)
  235. ? ShapeFlags.SUSPENSE
  236. : isPortal(type)
  237. ? ShapeFlags.PORTAL
  238. : isObject(type)
  239. ? ShapeFlags.STATEFUL_COMPONENT
  240. : isFunction(type)
  241. ? ShapeFlags.FUNCTIONAL_COMPONENT
  242. : 0
  243. const vnode: VNode = {
  244. _isVNode: true,
  245. type,
  246. props,
  247. key: props !== null && props.key !== undefined ? props.key : null,
  248. ref:
  249. props !== null && props.ref !== undefined
  250. ? [currentRenderingInstance!, props.ref]
  251. : null,
  252. scopeId: currentScopeId,
  253. children: null,
  254. component: null,
  255. suspense: null,
  256. dirs: null,
  257. transition: null,
  258. el: null,
  259. anchor: null,
  260. target: null,
  261. shapeFlag,
  262. patchFlag,
  263. dynamicProps,
  264. dynamicChildren: null,
  265. appContext: null
  266. }
  267. normalizeChildren(vnode, children)
  268. // presence of a patch flag indicates this node needs patching on updates.
  269. // component nodes also should always be patched, because even if the
  270. // component doesn't need to update, it needs to persist the instance on to
  271. // the next vnode so that it can be properly unmounted later.
  272. if (
  273. shouldTrack > 0 &&
  274. currentBlock !== null &&
  275. // the EVENTS flag is only for hydration and if it is the only flag, the
  276. // vnode should not be considered dynamic due to handler caching.
  277. patchFlag !== PatchFlags.HYDRATE_EVENTS &&
  278. (patchFlag > 0 ||
  279. shapeFlag & ShapeFlags.SUSPENSE ||
  280. shapeFlag & ShapeFlags.STATEFUL_COMPONENT ||
  281. shapeFlag & ShapeFlags.FUNCTIONAL_COMPONENT)
  282. ) {
  283. currentBlock.push(vnode)
  284. }
  285. return vnode
  286. }
  287. export function cloneVNode<T, U>(
  288. vnode: VNode<T, U>,
  289. extraProps?: Data & VNodeProps
  290. ): VNode<T, U> {
  291. // This is intentionally NOT using spread or extend to avoid the runtime
  292. // key enumeration cost.
  293. return {
  294. _isVNode: true,
  295. type: vnode.type,
  296. props: extraProps
  297. ? vnode.props
  298. ? mergeProps(vnode.props, extraProps)
  299. : extraProps
  300. : vnode.props,
  301. key: vnode.key,
  302. ref: vnode.ref,
  303. scopeId: vnode.scopeId,
  304. children: vnode.children,
  305. target: vnode.target,
  306. shapeFlag: vnode.shapeFlag,
  307. patchFlag: vnode.patchFlag,
  308. dynamicProps: vnode.dynamicProps,
  309. dynamicChildren: vnode.dynamicChildren,
  310. appContext: vnode.appContext,
  311. dirs: vnode.dirs,
  312. transition: vnode.transition,
  313. // These should technically only be non-null on mounted VNodes. However,
  314. // they *should* be copied for kept-alive vnodes. So we just always copy
  315. // them since them being non-null during a mount doesn't affect the logic as
  316. // they will simply be overwritten.
  317. component: vnode.component,
  318. suspense: vnode.suspense,
  319. el: vnode.el,
  320. anchor: vnode.anchor
  321. }
  322. }
  323. export function createTextVNode(text: string = ' ', flag: number = 0): VNode {
  324. return createVNode(Text, null, text, flag)
  325. }
  326. export function createStaticVNode(content: string): VNode {
  327. return createVNode(Static, null, content)
  328. }
  329. export function createCommentVNode(
  330. text: string = '',
  331. // when used as the v-else branch, the comment node must be created as a
  332. // block to ensure correct updates.
  333. asBlock: boolean = false
  334. ): VNode {
  335. return asBlock
  336. ? (openBlock(), createBlock(Comment, null, text))
  337. : createVNode(Comment, null, text)
  338. }
  339. export function normalizeVNode<T, U>(child: VNodeChild<T, U>): VNode<T, U> {
  340. if (child == null || typeof child === 'boolean') {
  341. // empty placeholder
  342. return createVNode(Comment)
  343. } else if (isArray(child)) {
  344. // fragment
  345. return createVNode(Fragment, null, child)
  346. } else if (typeof child === 'object') {
  347. // already vnode, this should be the most common since compiled templates
  348. // always produce all-vnode children arrays
  349. return child.el === null ? child : cloneVNode(child)
  350. } else {
  351. // strings and numbers
  352. return createVNode(Text, null, String(child))
  353. }
  354. }
  355. // optimized normalization for template-compiled render fns
  356. export function cloneIfMounted(child: VNode): VNode {
  357. return child.el === null ? child : cloneVNode(child)
  358. }
  359. export function normalizeChildren(vnode: VNode, children: unknown) {
  360. let type = 0
  361. if (children == null) {
  362. children = null
  363. } else if (isArray(children)) {
  364. type = ShapeFlags.ARRAY_CHILDREN
  365. } else if (typeof children === 'object') {
  366. type = ShapeFlags.SLOTS_CHILDREN
  367. if (!(children as RawSlots)._) {
  368. ;(children as RawSlots)._ctx = currentRenderingInstance
  369. }
  370. } else if (isFunction(children)) {
  371. children = { default: children, _ctx: currentRenderingInstance }
  372. type = ShapeFlags.SLOTS_CHILDREN
  373. } else {
  374. children = String(children)
  375. type = ShapeFlags.TEXT_CHILDREN
  376. }
  377. vnode.children = children as VNodeNormalizedChildren
  378. vnode.shapeFlag |= type
  379. }
  380. const handlersRE = /^on|^vnode/
  381. export function mergeProps(...args: (Data & VNodeProps)[]) {
  382. const ret: Data = {}
  383. extend(ret, args[0])
  384. for (let i = 1; i < args.length; i++) {
  385. const toMerge = args[i]
  386. for (const key in toMerge) {
  387. if (key === 'class') {
  388. if (ret.class !== toMerge.class) {
  389. ret.class = normalizeClass([ret.class, toMerge.class])
  390. }
  391. } else if (key === 'style') {
  392. ret.style = normalizeStyle([ret.style, toMerge.style])
  393. } else if (handlersRE.test(key)) {
  394. // on*, vnode*
  395. const existing = ret[key]
  396. const incoming = toMerge[key]
  397. if (existing !== incoming) {
  398. ret[key] = existing
  399. ? [].concat(existing as any, toMerge[key] as any)
  400. : incoming
  401. }
  402. } else {
  403. ret[key] = toMerge[key]
  404. }
  405. }
  406. }
  407. return ret
  408. }