vue 中Virtual Dom被创建的方法
本文将通过解读render函数的源码,来分析vue中的vNode是如何创建的。在vue2.x的版本中,无论是直接书写render函数,还是使用template或el属性,或是使用.vue单文件的形式,最终都需要编译成render函数进行vnode的创建,最终再渲染成真实的DOM。如果对vue源码的目录还不是很了解,推荐先阅读下深入vue--源码目录和编译过程。
01 render函数
render方法定义在文件src/core/instance/render.js中
Vue.prototype._render=function():VNode{ constvm:Component=this const{render,_parentVnode}=vm.$options //... //setparentvnode.thisallowsrenderfunctionstohaveaccess //tothedataontheplaceholdernode. vm.$vnode=_parentVnode //renderself letvnode try{ vnode=render.call(vm._renderProxy,vm.$createElement) }catch(e){ handleError(e,vm,`render`) //returnerrorrenderresult, //orpreviousvnodetopreventrendererrorcausingblankcomponent /*istanbulignoreelse*/ if(process.env.NODE_ENV!=='production'&&vm.$options.renderError){ try{ vnode=vm.$options.renderError.call(vm._renderProxy,vm.$createElement,e) }catch(e){ handleError(e,vm,`renderError`) vnode=vm._vnode } }else{ vnode=vm._vnode } } //ifthereturnedarraycontainsonlyasinglenode,allowit if(Array.isArray(vnode)&&vnode.length===1){ vnode=vnode[0] } //returnemptyvnodeincasetherenderfunctionerroredout if(!(vnodeinstanceofVNode)){ if(process.env.NODE_ENV!=='production'&&Array.isArray(vnode)){ warn( 'Multiplerootnodesreturnedfromrenderfunction.Renderfunction'+ 'shouldreturnasinglerootnode.', vm ) } vnode=createEmptyVNode() } //setparent vnode.parent=_parentVnode returnvnode }
_render定义在vue的原型上,会返回vnode,vnode通过代码render.call(vm._renderProxy,vm.$createElement)进行创建。
在创建vnode过程中,如果出现错误,就会执行catch中代码做降级处理。
_render中最核心的代码就是:
vnode=render.call(vm._renderProxy,vm.$createElement)
接下来,分析下这里的render,vm._renderProxy,vm.$createElement分别是什么。
render函数
const{render,_parentVnode}=vm.$options
render方法是从$options中提取的。render方法有两种途径得来:
在组件中开发者直接手写的render函数
通过编译template属性生成
参数vm._renderProxy
vm._renderProxy定义在src/core/instance/init.js中,是call的第一个参数,指定render函数执行的上下文。
/*istanbulignoreelse*/ if(process.env.NODE_ENV!=='production'){ initProxy(vm) }else{ vm._renderProxy=vm }
生产环境:
vm._renderProxy=vm,也就是说,在生产环境,render函数执行的上下文就是当前vue实例,即当前组件的this。
开发环境:
开发环境会执行initProxy(vm),initProxy定义在文件src/core/instance/proxy.js中。
letinitProxy //... initProxy=functioninitProxy(vm){ if(hasProxy){ //determinewhichproxyhandlertouse constoptions=vm.$options consthandlers=options.render&&options.render._withStripped ?getHandler :hasHandler vm._renderProxy=newProxy(vm,handlers) }else{ vm._renderProxy=vm } }
hasProxy的定义如下
consthasProxy= typeofProxy!=='undefined'&&isNative(Proxy)
用来判断浏览器是否支持es6的Proxy。
Proxy作用是在访问一个对象时,对其进行拦截,newProxy的第一个参数表示所要拦截的对象,第二个参数是用来定制拦截行为的对象。
开发环境,如果支持Proxy就会对vm实例进行拦截,否则和生产环境相同,直接将vm赋值给vm._renderProxy。具体的拦截行为通过handlers对象指定。
当手写render函数时,handlers=hasHandler,通过template生成的render函数,handlers=getHandler。hasHandler代码:
consthasHandler={ has(target,key){ consthas=keyintarget constisAllowed=allowedGlobals(key)|| (typeofkey==='string'&&key.charAt(0)==='_'&&!(keyintarget.$data)) if(!has&&!isAllowed){ if(keyintarget.$data)warnReservedPrefix(target,key) elsewarnNonPresent(target,key) } returnhas||!isAllowed } }
getHandler代码
constgetHandler={ get(target,key){ if(typeofkey==='string'&&!(keyintarget)){ if(keyintarget.$data)warnReservedPrefix(target,key) elsewarnNonPresent(target,key) } returntarget[key] } }
hasHandler,getHandler分别是对vm对象的属性的读取和propKeyinproxy的操作进行拦截,并对vm的参数进行校验,再调用warnNonPresent和warnReservedPrefix进行Warn警告。
可见,initProxy方法的主要作用就是在开发时,对vm实例进行拦截发现问题并抛出错误,方便开发者及时修改问题。
参数vm.$createElement
vm.$createElement就是手写render函数时传入的createElement函数,它定义在initRender方法中,initRender在newVue初始化时执行,参数是实例vm。
exportfunctioninitRender(vm:Component){ //... //bindthecreateElementfntothisinstance //sothatwegetproperrendercontextinsideit. //argsorder:tag,data,children,normalizationType,alwaysNormalize //internalversionisusedbyrenderfunctionscompiledfromtemplates vm._c=(a,b,c,d)=>createElement(vm,a,b,c,d,false) //normalizationisalwaysappliedforthepublicversion,usedin //user-writtenrenderfunctions. vm.$createElement=(a,b,c,d)=>createElement(vm,a,b,c,d,true) //... }
从代码的注释可以看出:vm.$createElement是为开发者手写render函数提供的方法,vm._c是为通过编译template生成的render函数使用的方法。它们都会调用createElement方法。
02 createElement方法
createElement方法定义在src/core/vdom/create-element.js文件中
constSIMPLE_NORMALIZE=1 constALWAYS_NORMALIZE=2 //wrapperfunctionforprovidingamoreflexibleinterface //withoutgettingyelledatbyflow exportfunctioncreateElement( context:Component, tag:any, data:any, children:any, normalizationType:any, alwaysNormalize:boolean ):VNode|Array{ if(Array.isArray(data)||isPrimitive(data)){ normalizationType=children children=data data=undefined } if(isTrue(alwaysNormalize)){ normalizationType=ALWAYS_NORMALIZE } return_createElement(context,tag,data,children,normalizationType) }
createElement方法主要是对参数做一些处理,再调用_createElement方法创建vnode。
下面看一下vue文档中createElement能接收的参数。
//@returns{VNode} createElement( //{String|Object|Function} //一个HTML标签字符串,组件选项对象,或者 //解析上述任何一种的一个async异步函数。必需参数。 'div', //{Object} //一个包含模板相关属性的数据对象 //你可以在template中使用这些特性。可选参数。 { }, //{String|Array} //子虚拟节点(VNodes),由`createElement()`构建而成, //也可以使用字符串来生成“文本虚拟节点”。可选参数。 [ '先写一些文字', createElement('h1','一则头条'), createElement(MyComponent,{ props:{ someProp:'foobar' } }) ] )
文档中除了第一个参数是必选参数,其他都是可选参数。也就是说使用createElement方法的时候,可以不传第二个参数,只传第一个参数和第三个参数。刚刚说的参数处理就是对这种情况做处理。
if(Array.isArray(data)||isPrimitive(data)){ normalizationType=children children=data data=undefined }
通过判断data是否是数组或者是基础类型,如果满足这个条件,说明这个位置传的参数是children,然后对参数依次重新赋值。这种方式被称为重载。
重载:函数名相同,函数的参数列表不同(包括参数个数和参数类型),至于返回类型可同可不同。
处理好参数后调用_createElement方法创建vnode。下面是_createElement方法的核心代码。
exportfunction_createElement( context:Component, tag?:string|Class|Function|Object, data?:VNodeData, children?:any, normalizationType?:number ):VNode|Array { //... if(normalizationType===ALWAYS_NORMALIZE){ children=normalizeChildren(children) }elseif(normalizationType===SIMPLE_NORMALIZE){ children=simpleNormalizeChildren(children) } letvnode,ns if(typeoftag==='string'){ letCtor //... if(config.isReservedTag(tag)){ //platformbuilt-inelements vnode=newVNode( config.parsePlatformTagName(tag),data,children, undefined,undefined,context ) }elseif((!data||!data.pre)&&isDef(Ctor=resolveAsset(context.$options,'components',tag))){ //component vnode=createComponent(Ctor,data,context,children,tag) }else{ //unknownorunlistednamespacedelements //checkatruntimebecauseitmaygetassignedanamespacewhenits //parentnormalizeschildren vnode=newVNode( tag,data,children, undefined,undefined,context ) } }else{ //directcomponentoptions/constructor vnode=createComponent(tag,data,context,children) } if(Array.isArray(vnode)){ returnvnode }elseif(isDef(vnode)){ if(isDef(ns))applyNS(vnode,ns) if(isDef(data))registerDeepBindings(data) returnvnode }else{ returncreateEmptyVNode() } }
方法开始会做判断,如果data是响应式的数据,component的is属性不是真值的时候,都会去调用createEmptyVNode方法,创建一个空的vnode。接下来,根据normalizationType的值,调用normalizeChildren或simpleNormalizeChildren方法对参数children进行处理。这两个方法定义在src/core/vdom/helpers/normalize-children.js文件下。
//1.Whenthechildrencontainscomponents-becauseafunctionalcomponent //mayreturnanArrayinsteadofasingleroot.Inthiscase,justasimple //normalizationisneeded-ifanychildisanArray,weflattenthewhole //thingwithArray.prototype.concat.Itisguaranteedtobeonly1-leveldeep //becausefunctionalcomponentsalreadynormalizetheirownchildren. exportfunctionsimpleNormalizeChildren(children:any){ for(leti=0;i, ,v-for,orwhenthechildrenisprovidedbyuser //withhand-writtenrenderfunctions/JSX.Insuchcasesafullnormalization //isneededtocatertoallpossibletypesofchildrenvalues. exportfunctionnormalizeChildren(children:any):?Array { returnisPrimitive(children) ?[createTextVNode(children)] :Array.isArray(children) ?normalizeArrayChildren(children) :undefined }
normalizeChildren和simpleNormalizeChildren的目的都是将children数组扁平化处理,最终返回一个vnode的一维数组。
simpleNormalizeChildren是针对函数式组件做处理,所以只需要考虑children是二维数组的情况。normalizeChildren方法会考虑children是多层嵌套的数组的情况。normalizeChildren开始会判断children的类型,如果children是基础类型,直接创建文本vnode,如果是数组,调用normalizeArrayChildren方法,并在normalizeArrayChildren方法里面进行递归调用,最终将children转成一维数组。
接下来,继续看_createElement方法,如果tag参数的类型不是String类型,是组件的话,调用createComponent创建vnode。如果tag是String类型,再去判断tag是否是html的保留标签,是否是不认识的节点,通过调用newVNode(),传入不同的参数来创建vnode实例。
无论是哪种情况,最终都是通过VNode这个class来创建vnode,下面是类VNode的源码,在文件src/core/vdom/vnode.js中定义
exportdefaultclassVNode{ tag:string|void; data:VNodeData|void; children:?Array; text:string|void; elm:Node|void; ns:string|void; context:Component|void;//renderedinthiscomponent'sscope key:string|number|void; componentOptions:VNodeComponentOptions|void; componentInstance:Component|void;//componentinstance parent:VNode|void;//componentplaceholdernode //strictlyinternal raw:boolean;//containsrawHTML?(serveronly) isStatic:boolean;//hoistedstaticnode isRootInsert:boolean;//necessaryforentertransitioncheck isComment:boolean;//emptycommentplaceholder? isCloned:boolean;//isaclonednode? isOnce:boolean;//isav-oncenode? asyncFactory:Function|void;//asynccomponentfactoryfunction asyncMeta:Object|void; isAsyncPlaceholder:boolean; ssrContext:Object|void; fnContext:Component|void;//realcontextvmforfunctionalnodes fnOptions:?ComponentOptions;//forSSRcaching devtoolsMeta:?Object;//usedtostorefunctionalrendercontextfordevtools fnScopeId:?string;//functionalscopeidsupport constructor( tag?:string, data?:VNodeData, children?:?Array , text?:string, elm?:Node, context?:Component, componentOptions?:VNodeComponentOptions, asyncFactory?:Function ){ this.tag=tag//标签名 this.data=data//当前节点数据 this.children=children//子节点 this.text=text//文本 this.elm=elm//对应的真实DOM节点 this.ns=undefined//命名空间 this.context=context//当前节点上下文 this.fnContext=undefined//函数化组件上下文 this.fnOptions=undefined//函数化组件配置参数 this.fnScopeId=undefined//函数化组件ScopeId this.key=data&&data.key//子节点key属性 this.componentOptions=componentOptions//组件配置项 this.componentInstance=undefined//组件实例 this.parent=undefined//父节点 this.raw=false//是否是原生的HTML片段或只是普通文本 this.isStatic=false//静态节点标记 this.isRootInsert=true//是否作为根节点插入 this.isComment=false//是否为注释节点 this.isCloned=false//是否为克隆节点 this.isOnce=false//是否有v-once指令 this.asyncFactory=asyncFactory//异步工厂方法 this.asyncMeta=undefined//异步Meta this.isAsyncPlaceholder=false//是否异步占位 } //DEPRECATED:aliasforcomponentInstanceforbackwardscompat. /*istanbulignorenext*/ getchild():Component|void{ returnthis.componentInstance } }
VNode类定义的数据,都是用来描述VNode的。
至此,render函数创建vdom的源码就分析完了,我们简单的总结梳理一下。
_render定义在Vue.prototype上,_render函数执行会调用方法render,在开发环境下,会对vm实例进行代理,校验vm实例数据正确性。render函数内,会执行render的参数createElement方法,createElement会对参数进行处理,处理参数后调用_createElement,_createElement方法内部最终会直接或间接调用newVNode(),创建vnode实例。
03 vnode&&vdom
createElement返回的vnode并不是真正的dom元素,VNode的全称叫做“虚拟节点(VirtualNode)”,它所包含的信息会告诉Vue页面上需要渲染什么样的节点,及其子节点。我们常说的“虚拟DOM(VirtualDom)”是对由Vue组件树建立起来的整个VNode树的称呼。
04 心得
读源码切忌只看源码,一定要结合具体的使用一起分析,这样才能更清楚的了解某段代码的意图。就像本文render函数,如果从来没有使用过render函数,直接就阅读这块源码可能会比较吃力,不妨先看看文档,写个demo,看看具体的使用,再对照使用来分析源码,这样很多比较困惑的问题就迎刃而解了。
总结
以上所述是小编给大家介绍的vue中VirtualDom被创建的方法,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对毛票票网站的支持!
如果你觉得本文对你有帮助,欢迎转载,烦请注明出处,谢谢!