patch.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469
  1. /**
  2. * Virtual DOM implementation based on Snabbdom by
  3. * Simon Friis Vindum (@paldepind)
  4. * with custom modifications.
  5. *
  6. * Not type-checking this because this file is perf-critical and the cost
  7. * of making flow understand it is not worth it.
  8. */
  9. import config from '../config'
  10. import VNode from './vnode'
  11. import { isPrimitive, _toString, warn } from '../util/index'
  12. const emptyData = {}
  13. const emptyNode = new VNode('', emptyData, [])
  14. const hooks = ['create', 'update', 'postpatch', 'remove', 'destroy']
  15. function isUndef (s) {
  16. return s == null
  17. }
  18. function isDef (s) {
  19. return s != null
  20. }
  21. function sameVnode (vnode1, vnode2) {
  22. if (vnode1.isStatic || vnode2.isStatic) {
  23. return vnode1 === vnode2
  24. }
  25. return (
  26. vnode1.key === vnode2.key &&
  27. vnode1.tag === vnode2.tag &&
  28. !vnode1.data === !vnode2.data
  29. )
  30. }
  31. function createKeyToOldIdx (children, beginIdx, endIdx) {
  32. let i, key
  33. const map = {}
  34. for (i = beginIdx; i <= endIdx; ++i) {
  35. key = children[i].key
  36. if (isDef(key)) map[key] = i
  37. }
  38. return map
  39. }
  40. export function createPatchFunction (backend) {
  41. let i, j
  42. const cbs = {}
  43. const { modules, nodeOps } = backend
  44. for (i = 0; i < hooks.length; ++i) {
  45. cbs[hooks[i]] = []
  46. for (j = 0; j < modules.length; ++j) {
  47. if (modules[j][hooks[i]] !== undefined) cbs[hooks[i]].push(modules[j][hooks[i]])
  48. }
  49. }
  50. function emptyNodeAt (elm) {
  51. return new VNode(nodeOps.tagName(elm).toLowerCase(), {}, [], undefined, elm)
  52. }
  53. function createRmCb (childElm, listeners) {
  54. function remove () {
  55. if (--remove.listeners === 0) {
  56. removeElement(childElm)
  57. }
  58. }
  59. remove.listeners = listeners
  60. return remove
  61. }
  62. function removeElement (el) {
  63. const parent = nodeOps.parentNode(el)
  64. nodeOps.removeChild(parent, el)
  65. }
  66. function createElm (vnode, insertedVnodeQueue) {
  67. let i, elm
  68. const data = vnode.data
  69. if (isDef(data)) {
  70. if (isDef(i = data.hook) && isDef(i = i.init)) i(vnode)
  71. // after calling the init hook, if the vnode is a child component
  72. // it should've created a child instance and mounted it. the child
  73. // component also has set the placeholder vnode's elm.
  74. // in that case we can just return the element and be done.
  75. if (isDef(i = vnode.child)) {
  76. if (vnode.data.pendingInsert) {
  77. insertedVnodeQueue.push.apply(insertedVnodeQueue, vnode.data.pendingInsert)
  78. }
  79. vnode.elm = vnode.child.$el
  80. invokeCreateHooks(vnode, insertedVnodeQueue)
  81. setScope(vnode)
  82. return vnode.elm
  83. }
  84. }
  85. const children = vnode.children
  86. const tag = vnode.tag
  87. if (isDef(tag)) {
  88. if (process.env.NODE_ENV !== 'production') {
  89. if (
  90. !vnode.ns &&
  91. !(config.ignoredElements && config.ignoredElements.indexOf(tag) > -1) &&
  92. config.isUnknownElement(tag)
  93. ) {
  94. warn(
  95. 'Unknown custom element: <' + tag + '> - did you ' +
  96. 'register the component correctly? For recursive components, ' +
  97. 'make sure to provide the "name" option.',
  98. vnode.context
  99. )
  100. }
  101. }
  102. elm = vnode.elm = vnode.ns
  103. ? nodeOps.createElementNS(vnode.ns, tag)
  104. : nodeOps.createElement(tag)
  105. setScope(vnode)
  106. if (Array.isArray(children)) {
  107. for (i = 0; i < children.length; ++i) {
  108. nodeOps.appendChild(elm, createElm(children[i], insertedVnodeQueue))
  109. }
  110. } else if (isPrimitive(vnode.text)) {
  111. nodeOps.appendChild(elm, nodeOps.createTextNode(vnode.text))
  112. }
  113. if (isDef(data)) {
  114. invokeCreateHooks(vnode, insertedVnodeQueue)
  115. }
  116. } else {
  117. elm = vnode.elm = nodeOps.createTextNode(vnode.text)
  118. }
  119. return vnode.elm
  120. }
  121. function invokeCreateHooks (vnode, insertedVnodeQueue) {
  122. for (let i = 0; i < cbs.create.length; ++i) {
  123. cbs.create[i](emptyNode, vnode)
  124. }
  125. i = vnode.data.hook // Reuse variable
  126. if (isDef(i)) {
  127. if (i.create) i.create(emptyNode, vnode)
  128. if (i.insert) insertedVnodeQueue.push(vnode)
  129. }
  130. }
  131. // set scope id attribute for scoped CSS.
  132. // this is implemented as a special case to avoid the overhead
  133. // of going through the normal attribute patching process.
  134. function setScope (vnode) {
  135. let i
  136. if (isDef(i = vnode.host) && isDef(i = i.$options._scopeId)) {
  137. nodeOps.setAttribute(vnode.elm, i, '')
  138. }
  139. if (isDef(i = vnode.context) && isDef(i = i.$options._scopeId)) {
  140. nodeOps.setAttribute(vnode.elm, i, '')
  141. }
  142. }
  143. function addVnodes (parentElm, before, vnodes, startIdx, endIdx, insertedVnodeQueue) {
  144. for (; startIdx <= endIdx; ++startIdx) {
  145. nodeOps.insertBefore(parentElm, createElm(vnodes[startIdx], insertedVnodeQueue), before)
  146. }
  147. }
  148. function invokeDestroyHook (vnode) {
  149. let i, j
  150. const data = vnode.data
  151. if (isDef(data)) {
  152. if (isDef(i = data.hook) && isDef(i = i.destroy)) i(vnode)
  153. for (i = 0; i < cbs.destroy.length; ++i) cbs.destroy[i](vnode)
  154. }
  155. if (isDef(i = vnode.child) && !data.keepAlive) {
  156. invokeDestroyHook(i._vnode)
  157. }
  158. if (isDef(i = vnode.children)) {
  159. for (j = 0; j < vnode.children.length; ++j) {
  160. invokeDestroyHook(vnode.children[j])
  161. }
  162. }
  163. }
  164. function removeVnodes (parentElm, vnodes, startIdx, endIdx) {
  165. for (; startIdx <= endIdx; ++startIdx) {
  166. const ch = vnodes[startIdx]
  167. if (isDef(ch)) {
  168. if (isDef(ch.tag)) {
  169. invokeDestroyHook(ch)
  170. removeAndInvokeRemoveHook(ch)
  171. } else { // Text node
  172. nodeOps.removeChild(parentElm, ch.elm)
  173. }
  174. }
  175. }
  176. }
  177. function removeAndInvokeRemoveHook (vnode, rm) {
  178. if (rm || isDef(vnode.data)) {
  179. const listeners = cbs.remove.length + 1
  180. if (!rm) {
  181. // directly removing
  182. rm = createRmCb(vnode.elm, listeners)
  183. } else {
  184. // we have a recursively passed down rm callback
  185. // increase the listeners count
  186. rm.listeners += listeners
  187. }
  188. // recursively invoke hooks on child component root node
  189. if (isDef(i = vnode.child) && isDef(i = i._vnode) && isDef(i.data)) {
  190. removeAndInvokeRemoveHook(i, rm)
  191. }
  192. for (i = 0; i < cbs.remove.length; ++i) {
  193. cbs.remove[i](vnode, rm)
  194. }
  195. if (isDef(i = vnode.data.hook) && isDef(i = i.remove)) {
  196. i(vnode, rm)
  197. } else {
  198. rm()
  199. }
  200. } else {
  201. removeElement(vnode.elm)
  202. }
  203. }
  204. function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
  205. let oldStartIdx = 0
  206. let newStartIdx = 0
  207. let oldEndIdx = oldCh.length - 1
  208. let oldStartVnode = oldCh[0]
  209. let oldEndVnode = oldCh[oldEndIdx]
  210. let newEndIdx = newCh.length - 1
  211. let newStartVnode = newCh[0]
  212. let newEndVnode = newCh[newEndIdx]
  213. let oldKeyToIdx, idxInOld, elmToMove, before
  214. // removeOnly is a special flag used only by <transition-group>
  215. // to ensure removed elements stay in correct relative positions
  216. // during leaving transitions
  217. const canMove = !removeOnly
  218. while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
  219. if (isUndef(oldStartVnode)) {
  220. oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
  221. } else if (isUndef(oldEndVnode)) {
  222. oldEndVnode = oldCh[--oldEndIdx]
  223. } else if (sameVnode(oldStartVnode, newStartVnode)) {
  224. patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
  225. oldStartVnode = oldCh[++oldStartIdx]
  226. newStartVnode = newCh[++newStartIdx]
  227. } else if (sameVnode(oldEndVnode, newEndVnode)) {
  228. patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
  229. oldEndVnode = oldCh[--oldEndIdx]
  230. newEndVnode = newCh[--newEndIdx]
  231. } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
  232. patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
  233. canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
  234. oldStartVnode = oldCh[++oldStartIdx]
  235. newEndVnode = newCh[--newEndIdx]
  236. } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
  237. patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
  238. canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
  239. oldEndVnode = oldCh[--oldEndIdx]
  240. newStartVnode = newCh[++newStartIdx]
  241. } else {
  242. if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
  243. idxInOld = isDef(newStartVnode.key)
  244. ? oldKeyToIdx[newStartVnode.key]
  245. : newStartVnode.isStatic
  246. ? oldCh.indexOf(newStartVnode)
  247. : null
  248. if (isUndef(idxInOld) || idxInOld === -1) { // New element
  249. nodeOps.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm)
  250. newStartVnode = newCh[++newStartIdx]
  251. } else {
  252. elmToMove = oldCh[idxInOld]
  253. /* istanbul ignore if */
  254. if (process.env.NODE_ENV !== 'production' && !elmToMove) {
  255. warn(
  256. 'It seems there are duplicate keys that is causing an update error. ' +
  257. 'Make sure each v-for item has a unique key.'
  258. )
  259. }
  260. if (elmToMove.tag !== newStartVnode.tag) {
  261. // same key but different element. treat as new element
  262. nodeOps.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm)
  263. newStartVnode = newCh[++newStartIdx]
  264. } else {
  265. patchVnode(elmToMove, newStartVnode, insertedVnodeQueue)
  266. oldCh[idxInOld] = undefined
  267. canMove && nodeOps.insertBefore(parentElm, newStartVnode.elm, oldStartVnode.elm)
  268. newStartVnode = newCh[++newStartIdx]
  269. }
  270. }
  271. }
  272. }
  273. if (oldStartIdx > oldEndIdx) {
  274. before = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
  275. addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
  276. } else if (newStartIdx > newEndIdx) {
  277. removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
  278. }
  279. }
  280. function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) {
  281. if (oldVnode === vnode) return
  282. let i, hook
  283. const hasData = isDef(i = vnode.data)
  284. if (hasData && isDef(hook = i.hook) && isDef(i = hook.prepatch)) {
  285. i(oldVnode, vnode)
  286. }
  287. const elm = vnode.elm = oldVnode.elm
  288. const oldCh = oldVnode.children
  289. const ch = vnode.children
  290. if (hasData) {
  291. for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
  292. if (isDef(hook) && isDef(i = hook.update)) i(oldVnode, vnode)
  293. }
  294. if (isUndef(vnode.text)) {
  295. if (isDef(oldCh) && isDef(ch)) {
  296. if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
  297. } else if (isDef(ch)) {
  298. if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
  299. addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
  300. } else if (isDef(oldCh)) {
  301. removeVnodes(elm, oldCh, 0, oldCh.length - 1)
  302. } else if (isDef(oldVnode.text)) {
  303. nodeOps.setTextContent(elm, '')
  304. }
  305. } else if (oldVnode.text !== vnode.text) {
  306. nodeOps.setTextContent(elm, vnode.text)
  307. }
  308. if (hasData) {
  309. for (i = 0; i < cbs.postpatch.length; ++i) cbs.postpatch[i](oldVnode, vnode)
  310. if (isDef(hook) && isDef(i = hook.postpatch)) i(oldVnode, vnode)
  311. }
  312. }
  313. function invokeInsertHook (vnode, queue, initial) {
  314. // delay insert hooks for component root nodes, invoke them after the
  315. // element is really inserted
  316. if (initial && vnode.parent) {
  317. vnode.parent.data.pendingInsert = queue
  318. } else {
  319. for (let i = 0; i < queue.length; ++i) {
  320. queue[i].data.hook.insert(queue[i])
  321. }
  322. }
  323. }
  324. let bailed = false
  325. function hydrate (elm, vnode, insertedVnodeQueue) {
  326. if (process.env.NODE_ENV !== 'production') {
  327. if (!assertNodeMatch(elm, vnode)) {
  328. return false
  329. }
  330. }
  331. vnode.elm = elm
  332. const { tag, data, children } = vnode
  333. if (isDef(data)) {
  334. if (isDef(i = data.hook) && isDef(i = i.init)) i(vnode, true /* hydrating */)
  335. if (isDef(i = vnode.child)) {
  336. // child component. it should have hydrated its own tree.
  337. invokeCreateHooks(vnode, insertedVnodeQueue)
  338. return true
  339. }
  340. }
  341. if (isDef(tag)) {
  342. if (isDef(children)) {
  343. const childNodes = nodeOps.childNodes(elm)
  344. for (let i = 0; i < children.length; i++) {
  345. const success = hydrate(childNodes[i], children[i], insertedVnodeQueue)
  346. if (!success) {
  347. if (process.env.NODE_ENV !== 'production' && typeof console !== 'undefined' && !bailed) {
  348. bailed = true
  349. console.warn('Parent: ', elm)
  350. console.warn('Mismatching childNodes vs. VNodes: ', childNodes, children)
  351. }
  352. return false
  353. }
  354. }
  355. }
  356. if (isDef(data)) {
  357. invokeCreateHooks(vnode, insertedVnodeQueue)
  358. }
  359. }
  360. return true
  361. }
  362. function assertNodeMatch (node, vnode) {
  363. let match = true
  364. if (!node) {
  365. match = false
  366. } else if (vnode.tag) {
  367. match =
  368. vnode.tag.indexOf('vue-component') === 0 ||
  369. vnode.tag === nodeOps.tagName(node).toLowerCase()
  370. } else {
  371. match = _toString(vnode.text) === node.data
  372. }
  373. if (process.env.NODE_ENV !== 'production' && !match) {
  374. warn(
  375. 'The client-side rendered virtual DOM tree is not matching ' +
  376. 'server-rendered content. This is likely caused by incorrect HTML markup, ' +
  377. 'for example nesting block-level elements inside <p>, or missing <tbody>. ' +
  378. 'Bailing hydration and performing full client-side render.'
  379. )
  380. }
  381. return match
  382. }
  383. return function patch (oldVnode, vnode, hydrating, removeOnly) {
  384. let elm, parent
  385. let isInitialPatch = false
  386. const insertedVnodeQueue = []
  387. if (!oldVnode) {
  388. // empty mount, create new root element
  389. isInitialPatch = true
  390. createElm(vnode, insertedVnodeQueue)
  391. } else {
  392. const isRealElement = isDef(oldVnode.nodeType)
  393. if (!isRealElement && sameVnode(oldVnode, vnode)) {
  394. patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly)
  395. } else {
  396. if (isRealElement) {
  397. // mounting to a real element
  398. // check if this is server-rendered content and if we can perform
  399. // a successful hydration.
  400. if (oldVnode.hasAttribute('server-rendered')) {
  401. oldVnode.removeAttribute('server-rendered')
  402. hydrating = true
  403. }
  404. if (hydrating) {
  405. if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
  406. invokeInsertHook(vnode, insertedVnodeQueue, true)
  407. return oldVnode
  408. }
  409. }
  410. // either not server-rendered, or hydration failed.
  411. // create an empty node and replace it
  412. oldVnode = emptyNodeAt(oldVnode)
  413. }
  414. elm = oldVnode.elm
  415. parent = nodeOps.parentNode(elm)
  416. createElm(vnode, insertedVnodeQueue)
  417. // component root element replaced.
  418. // update parent placeholder node element.
  419. if (vnode.parent) {
  420. vnode.parent.elm = vnode.elm
  421. for (let i = 0; i < cbs.create.length; ++i) {
  422. cbs.create[i](emptyNode, vnode.parent)
  423. }
  424. }
  425. if (parent !== null) {
  426. nodeOps.insertBefore(parent, vnode.elm, nodeOps.nextSibling(elm))
  427. removeVnodes(parent, [oldVnode], 0, 0)
  428. } else if (isDef(oldVnode.tag)) {
  429. invokeDestroyHook(oldVnode)
  430. }
  431. }
  432. }
  433. invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
  434. return vnode.elm
  435. }
  436. }