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被创建的方法,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对毛票票网站的支持!
如果你觉得本文对你有帮助,欢迎转载,烦请注明出处,谢谢!