ssh的博客

Vue中的组件从初始化到挂载经历了什么

January 4, 2020 • ☕️ 5 min read

一个组件从初始化到挂载经历了什么

下面的所有解析都以这段代码为基准:

new Vue({
  el: "#app",
  render: h => h(AppSon)
});

其中 AppSon 就是组件,它是一个对象:

const AppSon = {
  name: "app-son",
  data() {
    return {
      msg: 123
    };
  },
  render(h) {
    return h("span", [this.msg]);
  }
};

这样一段代码,在 Vue 内部组件化的流程顺序:

  1. $createElement,其实 render 接受的参数 h 就是this.$createElement的别名
  2. createElement,做一下参数的整理,就进入下一步
  3. _createElement,比较关键的一步,在这个方法里会判断组件是span这样的 html 标签,还是用户写的自定义组件。
  4. createComponent,生成组件的 vnode,安装一些 vnode 的生命周期,返回 vnode

其实,render 函数最终返回的就是vnode

流程解析

$createElement

调用createElement方法,第一个参数是 vm 实例自身,剩余的参数原封不动的透传。

vm.$createElement = function(a, b, c, d) {
  return createElement(vm, a, b, c, d, true);
};

createElement

function createElement (
  // 上一步传进来的vm实例,在哪个组件的render里调用,context就是哪个组件的实例。
  context,
  // 在例子中,就是AppSon这个对象
  tag,
  // 可以传入props等交给子组件的选项
  data,
  // 子组件中间的内容
  children,
  ...
)

之后有一个判断

if (typeof tag === "string") {
  // html标签流程
} else {
  // 组件化流程
  vnode = createComponent(tag, data, context, children);
}

createComponent接受的四个参数就是上文的方法传进去的

createComponent

function createComponent(
  // 还是上文中的tag,本文中是AppSon对象
  Ctor,
  // 下面的都一致
  data,
  context,
  children
) {
  if (isObject(Ctor)) {
    Ctor = baseCtor.extend(Ctor);
  }

  // 给vnode安装一些生命周期函数(注意这里是vnode的生命周期,而不是created那些组件声明周期)
  installComponentHooks(data);

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

下面有一个逻辑

if (isObject(Ctor)) {
  Ctor = baseCtor.extend(Ctor);
}

其中baseCtor.extend(Ctor)就可以暂时理解为 Vue.extend,这是一个全局共用方法,从名字也可以看出它主要是做一些继承,让子组件的也拥有父组件的一些能力,这个方法返回的是一个新的构造函数。

组件对象最终都会用 extend 这个 api 变成一个组件构造函数,这个构造函数继承了父构造函数 Vue 的一些属性

extend 函数具体做了什么呢?

createComponent / Vue.extend

Vue.extend = function(extendOptions) {
  extendOptions = extendOptions || {};
  // this在这个例子其实就是Vue。
  var Super = this;

  // Appson这个组件的构造函数
  var Sub = function VueComponent(options) {
    // 这个_init就是调用的Vue.prototype._init
    this._init(options);
  };

  // 把Vue.prototype生成一个
  // { __proto__: Vue.prototype }这样的对象,
  // 直接赋值给子组件构造函数的prototype
  // 此时子组件构造函数的原型链上就可以拿到Vue的原型链的属性了
  Sub.prototype = Object.create(Super.prototype);
  Sub.prototype.constructor = Sub;

  // 合并Vue.option上的一些全局配置
  Sub.options = mergeOptions(Super.options, extendOptions);
  Sub["super"] = Super;

  // 拷贝静态函数
  Sub.extend = Super.extend;
  Sub.mixin = Super.mixin;
  Sub.use = Super.use;

  // 返回子组件的构造函数
  return Sub;
};

到了这一步,我们一开始定义的 Appson 组件对象,已经变成了一个函数,可以通过 new AppSon()来生成一个组件实例了,并且组件配置对象被合并到了Sub.options这个构造函数的静态属性上。

createComponent / installComponentHooks

installComponentHooks这个方法是为了给 vnode 上加入一些生命周期函数,

其中有一个init生命周期,这个周期后面被调用的时候再讲解。

createComponent / new VNode

可以看出,主要是生成 vnode 的实例,并且赋值给vnode.componentInstance,并且调用$mount方法挂载 dom 节点,注意这个init生命周期此时还没有调用。

到这为止render的流程就讲完了,现在我们拥有了一个vnode节点,它有一些关键的属性

  1. vnode.componentOptions.Ctor: 上一步extend生成的子组件构造函数。
  2. vnode.data.hook: 里面保存了init等 vnode 生命周期方法
  3. vnode.context: 调用$createElement 的是哪个实例,这个 context 就是谁。

$mount

最外层的组件调用了$mount后,组件在初次渲染的时候其实是递归去调用createElm的,而createElm中会去调用组件 vnode 的init钩子。

if (isDef((i = i.hook)) && isDef((i = i.init))) {
  i(vnode);
}

然后就会走进 vnode 的init生命周期的逻辑

const child = (vnode.componentInstance = createComponentInstanceForVnode(
  vnode,
  activeInstance
));
child.$mount(vnode.elm);

createComponentInstanceForVnode:

createComponentInstanceForVnode (
  vnode: any,
  parent: any,
): Component {
  const options: InternalComponentOptions = {
    // 标记这是一个组件节点
    _isComponent: true,
    // Appson组件的vnode
    _parentVnode: vnode,
    // 当前正在活跃的父组件实例,在本例中就是根Vue实例
    // new Vue({
    //   el: "#app",
    //   render: h => h(AppSon)
    // });
    parent
  }

  return new vnode.componentOptions.Ctor(options)
}

可以看出,最终调用组件构造函数,然后调用\_init 方法,它接受到的 options 不再是

{
  data() {

  },
  props: {

  },
  methods() {

  }
}

这样的传统 Vue 对象了,而是

{
    _isComponent: true,
    _parentVnode: vnode,
    parent,
  }

这样的一个对象,然后_init 内部会针对这样特征的对象,调用initInternalComponent做一些特殊的处理, 这里有一个疑惑点,那刚刚子组件声明的 data 那些选项哪去了呢? 其实是被保存在Ctor.options里了。

然后在initInternalComponent中,把子组件构造函数上保存的 options 再转移到vm.$options.__proto__上。

var opts = (vm.$options = Object.create(vm.constructor.options));

之后生成了子组件的实例后,又会调用child.$mount(vnode.elm),继续的去递归这个初始化的过程。