We read every piece of feedback, and take your input very seriously.
To see all available qualifiers, see our documentation.
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
这里经常会问 template 会什么只能有一个根节点? 参考这里 设计如此, 因为template 会转化为 vNode, 而 diff 算法是比较的 两颗 vnode 树; 一个 tree 结构能有两个根吗? emm...
还有 key 是用来干嘛的?
我们来看一下 patch 方法,patch会将新老VNode节点进行比对,然后将根据两者的比较结果进行最小单位地修改视图,而不是将整个视图根据新的VNode重绘
function patch (oldVnode, vnode, hydrating, removeOnly, parentElm, refElm) { // vnode不存在则直接调用销毁钩子 if (isUndef(vnode)) { if (isDef(oldVnode)) invokeDestroyHook(oldVnode) return } let isInitialPatch = false const insertedVnodeQueue = [] if (isUndef(oldVnode)) { // 初始态 patch, 老节点不存在 isInitialPatch = true createElm(vnode, insertedVnodeQueue, parentElm, refElm) // 生成 DOM } else { const isRealElement = isDef(oldVnode.nodeType) /* * 只有新旧VNode节点判定为同一节点的时候才会进行patchVnode这个过程 * 否则就是创建新的DOM,移除旧的DOM * diff 过程在patchVnode里执行 */ if (!isRealElement && sameVnode(oldVnode, vnode)) { patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly) } else { if (isRealElement) { if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) { // 当旧的VNode是服务端渲染的元素,hydrating记为true oldVnode.removeAttribute(SSR_ATTR) hydrating = true } // 如果不是服务端渲染或者合并到真实DOM失败,则创建一个空的VNode节点替换它 // 创建空节点 oldVnode = emptyNodeAt(oldVnode) } // 省略... // 移除旧节点 if (isDef(parentElm)) { removeVnodes(parentElm, [oldVnode], 0, 0) } else if (isDef(oldVnode.tag)) { invokeDestroyHook(oldVnode) } } } // 省略... }
/* 判断两个VNode节点是否是同一个节点,需要满足以下条件 key相同 tag(当前节点的标签名)相同 isComment(是否为注释节点)相同 是否data(当前节点对应的对象,包含了具体的一些数据信息,是一个VNodeData类型 当标签是<input>的时候,type必须相同 */ function sameVnode (a, b) { return ( a.key === b.key && a.tag === b.tag && a.isComment === b.isComment && isDef(a.data) === isDef(b.data) && sameInputType(a, b) ) } function sameInputType (a, b) { if (a.tag !== 'input') return true let i // 判断当标签是<input>的时候,type是否相同 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 }
function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) { // 1.两个VNode节点相同则直接返回, 直接使用老节点 if (oldVnode === vnode) { return } // 2.如果新旧节点满足 static,key,once: 新替换旧,直接赋值 if (isTrue(vnode.isStatic) && // 如果新旧VNode都是静态的 isTrue(oldVnode.isStatic) && vnode.key === oldVnode.key && // 同时它们的key相同 // 新的VNode是clone或者是标记了once(标记v-once属性,只渲染一次) (isTrue(vnode.isCloned) || isTrue(vnode.isOnce)) ) { // 只需要替换真实elm以及componentInstance vnode.elm = oldVnode.elm 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 elm = vnode.elm = oldVnode.elm const oldCh = oldVnode.children // 老节点的子节点 const ch = vnode.children // 新节点的子节点 // 调用update回调以及update钩子 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) } // 3.当新节点没有 text 时 if (isUndef(vnode.text)) { // 1.新老节点均有children子节点 if (isDef(oldCh) && isDef(ch)) { // 对比子节点, 不相同则进行 diff if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly) } // 2.老节点没有子节点而新节点存在子节点 else if (isDef(ch)) { // 先清空elm的文本内容,然后为当前节点加入子节点 if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '') addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue) } // 3.当新节点没有子节点而老节点有子节点 else if (isDef(oldCh)) { // 移除所有elm的子节点 removeVnodes(elm, oldCh, 0, oldCh.length - 1) } // 4.当新老节点都无子节点的时候 else if (isDef(oldVnode.text)) { // 只是文本的替换,因为这个逻辑中新节点text不存在,所以直接去除ele的文本 nodeOps.setTextContent(elm, '') } } else if (oldVnode.text !== vnode.text) { // 当新老节点text不一样时,直接替换这段文本 nodeOps.setTextContent(elm, vnode.text) } if (isDef(data)) { if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode) } }
// 新老节点均有children子节点时进入这一步 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, refElm // 临时变量 // during leaving transitions const canMove = !removeOnly // 如果存在key,并且满足sameVnode,会将该DOM节点进行复用,否则则会创建一个新的DOM节点 while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { if (isUndef(oldStartVnode)) { // 老节点头指针 右移 oldStartVnode = oldCh[++oldStartIdx] } else if (isUndef(oldEndVnode)) { // 老节点尾指针 左移 oldEndVnode = oldCh[--oldEndIdx] } else if (sameVnode(oldStartVnode, newStartVnode)) { // 1.比较新旧头指针节点 patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue) oldStartVnode = oldCh[++oldStartIdx] newStartVnode = newCh[++newStartIdx] } else if (sameVnode(oldEndVnode, newEndVnode)) { // 2.比较新旧尾指针节点 patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue) oldEndVnode = oldCh[--oldEndIdx] newEndVnode = newCh[--newEndIdx] } else if (sameVnode(oldStartVnode, newEndVnode)) { // 3.比较旧头 和 新尾 // 说明 oldStartVnode 已经跑到了 oldEndVnode后面, 同时真实 DOM 位移 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)) { // 4.比较旧尾 和 新头 // 说明oldEndVnode跑到了oldStartVnode的前面,进行patchVnode的同时真实的DOM节点移动到了oldStartVnode的前面 patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue) canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm) oldEndVnode = oldCh[--oldEndIdx] newStartVnode = newCh[++newStartIdx] } else { /* 以上4种情况均不符合时: 生成一个key与旧VNode的key对应的哈希表(只有第一次进来undefined的时候会生成,也为后面检测重复的key值做准备) 比如childre是这样的 [{xx: xx, key: 'key0'}, {xx: xx, key: 'key1'}, {xx: xx, key: 'key2'}] beginIdx = 0 endIdx = 2 结果生成{key0: 0, key1: 1, key2: 2} */ if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx) // 如果newStartVnode新的VNode节点存在key并且这个key在oldVnode中能找到 // 则返回这个节点的idxInOld(即第几个节点,下标) idxInOld = isDef(newStartVnode.key) ? oldKeyToIdx[newStartVnode.key] : null if (isUndef(idxInOld)) { // newStartVnode没有key或者是该key没有在老节点中找到则创建一个新的节点 createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm) newStartVnode = newCh[++newStartIdx] } else { // 获取相同key的老节点 elmToMove = oldCh[idxInOld] if (process.env.NODE_ENV !== 'production' && !elmToMove) { // 如果elmToMove不存在说明之前已经有新节点放入过这个key的DOM中,提示可能存在重复的key, // 确保v-for的时候item有唯一的key值 warn( 'It seems there are duplicate keys that is causing an update error. ' + 'Make sure each v-for item has a unique key.' ) } if (sameVnode(elmToMove, newStartVnode)) { // 如果新VNode与得到的有相同key的节点是同一个VNode则进行patchVnode patchVnode(elmToMove, newStartVnode, insertedVnodeQueue) // 因为已经patchVnode进去了,所以将这个老节点赋值undefined, // 之后如果还有新节点与该节点key相同可以检测出来提示已有重复的key oldCh[idxInOld] = undefined // 当有标识位canMove实可以直接插入oldStartVnode对应的真实DOM节点前面 canMove && nodeOps.insertBefore(parentElm, newStartVnode.elm, oldStartVnode.elm) newStartVnode = newCh[++newStartIdx] } else { // 当新的VNode与找到的同样key的VNode不是sameVNode的时候 // 比如说tag不一样或者是有不一样type的input标签),创建一个新的节点 createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm) newStartVnode = newCh[++newStartIdx] } } } } if (oldStartIdx > oldEndIdx) { // 全部比较完成以后,发现oldStartIdx > oldEndIdx的话,说明老节点已经遍历完了,新节点比老节点多, // 所以这时候多出来的新节点需要一个一个创建出来加入到真实DOM中 refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue) } else if (newStartIdx > newEndIdx) { // 如果全部比较完成以后发现newStartIdx > newEndIdx,则说明新节点已经遍历完了,老节点多余新节点, // 这个时候需要将多余的老节点从真实DOM中移除 removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx) } }
对比新旧 vnode 对象, 新旧替换的过程(如何减少新旧替换的开销和效率),有一套规则
1.前置条件: 新旧 vnode 相同? 相同则patch, 不同则销毁旧创建新 2.进入 patch 过程(5种情况) - 1.老新节点都是静态的,同时它们的key相同(代表同一节点),并且新的VNode是clone或者是标记了v-once: 只替换elm以及componentInstance - 2.老有儿子,新的没儿子: 移除该DOM节点的所有子节点 - 3.老没儿子,新的有儿子: 先清空老节点DOM的文本内容,然后为该DOM节点加入子节点 - 4.老,新都没儿子: 只是文本的替换 - 5.新老节点均有儿子(diff): 进入diff过程 遍历老新节点对象并比较同层的树节点, 然后递归比较子节点对象 通过判断 key 和 sameVnode, 是否将该DOM节点进行复用,否则则会创建一个新的DOM节点 - 1.如果只是节点移动 只是位移节点, 同时位移 DOM 元素 - 2.如果有增删改 会生成一个key与旧VNode的key对应的哈希表, 判断新VNode 中是否有 key与旧的比对, 进而进行创建或者移除操作
The text was updated successfully, but these errors were encountered:
No branches or pull requests
patch 过程中的 diff 算法
这里经常会问 template 会什么只能有一个根节点?
参考这里
设计如此, 因为template 会转化为 vNode, 而 diff 算法是比较的 两颗 vnode 树;
一个 tree 结构能有两个根吗? emm...
还有 key 是用来干嘛的?
patch 方法
我们来看一下 patch 方法,patch会将新老VNode节点进行比对,然后将根据两者的比较结果进行最小单位地修改视图,而不是将整个视图根据新的VNode重绘
patchVnode 方法
patch 前置条件
patch 过程
patch 子节点方法 updateChildren
diff 规则小结
对比新旧 vnode 对象, 新旧替换的过程(如何减少新旧替换的开销和效率),有一套规则
The text was updated successfully, but these errors were encountered: