乐趣区

关于前端:vue源码分析动态组件

后面花了两节的内容介绍了组件,从组件的原理讲到组件的利用,包含异步组件和函数式组件的实现和应用场景。家喻户晓,组件是贯通整个 Vue 设计理念的货色,并且也是领导咱们开发的核心思想,所以接下来的几篇文章,将从新回到组件的内容去做源码剖析,首先会从罕用的动静组件开始,包含内联模板的原理,最初会简略的提到内置组件的概念,为之后的文章埋下伏笔。

12.1 动静组件

动静组件我置信大部分在开发的过程中都会用到,当咱们须要在不同的组件之间进行状态切换时,动静组件能够很好的满足咱们的需要,其中的外围是 component 标签和 is 属性的应用。

12.1.1 根本用法

例子是一个动静组件的根本应用场景,当点击按钮时,视图依据 this.chooseTabs 值在组件 child1,child2,child3 间切换。

// vue
<div id="app">
  <button @click="changeTabs('child1')">child1</button>
  <button @click="changeTabs('child2')">child2</button>
  <button @click="changeTabs('child3')">child3</button>
  <component :is="chooseTabs">
  </component>
</div>
// js
var child1 = {template: '<div>content1</div>',}
var child2 = {template: '<div>content2</div>'}
var child3 = {template: '<div>content3</div>'}
var vm = new Vue({
  el: '#app',
  components: {
    child1,
    child2,
    child3
  },
  methods: {changeTabs(tab) {this.chooseTabs = tab;}
  }
})
12.1.2 AST 解析

<component>的解读和后面几篇内容统一,会从 AST 解析阶段说起,过程也不会专一每一个细节,而是把和以往解决形式不同的中央特地阐明。针对动静组件解析的差别,集中在 processComponent 上,因为 标签上 is 属性的存在,它会在最终的 ast 树上打上 component 属性的标记。

//  针对动静组件的解析
function processComponent (el) {
  var binding;
  // 拿到 is 属性所对应的值
  if ((binding = getBindingAttr(el, 'is'))) {
    // ast 树上多了 component 的属性
    el.component = binding;
  }
  if (getAndRemoveAttr(el, 'inline-template') != null) {el.inlineTemplate = true;}
}

最终的 ast 树如下:

12.1.3 render 函数

有了 ast 树,接下来是依据 ast 树生成可执行的 render 函数,因为有 component 属性,render函数的产生过程会走 genComponent 分支。

// render 函数生成函数
var code = generate(ast, options);

// generate 函数的实现
function generate (ast,options) {var state = new CodegenState(options);
  var code = ast ? genElement(ast, state) : '_c("div")';
  return {render: ("with(this){return" + code + "}"),
    staticRenderFns: state.staticRenderFns
  }
}

function genElement(el, state) {
  ···
  var code;
  // 动静组件分支
  if (el.component) {code = genComponent(el.component, el, state);
  }
}

针对动静组件的解决逻辑其实很简略,当没有内联模板标记时 (前面会讲), 拿到后续的子节点进行拼接,和一般组件惟一的区别在于,_c 的第一个参数不再是一个指定的字符串,而是一个代表组件的变量。

// 针对动静组件的解决
  function genComponent (componentName,    el,    state) {
    // 领有 inlineTemplate 属性时,children 为 null
    var children = el.inlineTemplate ? null : genChildren(el, state, true);
    return ("_c(" + componentName + "," + (genData$2(el, state)) + (children ? ("," + children) : '') +")")
  }
12.1.4 一般组件和动静组件的比照

其实咱们能够比照一般组件和动静组件在 render 函数上的区别,后果高深莫测。

一般组件的 render 函数

"with(this){return _c('div',{attrs:{"id":"app"}},[_c('child1',[_v(_s(test))])],1)}"

动静组件的 render 函数

"with(this){return _c('div',{attrs:{"id":"app"}},[_c(chooseTabs,{tag:"component"})],1)}"

简略的总结,动静组件和一般组件的区别在于:

    1. ast阶段新增了 component 属性,这是动静组件的标记
    1. 产生 render 函数阶段因为 component 属性的存在,会执行 genComponent 分支,genComponent会针对动静组件的执行函数进行非凡的解决,和一般组件不同的是,_c的第一个参数不再是不变的字符串,而是指定的组件名变量。
    1. rendervnode 阶段和一般组件的流程雷同,只是字符串换成了变量,并有 {tag: 'component'}data属性。例子中 chooseTabs 此时取的是child1

有了 render 函数,接下来从 vnode 到实在节点的过程和一般组件在流程和思路上基本一致,这一阶段能够回顾之前介绍组件流程的剖析

12.1.5 纳闷

因为本人对源码的了解还不够透彻, 读了动静组件的创立流程之后,心中产生了一个疑难,从原理的过程剖析,动静组件的外围其实是 is 这个关键字,它在编译阶段就以 component 属性将该组件定义为动静组件,而 component 作为标签如同并没有特地大的用处,只有有 is 关键字的存在,组件标签名设置为任意自定义标签都能够达到动静组件的成果?(componenta, componentb)。这个字符串仅以 {tag: 'component'} 的模式存在于 vnodedata属性存在。那是不是阐明,所谓动静组件只是因为 is 的单方面限度?那 component 标签的意义又在哪里?(求教大佬!!)

12.2 内联模板

因为动静组件除了有 is 作为传值外,还能够有 inline-template 作为配置, 借此前提,刚好能够理分明 Vue 中内联模板的原理和设计思维。Vue在官网有一句醒目的话,提醒咱们 inline-template 会让模板的作用域变得更加难以了解。因而倡议尽量应用template 选项来定义模板,而不是用内联模板的模式。接下来,咱们通过源码去定位一下所谓作用域难以了解的起因。

咱们先简略调整下面的例子,从应用角度上动手:

参考 前端进阶面试题具体解答

// html
<div id="app">
  <button @click="changeTabs('child1')">child1</button>
  <button @click="changeTabs('child2')">child2</button>
  <button @click="changeTabs('child3')">child3</button>
  <component :is="chooseTabs" inline-template>
    <span>{{test}}</span>
  </component>
</div>
// js
var child1 = {data() {
    return {test: 'content1'}
  }
}
var child2 = {data() {
    return {test: 'content2'}
  }
}
var child3 = {data() {
    return {test: 'content3'}
  }
}
var vm = new Vue({
  el: '#app',
  components: {
    child1,
    child2,
    child3
  },
  data() {
    return {chooseTabs: 'child1',}
  },
  methods: {changeTabs(tab) {this.chooseTabs = tab;}
  }
})

例子中达到的成果和文章第一个例子统一,很显著和以往认知最大的差别在于,父组件里的环境能够拜访到子组件外部的环境变量。初看感觉挺不堪设想的。咱们回顾一下之前父组件能拜访到子组件的情景,从大的方向上有两个:

– 1. 采纳事件机制,子组件通过 $emit 事件,将子组件的状态告知父组件,达到父拜访子的目标。 – 2. 利用作用域插槽的形式,将子的变量通过 props 的模式传递给父,而父通过 v-slot 的语法糖去接管,而咱们之前剖析的后果是,这种形式实质上还是通过事件派发的模式去告诉父组件。

之前剖析过程也有提过父组件无法访问到子环境的变量,其外围的起因在于:父级模板里的所有内容都是在父级作用域中编译的;子模板里的所有内容都是在子作用域中编译的。 那么咱们有理由猜测,内联模板是不是违反了这一准则,让父的内容放到了子组件创立过程去编译呢?咱们接着往下看:

回到 ast 解析阶段,后面剖析到,针对动静组件的解析,关键在于 processComponent 函数对 is 属性的解决,其中还有一个要害是对 inline-template 的解决,它会在 ast 树上减少 inlineTemplate 属性。

//  针对动静组件的解析
  function processComponent (el) {
    var binding;
    // 拿到 is 属性所对应的值
    if ((binding = getBindingAttr(el, 'is'))) {
      // ast 树上多了 component 的属性
      el.component = binding;
    }
    // 增加 inlineTemplate 属性
    if (getAndRemoveAttr(el, 'inline-template') != null) {el.inlineTemplate = true;}
  }

render函数生成阶段因为 inlineTemplate 的存在,父的 render 函数的子节点为 null, 这一步也决定了inline-template 下的模板并不是在父组件阶段编译的 , 那模板是如何传递到子组件的编译过程呢? 答案是模板以属性的模式存在,待到子实例时拿到属性值

function genComponent (componentName,el,state) {
  // 领有 inlineTemplate 属性时,children 为 null
  var children = el.inlineTemplate ? null : genChildren(el, state, true);
  return ("_c(" + componentName + "," + (genData$2(el, state)) + (children ? ("," + children) : '') +")")
}

咱们看看最终 render 函数的后果,其中模板以 {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 属性中。

// vnode 后果
{
  data: {
    inlineTemplate: {render: function() {}},
    tag: 'component'
  },
  tag: "vue-component-1-child1"
}

有了 vnode 后,来到了要害的最初一步,依据 vnode 生成实在节点的过程。从根节点开始,遇到 vue-component-1-child1,会经验实例化创立子组件的过程,实例化子组件前会先对inlineTemplate 属性进行解决。

function createComponentInstanceForVnode (vnode,parent) {
    // 子组件的默认选项
    var options = {
      _isComponent: true,
      _parentVnode: vnode,
      parent: parent
    };
    var inlineTemplate = vnode.data.inlineTemplate;
    // 内联模板的解决,别离拿到 render 函数和 staticRenderFns
    if (isDef(inlineTemplate)) {
      options.render = inlineTemplate.render;
      options.staticRenderFns = inlineTemplate.staticRenderFns;
    }
    // 执行 vue 子组件实例化
    return new vnode.componentOptions.Ctor(options)
  }

子组件的默认选项配置会依据 vnode 上的 inlineTemplate 属性拿到模板的 render 函数。剖析到这一步论断曾经很分明了。内联模板的内容最终会在子组件中解析,所以模板中能够拿到子组件的作用域这个景象也难能可贵了。

12.3 内置组件

最初说说 Vue 思维中的另一个概念,内置组件,其实 vue 的官网文档有对内置组件进行了列举,别离是 component, transition, transition-group, keep-alive, slot,其中<slot> 咱们在插槽这一节曾经具体介绍过,而 component 的应用这一节也花了大量的篇幅从应用到原理进行了剖析。然而学习了 slot,component 之后,我开始意识到 slotcomponent并不是真正的内置组件。内置组件是曾经在源码初始化阶段就全局注册好的组件。<slot><component>并没有被当成一个组件去解决,因而也没有组件的生命周期。slot只会在 render 函数阶段转换成 renderSlot 函数进行解决,而 component 也只是借助 is 属性将 createElement 的第一个参数从字符串转换为变量,仅此而已。因而从新回到概念的了解,内置组件是源码本身提供的组件,所以这一部分内容的重点,会放在内置组件是什么时候注册的,编译时有哪些不同这两个问题上来。这一部分只是一个抛砖引玉,接下来会有两篇文章专门具体介绍 keep-alive,transition, transition-group 的实现原理。

12.3.1 结构器定义组件

Vue初始化阶段会在结构器的 components 属性增加三个组件对象, 每个组件对象的写法和咱们在自定义组件过程的写法统一,有 render 函数,有生命周期,也会定义各种数据。

// keep-alive 组件选项
var KeepAlive = {render: function() {}}

// transition 组件选项
var Transition = {render: function() {}}

// transition-group 组件选项
var TransitionGroup = {render: function() {},
  methods: {},
  ···
}

var builtInComponents = {KeepAlive: KeepAlive};

var platformComponents = {
  Transition: Transition,
  TransitionGroup: TransitionGroup
};

// Vue 结构器的选项配置,compoents 选项合并
extend(Vue.options.components, builtInComponents);
extend(Vue.options.components, platformComponents);

extend办法咱们在系列的结尾,剖析选项合并的时候有说过,将对象上的属性合并到源对象中,属性雷同则笼罩。

// 将_from 对象合并到 to 对象,属性雷同时,则笼罩 to 对象的属性
function extend (to, _from) {for (var key in _from) {to[key] = _from[key];
  }
  return to
}

最终 Vue 结构器领有了三个组件的配置选项。

Vue.components = {keepAlive: {},
  transition: {},
  transition-group: {},}
12.3.2 注册内置组件

仅仅有定义是不够的。组件须要被全局应用还得进行全局的注册。Vue 实例在初始化过程中,最重要的第一步是进行选项的合并,而像内置组件这些资源类选项会有专门的选项合并策略,最终结构器上的组件选项会以原型链的模式注册到实例的 compoonents 选项中(指令和过滤器同理)。

// 资源选项
var ASSET_TYPES = [
  'component',
  'directive',
  'filter'
];

// 定义资源合并的策略
ASSET_TYPES.forEach(function (type) {strats[type + 's'] = mergeAssets; // 定义默认策略
});

function mergeAssets (parentVal,childVal,vm,key) {var res = Object.create(parentVal || null); // 以 parentVal 为原型创立一个空对象
    if (childVal) {assertObjectType(key, childVal, vm); // components,filters,directives 选项必须为对象
      return extend(res, childVal) // 子类选项赋值给空对象
    } else {return res}
  }

要害的两步一个是 var res = Object.create(parentVal || null);,它会以parentVal 为原型创立一个空对象,最初是通过 extend 将用户自定义的 component 选项复制到空对象中。选项合并之后,内置组件也因而在全局实现了注册。

{
  components: {
    child1,
    __proto__: {keepAlive: {},
      transition: {},
      transitionGroup: {}}
  }
}

最初咱们看看内置组件对象中并没有 template 模板,而是 render 函数,除了缩小了耗性能的模板解析过程,我认为重要的起因是内置组件并没有渲染的实体。最初的最初,让咱们一起期待后续对 keep-alivetransition的原理剖析,敬请期待。

退出移动版