patch.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581
  1. /**
  2. * Virtual DOM patching algorithm based on Snabbdom by
  3. * Simon Friis Vindum (@paldepind)
  4. * Licensed under the MIT License
  5. * https://github.com/paldepind/snabbdom/blob/master/LICENSE
  6. *
  7. * modified by Evan You (@yyx990803)
  8. *
  9. /*
  10. * Not type-checking this because this file is perf-critical and the cost
  11. * of making flow understand it is not worth it.
  12. */
  13. import config from '../config'
  14. import VNode from './vnode'
  15. import { isPrimitive, _toString, warn } from '../util/index'
  16. import { activeInstance } from '../instance/lifecycle'
  17. import { registerRef } from './modules/ref'
  18. export const emptyNode = new VNode('', {}, [])
  19. const hooks = ['create', 'update', 'remove', 'destroy']
  20. function isUndef (s) {
  21. return s == null
  22. }
  23. function isDef (s) {
  24. return s != null
  25. }
  26. function sameVnode (vnode1, vnode2) {
  27. return (
  28. vnode1.key === vnode2.key &&
  29. vnode1.tag === vnode2.tag &&
  30. vnode1.isComment === vnode2.isComment &&
  31. !vnode1.data === !vnode2.data
  32. )
  33. }
  34. function createKeyToOldIdx (children, beginIdx, endIdx) {
  35. let i, key
  36. const map = {}
  37. for (i = beginIdx; i <= endIdx; ++i) {
  38. key = children[i].key
  39. if (isDef(key)) map[key] = i
  40. }
  41. return map
  42. }
  43. export function createPatchFunction (backend) {
  44. let i, j
  45. const cbs = {}
  46. const { modules, nodeOps } = backend
  47. for (i = 0; i < hooks.length; ++i) {
  48. cbs[hooks[i]] = []
  49. for (j = 0; j < modules.length; ++j) {
  50. if (modules[j][hooks[i]] !== undefined) cbs[hooks[i]].push(modules[j][hooks[i]])
  51. }
  52. }
  53. function emptyNodeAt (elm) {
  54. return new VNode(nodeOps.tagName(elm).toLowerCase(), {}, [], undefined, elm)
  55. }
  56. function createRmCb (childElm, listeners) {
  57. function remove () {
  58. if (--remove.listeners === 0) {
  59. removeElement(childElm)
  60. }
  61. }
  62. remove.listeners = listeners
  63. return remove
  64. }
  65. function removeElement (el) {
  66. const parent = nodeOps.parentNode(el)
  67. // element may have already been removed due to v-html
  68. if (parent) {
  69. nodeOps.removeChild(parent, el)
  70. }
  71. }
  72. let inPre = 0
  73. function createElm (vnode, insertedVnodeQueue, parentElm, refElm, nested) {
  74. let i, isReactivated
  75. const data = vnode.data
  76. vnode.isRootInsert = !nested
  77. if (isDef(data)) {
  78. if (isDef(i = data.hook) && isDef(i = i.init)) {
  79. isReactivated = i(vnode, false /* hydrating */, parentElm, refElm)
  80. }
  81. // after calling the init hook, if the vnode is a child component
  82. // it should've created a child instance and mounted it. the child
  83. // component also has set the placeholder vnode's elm.
  84. // in that case we can just return the element and be done.
  85. if (isDef(i = vnode.child)) {
  86. initComponent(vnode, insertedVnodeQueue)
  87. if (isReactivated) {
  88. // unlike a newly created component,
  89. // a reactivated keep-alive component doesn't insert itself
  90. insert(parentElm, vnode.child.$el, refElm)
  91. }
  92. return
  93. }
  94. }
  95. const children = vnode.children
  96. const tag = vnode.tag
  97. if (isDef(tag)) {
  98. if (process.env.NODE_ENV !== 'production') {
  99. if (data && data.pre) {
  100. inPre++
  101. }
  102. if (
  103. !inPre &&
  104. !vnode.ns &&
  105. !(config.ignoredElements && config.ignoredElements.indexOf(tag) > -1) &&
  106. config.isUnknownElement(tag)
  107. ) {
  108. warn(
  109. 'Unknown custom element: <' + tag + '> - did you ' +
  110. 'register the component correctly? For recursive components, ' +
  111. 'make sure to provide the "name" option.',
  112. vnode.context
  113. )
  114. }
  115. }
  116. vnode.elm = vnode.ns
  117. ? nodeOps.createElementNS(vnode.ns, tag)
  118. : nodeOps.createElement(tag, vnode)
  119. setScope(vnode)
  120. /* istanbul ignore if */
  121. if (__WEEX__) {
  122. // in Weex, the default insertion order is parent-first.
  123. // List items can be optimized to use children-first insertion
  124. // with append="tree".
  125. const appendAsTree = data && data.appendAsTree
  126. if (!appendAsTree) {
  127. if (isDef(data)) {
  128. invokeCreateHooks(vnode, insertedVnodeQueue)
  129. }
  130. insert(parentElm, vnode.elm, refElm)
  131. }
  132. createChildren(vnode, children, insertedVnodeQueue)
  133. if (appendAsTree) {
  134. if (isDef(data)) {
  135. invokeCreateHooks(vnode, insertedVnodeQueue)
  136. }
  137. insert(parentElm, vnode.elm, refElm)
  138. }
  139. } else {
  140. createChildren(vnode, children, insertedVnodeQueue)
  141. if (isDef(data)) {
  142. invokeCreateHooks(vnode, insertedVnodeQueue)
  143. }
  144. insert(parentElm, vnode.elm, refElm)
  145. }
  146. if (process.env.NODE_ENV !== 'production' && data && data.pre) {
  147. inPre--
  148. }
  149. } else if (vnode.isComment) {
  150. vnode.elm = nodeOps.createComment(vnode.text)
  151. insert(parentElm, vnode.elm, refElm)
  152. } else {
  153. vnode.elm = nodeOps.createTextNode(vnode.text)
  154. insert(parentElm, vnode.elm, refElm)
  155. }
  156. }
  157. function insert (parent, elm, ref) {
  158. if (parent) {
  159. nodeOps.insertBefore(parent, elm, ref)
  160. }
  161. }
  162. function createChildren (vnode, children, insertedVnodeQueue) {
  163. if (Array.isArray(children)) {
  164. for (let i = 0; i < children.length; ++i) {
  165. createElm(children[i], insertedVnodeQueue, vnode.elm, null, true)
  166. }
  167. } else if (isPrimitive(vnode.text)) {
  168. nodeOps.appendChild(vnode.elm, nodeOps.createTextNode(vnode.text))
  169. }
  170. }
  171. function isPatchable (vnode) {
  172. while (vnode.child) {
  173. vnode = vnode.child._vnode
  174. }
  175. return isDef(vnode.tag)
  176. }
  177. function invokeCreateHooks (vnode, insertedVnodeQueue) {
  178. for (let i = 0; i < cbs.create.length; ++i) {
  179. cbs.create[i](emptyNode, vnode)
  180. }
  181. i = vnode.data.hook // Reuse variable
  182. if (isDef(i)) {
  183. if (i.create) i.create(emptyNode, vnode)
  184. if (i.insert) insertedVnodeQueue.push(vnode)
  185. }
  186. }
  187. function initComponent (vnode, insertedVnodeQueue) {
  188. if (vnode.data.pendingInsert) {
  189. insertedVnodeQueue.push.apply(insertedVnodeQueue, vnode.data.pendingInsert)
  190. }
  191. vnode.elm = vnode.child.$el
  192. if (isPatchable(vnode)) {
  193. invokeCreateHooks(vnode, insertedVnodeQueue)
  194. setScope(vnode)
  195. } else {
  196. // empty component root.
  197. // skip all element-related modules except for ref (#3455)
  198. registerRef(vnode)
  199. // make sure to invoke the insert hook
  200. insertedVnodeQueue.push(vnode)
  201. }
  202. }
  203. // set scope id attribute for scoped CSS.
  204. // this is implemented as a special case to avoid the overhead
  205. // of going through the normal attribute patching process.
  206. function setScope (vnode) {
  207. let i
  208. if (isDef(i = vnode.context) && isDef(i = i.$options._scopeId)) {
  209. nodeOps.setAttribute(vnode.elm, i, '')
  210. }
  211. if (isDef(i = activeInstance) &&
  212. i !== vnode.context &&
  213. isDef(i = i.$options._scopeId)) {
  214. nodeOps.setAttribute(vnode.elm, i, '')
  215. }
  216. }
  217. function addVnodes (parentElm, refElm, vnodes, startIdx, endIdx, insertedVnodeQueue) {
  218. for (; startIdx <= endIdx; ++startIdx) {
  219. createElm(vnodes[startIdx], insertedVnodeQueue, parentElm, refElm)
  220. }
  221. }
  222. function invokeDestroyHook (vnode) {
  223. let i, j
  224. const data = vnode.data
  225. if (isDef(data)) {
  226. if (isDef(i = data.hook) && isDef(i = i.destroy)) i(vnode)
  227. for (i = 0; i < cbs.destroy.length; ++i) cbs.destroy[i](vnode)
  228. }
  229. if (isDef(i = vnode.children)) {
  230. for (j = 0; j < vnode.children.length; ++j) {
  231. invokeDestroyHook(vnode.children[j])
  232. }
  233. }
  234. }
  235. function removeVnodes (parentElm, vnodes, startIdx, endIdx) {
  236. for (; startIdx <= endIdx; ++startIdx) {
  237. const ch = vnodes[startIdx]
  238. if (isDef(ch)) {
  239. if (isDef(ch.tag)) {
  240. removeAndInvokeRemoveHook(ch)
  241. invokeDestroyHook(ch)
  242. } else { // Text node
  243. nodeOps.removeChild(parentElm, ch.elm)
  244. }
  245. }
  246. }
  247. }
  248. function removeAndInvokeRemoveHook (vnode, rm) {
  249. if (rm || isDef(vnode.data)) {
  250. const listeners = cbs.remove.length + 1
  251. if (!rm) {
  252. // directly removing
  253. rm = createRmCb(vnode.elm, listeners)
  254. } else {
  255. // we have a recursively passed down rm callback
  256. // increase the listeners count
  257. rm.listeners += listeners
  258. }
  259. // recursively invoke hooks on child component root node
  260. if (isDef(i = vnode.child) && isDef(i = i._vnode) && isDef(i.data)) {
  261. removeAndInvokeRemoveHook(i, rm)
  262. }
  263. for (i = 0; i < cbs.remove.length; ++i) {
  264. cbs.remove[i](vnode, rm)
  265. }
  266. if (isDef(i = vnode.data.hook) && isDef(i = i.remove)) {
  267. i(vnode, rm)
  268. } else {
  269. rm()
  270. }
  271. } else {
  272. removeElement(vnode.elm)
  273. }
  274. }
  275. function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
  276. let oldStartIdx = 0
  277. let newStartIdx = 0
  278. let oldEndIdx = oldCh.length - 1
  279. let oldStartVnode = oldCh[0]
  280. let oldEndVnode = oldCh[oldEndIdx]
  281. let newEndIdx = newCh.length - 1
  282. let newStartVnode = newCh[0]
  283. let newEndVnode = newCh[newEndIdx]
  284. let oldKeyToIdx, idxInOld, elmToMove, refElm
  285. // removeOnly is a special flag used only by <transition-group>
  286. // to ensure removed elements stay in correct relative positions
  287. // during leaving transitions
  288. const canMove = !removeOnly
  289. while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
  290. if (isUndef(oldStartVnode)) {
  291. oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
  292. } else if (isUndef(oldEndVnode)) {
  293. oldEndVnode = oldCh[--oldEndIdx]
  294. } else if (sameVnode(oldStartVnode, newStartVnode)) {
  295. patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
  296. oldStartVnode = oldCh[++oldStartIdx]
  297. newStartVnode = newCh[++newStartIdx]
  298. } else if (sameVnode(oldEndVnode, newEndVnode)) {
  299. patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
  300. oldEndVnode = oldCh[--oldEndIdx]
  301. newEndVnode = newCh[--newEndIdx]
  302. } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
  303. patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
  304. canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
  305. oldStartVnode = oldCh[++oldStartIdx]
  306. newEndVnode = newCh[--newEndIdx]
  307. } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
  308. patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
  309. canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
  310. oldEndVnode = oldCh[--oldEndIdx]
  311. newStartVnode = newCh[++newStartIdx]
  312. } else {
  313. if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
  314. idxInOld = isDef(newStartVnode.key) ? oldKeyToIdx[newStartVnode.key] : null
  315. if (isUndef(idxInOld)) { // New element
  316. createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
  317. newStartVnode = newCh[++newStartIdx]
  318. } else {
  319. elmToMove = oldCh[idxInOld]
  320. /* istanbul ignore if */
  321. if (process.env.NODE_ENV !== 'production' && !elmToMove) {
  322. warn(
  323. 'It seems there are duplicate keys that is causing an update error. ' +
  324. 'Make sure each v-for item has a unique key.'
  325. )
  326. }
  327. if (elmToMove.tag !== newStartVnode.tag) {
  328. // same key but different element. treat as new element
  329. createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
  330. newStartVnode = newCh[++newStartIdx]
  331. } else {
  332. patchVnode(elmToMove, newStartVnode, insertedVnodeQueue)
  333. oldCh[idxInOld] = undefined
  334. canMove && nodeOps.insertBefore(parentElm, newStartVnode.elm, oldStartVnode.elm)
  335. newStartVnode = newCh[++newStartIdx]
  336. }
  337. }
  338. }
  339. }
  340. if (oldStartIdx > oldEndIdx) {
  341. refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
  342. addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
  343. } else if (newStartIdx > newEndIdx) {
  344. removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
  345. }
  346. }
  347. function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) {
  348. if (oldVnode === vnode) {
  349. return
  350. }
  351. // reuse element for static trees.
  352. // note we only do this if the vnode is cloned -
  353. // if the new node is not cloned it means the render functions have been
  354. // reset by the hot-reload-api and we need to do a proper re-render.
  355. if (vnode.isStatic &&
  356. oldVnode.isStatic &&
  357. vnode.key === oldVnode.key &&
  358. (vnode.isCloned || vnode.isOnce)) {
  359. vnode.elm = oldVnode.elm
  360. vnode.child = oldVnode.child
  361. return
  362. }
  363. let i
  364. const data = vnode.data
  365. const hasData = isDef(data)
  366. if (hasData && isDef(i = data.hook) && isDef(i = i.prepatch)) {
  367. i(oldVnode, vnode)
  368. }
  369. const elm = vnode.elm = oldVnode.elm
  370. const oldCh = oldVnode.children
  371. const ch = vnode.children
  372. if (hasData && isPatchable(vnode)) {
  373. for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
  374. if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
  375. }
  376. if (isUndef(vnode.text)) {
  377. if (isDef(oldCh) && isDef(ch)) {
  378. if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
  379. } else if (isDef(ch)) {
  380. if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
  381. addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
  382. } else if (isDef(oldCh)) {
  383. removeVnodes(elm, oldCh, 0, oldCh.length - 1)
  384. } else if (isDef(oldVnode.text)) {
  385. nodeOps.setTextContent(elm, '')
  386. }
  387. } else if (oldVnode.text !== vnode.text) {
  388. nodeOps.setTextContent(elm, vnode.text)
  389. }
  390. if (hasData) {
  391. if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
  392. }
  393. }
  394. function invokeInsertHook (vnode, queue, initial) {
  395. // delay insert hooks for component root nodes, invoke them after the
  396. // element is really inserted
  397. if (initial && vnode.parent) {
  398. vnode.parent.data.pendingInsert = queue
  399. } else {
  400. for (let i = 0; i < queue.length; ++i) {
  401. queue[i].data.hook.insert(queue[i])
  402. }
  403. }
  404. }
  405. let bailed = false
  406. function hydrate (elm, vnode, insertedVnodeQueue) {
  407. if (process.env.NODE_ENV !== 'production') {
  408. if (!assertNodeMatch(elm, vnode)) {
  409. return false
  410. }
  411. }
  412. vnode.elm = elm
  413. const { tag, data, children } = vnode
  414. if (isDef(data)) {
  415. if (isDef(i = data.hook) && isDef(i = i.init)) i(vnode, true /* hydrating */)
  416. if (isDef(i = vnode.child)) {
  417. // child component. it should have hydrated its own tree.
  418. initComponent(vnode, insertedVnodeQueue)
  419. return true
  420. }
  421. }
  422. if (isDef(tag)) {
  423. if (isDef(children)) {
  424. const childNodes = nodeOps.childNodes(elm)
  425. // empty element, allow client to pick up and populate children
  426. if (!childNodes.length) {
  427. createChildren(vnode, children, insertedVnodeQueue)
  428. } else {
  429. let childrenMatch = true
  430. if (childNodes.length !== children.length) {
  431. childrenMatch = false
  432. } else {
  433. for (let i = 0; i < children.length; i++) {
  434. if (!hydrate(childNodes[i], children[i], insertedVnodeQueue)) {
  435. childrenMatch = false
  436. break
  437. }
  438. }
  439. }
  440. if (!childrenMatch) {
  441. if (process.env.NODE_ENV !== 'production' &&
  442. typeof console !== 'undefined' &&
  443. !bailed) {
  444. bailed = true
  445. console.warn('Parent: ', elm)
  446. console.warn('Mismatching childNodes vs. VNodes: ', childNodes, children)
  447. }
  448. return false
  449. }
  450. }
  451. }
  452. if (isDef(data)) {
  453. invokeCreateHooks(vnode, insertedVnodeQueue)
  454. }
  455. }
  456. return true
  457. }
  458. function assertNodeMatch (node, vnode) {
  459. if (vnode.tag) {
  460. return (
  461. vnode.tag.indexOf('vue-component') === 0 ||
  462. vnode.tag.toLowerCase() === nodeOps.tagName(node).toLowerCase()
  463. )
  464. } else {
  465. return _toString(vnode.text) === node.data
  466. }
  467. }
  468. return function patch (oldVnode, vnode, hydrating, removeOnly, parentElm, refElm) {
  469. if (!vnode) {
  470. if (oldVnode) invokeDestroyHook(oldVnode)
  471. return
  472. }
  473. let elm, parent
  474. let isInitialPatch = false
  475. const insertedVnodeQueue = []
  476. if (!oldVnode) {
  477. // empty mount (likely as component), create new root element
  478. isInitialPatch = true
  479. createElm(vnode, insertedVnodeQueue, parentElm, refElm)
  480. } else {
  481. const isRealElement = isDef(oldVnode.nodeType)
  482. if (!isRealElement && sameVnode(oldVnode, vnode)) {
  483. // patch existing root node
  484. patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly)
  485. } else {
  486. if (isRealElement) {
  487. // mounting to a real element
  488. // check if this is server-rendered content and if we can perform
  489. // a successful hydration.
  490. if (oldVnode.nodeType === 1 && oldVnode.hasAttribute('server-rendered')) {
  491. oldVnode.removeAttribute('server-rendered')
  492. hydrating = true
  493. }
  494. if (hydrating) {
  495. if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
  496. invokeInsertHook(vnode, insertedVnodeQueue, true)
  497. return oldVnode
  498. } else if (process.env.NODE_ENV !== 'production') {
  499. warn(
  500. 'The client-side rendered virtual DOM tree is not matching ' +
  501. 'server-rendered content. This is likely caused by incorrect ' +
  502. 'HTML markup, for example nesting block-level elements inside ' +
  503. '<p>, or missing <tbody>. Bailing hydration and performing ' +
  504. 'full client-side render.'
  505. )
  506. }
  507. }
  508. // either not server-rendered, or hydration failed.
  509. // create an empty node and replace it
  510. oldVnode = emptyNodeAt(oldVnode)
  511. }
  512. // replacing existing element
  513. elm = oldVnode.elm
  514. parent = nodeOps.parentNode(elm)
  515. createElm(vnode, insertedVnodeQueue, parent, nodeOps.nextSibling(elm))
  516. if (vnode.parent) {
  517. // component root element replaced.
  518. // update parent placeholder node element, recursively
  519. let ancestor = vnode.parent
  520. while (ancestor) {
  521. ancestor.elm = vnode.elm
  522. ancestor = ancestor.parent
  523. }
  524. if (isPatchable(vnode)) {
  525. for (let i = 0; i < cbs.create.length; ++i) {
  526. cbs.create[i](emptyNode, vnode.parent)
  527. }
  528. }
  529. }
  530. if (parent !== null) {
  531. removeVnodes(parent, [oldVnode], 0, 0)
  532. } else if (isDef(oldVnode.tag)) {
  533. invokeDestroyHook(oldVnode)
  534. }
  535. }
  536. }
  537. invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
  538. return vnode.elm
  539. }
  540. }