/** * Virtual DOM patching algorithm based on Snabbdom by * Simon Friis Vindum (@paldepind) * Licensed under the MIT License * https://github.com/paldepind/snabbdom/blob/master/LICENSE * * modified by Evan You (@yyx990803) * * Not type-checking this because this file is perf-critical and the cost * of making flow understand it is not worth it. */ import VNode, { cloneVNode } from './vnode' import config from '../config' import { SSR_ATTR } from 'shared/constants' import { registerRef } from './modules/template-ref' import { traverse } from '../observer/traverse' import { activeInstance } from '../instance/lifecycle' import { isTextInputType } from 'web/util/element' import { warn, isDef, isUndef, isTrue, isArray, makeMap, isRegExp, isPrimitive } from '../util/index' export const emptyNode = new VNode('', {}, []) const hooks = ['create', 'activate', 'update', 'remove', 'destroy'] function sameVnode(a, b) { return ( a.key === b.key && a.asyncFactory === b.asyncFactory && ((a.tag === b.tag && a.isComment === b.isComment && isDef(a.data) === isDef(b.data) && sameInputType(a, b)) || (isTrue(a.isAsyncPlaceholder) && isUndef(b.asyncFactory.error))) ) } function sameInputType(a, b) { if (a.tag !== 'input') return true let i const typeA = isDef((i = a.data)) && isDef((i = i.attrs)) && i.type const typeB = isDef((i = b.data)) && isDef((i = i.attrs)) && i.type return typeA === typeB || (isTextInputType(typeA) && isTextInputType(typeB)) } 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: any = {} const { modules, nodeOps } = backend for (i = 0; i < hooks.length; ++i) { cbs[hooks[i]] = [] for (j = 0; j < modules.length; ++j) { if (isDef(modules[j][hooks[i]])) { 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) { removeNode(childElm) } } remove.listeners = listeners return remove } function removeNode(el) { const parent = nodeOps.parentNode(el) // element may have already been removed due to v-html / v-text if (isDef(parent)) { nodeOps.removeChild(parent, el) } } function isUnknownElement(vnode, inVPre) { return ( !inVPre && !vnode.ns && !( config.ignoredElements.length && config.ignoredElements.some(ignore => { return isRegExp(ignore) ? ignore.test(vnode.tag) : ignore === vnode.tag }) ) && config.isUnknownElement(vnode.tag) ) } let creatingElmInVPre = 0 function createElm( vnode, insertedVnodeQueue, parentElm?: any, refElm?: any, nested?: any, ownerArray?: any, index?: any ) { if (isDef(vnode.elm) && isDef(ownerArray)) { // This vnode was used in a previous render! // now it's used as a new node, overwriting its elm would cause // potential patch errors down the road when it's used as an insertion // reference node. Instead, we clone the node on-demand before creating // associated DOM element for it. vnode = ownerArray[index] = cloneVNode(vnode) } vnode.isRootInsert = !nested // for transition enter check if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) { return } const data = vnode.data const children = vnode.children const tag = vnode.tag if (isDef(tag)) { if (__DEV__) { if (data && data.pre) { creatingElmInVPre++ } if (isUnknownElement(vnode, creatingElmInVPre)) { warn( 'Unknown custom element: <' + tag + '> - did you ' + 'register the component correctly? For recursive components, ' + 'make sure to provide the "name" option.', vnode.context ) } } vnode.elm = vnode.ns ? nodeOps.createElementNS(vnode.ns, tag) : nodeOps.createElement(tag, vnode) setScope(vnode) createChildren(vnode, children, insertedVnodeQueue) if (isDef(data)) { invokeCreateHooks(vnode, insertedVnodeQueue) } insert(parentElm, vnode.elm, refElm) if (__DEV__ && data && data.pre) { creatingElmInVPre-- } } else if (isTrue(vnode.isComment)) { vnode.elm = nodeOps.createComment(vnode.text) insert(parentElm, vnode.elm, refElm) } else { vnode.elm = nodeOps.createTextNode(vnode.text) insert(parentElm, vnode.elm, refElm) } } function createComponent(vnode, insertedVnodeQueue, parentElm, refElm) { let i = vnode.data if (isDef(i)) { const isReactivated = isDef(vnode.componentInstance) && i.keepAlive if (isDef((i = i.hook)) && isDef((i = i.init))) { i(vnode, false /* hydrating */) } // 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(vnode.componentInstance)) { initComponent(vnode, insertedVnodeQueue) insert(parentElm, vnode.elm, refElm) if (isTrue(isReactivated)) { reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm) } return true } } } function initComponent(vnode, insertedVnodeQueue) { if (isDef(vnode.data.pendingInsert)) { insertedVnodeQueue.push.apply( insertedVnodeQueue, vnode.data.pendingInsert ) vnode.data.pendingInsert = null } vnode.elm = vnode.componentInstance.$el if (isPatchable(vnode)) { invokeCreateHooks(vnode, insertedVnodeQueue) setScope(vnode) } else { // empty component root. // skip all element-related modules except for ref (#3455) registerRef(vnode) // make sure to invoke the insert hook insertedVnodeQueue.push(vnode) } } function reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm) { let i // hack for #4339: a reactivated component with inner transition // does not trigger because the inner node's created hooks are not called // again. It's not ideal to involve module-specific logic in here but // there doesn't seem to be a better way to do it. let innerNode = vnode while (innerNode.componentInstance) { innerNode = innerNode.componentInstance._vnode if (isDef((i = innerNode.data)) && isDef((i = i.transition))) { for (i = 0; i < cbs.activate.length; ++i) { cbs.activate[i](emptyNode, innerNode) } insertedVnodeQueue.push(innerNode) break } } // unlike a newly created component, // a reactivated keep-alive component doesn't insert itself insert(parentElm, vnode.elm, refElm) } function insert(parent, elm, ref) { if (isDef(parent)) { if (isDef(ref)) { if (nodeOps.parentNode(ref) === parent) { nodeOps.insertBefore(parent, elm, ref) } } else { nodeOps.appendChild(parent, elm) } } } function createChildren(vnode, children, insertedVnodeQueue) { if (isArray(children)) { if (__DEV__) { checkDuplicateKeys(children) } for (let i = 0; i < children.length; ++i) { createElm( children[i], insertedVnodeQueue, vnode.elm, null, true, children, i ) } } else if (isPrimitive(vnode.text)) { nodeOps.appendChild(vnode.elm, nodeOps.createTextNode(String(vnode.text))) } } function isPatchable(vnode) { while (vnode.componentInstance) { vnode = vnode.componentInstance._vnode } return isDef(vnode.tag) } 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 (isDef(i.create)) i.create(emptyNode, vnode) if (isDef(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.fnScopeId))) { nodeOps.setStyleScope(vnode.elm, i) } else { let ancestor = vnode while (ancestor) { if (isDef((i = ancestor.context)) && isDef((i = i.$options._scopeId))) { nodeOps.setStyleScope(vnode.elm, i) } ancestor = ancestor.parent } } // for slot content they should also get the scopeId from the host instance. if ( isDef((i = activeInstance)) && i !== vnode.context && i !== vnode.fnContext && isDef((i = i.$options._scopeId)) ) { nodeOps.setStyleScope(vnode.elm, i) } } function addVnodes( parentElm, refElm, vnodes, startIdx, endIdx, insertedVnodeQueue ) { for (; startIdx <= endIdx; ++startIdx) { createElm( vnodes[startIdx], insertedVnodeQueue, parentElm, refElm, false, vnodes, startIdx ) } } 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.children))) { for (j = 0; j < vnode.children.length; ++j) { invokeDestroyHook(vnode.children[j]) } } } function removeVnodes(vnodes, startIdx, endIdx) { for (; startIdx <= endIdx; ++startIdx) { const ch = vnodes[startIdx] if (isDef(ch)) { if (isDef(ch.tag)) { removeAndInvokeRemoveHook(ch) invokeDestroyHook(ch) } else { // Text node removeNode(ch.elm) } } } } function removeAndInvokeRemoveHook(vnode, rm?: any) { if (isDef(rm) || isDef(vnode.data)) { let i const listeners = cbs.remove.length + 1 if (isDef(rm)) { // we have a recursively passed down rm callback // increase the listeners count rm.listeners += listeners } else { // directly removing rm = createRmCb(vnode.elm, listeners) } // recursively invoke hooks on child component root node if ( isDef((i = vnode.componentInstance)) && 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 { removeNode(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, vnodeToMove, refElm // removeOnly is a special flag used only by // to ensure removed elements stay in correct relative positions // during leaving transitions const canMove = !removeOnly if (__DEV__) { checkDuplicateKeys(newCh) } 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, newCh, newStartIdx ) oldStartVnode = oldCh[++oldStartIdx] newStartVnode = newCh[++newStartIdx] } else if (sameVnode(oldEndVnode, newEndVnode)) { patchVnode( oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx ) oldEndVnode = oldCh[--oldEndIdx] newEndVnode = newCh[--newEndIdx] } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right patchVnode( oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx ) 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, newCh, newStartIdx ) 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] : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx) if (isUndef(idxInOld)) { // New element createElm( newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx ) } else { vnodeToMove = oldCh[idxInOld] if (sameVnode(vnodeToMove, newStartVnode)) { patchVnode( vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx ) oldCh[idxInOld] = undefined canMove && nodeOps.insertBefore( parentElm, vnodeToMove.elm, oldStartVnode.elm ) } else { // same key but different element. treat as new element createElm( newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx ) } } newStartVnode = newCh[++newStartIdx] } } if (oldStartIdx > oldEndIdx) { refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm addVnodes( parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue ) } else if (newStartIdx > newEndIdx) { removeVnodes(oldCh, oldStartIdx, oldEndIdx) } } function checkDuplicateKeys(children) { const seenKeys = {} for (let i = 0; i < children.length; i++) { const vnode = children[i] const key = vnode.key if (isDef(key)) { if (seenKeys[key]) { warn( `Duplicate keys detected: '${key}'. This may cause an update error.`, vnode.context ) } else { seenKeys[key] = true } } } } function findIdxInOld(node, oldCh, start, end) { for (let i = start; i < end; i++) { const c = oldCh[i] if (isDef(c) && sameVnode(node, c)) return i } } function patchVnode( oldVnode, vnode, insertedVnodeQueue, ownerArray, index, removeOnly?: any ) { if (oldVnode === vnode) { return } if (isDef(vnode.elm) && isDef(ownerArray)) { // clone reused vnode vnode = ownerArray[index] = cloneVNode(vnode) } const elm = (vnode.elm = oldVnode.elm) if (isTrue(oldVnode.isAsyncPlaceholder)) { if (isDef(vnode.asyncFactory.resolved)) { hydrate(oldVnode.elm, vnode, insertedVnodeQueue) } else { vnode.isAsyncPlaceholder = true } return } // reuse element for static trees. // note we only do this if the vnode is cloned - // if the new node is not cloned it means the render functions have been // reset by the hot-reload-api and we need to do a proper re-render. if ( isTrue(vnode.isStatic) && isTrue(oldVnode.isStatic) && vnode.key === oldVnode.key && (isTrue(vnode.isCloned) || isTrue(vnode.isOnce)) ) { vnode.componentInstance = oldVnode.componentInstance return } let i const data = vnode.data if (isDef(data) && isDef((i = data.hook)) && isDef((i = i.prepatch))) { i(oldVnode, vnode) } const oldCh = oldVnode.children const ch = vnode.children if (isDef(data) && isPatchable(vnode)) { for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode) if (isDef((i = data.hook)) && isDef((i = i.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 (__DEV__) { checkDuplicateKeys(ch) } if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '') addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue) } else if (isDef(oldCh)) { removeVnodes(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 (isDef(data)) { if (isDef((i = data.hook)) && isDef((i = i.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 (isTrue(initial) && isDef(vnode.parent)) { vnode.parent.data.pendingInsert = queue } else { for (let i = 0; i < queue.length; ++i) { queue[i].data.hook.insert(queue[i]) } } } let hydrationBailed = false // list of modules that can skip create hook during hydration because they // are already rendered on the client or has no need for initialization // Note: style is excluded because it relies on initial clone for future // deep updates (#7063). const isRenderedModule = makeMap('attrs,class,staticClass,staticStyle,key') // Note: this is a browser-only function so we can assume elms are DOM nodes. function hydrate(elm, vnode, insertedVnodeQueue, inVPre?: boolean) { let i const { tag, data, children } = vnode inVPre = inVPre || (data && data.pre) vnode.elm = elm if (isTrue(vnode.isComment) && isDef(vnode.asyncFactory)) { vnode.isAsyncPlaceholder = true return true } // assert node match if (__DEV__) { if (!assertNodeMatch(elm, vnode, inVPre)) { return false } } if (isDef(data)) { if (isDef((i = data.hook)) && isDef((i = i.init))) i(vnode, true /* hydrating */) if (isDef((i = vnode.componentInstance))) { // child component. it should have hydrated its own tree. initComponent(vnode, insertedVnodeQueue) return true } } if (isDef(tag)) { if (isDef(children)) { // empty element, allow client to pick up and populate children if (!elm.hasChildNodes()) { createChildren(vnode, children, insertedVnodeQueue) } else { // v-html and domProps: innerHTML if ( isDef((i = data)) && isDef((i = i.domProps)) && isDef((i = i.innerHTML)) ) { if (i !== elm.innerHTML) { /* istanbul ignore if */ if ( __DEV__ && typeof console !== 'undefined' && !hydrationBailed ) { hydrationBailed = true console.warn('Parent: ', elm) console.warn('server innerHTML: ', i) console.warn('client innerHTML: ', elm.innerHTML) } return false } } else { // iterate and compare children lists let childrenMatch = true let childNode = elm.firstChild for (let i = 0; i < children.length; i++) { if ( !childNode || !hydrate(childNode, children[i], insertedVnodeQueue, inVPre) ) { childrenMatch = false break } childNode = childNode.nextSibling } // if childNode is not null, it means the actual childNodes list is // longer than the virtual children list. if (!childrenMatch || childNode) { /* istanbul ignore if */ if ( __DEV__ && typeof console !== 'undefined' && !hydrationBailed ) { hydrationBailed = true console.warn('Parent: ', elm) console.warn( 'Mismatching childNodes vs. VNodes: ', elm.childNodes, children ) } return false } } } } if (isDef(data)) { let fullInvoke = false for (const key in data) { if (!isRenderedModule(key)) { fullInvoke = true invokeCreateHooks(vnode, insertedVnodeQueue) break } } if (!fullInvoke && data['class']) { // ensure collecting deps for deep class bindings for future updates traverse(data['class']) } } } else if (elm.data !== vnode.text) { elm.data = vnode.text } return true } function assertNodeMatch(node, vnode, inVPre) { if (isDef(vnode.tag)) { return ( vnode.tag.indexOf('vue-component') === 0 || (!isUnknownElement(vnode, inVPre) && vnode.tag.toLowerCase() === (node.tagName && node.tagName.toLowerCase())) ) } else { return node.nodeType === (vnode.isComment ? 8 : 3) } } return function patch(oldVnode, vnode, hydrating, removeOnly) { if (isUndef(vnode)) { if (isDef(oldVnode)) invokeDestroyHook(oldVnode) return } let isInitialPatch = false const insertedVnodeQueue: any[] = [] if (isUndef(oldVnode)) { // empty mount (likely as component), create new root element isInitialPatch = true createElm(vnode, insertedVnodeQueue) } else { const isRealElement = isDef(oldVnode.nodeType) if (!isRealElement && sameVnode(oldVnode, vnode)) { // patch existing root node patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, 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.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) { oldVnode.removeAttribute(SSR_ATTR) hydrating = true } if (isTrue(hydrating)) { if (hydrate(oldVnode, vnode, insertedVnodeQueue)) { invokeInsertHook(vnode, insertedVnodeQueue, true) return oldVnode } else if (__DEV__) { 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.' ) } } // either not server-rendered, or hydration failed. // create an empty node and replace it oldVnode = emptyNodeAt(oldVnode) } // replacing existing element const oldElm = oldVnode.elm const parentElm = nodeOps.parentNode(oldElm) // create new node createElm( vnode, insertedVnodeQueue, // extremely rare edge case: do not insert if old element is in a // leaving transition. Only happens when combining transition + // keep-alive + HOCs. (#4590) oldElm._leaveCb ? null : parentElm, nodeOps.nextSibling(oldElm) ) // update parent placeholder node element, recursively if (isDef(vnode.parent)) { let ancestor = vnode.parent const patchable = isPatchable(vnode) while (ancestor) { for (let i = 0; i < cbs.destroy.length; ++i) { cbs.destroy[i](ancestor) } ancestor.elm = vnode.elm if (patchable) { for (let i = 0; i < cbs.create.length; ++i) { cbs.create[i](emptyNode, ancestor) } // #6513 // invoke insert hooks that may have been merged by create hooks. // e.g. for directives that uses the "inserted" hook. const insert = ancestor.data.hook.insert if (insert.merged) { // start at index 1 to avoid re-invoking component mounted hook for (let i = 1; i < insert.fns.length; i++) { insert.fns[i]() } } } else { registerRef(ancestor) } ancestor = ancestor.parent } } // destroy old node if (isDef(parentElm)) { removeVnodes([oldVnode], 0, 0) } else if (isDef(oldVnode.tag)) { invokeDestroyHook(oldVnode) } } } invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch) return vnode.elm } }