-
前言
偶然看到在CSDN上看到有人总结的vdom和真实dom的区别,第一条就是real dom 更新缓慢,virtual dom更新更快,然而在这个链接案例中操作真实dom是渲染速度是最快的,对于这样的情况,我们需要从vdom本身来进行研究。
-
snabbdom
- snabbdom是著名的虚拟dom库,是diff算法的鼻祖
- vue2.x借鉴了snabbdom
- 仓库地址:github.com/snabbdom/sn…
-
snabbdom案例
-
安装snabbdom
npm i snabbdom -D -
安装webpack并进行配置
npm i webpack@5 webpack-cli@3 webpack-dev-server@3 -D //webpack.config.js module.exports = { entry:'./src/index.js', output:{ publicPath:'dist', filename:'index.js', }, devServer:{ contentBase:'public', port:'8080' }, } -
引入案例
// 引入案例 import { init, classModule, propsModule, styleModule, eventListenersModule, h, } from "snabbdom"; const patch = init([ // Init patch function with chosen modules classModule, // makes it easy to toggle classes propsModule, // for setting properties on DOM elements styleModule, // handles styling on elements with support for animations eventListenersModule, // attaches event listeners ]); const container = document.getElementById("container"); const vnode = h("div#container.two.classes", { on: { click: someFn } }, [ h("span", { style: { fontWeight: "bold" } }, "This is bold"), " and this is just normal text", h("a", { props: { href: "/foo" } }, "I'll take you places!"), ]); // Patch into empty DOM element – this modifies the DOM as a side effect patch(container, vnode); const newVnode = h( "div#container.two.classes", { on: { click: anotherEventHandler } }, [ h( "span", { style: { fontWeight: "normal", fontStyle: "italic" } }, "This is now italic type" ), " and this is still just normal text", h("a", { props: { href: "/bar" } }, "I'll take you places!"), ] ); // Second `patch` invocation patch(vnode, newVnode); // Snabbdom efficiently updates the old view to the new state
-
-
虚拟dom和h函数
- 虚拟dom
- 虚拟dom是用来描述dom层次结构的一个javascript对象,dom中的一切属性都在vdom中有相对应的属性
- diff算法是发生的虚拟dom上的,新虚拟dom和老虚拟dom进行diff算法处理,算出应该如何最小量更新,最后反映到真实dom上
- h函数
- h函数用来产生虚拟节点
// 这样调用h函数 h('a',{props:{href:'https://www.jd.com'},'京东') // 将得到这样的虚拟dom { sel:'a', data:{ props:{ href:'https://www.jd.com', } }, text:'京东', key:undefined, elm:undefined, children:undefined } //它表示一个真正的dom节点 <a href='https://www.jd.com'>京东</a> - h函数也可以嵌套使用
h('ul',{},[ h('li',{key:'vue'},'vue'), h('li',{key:'react'}, 'react'), h('li',{key:'knockout'},'knockout') ]) //将会得到如下一个vdom { sel:'ul', data:{}, children:[ {sel:'li',key:'vue','text':'vue'}, {sel:'li',key:'react','text':'react'}, {sel:'li',key:'knockout','text':'knockout'}, ] } - h函数解析
-
export function h(sel, b, c) { let data = {}; let children; let text; let i; if (c !== undefined) { if (b !== null) { data = b; } if (is.array(c)) { //判断c是否为array children = c; } else if (is.primitive(c)) { //判断c是否是string 或者number text = c.toString(); } else if (c && c.sel) { children = [c]; } } // if c doesn't exist, like h('div',['html','js','css']) else if (b !== undefined && b !== null) { if (is.array(b)) { children = b; } else if (is.primitive(b)) { text = b.toString(); } else if (b && b.sel) { children = [b]; } else { data = b; } } if (children !== undefined) { for (i = 0; i < children.length; ++i) { if (is.primitive(children[i])) children[i] = vnode(undefined, undefined, undefined, children[i], undefined); } } if (sel[0] === "s" && sel[1] === "v" && sel[2] === "g" && (sel.length === 3 || sel[3] === "." || sel[3] === "#")) { addNS(data, children, sel); } return vnode(sel, data, children, text, undefined); } export function vnode(sel, data, children, text, elm) { const key = data === undefined ? undefined : data.key; return { sel, data, children, text, elm, key }; }
-
- h函数用来产生虚拟节点
- 虚拟dom
-
diff算法
之前说到,diff算法是发生在虚拟dom上的,所以diff总的来说是一种对比算法,对比两者是旧的虚拟dom和新的虚拟dom,对比出是哪个虚拟节点更新了,找出这个虚拟节点,并只更新这个虚拟节点所对应的真实节点,而不用更新其他没发生改变的节点,实现精准的更新真实dom。
-
diff算法原理 新旧虚拟dom对比的时候,diff算法只会在同级进行比较,不会跨级进行比较
-
diff对比流程 以vue2.x为例,当数据改变是,会触发setter,并且通过Dep.notify()去通知所有watcher,watcher会调用patch方法,给真实dom打补丁,更新相应的视图
-
patch函数
-
function patch(oldVnode,vnode){ if(sameVnode(oldVnode,vnode)){ patchVnode(oldVnode,vnode) }else{ const oEl = oldVnode.elm let parentEle = api.parentNode(oEl) //父元素 createEle(vnode) if(parentEle !== null){ api.insertBefore(parentEle, vnode.el, api.nextSibling(oEl)) api.removeChild(parentEle, oldVnode.el) oldVnode = null } } return newVnode } -
patch函数接收两个参数 oldVnode和vNode 分别代表旧的vdom和新的vdom
-
sameNode判断是否为统一节点
-
如果不是同一个节点,执行createEle创建一个新节点,用新的节点替换旧的节点
-
如果是同一个节点,执行patchVnode函数
-
patchVnode函数
- 找到对应真实dom,赋值给el
- 判断vnode和oldVnode是否相等,如果相等直接return
- 如果都没有子节点,将el文本设置为vNode.text,如果oldVnode有子节点而Vnode有,将vnode子节点真实化之后添加到el,如果vnode没有子节点而oldVnode有,删除el的子节点
- 如果都有子节点,则执行updateChildren方法比较子节点
-
updateChildren函数
function updateChildren(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 = null while (newStartIdx <= newEndIdx && oldStartIdx <= oldEndIdx) { console.log('---come into diff---') if (oldCh[oldStartIdx] == undefined) { oldStartVnode = oldCh[++oldStartIdx] } else if (oldCh[oldEndIdx] == undefined) { oldEndVnode = oldCh[--oldEndIdx] } else if (newCh[newStartIdx] == undefined) { newStartVnode = newCh[++newStartIdx] } else if (newCh[newEndIdx] == undefined) { newEndVnode = newCh[--newEndIdx] } // 1 匹配成功 oldStartIdx ++ ;newStartIdx ++,不成功走下一步 else if (sameVnode(oldStartVnode, newStartVnode)) { patchVnode(oldStartVnode, newStartVnode) newStartVnode = newCh[++newStartIdx] oldStartVnode = oldCh[++oldStartIdx] } // 2匹配成功 oldEndIdx -- ;newEndIdx --; 不成功走下一步 else if (sameVnode(oldEndVnode, newEndVnode)) { patchVnode(oldEndVnode, newEndVnode) newEndVnode = newCh[--newEndIdx] oldEndVnode = oldCh[--oldEndIdx] } // 3 匹配成功 oldStartIdx -- ;newEndIdx++; 移动节点 ;不成功走下一步 else if (sameVnode(oldStartVnode, newEndVnode)) { patchVnode(oldStartVnode, newEndVnode) parentElm.insertBefore(oldStartVnode.elm, oldEndVnode.elm.nextSibling) newEndVnode = newCh[--newEndIdx] oldStartVnode = oldCh[++oldStartIdx] } // 4 匹配成功 oldEndIdx -- newStartIdx ++ 移动节点 不成功走下一步 else if (sameVnode(oldEndVnode, newStartVnode)) { patchVnode(oldEndVnode, newStartVnode) parentElm.insertBefore(oldEndVnode.elm, oldStartVnode.elm) newStartVnode = newCh[++newStartIdx] oldEndVnode = oldCh[--oldEndIdx] } else { // catch oldVnode if (!keyMap) { // init keyMap keyMap = {} for (let i = oldStartIdx; i < oldEndIdx; i++) { const key = oldCh[i].data.key // key !== undefined if (!key) keyMap[key] = i } } let idInOld = keyMap[newStartIdx.data] ? keyMap[newStartIdx.data.key] : undefined if (idInOld) { let moveElm = oldCh[idInOld] patchVnode(moveElm, newStartVnode) oldCh[idInOld] = undefined parentElm.insertBefore(moveElm.elm, oldStartVnode.elm) } else { console.log('add new node') parentElm.insertBefore(createElm(newStartVnode), oldStartVnode.elm) } newStartVnode = newCh[++newStartIdx] } } if (newStartIdx <= newEndIdx) { let beforeFlag = newCh[newEndIdx + 1] ? newCh[newEndIdx + 1] : null for (let i = newStartIdx; i <= newEndIdx; i++) { parentElm.insertBefore(createElm(newCh[i]), beforeFlag) } } else if (oldStartIdx <= oldEndIdx) { for (let i = oldStartIdx; i <= oldEndIdx; i++) { if (oldCh[i].elm) parentElm.removeChild(oldCh[i].elm) } } }- updateChildren函数设置四个指针,分别指向新旧节点子节点的首尾
- 根据 新前-旧前 新后-旧后 旧前-新后 新前-旧后节点进行比较
- 如果上面四种匹配都没命中,则会根据oldCh的key生成一个hash表,用newStartIdx的key与hash表做匹配,匹配成功就取出酒店接中当前项,调用patchVnode进行比较,再将旧节点当前项设置为undefind,移动当前项到旧前之前,匹配不成功则说明是要增加的项,将newStartNode生成real dom之后,插入到旧前之前
- 判断循环结束时机
- 如果oldStartIdx > oldEndIdx先发生,新增新前与新后之间的节点
- 如果newStartIdx > newEndIdx先发生,删除旧前与旧后之间的节点
-
-
-
总结:虚拟dom比真实dom更新更快的正确性,或者更严谨的说法
-
没有任何框架可以比纯手动的优化dom操作更快,框架的dom操作层需要应对任何上层可能产生的操作,它的实现必须是普遍适用的
-
和dom操作比起来,,js计算是极其便宜的,它保证了不管数据变化多少,每次重绘的性能都可以接受
-
在构建一个实际应用中,如果有大量数据进行修改,直接重置innerhtml还算一个合理的操作,如果只有一行数据变了,也重置整个innerhtml,显然会有大量的浪费,vDom + diff 给你的保证是,在不需要手动优化的时候,依然可以提供过得去的性能。
-