深入理解Vue2.x的虚拟DOM diff原理
前言
经常看到讲解Vue2的虚拟Domdiff原理的,但很多都是在原代码的基础上添加些注释等等,这里从0行代码开始实现一个Vue2的虚拟DOM
实现VNode
src/core/vdom/Vnode.js
exportclassVNode{
constructor(
tag,//标签名
children,//孩子[VNode,VNode],
text,//文本节点
elm//对应的真实dom对象
){
this.tag=tag;
this.children=children
this.text=text;
this.elm=elm;
}
}
exportfunctioncreateTextNode(val){
//为什么这里默认把elm置为undefined,不直接根据tag用document.createElement(tagName)把elm赋值?而要等后面createElm时候再赋值呢?
returnnewVNode(undefined,undefined,String(val),undefined)
}
exportfunctioncreateCommentNode(tag,children){
if(children){
for(vari=0;i
定义一个Vnode类,创建节点分为两类,一类为text节点,一类非text节点
src/main.js
import{VNode,createCommentNode}from'./core/vdom/vnode'
varnewVonde=createCommentNode('ul',[createCommentNode('li',['item1']),createCommentNode('li',['item2']),createCommentNode('li',['item3'])])
在main.js就可以根据Vnode生成对应的Vnode对象,上述代码对应的dom表示
- item1
- item2
- item3
先实现不用diff把Vnode渲染到页面中来
为什么先来实现不用diff渲染Vnode的部分,这里也是为了统计渲染的时间,来表明一个道理。并不是diff就比非diff要开,虚拟DOM并不是任何时候性能都比非虚拟DOM要快
先来实现一个工具函数,不熟悉的人可以手工敲下代码熟悉下
//真实的dom操作
//src/core/vdom/node-ops.js
exportfunctioncreateElement(tagName){
returndocument.createElement(tagName)
}
exportfunctioncreateTextNode(text){
returndocument.createTextNode(text)
}
exportfunctioncreateComment(text){
returndocument.createComment(text)
}
exportfunctioninsertBefore(parentNode,newNode,referenceNode){
parentNode.insertBefore(newNode,referenceNode)
}
exportfunctionremoveChild(node,child){
node.removeChild(child)
}
exportfunctionappendChild(node,child){
node.appendChild(child)
}
exportfunctionparentNode(node){
returnnode.parentNode
}
exportfunctionnextSibling(node){
returnnode.nextSibling
}
exportfunctiontagName(node){
returnnode.tagName
}
exportfunctionsetTextContent(node,text){
node.textContent=text
}
exportfunctionsetAttribute(node,key,val){
node.setAttribute(key,val)
}
src/main.js
import{VNode,createCommentNode}from'./core/vdom/vnode'
importpatchfrom'./core/vdom/patch'
varcontainer=document.getElementById("app");
varoldVnode=newVNode(container.tagName,[],undefined,container);
varnewVonde=createCommentNode('ul',[createCommentNode('li',['item1']),createCommentNode('li',['item2']),createCommentNode('li',['item3'])])
console.time('start');
patch(oldVnode,newVonde);//渲染页面
console.timeEnd('start');
这里我们要实现一个patch方法,把Vnode渲染到页面中
src/core/vdom/patch.js
import*asnodeOpsfrom'./node-ops'
importVNodefrom'./vnode'
exportdefaultfunctionpatch(oldVnode,vnode){
letisInitialPatch=false;
if(sameVnode(oldVnode,vnode)){
//如果两个Vnode节点的根一致开始diff
patchVnode(oldVnode,vnode)
}else{
//这里就是不借助diff的实现
constoldElm=oldVnode.elm;
constparentElm=nodeOps.parentNode(oldElm);
createElm(
vnode,
parentElm,
nodeOps.nextSibling(oldElm)
)
if(parentElm!=null){
removeVnodes(parentElm,[oldVnode],0,0)
}
}
returnvnode.elm;
}
functionpatchVnode(oldVnode,vnode,removeOnly){
if(oldVnode===vnode){
return
}
constelm=vnode.elm=oldVnode.elm
constoldCh=oldVnode.children;
constch=vnode.children
if(isUndef(vnode.text)){
//非文本节点
if(isDef(oldCh)&&isDef(ch)){
//都有字节点
if(oldCh!==ch){
//更新children
updateChildren(elm,oldCh,ch,removeOnly);
}
}elseif(isDef(ch)){
//新的有子节点,老的没有
if(isDef(oldVnode.text)){
nodeOps.setTextContent(elm,'');
}
//添加子节点
addVnodes(elm,null,ch,0,ch.length-1)
}elseif(isDef(oldCh)){
//老的有子节点,新的没有
removeVnodes(elm,oldCh,0,oldCh.length-1)
}elseif(isDef(oldVnode.text)){
//否则老的有文本内容直接置空就行
nodeOps.setTextContent(elm,'');
}
}elseif(oldVnode.text!==vnode.text){
//直接修改文本
nodeOps.setTextContent(elm,vnode.text);
}
}
functionupdateChildren(parentElm,oldCh,newCh,removeOnly){
//这里认真读下,没什么难度的,不行的话也可以搜索下图文描述这段过程的
letoldStartIdx=0;
letnewStartIdx=0;
letoldEndIdx=oldCh.length-1;
letoldStartVnode=oldCh[0];
letoldEndVnode=oldCh[oldEndIdx];
letnewEndIdx=newCh.length-1;
letnewStartVnode=newCh[0]
letnewEndVnode=newCh[newEndIdx]
letrefElm;
constcanMove=!removeOnly
while(oldStartIdx<=oldEndIdx&&newStartIdx<=newEndIdx){
if(isUndef(oldStartVnode)){
oldStartVnode=oldCh[++oldStartIdx]
}elseif(isUndef(oldEndVnode)){
oldEndVnode=oldCh[--oldEndIdx]
}elseif(sameVnode(oldStartVnode,newStartVnode)){
patchVnode(oldStartVnode,newStartVnode)
oldStartVnode=oldCh[++oldStartIdx]
newStartVnode=newCh[++newStartIdx]
}elseif(sameVnode(oldEndVnode,newEndVnode)){
patchVnode(oldEndVnode,newEndVnode)
oldEndVnode=oldCh[--oldEndIdx];
newEndVnode=newCh[--newEndIdx];
}elseif(sameVnode(oldStartVnode,newEndVnode)){
patchVnode(oldStartVnode,newEndVnode);
//更换顺序
canMove&&nodeOps.insertBefore(parentElm,oldStartVnode.elm,nodeOps.nextSibling(oldEndVnode.elm))
oldStartVnode=oldCh[++oldStartIdx]
newEndVnode=newCh[--newEndIdx]
}elseif(sameVnode(oldEndVnode,newStartVnode)){
patchVnode(oldEndVnode,newStartVnode)
canMove&&nodeOps.insertBefore(parentElm,oldEndVnode.elm,oldStartVnode.elm)
oldEndVnode=oldCh[--oldEndIdx]
newStartVnode=newCh[++newStartIdx]
}else{
createElm(newStartVnode,parentElm,oldStartVnode.elm)
newStartVnode=newCh[++newStartIdx];
}
}
if(oldStartIdx>oldEndIdx){
//老的提前相遇,添加新节点中没有比较的节点
refElm=isUndef(newCh[newEndIdx+1])?null:newCh[newEndIdx+1].elm
addVnodes(parentElm,refElm,newCh,newStartIdx,newEndIdx)
}else{
//新的提前相遇删除多余的节点
removeVnodes(parentElm,oldCh,oldStartIdx,oldEndIdx)
}
}
functionremoveVnodes(parentElm,vnodes,startIdx,endIdx){
for(;startIdx<=endIdx;++startIdx){
constch=vnodes[startIdx];
if(isDef(ch)){
removeNode(ch.elm)
}
}
}
functionaddVnodes(parentElm,refElm,vnodes,startIdx,endIdx){
for(;startIdx<=endIdx;++startIdx){
createElm(vnodes[startIdx],parentElm,refElm)
}
}
functionsameVnode(vnode1,vnode2){
returnvnode1.tag===vnode2.tag
}
functionremoveNode(el){
constparent=nodeOps.parentNode(el)
if(parent){
nodeOps.removeChild(parent,el)
}
}
functionremoveVnodes(parentElm,vnodes,startIdx,endIdx){
for(;startIdx<=endIdx;++startIdx){
constch=vnodes[startIdx]
if(isDef(ch)){
removeNode(ch.elm)
}
}
}
functionisDef(s){
returns!=null
}
functionisUndef(s){
returns==null
}
functioncreateChildren(vnode,children){
if(Array.isArray(children)){
for(leti=0;i
这就是完整实现了
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持毛票票。