Vue 3源码深度解析之:`Vue`的`runtime-dom`和`runtime-core`:它们在构建时的解耦。

同学们,老规矩,先来个灵魂拷问:你们有没有好奇过,Vue 3 既能跑在浏览器里,又能跑在 Node.js 环境下?这背后隐藏着什么黑魔法?今天,咱们就来扒一扒 Vue 3 的 runtime-domruntime-core 这对好基友,看看它们是如何在构建时实现“你侬我侬,又保持距离”的解耦的。

一、开胃小菜:为什么要解耦?

想象一下,如果你把所有代码都塞到一个文件里,那维护起来简直就是一场噩梦。Vue 3 这么庞大的框架,更是如此。解耦,就是把不同的功能模块分开,让它们各司其职,互不干扰。

具体到 runtime-domruntime-core,它们解耦的原因主要有以下几点:

  • 跨平台性: runtime-core 负责核心的渲染逻辑,不依赖任何特定的平台 API。而 runtime-dom 则负责与浏览器 DOM API 打交道。这样,只要替换不同的 runtime,Vue 就能运行在不同的平台。
  • 可维护性: 核心逻辑和平台相关的逻辑分开,修改起来更方便,也更不容易出错。
  • 可测试性: 核心逻辑可以单独进行单元测试,而不需要依赖浏览器环境。

二、正餐:runtime-core 核心渲染逻辑

runtime-core 是 Vue 3 的灵魂所在,它包含了组件的创建、更新、渲染、生命周期管理等核心逻辑。它就像一个抽象的渲染器,不关心最终渲染到哪里,只负责计算出需要渲染的内容。

我们先来看一段简单的 runtime-core 代码片段,模拟一个简单的组件渲染:

// runtime-core/renderer.ts (简化版)

import { createVNode, isVNode } from './vnode';
import { effect } from '@vue/reactivity';
import { invokeArrayFns } from './helpers/index';

export function createRenderer(options) {
  const {
    createElement: hostCreateElement,
    patchProp: hostPatchProp,
    insert: hostInsert,
    remove: hostRemove,
    setElementText: hostSetElementText,
  } = options;

  const patch = (n1, n2, container, anchor = null) => {
    // n1 存在,说明是更新
    if (n1) {
      // TODO: Diffing 算法
      updateElement(n1, n2);
    } else {
      // n1 不存在,说明是初次渲染
      processElement(n2, container, anchor);
    }
  };

  const processElement = (vnode, container, anchor) => {
    mountElement(vnode, container, anchor);
  };

  const mountElement = (vnode, container, anchor) => {
    const { type, props, children } = vnode;
    const el = (vnode.el = hostCreateElement(type)); // 调用平台相关的 createElement

    if (props) {
      for (const key in props) {
        const val = props[key];
        hostPatchProp(el, key, null, val); // 调用平台相关的 patchProp
      }
    }

    if (Array.isArray(children)) {
      mountChildren(children, el, anchor);
    } else if (typeof children === 'string') {
      hostSetElementText(el, children); // 调用平台相关的 setElementText
    }

    hostInsert(el, container, anchor); // 调用平台相关的 insert
  };

  const mountChildren = (children, container, anchor) => {
    children.forEach((child) => {
      child = createVNode(child);
      patch(null, child, container, anchor);
    });
  };

    const updateElement = (n1, n2) => {
      const el = (n2.el = n1.el);
      const oldProps = n1.props || {};
      const newProps = n2.props || {};

      updateProps(el, newProps, oldProps);
    }

    const updateProps = (el, newProps, oldProps) => {
        // 处理新的属性
        for (const key in newProps) {
            if(newProps[key] !== oldProps[key]){
                hostPatchProp(el, key, oldProps[key], newProps[key]);
            }
        }

        // 处理旧的属性
        for(const key in oldProps){
            if(!(key in newProps)){
                hostPatchProp(el, key, oldProps[key], null);
            }
        }
    }

  const unmount = (vnode) => {
      hostRemove(vnode.el);
  }

  const render = (vnode, container) => {
    if (vnode) {
      patch(null, vnode, container);
    } else {
      // 如果 vnode 为 null,说明是卸载
      container.innerHTML = '';
    }
  };

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

function createAppAPI(render) {
  return function createApp(rootComponent) {
    return {
      mount(rootContainer) {
        // 先转换成 vnode
        const vnode = createVNode(rootComponent);

        render(vnode, rootContainer);
      },
    };
  };
}

这段代码的核心是 createRenderer 函数,它接受一个 options 对象,这个 options 对象包含了与平台相关的 API,比如 createElementpatchPropinsert 等。createRenderer 函数返回一个 render 函数,这个 render 函数负责将虚拟 DOM 渲染到指定的容器中。

注意,这段代码中,所有的平台相关的 API 都来自于 options 对象,runtime-core 本身并不依赖任何特定的平台 API。

三、佐餐:runtime-dom 与浏览器 DOM API 的亲密接触

runtime-dom 负责与浏览器 DOM API 打交道,它实现了 runtime-core 中定义的平台相关的 API。它就像一个翻译器,将 runtime-core 的指令翻译成浏览器能够理解的 DOM 操作。

我们来看一段简单的 runtime-dom 代码片段:

// runtime-dom/index.ts

import { createRenderer } from '@vue/runtime-core';

function createElement(type) {
  console.log('createElement----');
  return document.createElement(type);
}

function patchProp(el, key, prevVal, nextVal) {
  console.log('patchProp----');
  if (key === 'class') {
    el.className = nextVal || '';
  } else if (key === 'style') {
    if (!nextVal) {
      el.removeAttribute('style');
    } else {
      for (const styleName in nextVal) {
        el.style[styleName] = nextVal[styleName];
      }
    }
  } else if (/^on[A-Z]/.test(key)) {
    const event = key.slice(2).toLowerCase();
    if(prevVal){
        el.removeEventListener(event, prevVal);
    }

    if(nextVal){
        el.addEventListener(event, nextVal);
    }

  }
  else {
    if (nextVal === null || nextVal === undefined) {
      el.removeAttribute(key);
    } else {
      el.setAttribute(key, nextVal);
    }
  }
}

function insert(el, parent, anchor = null) {
  console.log('insert----');
  parent.insertBefore(el, anchor);
}

function remove(el){
    const parent = el.parentNode;
    if(parent){
        parent.removeChild(el);
    }
}

function setElementText(el, text) {
  console.log('setElementText----');
  el.textContent = text;
}

const rendererOptions = {
  createElement,
  patchProp,
  insert,
  remove,
  setElementText,
};

// createApp
export function createApp(rootComponent) {
  return createRenderer(rendererOptions).createApp(rootComponent);
}

export * from '@vue/runtime-core';

这段代码实现了 runtime-core 中定义的 createElementpatchPropinsert 等 API,它们都直接调用了浏览器 DOM API。

四、构建时的解耦魔法:options 对象

现在,我们来揭秘 Vue 3 是如何实现构建时解耦的。关键就在于 createRenderer 函数接受的 options 对象。

在构建时,Vue 3 会根据不同的平台,生成不同的 options 对象。比如,在浏览器环境下,会生成 runtime-dom 中定义的 options 对象;而在 Node.js 环境下,会生成与 Node.js 相关的 options 对象。

然后,Vue 3 会将生成的 options 对象传递给 createRenderer 函数,从而生成不同的渲染器。

我们可以用一个表格来总结一下:

模块 功能 依赖平台 API
runtime-core 核心渲染逻辑,组件创建、更新、渲染等
runtime-dom 与浏览器 DOM API 打交道

五、饭后甜点:代码示例

为了更好地理解 runtime-domruntime-core 的解耦,我们来看一个完整的代码示例:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Vue 3 Runtime DOM Example</title>
</head>
<body>
  <div id="app"></div>
  <script type="module">
    import { createApp, h, ref, computed } from './runtime-dom/index.ts';

    const App = {
      setup() {
        const count = ref(0);
        const message = computed(() => `Count is: ${count.value}`);

        const increment = () => {
          count.value++;
        };

        return {
          count,
          message,
          increment,
        };
      },
      render() {
        return h('div', { id: 'my-app' }, [
          h('h1', null, this.message),
          h('button', { onClick: this.increment }, `Increment (${this.count})`),
        ]);
      },
    };

    const app = createApp(App);
    app.mount('#app');
  </script>
  <script>
    // 引入之前实现的 runtime-dom
    // import { createApp, h, ref, computed } from './runtime-dom/index.ts';
  </script>
</body>
</html>

在这个示例中,我们使用了 runtime-dom 中提供的 createApphrefcomputed 等 API,创建了一个简单的 Vue 应用,并将它渲染到 idapp 的 DOM 元素中。

六、加餐:深入源码

如果你想更深入地了解 runtime-domruntime-core 的解耦,可以去 Vue 3 的源码中一探究竟。

  • packages/runtime-core/src/ 目录包含了 runtime-core 的所有代码。
  • packages/runtime-dom/src/ 目录包含了 runtime-dom 的所有代码。

七、总结

今天,我们一起学习了 Vue 3 的 runtime-domruntime-core 的解耦。通过 options 对象,Vue 3 实现了核心渲染逻辑与平台相关的 API 的分离,从而实现了跨平台性、可维护性和可测试性。

希望今天的课程能帮助你更好地理解 Vue 3 的架构,并在实际开发中更加得心应手。

好啦,今天的讲座就到这里,大家有什么问题吗?

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注