Vue源码阅读——从单文件组件到dom渲染

导入一个单文件组件并用 console.log 输出,可以看到就是导入了如下这样的对象:

2TBXTO.png

渲染单文件组件:

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

h 函数就是$createElement函数,$createElement 函数的第一个参数应该是 tag,也就是渲染单文件组件时,传入的 tag 是一个如上图的对象,$createElement 的处理也很直接,如果 tag 不是 string 类型的话,直接去 createComponent:

if (typeof tag === 'string') {
  //...
} else {
  vnode = createComponent(tag, data, context, children); //主要关注tag参数
}

通过赋值的变量名知道,createComponent 是返回了一个 vnode 对象,看一下具体处理:

function createComponent(Ctor, data, context, children) {
  //Ctor对应调用时传入的tag
  const baseCtor = context.$options._base; //在initGlobalAPI时被赋值为Vue实例

  if (isObject(Ctor)) {
    Ctor = baseCtor.extend(Ctor); //创建Vue的子类
  }
  data = data || {};
  installComponentHooks(data);
  return new VNode(
    `vue-component-${Ctor.cid}`,
    data,
    undefined,
    undefined,
    undefined,
    context,
    {
      Ctor,
      /*其他*/
    },
  );
}

extend 函数是创建 Vue 的子类,创建 VNode 实例时第七个参数是 componentOptions,将这个子类包含进去了,之后会用到,这里先知道 createComponent 实际就是返回了一个 vnode,可以理解为这里生成vnode是一个组件的占位vnode。

生成 vnode 后,下一步就是去 patch,看下面的代码(简化后)

function (oldVnode, vnode) {
    const insertedVnodeQueue = []
    if (isUndef(oldVnode)) {
      createElm(vnode, insertedVnodeQueue)
    } else {
      const isRealElement = isDef(oldVnode.nodeType)
      if (isRealElement) {
        oldVnode = emptyNodeAt(oldVnode)
      }
      const oldElm = oldVnode.elm;
      const parentElm = nodeOps.parentNode(oldElm)
      createElm(vnode, insertedVnodeQueue, parentElm, nodeOps.nextSibling(oldElm))

      if (isDef(parentElm)) {
        removeVnodes([oldVnode], 0, 0)
      }
    }
    return vnode.elm
  }

第一次挂载,这里的 oldVnode 就是#app 元素,它是一个真实的 dom 节点,会创建一个空的 vnode,然后执行 createElm:

function createElm(vnode, insertedVnodeQueue, parentElm, refElm) {
  if (createComponent(vnode, parentElm, refElm)) {
    return;
  }
}
function createComponent(vnode, parentElm, refElm) {
  let i = vnode.data;
  if (isDef(i)) {
    if (isDef((i = i.hook)) && isDef((i = i.init))) {
      i(vnode);
      if (vnode.componentInstance) {
        initComponent(vnode);
        insert(parentElm, vnode.elm, refElm);
      }
      return true;
    }
  }
}
function initComponent(vnode) {
    vnode.elm = vnode.componentInstance.$el
}

i.init方法:

const componentVNodeHooks = {
  init(vnode) {
    const child = vnode.componentInstance = new vnode.componentOptions.Ctor({
      _isComponent: true,
      _parentVnode: vnode,
      parent
    })
    child.$mount();
    return child;
  }
}

上述都是简化后的代码,步骤就是根据上面使用extend创建Vue子类创建组件实例,并调用$mount挂载。

挂载的过程首先会调用render生成vnode,这次调用render,将会调用组件真正的render,获取组件的vnode树,因为是第一次挂载,patch的过程中直接生成dom节点,子节点插入父节点,组成dom树,patch方法最终返回的vnode.elm,就是真实的dom树,它将被复制给组件实例的$el属性,组件实例的$el属性又会在组件挂载结束后被赋值给组件占位vnode的elm属性,最终插入#app节点,完成组件的渲染