vue源码分析-动态组件

news2024/9/28 11:19:30

前面花了两节的内容介绍了组件,从组件的原理讲到组件的应用,包括异步组件和函数式组件的实现和使用场景。众所周知,组件是贯穿整个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的原理分析,敬请期待。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/375736.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

LLaMA-META发布单卡就能跑的大模型

2023年2月25日&#xff0c;Meta使用2048张A100 GPU&#xff0c;花费21天训练的Transformer大模型LLaMA开源了。 1.4T tokenstakes approximately 21 days 以下是觉得论文中重要的一些要点 1&#xff09;相对较小的模型也可以获得不错的性能 研究者发现在给定计算能力限制的情…

【YOLO系列】YOLOv4论文超详细解读1(翻译 +学习笔记)

前言 经过上一期的开篇介绍&#xff0c;我们知道YOLO之父Redmon在twitter正式宣布退出cv界&#xff0c;大家都以为YOLO系列就此终结的时候&#xff0c;天空一声巨响&#xff0c;YOLOv4闪亮登场&#xff01;v4作者是AlexeyAB大神&#xff0c;虽然换人了&#xff0c;但论文中给出…

【BN层的作用】论文阅读 | How Does Batch Normalization Help Optimization?

前言&#xff1a;15年Google提出Batch Normalization&#xff0c;成为深度学习最成功的设计之一&#xff0c;18年MIT团队将原论文中提出的BN层的作用进行了一一反驳&#xff0c;重新揭示BN层的意义 2015年Google团队论文&#xff1a;【here】 2018年MIT团队论文&#xff1a;【h…

谷粒学苑第六天

谷粒学苑第六天 https://hyy-0212.oss-cn-hangzhou.aliyuncs.com/%E5%A4%B4%E5%83%8F.jpg AccessKey看私密文件 后端&#xff1a; idea导入项目后文件颜色不对解决方法 解决&#xff1a;https://blog.csdn.net/m0_37735176/article/details/88916844 后端 私有转为公有&am…

C语言——字符串函数与内存函数

目录 前言 一. 函数介绍 1.1 strlen 1.2 strcpy 1.3 strcat 1.4 strcmp 1.5 strncpy 1.6 strncat 1.7 strncmp 1.8 strstr 1.9 strtok 1.10 strerror 1.11 字符分类函数 1.12 memcpy 1.13 memmove 1.14 memcmp 二. 函数的模拟实现 2.1 模拟实现strlen 2.2 模拟实现st…

阿里测试员晒薪资条,看完真的扎心了...

前几天&#xff0c;有位老粉私信我&#xff0c;说看到某95后学弟晒出阿里的工资单&#xff0c;他是真酸了…想狠补下技术&#xff0c;努力冲一把大厂。 为了帮到他&#xff0c;也为了大家能在最短的时间内做面试复习&#xff0c;我把软件测试面试系列都汇总在这一篇文章了。 …

Windows10 把两张图片合并成一张图片

Windows10把两张图片合并成一张图片 文章目录Windows10把两张图片合并成一张图片1. 背景2. "画图"实现多图拼接1. 背景 相比截图功能&#xff0c;在 Google 的 Chrome 浏览器上&#xff0c;整页截屏功能仍需要安装额外的插件才能完成&#xff0c;这一点 微软的 bing…

【K3s】第18篇 详解 Kubernetes 调度器工作原理

目录 1、概述 2、kube-scheduler 3、kube-scheduler 调度流程 4、支持两种方式配置调度器的过滤和打分行为 4.1 调度策略 4.2 调度策略 5、总结 1、概述 在 Kubernetes 中&#xff0c;调度 是指将Pod放置到合适的节点上&#xff0c;以便对应节点上kubelet能够运行这些 P…

ChatGPT来临,架构师何去何从?

你好&#xff0c;我是李运华。 最近科技圈大火的事件就是ChatGPT的全球火热流行。这款由OpenAI公司在2022年11月底推出的聊天机器人&#xff0c;创造了史上消费应用最快达成1亿用户的历史&#xff0c;在推出仅仅两个月后&#xff0c;月活跃用户估计已达1亿&#xff0c;成为历史…

dbever连接kerberos认证的hive

文章目录一、本地安装kerberos客户端二、本地kerberos客户端登录三、dbever连接hive一、本地安装kerberos客户端 下载地址&#xff1a;https://web.mit.edu/kerberos/dist/index.html 安装&#xff1a;下一步或者自定义安装即可 安装后会自动生成配置文件&#xff1a;C:\Pro…

强化学习RL 01: Reinforcement Learning 基础

目录 RL理解要点 1. RL数学基础 1.1 Random Variable 随机变量 1.2 概率密度函数 Probability Density Function(PDF) 1.3 期望 Expectation 1.4 随机抽样 Random Sampling 2. RL术语 Terminologies 2.1 agent、state 和 action 2.2 策略 policy π 2.3 奖励 reward …

利用GPT-3 Fine-tunes训练专属语言模型

利用GPT-3 Fine-tunes训练专属语言模型 文章目录什么是模型微调&#xff08;fine-tuning&#xff09;&#xff1f;为什么需要模型微调&#xff1f;微调 vs 重新训练微调 vs 提示设计训练专属模型数据准备清洗数据构建模型微调模型评估模型部署模型总结什么是模型微调&#xff0…

cesium: 绘制线段(008)

第008个 点击查看专栏目录 本示例的目的是介绍如何在vue+cesium中绘制线段,左键点击开始绘制,右键点击取消绘制 直接复制下面的 vue+cesium源代码,操作2分钟即可运行实现效果. 文章目录 示例效果配置方式示例源代码(共139行)相关API参考:专栏目标示例效果 配置方式 1)…

Win11的两个实用技巧系列清理磁盘碎片、设置系统还原点的方法

Win11如何清理磁盘碎片?Win11清理磁盘碎片的方法磁盘碎片过多&#xff0c;会影响电脑的运行速度&#xff0c;所以需要定期清理&#xff0c;这篇文章将以Win11为例&#xff0c;给大家分享的整理磁盘碎片方法相信很多用户都会发现&#xff0c;随着电脑使用时间的增加&#xff0c…

一文了解 requestAnimationFrame

requestAnimationFrame 的基本使用 requestAnimationFrame 是什么 window.requestAnimationFrame() 告诉浏览器——你希望执行一个动画&#xff0c;并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数&#xff0c;该回调函数会在浏…

想要实现无人机控制算法二次开发,一定不要错过这个matlab工具包

什么是Matlab工具包 Matlab工具包提供了无人机位置环控制算法、Simulink示例程序&#xff0c;通过Matlab ROS Toolbox建立Matlab&#xff08;Simulink&#xff09;与ROS之间的通信链路&#xff0c;Prometheus项目中将提供转为Matlab设计的控制接口&#xff0c;同时将继承Prome…

Scala-特质

特质 特质的混入用法 特质的叠加 特质和抽象类的区别 特质自身类型 特质 Scala 语言中&#xff0c;采用特质 trait&#xff08;特征&#xff09;来代替接口的概念&#xff0c;也就是说&#xff0c;多个类具有相同 的特质&#xff08;特征&#xff09;时&#xff0c;就可以…

ELK + Kafka 测试

配置file beat输出到 Kafkalogstash服务器从kafka获取数据并输出到es集群在es集群上查看索引kibana界面添加索引查看数据1.配置file beat输出到 Kafka 1.1 Filebeat机器配置数据采集和输出目标 做好域名解析 # vim /usr/local/filebeat/filebeat.yml # 修改输出目标为kafka…

CRM客户管理系统哪个好用?盘点前十名!

CRM客户管理系统排行&#xff1f;盘点前十名&#xff01; CRM客户管理系统是一种集成多种功能的软件系统&#xff0c;可以帮助企业跟进和管理客户关系、提高销售业绩、优化营销策略等。对于企业来说&#xff0c;选择一款适合自己的CRM系统非常重要&#xff0c;因为它能够直接影…

使用kotlin编写html dsl框架

前排提醒&#xff0c;这个框架就是我写着玩的&#xff0c;如果您已经会使用vue或其他前端框架&#xff0c;这篇文章可能对您没有什么意义。即使您不会如上提到的框架&#xff0c;也不要对该框架报有过高的期待&#xff0c;该框架更多的是&#xff0c;我自己的自娱自乐。 这里还…