Vue源码分析-动态组件_0
分两节介绍组件,从组件的原理到组件的应用,包括异步组件和函数式组件的实现和使用场景。众所周知,组件贯穿于整个Vue的设计理念,也是指导我们开发的核心思想。因此,在接下来的几篇文章中,我们将回归组件的内容进行源码分析。首先,我们将从常用的动态组件开始。包括内联模板的原理,内置组件的概念都会在最后简单提及,为以后的文章做铺垫。12.1动态组件相信大部分动态组件在开发过程中都会用到。当我们需要在不同的组件之间切换状态时,动态组件可以很好的满足我们的需求。核心是component标签和is属性的使用。12.1.1基本使用示例是一个动态组件的基本使用场景。当点击按钮时,视图根据this.chooseTabs的值在组件child1、child2、child3之间切换。//vue
//jsvarchild1={template:'
content1
',}varchild2={template:'
content2
'}varchild3={template:'
content3
'}varvm=newVue({el:'#app',components:{child1,child2,child3},methods:{changeTabs(tab){this.chooseTabs=tab;}}})12.1.2AST解析
的解释和前面的内容一致文章,会读到说到AST分析阶段,过程中并没有关注每一个细节,而是具体说明了与之前处理方式的不同之处。对于动态组件解析的差异,重点在于processComponent。由于标签上存在is属性,所以会在最终的ast树上标注component属性。//动态组件分析函数processComponent(el){varbinding;//获取is属性对应的值if((binding=getBindingAttr(el,'is'))){//ast树上的组件比较多Attributeel.component=binding;}if(getAndRemoveAttr(el,'inline-template')!=null){el.inlineTemplate=true;}}最终的ast树如下:12.1.3render函数有了ast树,接下来就是根据ast树生成可执行的render函数。由于component属性,render函数的生成过程会走genComponent分支。//渲染函数生成函数varcode=generate(ast,options);//生成函数的实现functiongenerate(ast,options){varstate=newCodegenState(options);var代码=ast?genElement(ast,state):'_c("div")';return{render:("with(this){return"+code+"}"),staticRenderFns:state.staticRenderFns}}functiongenElement(el,state){···varcode;//动态组件分支if(el.component){code=genComponent(el.component,el,state);}}动态组件的处理逻辑其实很简单,当没有inlinetemplateflag时(后面会讲到),获取后续的子节点进行拼接,唯一和普通组件不同的是_c的第一个参数是no不再是指定的字符串,而是表示组件的变量。//动态组件处理函数genComponent(componentName,el,state){//当有inlineTemplate属性时,children为nullvarchildren=el.inlineTemplate?null:genChildren(el,state,true);return("_c("+componentName+","+(genData$2(el,state))+(children?(","+children):'')+")")}12.1.4普通组件比较而动态组件其实我们可以在render函数中对比一下普通组件和动态组件的区别,结果一目了然。普通组件的渲染函数"with(this){return_c('div',{attrs:{"id":"app"}},[_c('child1',[_v(_s(test))])],1)}"动态组件渲染函数"with(this){return_c('div',{attrs:{"id":"app"}},[_c(chooseTabs,{tag:"component"})],1)}"简而言之,动态组件和普通组件的区别就是在ast阶段增加了一个新的组件属性,这是动态组件的标志。在render函数阶段,由于component属性的存在,会执行genComponent分支,genComponent会对动态组件的执行函数进行特殊处理。与普通组件不同的是,_c的第一个参数不再是一个常量字符串,而是一个指定的组件名称变量。从render到vnode的过程和普通组件一样,只是字符串换成了变量,还有一个数据属性{tag:'component'}。例子中chooseTabs此时取的是child1。有了render函数,接下来从vnode到realnode的流程和普通组件在流程和思路上基本是一样的。这个阶段可以回顾之前介绍的组件流程的分析。看完动态组件的创建过程,我的脑海里冒出一个疑问。从流程原理上分析,动态组件的核心其实就是关键字is。它在编译阶段就将组件定义为动态组件,并带有component属性,而component作为标签,似乎并不是特别好用。只要有is关键字,组件标签名称就可以设置为任意自定义标签,达到动态组件的效果?(组件a,组件b)。该字符串仅以{tag:'component'}的形式存在于vnode的data属性中。那是否意味着所谓的动态成分只是由于is的单方面限制?component标签的意义在哪里?(求指点!!)12.2内联模板由于动态组件除了is作为值外,还可以有inline-template作为配置,这个前提正好可以阐明Vue中内联模板的原理和设计思路。Vue在官网有一个醒目的声明,提醒我们inline-template会使模板的作用范围更难理解。因此,建议使用模板选项来定义模板而不是内联模板。接下来我们通过源码来定位所谓作用域难以理解的原因。我们先简单调整一下上面的例子,从使用的角度出发://html{{test}} //jsvarchild1={data(){return{test:'content1'}}}varchild2={data(){return{test:'content2'}}}varchild3={data(){return{test:'content3'}}}varvm=newVue({el:'#app',components:{child1,child2,child3},data(){return{chooseTabs:'child1',}},methods:{changeTabs(tab){this.chooseTabs=tab;}}})例子中实现的效果与文章第一个例子一致。显然,和之前认知最大的不同就是,父组件中的环境可以访问子组件内部的环境变量。乍一看,似乎不可思议。让我们回忆一下之前父组件可以访问子组件的情况。大致有两个方向:-1.使用事件机制,子组件通过$emit事件通知父组件子组件的状态,从而父组件可以访问子组件的目标。-2.使用scopeslot方法将child的变量以props的形式传递给parent,parent通过v-slot这个语法糖接收,而我们前面分析的结果是这个方法本质上是传递以事件派发的形式通知父组件。前面的分析过程也提到了父组件不能访问子环境的变量。核心原因是:父模板中的所有内容都是在父范围内编译的;子模板中的所有内容都是在作用域编译的子环境中编译的。那我们就有理由猜测,内联模板是不是违反了这个原则,让父组件的内容放到子组件创建过程中去编译呢?我们继续往下看:回到ast分析阶段,前面的分析,对于动态组件的分析,关键在于processComponent函数对is属性的处理,其中一个关键就是inline的处理-template,会在asttree上添加inlineTemplate属性。参考vue源码视频讲解:进入学习//分析动态组件函数processComponent(el){varbinding;//获取is属性对应的值if((binding=getBindingAttr(el,'is'))){//ast树上还有更多的组件属性el.component=binding;}//添加inlineTemplate属性if(getAndRemoveAttr(el,'inline-template')!=null){el.inlineTemplate=true;}}render函数的生成由于stage中存在inlineTemplate,所以parent的render函数的子节点为null。这一步也判断inline-template下的模板没有在父组件阶段编译。模板是如何传递给子组件的编译过程的?答案是模板以属性的形式存在,到达子实例时获取属性值functiongenComponent(componentName,el,state){//当inlineTemplate属性可用时,子实例为nullvarchildren=el.inlineTemplate?null:genChildren(el,state,true);return("_c("+componentName+","+(genData$2(el,state))+(children?(","+children):'')+")")}我们看一下结果最终的渲染函数,其中模板以{render:function(){}}的形式存在于父组件的inlineTemplate属性中。"_c('div',{attrs:{"id":"app"}},[_c(chooseTabs,{tag:"component",inlineTemplate:{render:function(){with(this){return_c('span',[_v(_s(test))])}},staticRenderFns:[]}})],1)”最终的vnode结果也表明inlineTemplate对象会保留在父组件的data属性中。//vnoderesult{data:{inlineTemplate:{render:function(){}},tag:'component'},tag:"vue-component-1-child1"}有了vnode之后,我们来到了关键的最后step,根据vnodes生成真实节点的过程。从根节点开始,遇到vue-component-1-child1,就会经历子组件实例化和创建的过程。在实例化子组件之前,您将首先处理inlineTemplate属性。functioncreateComponentInstanceForVnode(vnode,parent){//子组件的默认选项varoptions={_isComponent:true,_parentVnode:vnode,parent:parent};varinlineTemplate=vnode.data.inlineTemplate;//内联模板的处理,分别获取render函数和staticRenderFnsif(isDef(inlineTemplate)){options.render=inlineTemplate.render;options.staticRenderFns=inlineTemplate.staticRenderFns;}//执行vue子组件实例化returnnewvnode.componentOptions.Ctor(options)}子组件默认option配置会根据vnode上的inlineTemplate属性获取模板的render函数。至此分析的结论已经很明确了。内联模板的内容最终会在子组件中解析,所以在模板中可以获取到子组件的作用域也就不足为奇了。12.3内置组件最后说一下Vue思维中的另一个概念,内置组件。其实vue的官方文档列出了内置的组件,分别是component,transition,transition-group,keep-alive,slot,其中
我们已经在slots部分详细介绍过了,还有组件使用部分也花了大量篇幅从使用到原理进行了分析。但是,在了解了slots和components之后,我开始意识到slots和components并不是真正的内置组件。内置组件是在源代码初始化阶段已经全局注册的组件。但是,和不被视为组件,因此没有组件生命周期。slot只会在render函数阶段转化为renderSlot函数进行处理,组件只会使用is属性将createElement的第一个参数从字符串转化为变量,仅此而已。那么回到概念的理解上,内置组件就是源码自己提供的组件,所以这部分内容的重点会放在内置组件什么时候注册,编译的时候有什么区别。这部分只是一个介绍。接下来会有两篇文章专门详细介绍keep-alive、transition、transition-group的实现原理。12.3.1构造函数定义组件Vue会在初始化阶段将三个组件对象添加到构造函数的components属性中。每个组件对象的写法和我们在自定义组件过程中的写法是一致的。它有渲染功能,有生命周期,有定义各种数据。//keep-alive组件选项varKeepAlive={render:function(){}}//transition组件选项varTransition={render:function(){}}//transition-group组件选项varTransitionGroup={render:function(){},方法:{},···}varbuiltInComponents={KeepAlive:KeepAlive};varplatformComponents={Transition:Transition,TransitionGroup:TransitionGroup};//Vue构造函数选项配置,compoents选项合并系列开头在分析options的合并时,将对象上的属性合并到源对象中,属性相同被覆盖。//将_from对象合并到to对象中。如果属性相同,则覆盖to对象的属性functionextend(to,_from){for(varkeyin_from){to[key]=_from[key];}returnto}最后,Vue构造函数有三个组件的配置选项。Vue.components={keepAlive:{},transition:{},transition-group:{},}12.3.2注册内置组件仅仅有定义是不够的。组件需要全局使用,全局注册。在一个Vue实例的初始化过程中,最重要的第一步就是合并options,像内置组件这样的资源类options会有一个特殊的optionmerge策略,最后将构造函数上的componentoptions注册到表单中的原型链。在实例的组件选项中(指令和过滤器也是如此)。//资源选项varASSET_TYPES=['component','directive','filter'];//定义资源合并策略ASSET_TYPES.forEach(function(type){strats[type+'s']=mergeAssets;//定义默认策略});functionmergeAssets(parentVal,childVal,vm,key){varres=Object.create(parentVal||null);//创建一个以parentVal为原型的空对象if(childVal){assertObjectType(key,childVal,vm);//组件、过滤器、指令选项必须是对象returnextend(res,childVal)//子类选项被分配给空对象}else{returnres}}两个关键步骤是varres=Object.create(parentVal||null);,它会创建一个以parentVal为原型的空对象,最后通过extend将用户自定义的组件选项复制到空对象中。选项合并后,内置组件就这样全局注册了。{components:{child1,__proto__:{keepAlive:{},transition:{},transitionGroup:{}}}}最后我们看内置的组件对象中没有template模板,而是一个render函数,在除了减少耗费性能的模板解析过程,我认为重要的原因是内置组件没有渲染实体。最后让我们期待后续关于keep-alive和transition原理的分析,敬请期待。