首页IT科技vue 虚拟dom实现原理(vue虚拟dom和diff算法)

vue 虚拟dom实现原理(vue虚拟dom和diff算法)

时间2025-07-18 05:50:44分类IT科技浏览5042
导读:vue的虚拟dom和diff算法 1.虚拟dom...

vue的虚拟dom和diff算法

1.虚拟dom

虚拟dom            ,我的理解就是通过js对象的方式来具体化每一个节点                    ,把dom树上面的每个节点都变为对象里的一个元素      ,元素的子元素变为子节点         ,节点上面的class             、id                  、attribute等属性变为data内的值                    ,然后通过dom上面的createElement       、appendChild          、insertBefore等方法进行生成dom树             。

let VNode = { sel:div, data:{ key:0, props:{}, attrs:{}, class:{}, style:{}, fn:{} }, text:虚拟dom, elm:<div>虚拟dom</div> children:[ { sel:div, data:{ key:0, props:{}, attrs:{}, class:{}, style:{}, fn:{} }, text:虚拟dom children, elm:<div>虚拟dom children</div> children:[] } ] }

2.diff算法

看了diff算法后感觉写的真是巧妙         ,真正做到了最小量更新                   。

diff是当父节点相同时用来对子节点进行最小量更新的算法       。

diff算法采用四个指针:旧节点开始指针      ,旧节点结束指针                    ,新节点开始指针            ,新节点结束指针;

(上方虚拟节点中的key就是为了在进行diff算法时判断是否是同一个节点便于最小量更新)

while(旧节点开始指针<=旧节点结束指针&&新节点开始指针<=新节点结束指针){

分为以下五种情况:(前四种情况)

  当进行下面5种判断后可能会出现新节点[新节点开始指针] 旧节点[旧节点开始指针] 新节点[新节点结束指针] 旧节点[旧节点结束指针]为空值的情况   ,如果出现空值则代表当前节点已经处理过了                    ,所以就需要将指针++或者--

if(旧节点[旧节点开始指针] ==null){

旧节点开始指针++

}else if(旧节点[旧节点结束指针]==null){

旧节点结束指针--

}else if(新节点[新节点开始指针] ==null){

新节点开始指针++

}else if(新节点[新节点结束指针] ==null){

新节点结束指针--

}

1                  、新节点[新节点开始指针] 对比 旧节点[旧节点开始指针]

  如果符合此种情况                ,则代表新节点[新节点开始指针] 旧节点[旧节点开始指针] 为同一个节点,实行最小量更新                ,即只更新节点内的属性而不进行dom销毁创建操作                    ,完成更新后 新节点开始指针++   ,旧节点开始指针++

2          、新节点[新节点结束指针] 对比 旧节点[旧节点结束指针]

  如果符合此种情况            ,则代表新节点[新节点结束指针] 旧节点[旧节点结束指针] 为同一个节点                    ,实行最小量更新      ,即只更新节点内的属性而不进行dom销毁创建操作         ,完成更新后 新节点结束指针--                    ,旧节点结束指针--

3       、新节点[新节点结束指针] 对比 旧节点[旧节点开始指针]

  如果符合此种情况         ,则代表新节点[新节点结束指针] 旧节点[旧节点开始指针] 为同一个节点      ,实行最小量更新                    ,先更新节点内的属性            ,然后使用insertBefore将旧节点[旧节点开始指针] 移动到旧节点[旧节点结束指针] 之后   ,(注意:此处要移动到旧节点[旧节点结束节点] 后                    ,而不是所有旧节点后                ,因为这里的旧节点结束指针是会变化的),

父节点.insertBefore(旧节点[旧节点开始指针].elm, 旧节点[旧节点结束指针].elm.nextSibling)

  完成操作后 新节点结束指针--,旧节点开始指针++

4                  、新节点[新节点开始指针] 对比 旧节点[旧节点结束指针] 如果符合此种情况                ,则代表新节点[新节点开始指针]旧节点[旧节点结束指针] 为同一个节点                    ,实行最小量更新   ,先更新节点内的属性            ,然后使用insertBefore将旧节点[旧节点结束指针] 移动到旧节点[旧节点开始指针] 前                    ,(注意:此处要移动到旧节点[旧节点开始指针] 前      ,而不是所有旧节点前         ,因为旧节点开始指针也是会发生变化的)

父节点.insertBefore(旧节点[旧节点结束指针].elm, 旧节点[旧节点开始指针].elm)

  完成操作后                    ,旧节点结束指针--         ,新节点开始指针++

5             、遍历旧节点数组      ,生成一个以key为键                    ,index为值的对象为旧节点keyIndexMap            ,然后查询新节点[新节点开始指针]中的key是否在旧节点keyIndexMap中存在;

  如果不存在   ,则证明新节点[新节点开始指针]在旧节点列表中不存在                    ,此时需要创建新节点[新节点开始指针]为真实dom                ,并将其插入至旧节点[旧节点开始指针]前(因为此时新节点[新节点开始指针]一定处于全部未处理的旧节点前)

父节点.insertBefore(创建dom(新节点[新节点开始指针]), 旧节点[旧节点开始指针].elm)

  如果存在则先需要判断旧节点[旧节点keyIndexMap[新节点[新节点开始指针][key]]]新节点[新节点开始指针]的sel(标签)是否相同:

    如果相同则代表为同一个标签,则进行最小量更新                ,先更新节点内的属性                    ,然后insertBefore将旧节点[旧节点keyIndexMap[新节点[新节点开始指针][key]]]移动到旧节点[旧节点开始指针] 前   ,然后将旧节点[旧节点keyIndexMap[新节点[新节点开始指针][key]]]设置为undefined            ,代表当前节点处理过了;

    如果不同则代表不是同一个标签                    ,则只创建新节点[新节点开始指针]的真实dom      ,然后将其插入到旧节点[旧节点开始节点]          。

  最后新节点开始指针++

}

当以上循环完成后可能还会出现没有处理到的节点         ,所以还需要再查找没有处理到的节点:

  如果是新节点开始指针<=新节点结束指针                    ,则代表新节点列表内还有没有处理的节点         ,没有处理的节点全部为新增节点      ,此时需要遍历新节点[新节点开始指针](包含)至新节点[新节点结束指针](包含)之间的节点                    ,然后将其添加至新节点[新节点结束指针+1]之前(新节点[新节点结束指针+1]可能为空            ,新节点[新节点结束指针+1]为空时可添加到最后)

for (let i = 新节点开始节点; i <= 新节点结束节点; i++) { //insertBefore可以自动识别空值   ,如果是空值                    ,则插入到最后 父节点.insertBefore(创建dom(新节点[i]), 新节点[新节点结束节点-1]?.elm) }

   如果是旧节点开始指针<=旧节点结束指针                ,则代表旧节点内还有没有处理的节点,没有处理的节点全部为需要删除节点                ,此时需要遍历旧节点[旧节点开始指针](包含)至旧节点[旧节点结束指针](包含) 之间的节点                    ,然后将其全部删除                  。

for (let i = 旧节点开始指针; i <= 旧节点结束指针; i++) { 旧节点[i] && (父节点.removeChild(旧节点[i].elm)) }

以上就是我对diff算法的理解   ,下面贴上代码(阉割版            ,部分情况没有考虑                    ,旨在学习diff算法      ,可能会有bug):

//updateChildren文件 import { sameVnode } from ./is import patchVnode from ./patchVnode import createElement from ./createElement export default functionupdateChildren (parentElm, oldCh, newCh) { console.log(updateChildren) console.log(parentElm, oldCh, newCh) //旧前 let oldStartIdx = 0 //新前 let newStartIdx = 0 //旧后 let oldEndIdx = oldCh.length - 1 //新后 let newEndIdx = newCh.length - 1 //旧节点 let oldStartVnode = oldCh[0] //旧后节点 let oldEndVnode = oldCh[oldEndIdx] //新节点 let newStartVnode = newCh[0] //新后节点 let newEndVnode = newCh[newEndIdx] let keyMap = {} // 开始循环 while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { // debugger console.log(while) if (oldStartVnode === undefined || oldCh[oldStartIdx] === undefined) { oldStartVnode = oldCh[++oldStartIdx] } else if (oldEndVnode === undefined || oldCh[oldEndIdx] === undefined) { oldEndVnode = oldCh[--oldEndIdx] } else if (newStartVnode === undefined || newCh[newStartIdx] === undefined) { newStartVnode = newCh[++newStartIdx] } else if (newEndVnode === undefined || newCh[newEndIdx] === undefined) { newEndVnode = newCh[--newEndIdx] } else if (sameVnode(oldStartVnode, newStartVnode)) { //新前和旧前是同一个节点 console.log(新前和旧前是同一个节点) patchVnode(oldStartVnode, newStartVnode) oldStartVnode = oldCh[++oldStartIdx] newStartVnode = newCh[++newStartIdx] } else if (sameVnode(oldEndVnode, newEndVnode)) {//旧后和新后是同一个节点 console.log(旧后和新后是同一个节点) patchVnode(oldEndVnode, newEndVnode) oldEndVnode = oldCh[--oldEndIdx] newEndVnode = newCh[--newEndIdx] } else if (sameVnode(oldStartVnode, newEndVnode)) {//新后和旧前是同一个节点 console.log(新后和旧前是同一个节点) patchVnode(oldStartVnode, newEndVnode) //当新后节点是旧前节点时         ,此时需要移动节点                    ,移动旧前节点到旧后的后面 parentElm.insertBefore(oldStartVnode.elm, oldEndVnode.elm.nextSibling) oldStartVnode = oldCh[++oldStartIdx] newEndVnode = newCh[--newEndIdx] } else if (sameVnode(oldEndVnode, newStartVnode)) {//旧后和新前是同一个节点 console.log(旧后和新前是同一个节点) // 当旧后和新前是同一个节点时         ,此时需要移动旧后节点到旧前节点的前面 patchVnode(oldEndVnode, newStartVnode) parentElm.insertBefore(oldEndVnode.elm, oldStartVnode.elm) oldEndVnode = oldCh[--oldEndIdx] newStartVnode = newCh[++newStartIdx] } else { //前四种都没有命中 if (Object.keys(keyMap).length === 0) { keyMap = {} for (let i = oldStartIdx; i <= oldEndIdx; i++) { const key = oldCh[i].key if (key) { keyMap[key] = i } } } console.log(keyMap) //寻找当前节点在keyMap中的位置 const idxInOld = keyMap[newStartVnode.key] console.log(idxInOld) if (!idxInOld) { //新节点不在旧节点中 parentElm.insertBefore(createElement(newStartVnode), oldStartVnode.elm) } else { // 新节点在旧节点中,需要移动 const elmToMove = oldCh[idxInOld] if (elmToMove.sel !== newStartVnode.sel) { parentElm.insertBefore(createElement(newStartVnode), oldStartVnode.elm) } else { patchVnode(elmToMove, newStartVnode) // 把这项设置为undefined      ,表示已经移动过了 oldCh[idxInOld] = undefined parentElm.insertBefore(elmToMove.elm, oldStartVnode.elm) } } //指针向后移动 newStartVnode = newCh[++newStartIdx] } } // 继续查询是否有剩余节点 if (newStartIdx <= newEndIdx) { console.log(新节点还有剩余节点没有处理, newStartIdx, newEndIdx) const before = newCh[newEndIdx + 1]?.elm console.log(before) for (let i = newStartIdx; i <= newEndIdx; i++) { //insertBefore可以自动识别undefined                    ,如果是undefined            ,则插入到最后 parentElm.insertBefore(createElement(newCh[i]), before) } } else if (oldStartIdx <= oldEndIdx) { console.log(旧节点还有剩余节点没有处理, oldStartIdx, oldEndIdx) for (let i = oldStartIdx; i <= oldEndIdx; i++) { oldCh[i] && (parentElm.removeChild(oldCh[i].elm)) } } } let arr = [1, 1, 2, 35, 9, 2, 9] arr.reduce((p, n) => { return p ^ n }, 0) //is文件 export function sameVnode (vnode1, vnode2) { return vnode1.sel === vnode2.sel && vnode1.key === vnode2.key; } //createElement文件 //真正创建dom export default function createElement (vnode) { let domNode = document.createElement(vnode.sel); if ( vnode.text !== "" && (vnode.children === undefined || vnode.children.length === 0) ) { domNode.innerText = vnode.text; // 补充elm } else if (Array.isArray(vnode.children) && vnode.children.length > 0) { for (let i = 0; i < vnode.children.length; i++) { domNode.appendChild(createElement(vnode.children[i])); } } vnode.elm = domNode; return vnode.elm } //patchVnode文件 import createElement from ./createElement import updateChildren from ./updateChildren export default function patchVnode (oldVnode, newVnode) { // console.log(patchVnode) if (oldVnode === newVnode) return if (newVnode.text && (!newVnode.children || newVnode.children.length === 0)) {//判断newVnode的text是否为空   ,且不等于oldVnode的text                    ,如果满足以上条件                ,则更新text oldVnode.text !== newVnode.text && (oldVnode.elm.innerText = newVnode.text); } else {//newVnode的text为空,则判断newVnode的children是否为空                ,如果不为空                    ,则更新children // 新节点没有text属性 if (oldVnode.children && oldVnode.children.length > 0) { // 老节点有children   ,新节点也有children updateChildren(oldVnode.elm, oldVnode.children, newVnode.children); } else { // 老的没有children            ,新的有children oldVnode.elm.innerHTML = ; for (let i = 0; i < newVnode.children.length; i++) { let dom = createElement(newVnode.children[i]) oldVnode.elm.appendChild(dom) } } } } //patch文件 import vnode from "./vnode"; import createElement from "./createElement"; import patchVnode from ./patchVnode import { sameVnode } from ./is export default function (oldVnode, newVnode) { // console.log(oldVnode, newVnode) //判断传入的第一个参数                    ,是dom节点还是vnode if (oldVnode.sel === "" || oldVnode.sel === undefined) { //传入的如果是dom节点需要包装为虚拟节点 oldVnode = vnode( oldVnode.tagName.toLowerCase(), {}, [], undefined, oldVnode, ); } // 判断oldVnode和newVnode是否是同一个节点 if (sameVnode(oldVnode, newVnode)) { // console.log("是同一个节点"); patchVnode(oldVnode, newVnode); } else { // console.log("不是同一个节点"); let newVnodeElm = createElement(newVnode, oldVnode.elm); if (oldVnode.elm.parentNode && newVnodeElm) { oldVnode.elm.parentNode.insertBefore(newVnodeElm, oldVnode.elm); } oldVnode.elm.parentNode.removeChild(oldVnode.elm); } } //VNode文件 export default function (sel, data, children, text, elm) { const key = data.key return { sel, data, children, text, elm, key, } } //h文件 import vnode from ./vnode.js //h(div,{},文字) //h(div,{},[]) //h(div,{},h()) export default function (sel, data, c) { //检查参数个数 if (arguments.length !== 3) { throw new Error(h()参数个数不正确) } // 检查C类型 if (typeof c === string || typeof c === number) { return vnode(sel, data, undefined, c, undefined) } else if (Array.isArray(c)) { let children = [] for (let i = 0; i < c.length; i++) { if (!(typeof c[i] === object && c[i].hasOwnProperty(sel))) { throw new Error(传入的数组参数中有项不是h函数) } children.push(c[i]) } // 循环结束      ,children收集完毕 return vnode(sel, data, children, undefined, undefined) } else if (typeof c === object && c.hasOwnProperty(sel)) { let children = [c] return vnode(sel, data, children, undefined, undefined) } else { throw new Error(h()参数类型不正确) } }

创心域SEO版权声明:以上内容作者已申请原创保护,未经允许不得转载,侵权必究!授权事宜、对本内容有异议或投诉,敬请联系网站管理员,我们将尽快回复您,谢谢合作!

展开全文READ MORE
答题挣钱是什么兼职(有什么答题赚钱-小白入门手赚项目整合,答题,拍店)