深入剖析Vue源码 - 组件基础

栏目: 编程语言 · 发布时间: 6年前

内容简介:组件是Vue的一个重要核心,我们在进行项目工程化时,会将页面的结构组件化,组件化意味着独立和共享。编写Vue组件是掌握Vue开发的核心基础,Vue官网也花了大篇幅介绍了组件的知识,并且也深入讲解了Vue的使用。这一节内容,我们将深入Vue组件部分的源码,了解组件注册的实现思路和组件渲染挂载的基本流程。这将让我们今后在解决vue组件相关问题上更加得心应手。熟悉Vue的开发流程的都知道,Vue组件在使用之前需要进行注册,而注册的方式有两种,全局注册和局部注册。在进入源码分析之前,我们先回忆一下两者的用法,以便后

组件是Vue的一个重要核心,我们在进行项目工程化时,会将页面的结构组件化,组件化意味着独立和共享。编写Vue组件是掌握Vue开发的核心基础,Vue官网也花了大篇幅介绍了组件的知识,并且也深入讲解了Vue的使用。这一节内容,我们将深入Vue组件部分的源码,了解组件注册的实现思路和组件渲染挂载的基本流程。这将让我们今后在解决vue组件相关问题上更加得心应手。

5.1 组件两种注册方式

熟悉Vue的开发流程的都知道,Vue组件在使用之前需要进行注册,而注册的方式有两种,全局注册和局部注册。在进入源码分析之前,我们先回忆一下两者的用法,以便后续掌握两者的差异。

5.1.1 全局注册

Vue.component('my-test', {
    template: '<div>{{test}}</div>',
    data () {
        return {
            test: 1212
        }
    }
})
var vm = new Vue({
    el: '#app',
    template: '<div id="app"><my-test><my-test/></div>'
})
复制代码

其中组件的全局注册需要在全局实例化Vue前调用,注册之后可以用在任何新创建的 Vue 实例的模板中调用。

5.1.2 局部注册

var myTest = {
    template: '<div>{{test}}</div>',
    data () {
        return {
            test: 1212
        }
    }
}
var vm = new Vue({
    el: '#app',
    component: {
        myTest
    }
})
复制代码

当只需要在某个局部用到某个组件时,可以使用局部注册的方式进行组件注册,此时局部注册的组件只能在注册该组件的组件内部使用。

5.1.3 注册过程

简单回顾两种组件注册方式后,我们来看注册过程到底发生了什么,我们以全局组件注册为例。它通过 Vue.component(name, {...}) 进行组件注册, Vue.component 的定义在 Vue 源码的初始化中。

// 初始化全局api
initAssetRegisters(Vue);
var ASSET_TYPES = [
    'component',
    'directive',
    'filter'
];
function initAssetRegisters(Vue){
    // 定义ASSET_TYPES中每个属性的方法,其中包括component
    ASSET_TYPES.forEach(function (type) {
    // type: component,directive,filter
      Vue[type] = function (id,definition) {
          if (!definition) {
            // 直接返回注册组件的构造函数
            return this.options[type + 's'][id]
          }
          ...
          if (type === 'component') {
            // 验证component组件名字是否合法
            validateComponentName(id);
          }
          if (type === 'component' && isPlainObject(definition)) {
            // 组件名称设置
            definition.name = definition.name || id;
            // Vue.extend() 创建子组件,返回子类构造器
            definition = this.options._base.extend(definition);
          }
          // 为Vue.options 上的component属性添加将子类构造器
          this.options[type + 's'][id] = definition;
          return definition
        }
    });
}
复制代码

源码中全局注册组件的实际是调用构造函数 Vue 的静态方法 extend ,并且为 Vue.options 上的 component 属性添加子类构造器。其中extend方法的定义我们在 深入剖析Vue源码 - 选项合并(上) 中的子类构造器一段中有介绍这里不赘述,总结而言: Vue.extend 创建了一个基于父Vue的子类,创建过程会继承父类的方法,并对父类子类的配置进行合并,最终返回一个子类的构造器。这个子组件的名称默认以 Vue.component() 的第一个参数作为组件名,当组件选项有name属性时,则用name属性覆盖。当 Vue.component() 不传递第二个选项参数时,会返回已经注册过的子类构造器。

接下来留下一个问题,局部注册和全局注册的在实现上的区别在哪里?

5.2 组件Vnode创建

组件注册过后,会在实例 options.component 增加一个组件的配置属性,这个属性是一个子的 Vue 构造器。然而这个组件何时创建,何时进行实例化,何时渲染,何时挂载基础钩子是这一小节分析的核心。

5.2.1 Vnode创建流程图

深入剖析Vue源码 - 组件基础

5.2.2 具体流程分析

我们将上图的流程简单概括为以下几点:

    1. Vue 根实例初始化会执行 vm.$mount(vm.$options.el) 实例挂载的过程,按照之前 深入剖析Vue源码 - 完整渲染过程 所讲的逻辑,完整流程会经历 render 函数生成 Vnode ,以及 Vnode 生成真实 DOM 的过程。
    1. render 函数生成 Vnode 过程中,子会优先父执行生成 Vnode 过程,子执行过程中遇到子组件占位符如( <test></test> )时,会判断该占位符是否是注册过的组件标签,如果符合条件,则进入 createComponent 创建子组件的过程,如果为一般标签,则执行 new Vnode 过程。
    1. createComponent 是创建组件 Vnode 的过程,创建过程会合并子和父构造器的选项配置,并安装组件相关的钩子,最后通过 new Vnode() 生成以 vue-component 开头的 Virtual DOM
    1. render 函数执行过程也是一个循环递归调用创建 Vnode 的过程,执行3,4步之后,完整的生成了一个包含各个子组件的 Vnode tree

这其中一二步的源码是前一节所分析过的,我们重点分析遇到子组件占位符时差异的处理。

// 内部执行将render函数转化为Vnode的函数
function _createElement(context,tag,data,children,normalizationType) {
  ···
  if (typeof tag === 'string') {
    // 子节点的标签为普通的html标签,直接创建Vnode
    if (config.isReservedTag(tag)) {
      vnode = new VNode(
        config.parsePlatformTagName(tag), data, children,
        undefined, undefined, context
      );
    // 子节点标签为注册过的组件标签名,则子组件Vnode的创建过程
    } else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
      // 创建子组件Vnode
      vnode = createComponent(Ctor, data, context, children, tag);
    }
  }
}
复制代码

其中核心是在判断该子组件的占位符是否为已注册过的组件,在介绍全局注册时我们已经知道了,一个组件全局注册后,Vue实例的 options.component 对象上会新增一个带有构造器的组件选项。因此是否拥有这个选项也成为判断组件是否注册的标准。

// 需要明确组件是否已经被注册
  function resolveAsset (options,type,id,warnMissing) {
    // 标签为字符串
    if (typeof id !== 'string') {
      return
    }
    // 这里是 options.component
    var assets = options[type];
    // 这里的分支分别支持大小写,驼峰的命名规范
    if (hasOwn(assets, id)) { return assets[id] }
    var camelizedId = camelize(id);
    if (hasOwn(assets, camelizedId)) { return assets[camelizedId] }
    var PascalCaseId = capitalize(camelizedId);
    if (hasOwn(assets, PascalCaseId)) { return assets[PascalCaseId] }
    // fallback to prototype chain
    var res = assets[id] || assets[camelizedId] || assets[PascalCaseId];
    if (warnMissing && !res) {
      warn(
        'Failed to resolve ' + type.slice(0, -1) + ': ' + id,
        options
      );
    }
    // 最终返回子类的构造器
    return res
  }
复制代码

子组件创建Vnode的过程是调用 createComponent 方法。

// 创建子组件过程
  function createComponent (
    Ctor, // 子类构造器
    data,
    context, // vm实例
    children, // 子节点
    tag // 子组件占位符
  ) {
    ···
    // Vue.options里的_base属性存储Vue构造器
    var baseCtor = context.$options._base;

    // 针对局部组件注册场景
    if (isObject(Ctor)) {
      Ctor = baseCtor.extend(Ctor);
    }
    data = data || {};
    // 构造器配置合并
    resolveConstructorOptions(Ctor);
    // 挂载组件钩子
    installComponentHooks(data);

    // return a placeholder vnode
    var name = Ctor.options.name || tag;
    // 创建子组件vnode,名称以 vue-component- 开头
    var vnode = new VNode(("vue-component-" + (Ctor.cid) + (name ? ("-" + name) : '')),data, undefined, undefined, undefined, context,{ Ctor: Ctor, propsData: propsData, listeners: listeners, tag: tag, children: children },asyncFactory);

    return vnode
  }
复制代码

这里将大部分的代码都拿掉了,只留下创建vnode相关的代码,最终会通过 new Vue 实例化一个名称以 vue-component- 开头标记名称的 Vnode 节点返回。其中两个关键的步骤是配置合并和安装组件钩子函数,选项合并的内容可以查看这个系列的前两节,这里看看 installComponentHooks 安装组件钩子函数时发生了什么。

// 组件内部自带钩子
 var componentVNodeHooks = {
    init: function init (vnode, hydrating) {
    },
    prepatch: function prepatch (oldVnode, vnode) {
    },
    insert: function insert (vnode) {
    },
    destroy: function destroy (vnode) {
    }
  };
var hooksToMerge = Object.keys(componentVNodeHooks);
// 将componentVNodeHooks 钩子函数合并到组件data.hook中 
function installComponentHooks (data) {
    var hooks = data.hook || (data.hook = {});
    for (var i = 0; i < hooksToMerge.length; i++) {
      var key = hooksToMerge[i];
      var existing = hooks[key];
      var toMerge = componentVNodeHooks[key];
      // 如果钩子函数存在,则执行mergeHook$1方法合并
      if (existing !== toMerge && !(existing && existing._merged)) {
        hooks[key] = existing ? mergeHook$1(toMerge, existing) : toMerge;
      }
    }
  }
function mergeHook$1 (f1, f2) {
  // 返回一个依次执行f1,f2的函数
    var merged = function (a, b) {
      f1(a, b);
      f2(a, b);
    };
    merged._merged = true;
    return merged
  }
复制代码

组件默认自带几个钩子函数,这些钩子函数在后续 patch 过程中会在不同阶段执行, installComponentHooks 函数的目的是将这些默认的钩子函数和自定义的钩子函数合并,合并的原则是如果钩子函数存在,则合并两个函数,在执行阶段会依次执行。

5.2.3 局部注册和全局注册的区别

在说到全局注册和局部注册的用法时留下了一个问题,局部注册和全局注册两者的区别在哪里。上文源码分析讲到全局注册却没有提及局部注册,其实局部注册的原理同样简单,我们使用局部注册组件时会通过在父组件选项配置中的 component 添加子组件的对象配置,这和全局注册后在Vue的 options.component 添加子组件构造器的结果很相似。区别在于:

  • 1.局部注册添加的对象配置是在某个组件下,而全局注册添加的子组件是在根实例下。
  • 2.局部注册添加的是一个子组件的配置对象,而全局注册添加的是一个子类构造器。

因此局部注册中缺少了一步构建子类构造器的过程,这个过程放在哪里进行呢? 回到 createComponent 的源码,源码中根据传入对象和构造器的分类区分局部和全局注册组件,而局部注册依然会调用 父类的 extend 方法去创建子类构造器。

// 针对局部组件注册场景
if (isObject(Ctor)) {
    Ctor = baseCtor.extend(Ctor);
}
复制代码

5.3 组件Vnode渲染真实DOM

有了完整的 Vnode tree ,接下来是根据 Vnode tree 渲染真实的 DOM

5.3.1 真实节点渲染流程图

深入剖析Vue源码 - 组件基础

5.3.2 具体流程分析

    1. 经过 vm._render() 生成完整的 Virtual Dom 树后,紧接着执行Vnode渲染真实DOM的过程,即 vm.update() ,而 update 的核心方法是 vm.__patch__
    1. vm.__patch__ 内部会通过 createElm 去创建真实的 DOM 元素,期间遇到子 Vnode 会递归调用 createElm 方法。
    1. 递归调用过程中,判断该节点类型为组件类型是通过 createComponent 方法判断的,该方法和渲染 Vnode 阶段的方法 createComponent 不同,他会调用子组件的 init 初始化钩子函数,并完成组件的 DOM 插入。
    1. init 初始化钩子函数的核心是 new 实例化这个子组件,实例化子组件的过程又回到合并配置,初始化生命周期,初始化事件中心,初始化渲染的过程
    1. 完成所有子组件的实例化和节点挂载后,最后才回到根节点的挂载。

__patch__ 核心代码是通过 createElm 创建真实节点,当创建过程中遇到子 vnode 时,会调用 createChildren , createChildren 的目的是对子vnode递归调用 createElm 创建子组件节点。

// 创建真实dom
function createElm (vnode,insertedVnodeQueue,parentElm,refElm,nested,ownerArray,index) {
  ···
  // 递归创建子组件真实节点,直到完成所有子组件的渲染才进行根节点的真实节点插入
  if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
    return
  }
  ···
  var children = vnode.children;
  // 
  createChildren(vnode, children, insertedVnodeQueue);
  ···
  insert(parentElm, vnode.elm, refElm);
}
function createChildren(vnode, children, insertedVnodeQueue) {
  for (var i = 0; i < children.length; ++i) {
    // 遍历子节点,递归调用创建真实dom节点的方法 - createElm
    createElm(children[i], insertedVnodeQueue, vnode.elm, null, true, children, i);
  }
}
复制代码

对子组件的处理,放在 createComponent 方法中, createComponent 的核心是会判断这个 Vnode 是否为子组件,如果条件满足,则执行组件注册时安装的 init 方法(由于在组件注册过程中会安装一系列的钩子函数,所以是否有钩子函数可以作为判断组件的唯一条件)。

function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
  var i = vnode.data;
  // 是否有钩子函数可以作为判断是否为组件的唯一条件
  if (isDef(i = i.hook) && isDef(i = i.init)) {
    // 执行init钩子函数
    i(vnode, false /* hydrating */);
  }
  ···
}
复制代码

由于前面在介绍组件内部钩子函数时跳过了每个钩子内部实现功能的介绍,所以我们需要回头分析 init 钩子函数的执行逻辑(其中忽略 keeplive 分支逻辑)。

var componentVNodeHooks = {
  // 忽略keepAlive过程
  var child = vnode.componentInstance = createComponentInstanceForVnode(vnode,activeInstance);
  child.$mount(hydrating ? vnode.elm : undefined, hydrating);
}
function createComponentInstanceForVnode(vnode, parent) {
  ···
  // 实例化Vue子组件实例
  return new vnode.componentOptions.Ctor(options)
}
复制代码

init 执行过程中会调用 createComponentInstanceForVnode 对子组件进行实例化。 调用 createComponent 是一个递归调用实例化所有子组件的过程,只有会将所有的子组件实例化,并挂载到对应父节点上后,才最后进行根节点的挂载。

5.4 小结

这一节内容是组件部分的基础,介绍了组件注册到挂载渲染流程,组件的注册核心是定义了一个子类构造器,渲染过程中遵循先子后父的思想,逐级递归对子组件完成生成 vnode 到挂载真实 dom 的挂载,最后完成根节点的挂载。从源码内部分析完组件流程怎么工作后,接下来会介绍一些组件高级用法的实现原理。


以上所述就是小编给大家介绍的《深入剖析Vue源码 - 组件基础》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Coming of Age in Second Life

Coming of Age in Second Life

Tom Boellstorff / Princeton University Press / 2008-04-21 / USD 29.95

The gap between the virtual and the physical, and its effect on the ideas of personhood and relationships, is the most interesting aspect of Boellstorff's analysis... Boellstorff's portrayal of a virt......一起来看看 《Coming of Age in Second Life》 这本书的介绍吧!

CSS 压缩/解压工具
CSS 压缩/解压工具

在线压缩/解压 CSS 代码

RGB HSV 转换
RGB HSV 转换

RGB HSV 互转工具

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具