Vue3源码学习9——运行时+编译时

102 阅读3分钟

基于之前的运行时runtime渲染器render以及编译器compile,其实Vue3的核心逻辑模块都梳理完成了,最后要做的一步就是整合它们,以实现真实Vue3使用时的样子

实际使用Vue3的时候,大概是这么用的

const { createApp } = Vue

const APP = {
  template: `<div>hello world</div>`
}

const app = createApp(APP)
app.mount('#app')

这里的过程涉及两部分

  • app实例创建
    • 模板template编译render函数
    • 模板template渲染过程
  • app实例挂载

app实例的创建createApp

app实例的创建核心就是createApp方法,这个方法从结果上来看,返回了app实例,并且这个实例上面有mount方法,可以将实例挂载在指定根节点

如果先不考虑编译过程,实际上做的逻辑是下面这样的,即执行了APP这个组件的render函数,并把render渲染出来的内容挂载到app这个根节点

const { createApp, h } = Vue;

const APP = {
  render() {
    return h("div", "hello world");
  },
};

const app = createApp(APP);

app.mount("#app");

阅读Vue3源码可以知道,这个createApp其实调用了createAppAPI方法

createAppAPI方法内部返回了一个createApp方法,构建了app实例并返回

app实例中有mount方法,而mount方法的核心则是创建出vnode并调用render完成渲染

function createAppAPI(render) {
  return function createApp(rootComponent, rootProps = null) {
    const app = {
      _component: rootComponent,
      _container: null,
      // mount方法本质就是创建vnode并调用render渲染
      mount(rootContainer) {
        const vnode = createVNode(rootComponent, rootProps, null);
        render(vnode, rootContainer);
      },
    };
    return app;
  };
}

// 源码中createApp方法和render放一起
function baseCreateRenderer(options: RendererOptions): any {
  ......
  
  return {
    render,
    createApp: createAppAPI(render)
  }
}

导出逻辑

createApp方法的导出逻辑也参考了render方法的,使用ensureRender来导出

export const createApp = (...args) => {
  const app = ensureRenderer().createApp(...args);

  return app;
};

选择器处理

在app实例调用mount方法时候,传入的参数就是要挂载到的根节点,应该是一个DOM容器

但是我们并不一定直接传入一个DOM容器,也可能传入一个DOM选择器,这时候就要对DOM选择器做额外处理

export const createApp = (...args) => {
  const app = ensureRenderer().createApp(...args);

  // 对mount方法做改写,适配不同平台
  const { mount } = app;

  app.mount = (containerOrSelector: Element | string) => {
    const container = normalizeContainer(containerOrSelector);

    if (!container) return;

    mount(container);
  };

  return app;
};

// 传入的可以是选择器或者容器,最后统一返回容器
function normalizeContainer(container: Element | string): Element | null {
  if (isString(container)) {
    const res = document.querySelector(container);
    return res;
  }
  return container;
}

app实例中的模板编译

createApp方法创建好后,最后只缺一步,就是把模板template给编译成render函数

在之前的文章中,我们实现渲染器render和编译器compile分别进行的,二者没有一个联动,所以这里我们只需要建立一个关联就行了

这个关联的地方就在finishComponentSetup函数中

因为这里把组件中的render取出,挂载到实例render上,如果没有render则报错,所以在这里处理一下只有template的情况

function finishComponentSetup(instance) {
  const Component = instance.type;

  if (!instance.render) {
    // 如果有编译器而且没有render方法,那就是要根据模板生成render方法
    if (compile && !Component.render) {
      // 有模板的时候,根据模板生成render函数并挂载到Component上
      if (Component.template) {
        const template = Component.template;
        Component.render = compile(template);
      }
    }

    instance.render = Component.render;
  }

  applyOptions(instance);
}

compile方法在Vue源码中不是直接简单绑定baseCompile的,而是全局创建了一个compile变量,并执行了一个registerRuntimeCompile方法创建

let compile: any = null;

function registerRuntimeCompile(_compile: any) {
  compile = _compile;
}

// 这个compileToFunction就是之前把render函数字符串转换成render函数的方法
function compileToFunction(template, options?) {
  const { code } = compile(template, options);

  const render = new Function(code)();

  return render;
}

// 一旦执行过上面的方法,识别到这个文件,runtime的compile就会注册了
registerRuntimeCompile(compileToFunction);