/** * Virtual DOM implementation based on Snabbdom by * Simon Friis Vindum (@paldepind) * with custom modifications. * * Not type-checking this because this file is perf-critical and the cost * of making flow understand it is not worth it. */ import config from '../config' import VNode from './vnode' import { isPrimitive, _toString, warn } from '../util/index' const emptyData = {} const emptyNode = new VNode('', emptyData, []) const hooks = ['create', 'update', 'postpatch', 'remove', 'destroy'] function isUndef (s) { return s == null } function isDef (s) { return s != null } function sameVnode (vnode1, vnode2) { if (vnode1.isStatic || vnode2.isStatic) { return vnode1 === vnode2 } return ( vnode1.key === vnode2.key && vnode1.tag === vnode2.tag && !vnode1.data === !vnode2.data ) } function createKeyToOldIdx (children, beginIdx, endIdx) { let i, key const map = {} for (i = beginIdx; i <= endIdx; ++i) { key = children[i].key if (isDef(key)) map[key] = i } return map } export function createPatchFunction (backend) { let i, j const cbs = {} const { modules, nodeOps } = backend for (i = 0; i < hooks.length; ++i) { cbs[hooks[i]] = [] for (j = 0; j < modules.length; ++j) { if (modules[j][hooks[i]] !== undefined) cbs[hooks[i]].push(modules[j][hooks[i]]) } } function emptyNodeAt (elm) { return new VNode(nodeOps.tagName(elm).toLowerCase(), {}, [], undefined, elm) } function createRmCb (childElm, listeners) { function remove () { if (--remove.listeners === 0) { removeElement(childElm) } } remove.listeners = listeners return remove } function removeElement (el) { const parent = nodeOps.parentNode(el) nodeOps.removeChild(parent, el) } function createElm (vnode, insertedVnodeQueue) { let i, elm const data = vnode.data if (isDef(data)) { if (isDef(i = data.hook) && isDef(i = i.init)) i(vnode) // after calling the init hook, if the vnode is a child component // it should've created a child instance and mounted it. the child // component also has set the placeholder vnode's elm. // in that case we can just return the element and be done. if (isDef(i = vnode.child)) { if (vnode.data.pendingInsert) { insertedVnodeQueue.push.apply(insertedVnodeQueue, vnode.data.pendingInsert) } vnode.elm = vnode.child.$el invokeCreateHooks(vnode, insertedVnodeQueue) setScope(vnode) return vnode.elm } } const children = vnode.children const tag = vnode.tag if (isDef(tag)) { if (process.env.NODE_ENV !== 'production') { if ( !vnode.ns && !(config.ignoredElements && config.ignoredElements.indexOf(tag) > -1) && config.isUnknownElement(tag) ) { warn( 'Unknown custom element: <' + tag + '> - did you ' + 'register the component correctly? For recursive components, ' + 'make sure to provide the "name" option.', vnode.context ) } } elm = vnode.elm = vnode.ns ? nodeOps.createElementNS(vnode.ns, tag) : nodeOps.createElement(tag) setScope(vnode) if (Array.isArray(children)) { for (i = 0; i < children.length; ++i) { nodeOps.appendChild(elm, createElm(children[i], insertedVnodeQueue)) } } else if (isPrimitive(vnode.text)) { nodeOps.appendChild(elm, nodeOps.createTextNode(vnode.text)) } if (isDef(data)) { invokeCreateHooks(vnode, insertedVnodeQueue) } } else { elm = vnode.elm = nodeOps.createTextNode(vnode.text) } return vnode.elm } function invokeCreateHooks (vnode, insertedVnodeQueue) { for (let i = 0; i < cbs.create.length; ++i) { cbs.create[i](emptyNode, vnode) } i = vnode.data.hook // Reuse variable if (isDef(i)) { if (i.create) i.create(emptyNode, vnode) if (i.insert) insertedVnodeQueue.push(vnode) } } // set scope id attribute for scoped CSS. // this is implemented as a special case to avoid the overhead // of going through the normal attribute patching process. function setScope (vnode) { let i if (isDef(i = vnode.host) && isDef(i = i.$options._scopeId)) { nodeOps.setAttribute(vnode.elm, i, '') } if (isDef(i = vnode.context) && isDef(i = i.$options._scopeId)) { nodeOps.setAttribute(vnode.elm, i, '') } } function addVnodes (parentElm, before, vnodes, startIdx, endIdx, insertedVnodeQueue) { for (; startIdx <= endIdx; ++startIdx) { nodeOps.insertBefore(parentElm, createElm(vnodes[startIdx], insertedVnodeQueue), before) } } function invokeDestroyHook (vnode) { let i, j const data = vnode.data if (isDef(data)) { if (isDef(i = data.hook) && isDef(i = i.destroy)) i(vnode) for (i = 0; i < cbs.destroy.length; ++i) cbs.destroy[i](vnode) } if (isDef(i = vnode.child) && !data.keepAlive) { invokeDestroyHook(i._vnode) } if (isDef(i = vnode.children)) { for (j = 0; j < vnode.children.length; ++j) { invokeDestroyHook(vnode.children[j]) } } } function removeVnodes (parentElm, vnodes, startIdx, endIdx) { for (; startIdx <= endIdx; ++startIdx) { const ch = vnodes[startIdx] if (isDef(ch)) { if (isDef(ch.tag)) { invokeDestroyHook(ch) removeAndInvokeRemoveHook(ch) } else { // Text node nodeOps.removeChild(parentElm, ch.elm) } } } } function removeAndInvokeRemoveHook (vnode, rm) { if (rm || isDef(vnode.data)) { const listeners = cbs.remove.length + 1 if (!rm) { // directly removing rm = createRmCb(vnode.elm, listeners) } else { // we have a recursively passed down rm callback // increase the listeners count rm.listeners += listeners } // recursively invoke hooks on child component root node if (isDef(i = vnode.child) && isDef(i = i._vnode) && isDef(i.data)) { removeAndInvokeRemoveHook(i, rm) } for (i = 0; i < cbs.remove.length; ++i) { cbs.remove[i](vnode, rm) } if (isDef(i = vnode.data.hook) && isDef(i = i.remove)) { i(vnode, rm) } else { rm() } } else { removeElement(vnode.elm) } } function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) { let oldStartIdx = 0 let newStartIdx = 0 let oldEndIdx = oldCh.length - 1 let oldStartVnode = oldCh[0] let oldEndVnode = oldCh[oldEndIdx] let newEndIdx = newCh.length - 1 let newStartVnode = newCh[0] let newEndVnode = newCh[newEndIdx] let oldKeyToIdx, idxInOld, elmToMove, before // removeOnly is a special flag used only by // to ensure removed elements stay in correct relative positions // during leaving transitions const canMove = !removeOnly while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { if (isUndef(oldStartVnode)) { oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left } else if (isUndef(oldEndVnode)) { oldEndVnode = oldCh[--oldEndIdx] } else if (sameVnode(oldStartVnode, newStartVnode)) { patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue) oldStartVnode = oldCh[++oldStartIdx] newStartVnode = newCh[++newStartIdx] } else if (sameVnode(oldEndVnode, newEndVnode)) { patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue) oldEndVnode = oldCh[--oldEndIdx] newEndVnode = newCh[--newEndIdx] } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue) canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm)) oldStartVnode = oldCh[++oldStartIdx] newEndVnode = newCh[--newEndIdx] } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue) canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm) oldEndVnode = oldCh[--oldEndIdx] newStartVnode = newCh[++newStartIdx] } else { if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx) idxInOld = isDef(newStartVnode.key) ? oldKeyToIdx[newStartVnode.key] : newStartVnode.isStatic ? oldCh.indexOf(newStartVnode) : null if (isUndef(idxInOld) || idxInOld === -1) { // New element nodeOps.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm) newStartVnode = newCh[++newStartIdx] } else { elmToMove = oldCh[idxInOld] /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && !elmToMove) { warn( 'It seems there are duplicate keys that is causing an update error. ' + 'Make sure each v-for item has a unique key.' ) } if (elmToMove.tag !== newStartVnode.tag) { // same key but different element. treat as new element nodeOps.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm) newStartVnode = newCh[++newStartIdx] } else { patchVnode(elmToMove, newStartVnode, insertedVnodeQueue) oldCh[idxInOld] = undefined canMove && nodeOps.insertBefore(parentElm, newStartVnode.elm, oldStartVnode.elm) newStartVnode = newCh[++newStartIdx] } } } } if (oldStartIdx > oldEndIdx) { before = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue) } else if (newStartIdx > newEndIdx) { removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx) } } function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) { if (oldVnode === vnode) return let i, hook const hasData = isDef(i = vnode.data) if (hasData && isDef(hook = i.hook) && isDef(i = hook.prepatch)) { i(oldVnode, vnode) } const elm = vnode.elm = oldVnode.elm const oldCh = oldVnode.children const ch = vnode.children if (hasData) { for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode) if (isDef(hook) && isDef(i = hook.update)) i(oldVnode, vnode) } if (isUndef(vnode.text)) { if (isDef(oldCh) && isDef(ch)) { if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly) } else if (isDef(ch)) { if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '') addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue) } else if (isDef(oldCh)) { removeVnodes(elm, oldCh, 0, oldCh.length - 1) } else if (isDef(oldVnode.text)) { nodeOps.setTextContent(elm, '') } } else if (oldVnode.text !== vnode.text) { nodeOps.setTextContent(elm, vnode.text) } if (hasData) { for (i = 0; i < cbs.postpatch.length; ++i) cbs.postpatch[i](oldVnode, vnode) if (isDef(hook) && isDef(i = hook.postpatch)) i(oldVnode, vnode) } } function invokeInsertHook (vnode, queue, initial) { // delay insert hooks for component root nodes, invoke them after the // element is really inserted if (initial && vnode.parent) { vnode.parent.data.pendingInsert = queue } else { for (let i = 0; i < queue.length; ++i) { queue[i].data.hook.insert(queue[i]) } } } let bailed = false function hydrate (elm, vnode, insertedVnodeQueue) { if (process.env.NODE_ENV !== 'production') { if (!assertNodeMatch(elm, vnode)) { return false } } vnode.elm = elm const { tag, data, children } = vnode if (isDef(data)) { if (isDef(i = data.hook) && isDef(i = i.init)) i(vnode, true /* hydrating */) if (isDef(i = vnode.child)) { // child component. it should have hydrated its own tree. invokeCreateHooks(vnode, insertedVnodeQueue) return true } } if (isDef(tag)) { if (isDef(children)) { const childNodes = nodeOps.childNodes(elm) for (let i = 0; i < children.length; i++) { const success = hydrate(childNodes[i], children[i], insertedVnodeQueue) if (!success) { if (process.env.NODE_ENV !== 'production' && typeof console !== 'undefined' && !bailed) { bailed = true console.warn('Parent: ', elm) console.warn('Mismatching childNodes vs. VNodes: ', childNodes, children) } return false } } } if (isDef(data)) { invokeCreateHooks(vnode, insertedVnodeQueue) } } return true } function assertNodeMatch (node, vnode) { let match = true if (!node) { match = false } else if (vnode.tag) { match = vnode.tag.indexOf('vue-component') === 0 || vnode.tag === nodeOps.tagName(node).toLowerCase() } else { match = _toString(vnode.text) === node.data } if (process.env.NODE_ENV !== 'production' && !match) { warn( 'The client-side rendered virtual DOM tree is not matching ' + 'server-rendered content. This is likely caused by incorrect HTML markup, ' + 'for example nesting block-level elements inside

, or missing . ' + 'Bailing hydration and performing full client-side render.' ) } return match } return function patch (oldVnode, vnode, hydrating, removeOnly) { let elm, parent let isInitialPatch = false const insertedVnodeQueue = [] if (!oldVnode) { // empty mount, create new root element isInitialPatch = true createElm(vnode, insertedVnodeQueue) } else { const isRealElement = isDef(oldVnode.nodeType) if (!isRealElement && sameVnode(oldVnode, vnode)) { patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly) } else { if (isRealElement) { // mounting to a real element // check if this is server-rendered content and if we can perform // a successful hydration. if (oldVnode.hasAttribute('server-rendered')) { oldVnode.removeAttribute('server-rendered') hydrating = true } if (hydrating) { if (hydrate(oldVnode, vnode, insertedVnodeQueue)) { invokeInsertHook(vnode, insertedVnodeQueue, true) return oldVnode } } // either not server-rendered, or hydration failed. // create an empty node and replace it oldVnode = emptyNodeAt(oldVnode) } elm = oldVnode.elm parent = nodeOps.parentNode(elm) createElm(vnode, insertedVnodeQueue) // component root element replaced. // update parent placeholder node element. if (vnode.parent) { vnode.parent.elm = vnode.elm for (let i = 0; i < cbs.create.length; ++i) { cbs.create[i](emptyNode, vnode.parent) } } if (parent !== null) { nodeOps.insertBefore(parent, vnode.elm, nodeOps.nextSibling(elm)) removeVnodes(parent, [oldVnode], 0, 0) } else if (isDef(oldVnode.tag)) { invokeDestroyHook(oldVnode) } } } invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch) return vnode.elm } }