虚拟DOM的优势首先是可以对真实的元素节点进行抽象,抽象成VNode(虚拟节点),这样方便后续对其进行各种操作因为对于直接操作DOM来说是有很多的限制的,js每操作一次操作DOM,会使DOM进行重排或者重绘,每次操作都会消耗大量资源,比如diff、clone等等,但是使用JavaScript编程语言来操作这 些,就变得非常的简单我们可以使用JavaScript来表达非常多的逻辑,而对于DOM本身来说是非常不方便,用js操作虚拟DOM,DOM元素会在内存中更改,等所有操作结束之后,只需要一次更新即可,提高性能其次是方便实现跨平台,包括你可以将VNode节点渲染成任意你想要的节点如渲染在canvas、WebGL、SSR、Native(iOS、Android)上并且Vue允许你开发属于自己的渲染器(renderer),在其他的平台上渲染虚拟DOM的渲染过程 templaterender渲染函数虚拟DOM(VNode)真实DOM浏览器展示一、什么是虚拟DOM 虚拟DOM(VirtualDOM)这个概念相信大家都不陌生,从React到Vue,虚拟DOM为这两个框架都带来了跨平台的能力(ReactNative和Weex) 实际上它只是一层对真实DOM的抽象,以JavaScript对象(VNode节点)作为基础的树,用对象的属性来描述节点,最终可以通过一系列操作使这棵树映射到真实环境上 在Javascript对象中,虚拟DOM表现为一个Object对象。并且最少包含标签名(tag)、属性(attrs)和子元素对象(children)三个属性,不同框架对这三个属性的名命可能会有差别 创建虚拟DOM就是为了更好将虚拟的节点渲染到页面视图中,所以虚拟DOM对象的节点与真实DOM的属性一一照应 在vue中同样使用到了虚拟DOM技术 定义真实DOMpclassp节点内容h3{{foo}}h3 实例化vueconstappnewVue({el:app,data:{foo:foo}}) 观察render的render,我们能得到虚拟DOM(functionanonymous(){with(this){returnc(p,{attrs:{id:app}},〔c(p,{staticClass:p},〔v(节点内容)〕),v(),c(h3,〔v(s(foo))〕)〕)}}) 通过VNode,vue可以对这颗抽象树进行创建节点,删除节点以及修改节点的操作,经过diff算法得出一些需要修改的最小单位,再更新视图,减少了dom操作,提高了性能 在vue中,每一个组件都有一个render函数,这个函数会生成一个虚拟dom,这就意味着每一个组件都对应一个虚拟dom树。 二、为什么需要虚拟DOM DOM是很慢的,其元素非常庞大,页面的性能问题,大部分都是由DOM操作引起的 真实的DOM节点,哪怕一个最简单的p也包含着很多属性,可以打印出来直观感受一下: 这个主要是由vue结构所决定的,在vue中,渲染试图会调用render函数,不仅在创建视图的时候被调用,当组件所依赖的数据或者属性发生了改变的时候,也会调用render函数,如果是使用真实的dom,当创建,修改,删除,插入dom的话是非常消耗性能的,如下所示,当修改一个js对象远比操作真实的dom要有效率的多。运行结果: 由此可见,操作DOM的代价仍旧是昂贵的,频繁操作还是会出现页面卡顿,影响用户的体验 举个例子: 你用传统的原生api或jQuery去操作DOM时,浏览器会从构建DOM树开始从头到尾执行一遍流程 当你在一次操作时,需要更新10个DOM节点,浏览器没这么智能,收到第一个更新DOM请求后,并不知道后续还有9次更新操作,因此会马上执行流程,最终执行10次流程 而通过VNode,同样更新10个DOM节点,虚拟DOM不会立即操作DOM,而是将这10次更新的diff内容保存到本地的一个js对象中,最终将这个js对象一次性attach到DOM树上,避免大量的无谓计算 很多人认为虚拟DOM最大的优势是diff算法,减少JavaScript操作真实DOM的带来的性能消耗。虽然这一个虚拟DOM带来的一个优势,但并不是全部。虚拟DOM最大的优势在于抽象了原本的渲染过程,实现了跨平台的能力,而不仅仅局限于浏览器的DOM,可以是安卓和IOS的原生组件,可以是近期很火热的小程序,也可以是各种GUI 三、如何实现虚拟DOM 首先可以看看vue中VNode的结构 源码位置:srccorevdomvnode。jsexportdefaultclassVNode{tag:stringvoid;data:VNodeDatavoid;children:?ArrayVNode;text:stringvoid;elm:Nodevoid;ns:stringvoid;context:Componentvoid;renderedinthiscomponentsscopefunctionalContext:Componentvoid;onlyforfunctionalcomponentrootnodeskey:stringnumbervoid;componentOptions:VNodeComponentOptionsvoid;componentInstance:Componentvoid;componentinstanceparent:VNodevoid;componentplaceholdernoderaw:boolean;containsrawHTML?(serveronly)isStatic:boolean;hoistedstaticnodeisRootInsert:boolean;necessaryforentertransitioncheckisComment:boolean;emptycommentplaceholder?isCloned:boolean;isaclonednode?isOnce:boolean;isavoncenode?constructor(tag?:string,data?:VNodeData,children?:?ArrayVNode,text?:string,elm?:Node,context?:Component,componentOptions?:VNodeComponentOptions){当前节点的标签名this。tagtag当前节点对应的对象,包含了具体的一些数据信息,是一个VNodeData类型,可以参考VNodeData类型中的数据信息this。datadata当前节点的子节点,是一个数组this。childrenchildren当前节点的文本this。texttext当前虚拟节点对应的真实dom节点this。elmelm当前节点的名字空间this。nsundefined编译作用域this。contextcontext函数化组件作用域this。functionalContextundefined节点的key属性,被当作节点的标志,用以优化this。keydatadata。key组件的option选项this。componentOptionscomponentOptions当前节点对应的组件的实例this。componentInstanceundefined当前节点的父节点this。parentundefined简而言之就是是否为原生HTML或只是普通文本,innerHTML的时候为true,textContent的时候为falsethis。rawfalse静态节点标志this。isStaticfalse是否作为跟节点插入this。isRootInserttrue是否为注释节点this。isCommentfalse是否为克隆节点this。isClonedfalse是否有vonce指令this。isOncefalse}DEPRECATED:aliasforcomponentInstanceforbackwardscompat。istanbulignorenexthttps:github。comanswershutolearnVuegetchild():Componentvoid{returnthis。componentInstance}} 这里对VNode进行稍微的说明:所有对象的context选项都指向了Vue实例elm属性则指向了其相对应的真实DOM节点 vue是通过createElement生成VNode 源码位置:srccorevdomcreateelement。jsexportfunctioncreateElement(context:Component,tag:any,data:any,children:any,normalizationType:any,alwaysNormalize:boolean):VNodeArrayVNode{if(Array。isArray(data)isPrimitive(data)){normalizationTypechildrenchildrendatadataundefined}if(isTrue(alwaysNormalize)){normalizationTypeALWAYSNORMALIZE}returncreateElement(context,tag,data,children,normalizationType)} 上面可以看到createElement方法实际上是对createElement方法的封装,对参数的传入进行了判断exportfunctioncreateElement(context:Component,tag?:stringClassComponentFunctionObject,data?:VNodeData,children?:any,normalizationType?:number):VNodeArrayVNode{if(isDef(data)isDef((data:any)。ob)){process。env。NODEENV!productionwarn(Avoidusingobserveddataobjectasvnodedata:{JSON。stringify(data)}Alwayscreatefreshvnodedataobjectsineachrender!,context)returncreateEmptyVNode()}objectsyntaxinvbindif(isDef(data)isDef(data。is)){tagdata。is}if(!tag){incaseofcomponent:issettofalsyvaluereturncreateEmptyVNode()}。。。supportsinglefunctionchildrenasdefaultscopedslotif(Array。isArray(children)typeofchildren〔0〕function){datadata{}data。scopedSlots{default:children〔0〕}children。length0}if(normalizationTypeALWAYSNORMALIZE){childrennormalizeChildren(children)}elseif(SIMPLENORMALIZE){childrensimpleNormalizeChildren(children)}创建VNode。。。} 可以看到createElement接收5个参数:context表示VNode的上下文环境,是Component类型tag表示标签,它可以是一个字符串,也可以是一个Componentdata表示VNode的数据,它是一个VNodeData类型children表示当前VNode的子节点,它是任意类型的normalizationType表示子节点规范的类型,类型不同规范的方法也就不一样,主要是参考render函数是编译生成的还是用户手写的 根据normalizationType的类型,children会有不同的定义if(normalizationTypeALWAYSNORMALIZE){childrennormalizeChildren(children)}elseif(SIMPLENORMALIZE){childrensimpleNormalizeChildren(children)} simpleNormalizeChildren方法调用场景是render函数是编译生成的 normalizeChildren方法调用场景分为下面两种:render函数是用户手写的编译slot、vfor的时候会产生嵌套数组 无论是simpleNormalizeChildren还是normalizeChildren都是对children进行规范(使children变成了一个类型为VNode的Array),这里就不展开说了 规范化children的源码位置在:srccorevdomhelpersnormalziechildren。js 在规范化children后,就去创建VNodeletvnode,ns对tag进行判断if(typeoftagstring){letCtorns(context。vnodecontext。vnode。ns)config。getTagNamespace(tag)if(config。isReservedTag(tag)){如果是内置的节点,则直接创建一个普通VNodevnodenewVNode(config。parsePlatformTagName(tag),data,children,undefined,undefined,context)}elseif(isDef(CtorresolveAsset(context。options,components,tag))){component如果是component类型,则会通过createComponent创建VNode节点vnodecreateComponent(Ctor,data,context,children,tag)}else{vnodenewVNode(tag,data,children,undefined,undefined,context)}}else{directcomponentoptionsconstructorvnodecreateComponent(tag,data,context,children)} createComponent同样是创建VNodeexportfunctioncreateComponent(Ctor:ClassComponentFunctionObjectvoid,data:?VNodeData,context:Component,children:?ArrayVNode,tag?:string):VNodeArrayVNodevoid{if(isUndef(Ctor)){return}构建子类构造函数constbaseCtorcontext。options。baseplainoptionsobject:turnitintoaconstructorif(isObject(Ctor)){CtorbaseCtor。extend(Ctor)}ifatthisstageitsnotaconstructororanasynccomponentfactory,reject。if(typeofCtor!function){if(process。env。NODEENV!production){warn(InvalidComponentdefinition:{String(Ctor)},context)}return}asynccomponentletasyncFactoryif(isUndef(Ctor。cid)){asyncFactoryCtorCtorresolveAsyncComponent(asyncFactory,baseCtor,context)if(Ctorundefined){returncreateAsyncPlaceholder(asyncFactory,data,context,children,tag)}}datadata{}resolveconstructoroptionsincaseglobalmixinsareappliedaftercomponentconstructorcreationresolveConstructorOptions(Ctor)transformcomponentvmodeldataintopropseventsif(isDef(data。model)){transformModel(Ctor。options,data)}extractpropsconstpropsDataextractPropsFromVNodeData(data,Ctor,tag)functionalcomponentif(isTrue(Ctor。options。functional)){returncreateFunctionalComponent(Ctor,propsData,data,context,children)}extractlisteners,sincetheseneedstobetreatedaschildcomponentlistenersinsteadofDOMlistenersconstlistenersdata。onreplacewithlistenerswith。nativemodifiersoitgetsprocessedduringparentcomponentpatch。data。ondata。nativeOnif(isTrue(Ctor。options。abstract)){constslotdata。slotdata{}if(slot){data。slotslot}}安装组件钩子函数,把钩子函数合并到data。hook中installComponentHooks(data)实例化一个VNode返回。组件的VNode是没有children的constnameCtor。options。nametagconstvnodenewVNode(vuecomponent{Ctor。cid}{name?{name}:},data,undefined,undefined,undefined,context,{Ctor,propsData,listeners,tag,children},asyncFactory)if(WEEXisRecyclableComponent(vnode)){returnrenderRecyclableComponentTemplate(vnode)}returnvnode}微提下createComponent生成VNode的三个关键流程:构造子类构造函数CtorinstallComponentHooks安装组件钩子函数实例化vnode 虚拟dom是如何转换为真实dom的? 在组件渲染的时候会调用render函数,这个函数会生成一个虚拟的dom,再根据这个虚拟的dom来生成真实的dom,然后将这个真实的dom给挂载到页面的合适的部位,总的来说创建真实的dom是必不可少的一部分,如果视图只需要渲染一次,后续数据的改变不会影响到视图的改变,vue的效率其实要比真实操作dom的效率要低,因为vue还要创建一个虚拟的dom,但是如果组件内有响应式的数据,当数据发生改变的时候,就会调用render函数来生成一个新的虚拟dom树,将新的虚拟dom树和旧的虚拟dom树进行对比,然后找到必须修改的虚拟dom,最后根据修改的虚拟dom,来修改真实的dom,这样就保证了对真实的dom操作达到最小的改变, 在进行新的虚拟dom和旧的虚拟dom对比的算法是patch算法,它会找到新的虚拟dom树和旧的虚拟dom树之间的差别。 模板和虚拟dom的关系? 在vue中有一个compile模块,这个模块将template字符串编译成render函数,render函数最后返回一个虚拟dom。 编译过程分为两步: (1)将template转换为AST(抽象语法树) (2)将AST转换为render函数 使用