vue3 中把runtime-coreruntime-dom拆分开了,意义很明显,core 是跨平台的,比如ssrdom环境都是用的同一份 core,这个呢,说学习 react 也不过分, 毕竟 react 几年前就把 react 和 react-dom 拆分了。既然进行了拆分,那么自然我们要关心一下他们之间是如何合作的,这篇文章就是来做这个事情的。

我们先跟着代码进入:

createApp(App).mount("#app");

这个createApp来自runtime-dom,我们看一下他做了什么:

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

  const { mount } = app;
  app.mount = (containerOrSelector: Element | string): any => {
    // ...
  };

  return app;
}) as CreateAppFunction<Element>;

他调用了ensureRenderer方法,

function ensureRenderer() {
  return (
    renderer || (renderer = createRenderer<Node, Element>(rendererOptions))
  );
}

这里调用了createRenderer,这个方法则来自runtime-core

export function createRenderer<
  HostNode = RendererNode,
  HostElement = RendererElement
>(options: RendererOptions<HostNode, HostElement>) {
  return baseCreateRenderer<HostNode, HostElement>(options);
}

function baseCreateRenderer(
  options: RendererOptions,
  createHydrationFns?: typeof createHydrationFunctions
): any {
  // ... 2000行代码
  return {
    render,
    hydrate,
    createApp: createAppAPI(render, hydrate),
  };
}

好了到这里我们的 renderer 就创建完了,那么为什么要绕这么大个圈子来创建呢?重点在于调用createRenderer时传入的参数:

const rendererOptions = extend({ patchProp, forcePatchProp }, nodeOps);
createRenderer < Node, Element > rendererOptions;

rendererOptions是啥?

export const nodeOps: Omit<RendererOptions<Node, Element>, "patchProp"> = {
  insert: (child, parent, anchor) => {
    parent.insertBefore(child, anchor || null);
  },
  remove,
  createElement,
  createText,
  // ...
};

看上去是不是很熟悉,没错,这是一个 DOM API 操作的上层封装。而在 vue 的生态里面,我们称之为平台特性。未来我们需要在别的平台上运行 vue 的时候,比如可能出现的vue-native(别跟我提 weex,阿里自己人说的已经不维护了),只需要吧这里的nodeOps里面的方法按照平台逻辑挨个实现就可以创建一个renderer

这也是现在这些前端框架都如此设计的最大理由。

sequenceDiagram
dom->>core: createRenderer, I am dom
core->>dom: ok

当然这肯定不是 vue-dom 最主要的部分,实际上你用到的大部分 API 都是来自平台相关的包的,这一点 react 也是一样的。在 vue 里面的体现就是你用到的:

  • directives
  • v-model
  • transition

等都是来自于 dom 包,这些会在后续讲相关话题的时候再讲到。

知道 renderer 是如何创建之后,我们再来看看createApp,在上面的代码里面我们看到createApp是通过createAppAPI创建的:

return {
  render,
  hydrate,
  createApp: createAppAPI(render, hydrate),
};

那么就看看createAppAPI

export function createAppAPI<HostElement>(
  render: RootRenderFunction,
  hydrate?: RootHydrateFunction
): CreateAppFunction<HostElement> {
  return function createApp(rootComponent, rootProps = null) {
    const context = createAppContext();
    const installedPlugins = new Set();

    let isMounted = false;

    const app: App = {
      _component: rootComponent as Component,
      _props: rootProps,
      _container: null,
      _context: context,

      version,

      get config() {
        return context.config;
      },

      use() {},
      mixin() {},
      component() {},
      // ...
    };
  };
}

是不是又有了熟悉的感觉的,是的,像ues\mixin\component这些 API 在 vue2 中都是直接在Vue上面的,而到了 vue3 里面,则是挂在我们创建的app上面了。这么做有很多好处,在 vue 的 rfcs 里面就有写到:

Some of Vue's current global API and configurations permanently mutate global state. This leads to a few problems:

  • Global configuration makes it easy to accidentally pollute other test cases during testing. Users need to carefully store original global configuration and restore it after each test (e.g. resetting Vue.config.errorHandler). Some APIs (e.g. Vue.use, Vue.mixin) don't even have a way to revert their effects. This makes tests involving plugins particularly tricky.
    • vue-test-utils has to implement a special API createLocalVue to deal with this
  • This also makes it difficult to share the same copy of Vue between multiple "apps" on the same page, but with different global configurations

一方面是对于测试的时候不同的测试用例之间可能会相互修改全局配置。另一方面如果我们在一个页面中存在着两个不同的 vue 应用,那么很可能因为他们需要的全局配置不同而导致一系列问题。

所以对于这些 API 的实现,我们都可以在这里找到:runtime-core/apiCreateApp.ts。我想到这里我们应该也都可以理解所谓的 vue app 是什么了吧,有什么问题都可以根据下面的联系方式一起讨论哦。