/** * 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 NKDuy (@khanhduy1407) * * Not type-checking this because this file is perf-critical and the cost * of making flow understand it is not worth it. */ import KNode, { cloneKNode } from './knode' 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 KNode('', {}, []) const hooks = ['create', 'activate', 'update', 'remove', 'destroy'] function sameKnode(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 KNode(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 k-html / k-text if (isDef(parent)) { nodeOps.removeChild(parent, el) } } function isUnknownElement(knode, inKPre) { return ( !inKPre && !knode.ns && !( config.ignoredElements.length && config.ignoredElements.some(ignore => { return isRegExp(ignore) ? ignore.test(knode.tag) : ignore === knode.tag }) ) && config.isUnknownElement(knode.tag) ) } let creatingElmInKPre = 0 function createElm( knode, insertedKnodeQueue, parentElm?: any, refElm?: any, nested?: any, ownerArray?: any, index?: any ) { if (isDef(knode.elm) && isDef(ownerArray)) { // This knode 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. knode = ownerArray[index] = cloneKNode(knode) } knode.isRootInsert = !nested // for transition enter check if (createComponent(knode, insertedKnodeQueue, parentElm, refElm)) { return } const data = knode.data const children = knode.children const tag = knode.tag if (isDef(tag)) { if (__DEV__) { if (data && data.pre) { creatingElmInKPre++ } if (isUnknownElement(knode, creatingElmInKPre)) { warn( 'Unknown custom element: <' + tag + '> - did you ' + 'register the component correctly? For recursive components, ' + 'make sure to provide the "name" option.', knode.context ) } } knode.elm = knode.ns ? nodeOps.createElementNS(knode.ns, tag) : nodeOps.createElement(tag, knode) setScope(knode) createChildren(knode, children, insertedKnodeQueue) if (isDef(data)) { invokeCreateHooks(knode, insertedKnodeQueue) } insert(parentElm, knode.elm, refElm) if (__DEV__ && data && data.pre) { creatingElmInKPre-- } } else if (isTrue(knode.isComment)) { knode.elm = nodeOps.createComment(knode.text) insert(parentElm, knode.elm, refElm) } else { knode.elm = nodeOps.createTextNode(knode.text) insert(parentElm, knode.elm, refElm) } } function createComponent(knode, insertedKnodeQueue, parentElm, refElm) { let i = knode.data if (isDef(i)) { const isReactivated = isDef(knode.componentInstance) && i.keepAlive if (isDef((i = i.hook)) && isDef((i = i.init))) { i(knode, false /* hydrating */) } // after calling the init hook, if the knode is a child component // it should've created a child instance and mounted it. the child // component also has set the placeholder knode's elm. // in that case we can just return the element and be done. if (isDef(knode.componentInstance)) { initComponent(knode, insertedKnodeQueue) insert(parentElm, knode.elm, refElm) if (isTrue(isReactivated)) { reactivateComponent(knode, insertedKnodeQueue, parentElm, refElm) } return true } } } function initComponent(knode, insertedKnodeQueue) { if (isDef(knode.data.pendingInsert)) { insertedKnodeQueue.push.apply( insertedKnodeQueue, knode.data.pendingInsert ) knode.data.pendingInsert = null } knode.elm = knode.componentInstance.$el if (isPatchable(knode)) { invokeCreateHooks(knode, insertedKnodeQueue) setScope(knode) } else { // empty component root. // skip all element-related modules except for ref (#3455) registerRef(knode) // make sure to invoke the insert hook insertedKnodeQueue.push(knode) } } function reactivateComponent(knode, insertedKnodeQueue, 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 = knode while (innerNode.componentInstance) { innerNode = innerNode.componentInstance._knode if (isDef((i = innerNode.data)) && isDef((i = i.transition))) { for (i = 0; i < cbs.activate.length; ++i) { cbs.activate[i](emptyNode, innerNode) } insertedKnodeQueue.push(innerNode) break } } // unlike a newly created component, // a reactivated keep-alive component doesn't insert itself insert(parentElm, knode.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(knode, children, insertedKnodeQueue) { if (isArray(children)) { if (__DEV__) { checkDuplicateKeys(children) } for (let i = 0; i < children.length; ++i) { createElm( children[i], insertedKnodeQueue, knode.elm, null, true, children, i ) } } else if (isPrimitive(knode.text)) { nodeOps.appendChild(knode.elm, nodeOps.createTextNode(String(knode.text))) } } function isPatchable(knode) { while (knode.componentInstance) { knode = knode.componentInstance._knode } return isDef(knode.tag) } function invokeCreateHooks(knode, insertedKnodeQueue) { for (let i = 0; i < cbs.create.length; ++i) { cbs.create[i](emptyNode, knode) } i = knode.data.hook // Reuse variable if (isDef(i)) { if (isDef(i.create)) i.create(emptyNode, knode) if (isDef(i.insert)) insertedKnodeQueue.push(knode) } } // 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(knode) { let i if (isDef((i = knode.fnScopeId))) { nodeOps.setStyleScope(knode.elm, i) } else { let ancestor = knode while (ancestor) { if (isDef((i = ancestor.context)) && isDef((i = i.$options._scopeId))) { nodeOps.setStyleScope(knode.elm, i) } ancestor = ancestor.parent } } // for slot content they should also get the scopeId from the host instance. if ( isDef((i = activeInstance)) && i !== knode.context && i !== knode.fnContext && isDef((i = i.$options._scopeId)) ) { nodeOps.setStyleScope(knode.elm, i) } } function addKnodes( parentElm, refElm, knodes, startIdx, endIdx, insertedKnodeQueue ) { for (; startIdx <= endIdx; ++startIdx) { createElm( knodes[startIdx], insertedKnodeQueue, parentElm, refElm, false, knodes, startIdx ) } } function invokeDestroyHook(knode) { let i, j const data = knode.data if (isDef(data)) { if (isDef((i = data.hook)) && isDef((i = i.destroy))) i(knode) for (i = 0; i < cbs.destroy.length; ++i) cbs.destroy[i](knode) } if (isDef((i = knode.children))) { for (j = 0; j < knode.children.length; ++j) { invokeDestroyHook(knode.children[j]) } } } function removeKnodes(knodes, startIdx, endIdx) { for (; startIdx <= endIdx; ++startIdx) { const ch = knodes[startIdx] if (isDef(ch)) { if (isDef(ch.tag)) { removeAndInvokeRemoveHook(ch) invokeDestroyHook(ch) } else { // Text node removeNode(ch.elm) } } } } function removeAndInvokeRemoveHook(knode, rm?: any) { if (isDef(rm) || isDef(knode.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(knode.elm, listeners) } // recursively invoke hooks on child component root node if ( isDef((i = knode.componentInstance)) && isDef((i = i._knode)) && isDef(i.data) ) { removeAndInvokeRemoveHook(i, rm) } for (i = 0; i < cbs.remove.length; ++i) { cbs.remove[i](knode, rm) } if (isDef((i = knode.data.hook)) && isDef((i = i.remove))) { i(knode, rm) } else { rm() } } else { removeNode(knode.elm) } } function updateChildren( parentElm, oldCh, newCh, insertedKnodeQueue, removeOnly ) { let oldStartIdx = 0 let newStartIdx = 0 let oldEndIdx = oldCh.length - 1 let oldStartKnode = oldCh[0] let oldEndKnode = oldCh[oldEndIdx] let newEndIdx = newCh.length - 1 let newStartKnode = newCh[0] let newEndKnode = newCh[newEndIdx] let oldKeyToIdx, idxInOld, knodeToMove, 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(oldStartKnode)) { oldStartKnode = oldCh[++oldStartIdx] // Knode has been moved left } else if (isUndef(oldEndKnode)) { oldEndKnode = oldCh[--oldEndIdx] } else if (sameKnode(oldStartKnode, newStartKnode)) { patchKnode( oldStartKnode, newStartKnode, insertedKnodeQueue, newCh, newStartIdx ) oldStartKnode = oldCh[++oldStartIdx] newStartKnode = newCh[++newStartIdx] } else if (sameKnode(oldEndKnode, newEndKnode)) { patchKnode( oldEndKnode, newEndKnode, insertedKnodeQueue, newCh, newEndIdx ) oldEndKnode = oldCh[--oldEndIdx] newEndKnode = newCh[--newEndIdx] } else if (sameKnode(oldStartKnode, newEndKnode)) { // Knode moved right patchKnode( oldStartKnode, newEndKnode, insertedKnodeQueue, newCh, newEndIdx ) canMove && nodeOps.insertBefore( parentElm, oldStartKnode.elm, nodeOps.nextSibling(oldEndKnode.elm) ) oldStartKnode = oldCh[++oldStartIdx] newEndKnode = newCh[--newEndIdx] } else if (sameKnode(oldEndKnode, newStartKnode)) { // Knode moved left patchKnode( oldEndKnode, newStartKnode, insertedKnodeQueue, newCh, newStartIdx ) canMove && nodeOps.insertBefore(parentElm, oldEndKnode.elm, oldStartKnode.elm) oldEndKnode = oldCh[--oldEndIdx] newStartKnode = newCh[++newStartIdx] } else { if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx) idxInOld = isDef(newStartKnode.key) ? oldKeyToIdx[newStartKnode.key] : findIdxInOld(newStartKnode, oldCh, oldStartIdx, oldEndIdx) if (isUndef(idxInOld)) { // New element createElm( newStartKnode, insertedKnodeQueue, parentElm, oldStartKnode.elm, false, newCh, newStartIdx ) } else { knodeToMove = oldCh[idxInOld] if (sameKnode(knodeToMove, newStartKnode)) { patchKnode( knodeToMove, newStartKnode, insertedKnodeQueue, newCh, newStartIdx ) oldCh[idxInOld] = undefined canMove && nodeOps.insertBefore( parentElm, knodeToMove.elm, oldStartKnode.elm ) } else { // same key but different element. treat as new element createElm( newStartKnode, insertedKnodeQueue, parentElm, oldStartKnode.elm, false, newCh, newStartIdx ) } } newStartKnode = newCh[++newStartIdx] } } if (oldStartIdx > oldEndIdx) { refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm addKnodes( parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedKnodeQueue ) } else if (newStartIdx > newEndIdx) { removeKnodes(oldCh, oldStartIdx, oldEndIdx) } } function checkDuplicateKeys(children) { const seenKeys = {} for (let i = 0; i < children.length; i++) { const knode = children[i] const key = knode.key if (isDef(key)) { if (seenKeys[key]) { warn( `Duplicate keys detected: '${key}'. This may cause an update error.`, knode.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) && sameKnode(node, c)) return i } } function patchKnode( oldKnode, knode, insertedKnodeQueue, ownerArray, index, removeOnly?: any ) { if (oldKnode === knode) { return } if (isDef(knode.elm) && isDef(ownerArray)) { // clone reused knode knode = ownerArray[index] = cloneKNode(knode) } const elm = (knode.elm = oldKnode.elm) if (isTrue(oldKnode.isAsyncPlaceholder)) { if (isDef(knode.asyncFactory.resolved)) { hydrate(oldKnode.elm, knode, insertedKnodeQueue) } else { knode.isAsyncPlaceholder = true } return } // reuse element for static trees. // note we only do this if the knode 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(knode.isStatic) && isTrue(oldKnode.isStatic) && knode.key === oldKnode.key && (isTrue(knode.isCloned) || isTrue(knode.isOnce)) ) { knode.componentInstance = oldKnode.componentInstance return } let i const data = knode.data if (isDef(data) && isDef((i = data.hook)) && isDef((i = i.prepatch))) { i(oldKnode, knode) } const oldCh = oldKnode.children const ch = knode.children if (isDef(data) && isPatchable(knode)) { for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldKnode, knode) if (isDef((i = data.hook)) && isDef((i = i.update))) i(oldKnode, knode) } if (isUndef(knode.text)) { if (isDef(oldCh) && isDef(ch)) { if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedKnodeQueue, removeOnly) } else if (isDef(ch)) { if (__DEV__) { checkDuplicateKeys(ch) } if (isDef(oldKnode.text)) nodeOps.setTextContent(elm, '') addKnodes(elm, null, ch, 0, ch.length - 1, insertedKnodeQueue) } else if (isDef(oldCh)) { removeKnodes(oldCh, 0, oldCh.length - 1) } else if (isDef(oldKnode.text)) { nodeOps.setTextContent(elm, '') } } else if (oldKnode.text !== knode.text) { nodeOps.setTextContent(elm, knode.text) } if (isDef(data)) { if (isDef((i = data.hook)) && isDef((i = i.postpatch))) i(oldKnode, knode) } } function invokeInsertHook(knode, queue, initial) { // delay insert hooks for component root nodes, invoke them after the // element is really inserted if (isTrue(initial) && isDef(knode.parent)) { knode.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, knode, insertedKnodeQueue, inKPre?: boolean) { let i const { tag, data, children } = knode inKPre = inKPre || (data && data.pre) knode.elm = elm if (isTrue(knode.isComment) && isDef(knode.asyncFactory)) { knode.isAsyncPlaceholder = true return true } // assert node match if (__DEV__) { if (!assertNodeMatch(elm, knode, inKPre)) { return false } } if (isDef(data)) { if (isDef((i = data.hook)) && isDef((i = i.init))) i(knode, true /* hydrating */) if (isDef((i = knode.componentInstance))) { // child component. it should have hydrated its own tree. initComponent(knode, insertedKnodeQueue) return true } } if (isDef(tag)) { if (isDef(children)) { // empty element, allow client to pick up and populate children if (!elm.hasChildNodes()) { createChildren(knode, children, insertedKnodeQueue) } else { // k-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], insertedKnodeQueue, inKPre) ) { 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. KNodes: ', elm.childNodes, children ) } return false } } } } if (isDef(data)) { let fullInvoke = false for (const key in data) { if (!isRenderedModule(key)) { fullInvoke = true invokeCreateHooks(knode, insertedKnodeQueue) break } } if (!fullInvoke && data['class']) { // ensure collecting deps for deep class bindings for future updates traverse(data['class']) } } } else if (elm.data !== knode.text) { elm.data = knode.text } return true } function assertNodeMatch(node, knode, inKPre) { if (isDef(knode.tag)) { return ( knode.tag.indexOf('kdu-component') === 0 || (!isUnknownElement(knode, inKPre) && knode.tag.toLowerCase() === (node.tagName && node.tagName.toLowerCase())) ) } else { return node.nodeType === (knode.isComment ? 8 : 3) } } return function patch(oldKnode, knode, hydrating, removeOnly) { if (isUndef(knode)) { if (isDef(oldKnode)) invokeDestroyHook(oldKnode) return } let isInitialPatch = false const insertedKnodeQueue: any[] = [] if (isUndef(oldKnode)) { // empty mount (likely as component), create new root element isInitialPatch = true createElm(knode, insertedKnodeQueue) } else { const isRealElement = isDef(oldKnode.nodeType) if (!isRealElement && sameKnode(oldKnode, knode)) { // patch existing root node patchKnode(oldKnode, knode, insertedKnodeQueue, 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 (oldKnode.nodeType === 1 && oldKnode.hasAttribute(SSR_ATTR)) { oldKnode.removeAttribute(SSR_ATTR) hydrating = true } if (isTrue(hydrating)) { if (hydrate(oldKnode, knode, insertedKnodeQueue)) { invokeInsertHook(knode, insertedKnodeQueue, true) return oldKnode } 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 oldKnode = emptyNodeAt(oldKnode) } // replacing existing element const oldElm = oldKnode.elm const parentElm = nodeOps.parentNode(oldElm) // create new node createElm( knode, insertedKnodeQueue, // 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(knode.parent)) { let ancestor = knode.parent const patchable = isPatchable(knode) while (ancestor) { for (let i = 0; i < cbs.destroy.length; ++i) { cbs.destroy[i](ancestor) } ancestor.elm = knode.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 // clone insert hooks to avoid being mutated during iteration. // e.g. for customed directives under transition group. const cloned = insert.fns.slice(1) for (let i = 0; i < cloned.length; i++) { cloned[i]() } } } else { registerRef(ancestor) } ancestor = ancestor.parent } } // destroy old node if (isDef(parentElm)) { removeKnodes([oldKnode], 0, 0) } else if (isDef(oldKnode.tag)) { invokeDestroyHook(oldKnode) } } } invokeInsertHook(knode, insertedKnodeQueue, isInitialPatch) return knode.elm } }