vue 组件创建挂载过程源码分析

在这里插入图片描述

基本使用:
全局组件和局部组件

<script>
  // 全局组件
  Vue.component("parent-component", {
    template: `<div>我是全局组件</div>`,
  });

  let vm = new Vue({
    el: "#app",
    data() {
      return {
        aa: 1,
      };
    },
    template: `<div id="a">
      hello 这是我自己写的Vue{{aa}}
      <parent-component><parent-component>
      <child-component></child-component>
      </div>`,
    // 局部组件
    components: {
      "child-component": {
        template: `<div>我是局部组件</div>`,
      },
    },
  });
</script>

initGlobalApi
初始化子类构造器、组件注册方法

// src/global-api/index.js

import initExtend from "./initExtend";
import initAssetRegisters from "./assets";

const ASSETS_TYPE = ["component", "directive", "filter"];

export function initGlobalApi(Vue) {
  Vue.options = {}; // 全局的组件 指令 过滤器
  
  ASSETS_TYPE.forEach((type) => {
    Vue.options[type + "s"] = {};
  });
  
  //_base指向Vue
  Vue.options._base = Vue; 
	
	// extend方法定义
  initExtend(Vue); 
  //assets注册方法 包含组件 指令和过滤器
  initAssetRegisters(Vue); 
}

initExtend
初始化Vue.extend

//  src/global-api/initExtend.js

import { mergeOptions } from "../util/index";

export default function initExtend(Vue) {
  let cid = 0; //组件的唯一标识
  // 创建子类继承Vue父类 便于属性扩展
  Vue.extend = function (extendOptions) {
    // 创建子类的构造函数 并且调用初始化方法
    const Sub = function VueComponent(options) {
      this._init(options); //调用Vue初始化方法
    };
    //改变子类的原型
    Sub.cid = cid++;
    Sub.prototype = Object.create(this.prototype); // 子类原型指向父类,即指向Vue
    Sub.prototype.constructor = Sub; //constructor指向自己
    Sub.options = mergeOptions(this.options, extendOptions); //合并自己的options和父类的options
    
    return Sub;
  };
}

initAssetRegisters
创建全局组件的方法

// src/global-api/asset.js

// 初始化全局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
        }
    });
}


  • 调用extend方法为组件创建一个子类构造器,此时的this.options._base代表的就是Vue构造器。它会基于父类去创建一个子类,此时的父类是Vue,并且创建过程子类会继承父类的方法,并会和父类的选项进行合并,最终返回一个子类构造器
  • 代码处还有一个逻辑,Vue.component()默认会把第一个参数作为组件名称,但是如果组件选项有name属性时,name属性值会将组件名覆盖

组件的合并策略
全局组件挂载在 Vue.options.components 上 ,局部组件也定义在自己的 options.components 上面 ,合并时使用到了原型继承的方式来进行组件合并,组件内部优先查找自己局部定义的组件,找不到会向上查找原型中定义的组件

// src/util/index.js

const ASSETS_TYPE = ["component", "directive", "filter"];
// 组件 指令 过滤器的合并策略
function mergeAssets(parentVal, childVal) {
//比如有同名的全局组件和自己定义的局部组件 那么parentVal代表全局组件 自己定义的组件是childVal  首先会查找自已局部组件有就用自己的  没有就从原型继承全局组件  res.__proto__===parentVal
  const res = Object.create(parentVal); 
  if (childVal) {
    for (let k in childVal) {
      res[k] = childVal[k];
    }
  }
  return res;
}

// 定义组件的合并策略
ASSETS_TYPE.forEach((type) => {
  strats[type + "s"] = mergeAssets;
});

创建组件 Vnode
在这里插入图片描述

Vue.component('test', {
  template: '<span></span>'
})
var vm = new Vue({
  el: '#app',
  template: '<div><test></test></div>'
})

父render函数
function() {
  with(this){return _c('div',[_c('test')],1)}
}

  • render函数生成Vnode过程中,子会优先父执行生成Vnode过程,也就是_c(‘test’)函数会先被执行。'test’会先判断是普通的html标签还是组件的占位符。
  • 如果为一般标签,会执行new Vnode过程,这也是上一章节我们分析的过程;如果是组件的占位符,则会在判断组件已经被注册过的前提下进入createComponent创建子组件Vnode的过程。
  • createComponent是创建组件Vnode的过程,创建过程会再次合并选项配置,并安装组件相关的内部钩子(后面文章会再次提到内部钩子的作用),最后通过new Vnode()生成以vue-component开头的Virtual DOM
  • render函数执行过程也是一个循环递归调用创建Vnode的过程,执行3,4步之后,完整的生成了一个包含各个子组件的Vnode tree
// src/util/index.js

//判断是否是对象
export function isObject(data) {
  if (typeof data !== "object" || data == null) {
    return false;
  }
  return true;
}

 //判断是不是常规html标签
export function isReservedTag(tagName) {
  // 定义常见标签
  let str =
    "html,body,base,head,link,meta,style,title," +
    "address,article,aside,footer,header,h1,h2,h3,h4,h5,h6,hgroup,nav,section," +
    "div,dd,dl,dt,figcaption,figure,picture,hr,img,li,main,ol,p,pre,ul," +
    "a,b,abbr,bdi,bdo,br,cite,code,data,dfn,em,i,kbd,mark,q,rp,rt,rtc,ruby," +
    "s,samp,small,span,strong,sub,sup,time,u,var,wbr,area,audio,map,track,video," +
    "embed,object,param,source,canvas,script,noscript,del,ins," +
    "caption,col,colgroup,table,thead,tbody,td,th,tr," +
    "button,datalist,fieldset,form,input,label,legend,meter,optgroup,option," +
    "output,progress,select,textarea," +
    "details,dialog,menu,menuitem,summary," +
    "content,element,shadow,template,blockquote,iframe,tfoot";
  let obj = {};
  str.split(",").forEach((tag) => {
    obj[tag] = true;
  });
  return obj[tagName];
}

createElement

// src/vdom/index.js

import { isObject, isReservedTag } from "../util/index";

// 创建元素vnode 等于render函数里面的 h=>h(App)
// 内部执行将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);
    }
  }
}



resolveAsset判断组件是否已注册并返回组件构造器

// 需要明确组件是否已经被注册
  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
  }

createComponent

  • 拿到注册过的子类构造器后,调用createComponent方法创建子组件Vnode
function createComponent(vm, tag, data, key, children, Ctor) {
	
	//针对局部注册的组件
  if (isObject(Ctor)) {
    //如果没有被改造成构造函数,再调用extend方法
    Ctor = vm.$options._base.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
  • 局部注册添加的是一个子组件的配置对象,而全局注册添加的是一个子类构造器。
  • 因此局部注册中缺少了一步构建子类构造器的过程,这个过程放到createComponent的源码,源码中根据选项是对象还是函数来区分局部和全局注册组件,如果选项的值是对象,则该组件是局部注册的组件,此时在创建子Vnode时会调用 父类的extend方法去创建一个子类构造器

installComponentHooks

  // 组件内部自带钩子
 var componentVNodeHooks = {
    // 组件创建过程的自身初始化方法
    init(vnode) {
      let child = (vnode.componentInstance = new Ctor({ _isComponent: true })); //实例化组件
      child.$mount(hydrating ? vnode.elm : undefined, 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过程的不同阶段执行

渲染组件真实节点

  • 不管是全局注册的组件还是局部注册的组件,组件并没有进行实例化,那么组件实例化的过程发生在哪个阶段呢?我们接着看Vnode tree渲染真实DOM的过程
    在这里插入图片描述

  • 经过vm._render()生成完整的Virtual Dom树后,紧接着执行Vnode渲染真实DOM的过程,这个过程是vm.update()方法的执行,而其核心是vm.patch

  • vm.__patch__内部会通过 createElm去创建真实的DOM元素,期间遇到子Vnode会递归调用createElm方法。

  • 递归调用过程中,判断该节点类型是否为组件类型是通过createComponent方法判断的,该方法和渲染Vnode阶段的方法createComponent不同,他会调用子组件的init初始化钩子函数,并完成组件的DOM插入。

  • init初始化钩子函数的核心是new实例化这个子组件并将子组件进行挂载,实例化子组件的过程又回到合并配置,初始化生命周期,初始化事件中心,初始化渲染的过程。实例挂载又会执行$mount过程。

  • 完成所有子组件的实例化和节点挂载后,最后才回到根节点的挂载。

// src/vdom/patch.js

// patch用来渲染和更新视图
export function patch(oldVnode, vnode) {
  if (!oldVnode) {
    // 组件的创建过程是没有el属性的
    return createElm(vnode);
  } else {
    //   非组件创建过程省略
  }
}

// 判断是否是组件Vnode
function createComponent(vnode) {
  // 初始化组件
  // 创建组件实例
  let i = vnode.data;
  //   下面这句话很关键 调用组件data.hook.init方法进行组件初始化过程 最终组件的vnode.componentInstance.$el就是组件渲染好的真实dom
  if ((i = i.hook) && (i = i.init)) {
  	//调用init方法实例化组件
    i(vnode);
  }
  // 如果组件实例化完毕有componentInstance属性 那证明是组件
  if (vnode.componentInstance) {
    return true;
  }
}

// 虚拟dom转成真实dom
function createElm(vnode) {
  const { tag, data, key, children, text } = vnode;
  //   判断虚拟dom 是元素节点还是文本节点
  if (typeof tag === "string") {
    if (createComponent(vnode)) {
      // 如果是组件 返回真实组件渲染的真实dom
      return vnode.componentInstance.$el;
    }
    //   虚拟dom的el属性指向真实dom 方便后续更新diff算法操作
    // vnode.el = document.createElement(tag);
    // 解析虚拟dom属性
    //updateProperties(vnode);
    // 如果有子节点就递归插入到父节点里面
    //children.forEach((child) => {
      //return vnode.el.appendChild(createElm(child));
    //});
		
	createChildren(vnode, children, insertedVnodeQueue);
	···
	//子节点插入到父节点里面
  	insert(parentElm, vnode.elm, refElm);
  	
  } else {
    //   文本节点
    vnode.el = document.createTextNode(text);
  }
  return vnode.el;
}

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);
  }
}


  • patch过程如果遇到子Vnode,会优先实例化子组件,并且执行子组件的挂载流程,而挂载流程又会回到_render,_update的过程。在所有的子Vnode递归挂载后,最终才会真正挂载根节点

建立组件联系

  • Vue在组件和组件之间建立了一层关联

在初始化实例阶段调用initLifecycle

function initLifecycle (vm) {
    var options = vm.$options;
    // 子组件注册时,会把父组件的实例挂载到自身选项的parent上
    var parent = options.parent;
    // 如果是子组件,并且该组件不是抽象组件时,将该组件的实例添加到父组件的$parent属性上
    //如果父组件是抽象组件,则一直往上层寻找,直到该父级组件不是抽象组件,并将,将该组件的实例添加到父组件的$parent属性
    if (parent && !options.abstract) {
        while (parent.$options.abstract && parent.$parent) {
        parent = parent.$parent;
        }
        parent.$children.push(vm);
    }
    // 将自身的$parent属性指向父实例。
    vm.$parent = parent;
    vm.$root = parent ? parent.$root : vm;

    vm.$children = [];
    vm.$refs = {};

    vm._watcher = null;
    vm._inactive = null;
    vm._directInactive = false;
    // 该实例是否挂载
    vm._isMounted = false;
    // 该实例是否被销毁
    vm._isDestroyed = false;
    // 该实例是否正在被销毁
    vm._isBeingDestroyed = false;
}


  • 在vue中有很多内置的抽象组件,例如<keep-alive>,<slot>等,这些抽象组件并不会出现在子父级的路径上,并且它们也不会参与DOM的渲染。

总结

  • 全局组件需要进行全局注册,核心方法是Vue.component,他需要在根组件实例化前进行声明注册,原因是我们需要在实例化前拿到组件的配置信息并合并到options.components选项中。注册的本质是调用extend创建一个子类构造器,全局和局部的不同是局部创建子类构造器是发生在创建子组件Vnode阶段的createComponent中
  • 而创建子Vnode阶段最关键的一步是定义了很多内部使用的钩子
  • 有了一个完整的Vnode tree接下来会进入真正DOM的生成,在这个阶段如果遇到子组件Vnode会进行子构造器的实例化,并完成子组件的挂载
  • 递归完成子组件的挂载后,最终才又回到根组件的挂载。
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值