手撸mini-vue之runtime-core初始化主流程及roll up打包库

132 阅读2分钟

happy path

// index.html
<!DOCTYPE html>
<html lang="en">

<head>
  <style>
    .red {
      color: red;
    }

    .blue {
      color: blue;
    }
  </style>
</head>

<body>
  <div id="app"></div>
  <script src="main.js" type="module"></script>
</body>

</html>
// main.js
import { createApp } from "../../lib/5c24-mini-vue.esm.js";
import { App } from "./App.js";

const rootContainer = document.querySelector("#app");
createApp(App).mount(rootContainer);
// App.js
import { h } from "../../lib/5c24-mini-vue.esm.js";

export const App = {
  render() {
    return h(
      "div",
      {
        id: "root",
        class: ["red", "blue"],
      },
      // "hi, " + this.msg
      // "hi, mini-vue"
      [h("p", { class: "red" }, "hi,"), h("p", { class: "blue" }, "mini-vue")]
    );
  },

  setup() {
    return {
      msg: "mini-vue",
    };
  },
};

以上是一个简单的 happy path image.png 最终要实现的目标是在浏览器渲染出上图内容

runtime-core 初始化

分析:

  • createApp接收一个根组件,return一个对象包含mount方法
  • mount接收一个Element实例,后续所有渲染出来的内部元素都添加到这个Element实例上。在happy path中就是这个idapp的根容器上
  • vue3中所有的东西都会先转换成虚拟节点vnode,后续所有的操作都基于vnode
// createApp.ts
export function createApp(rootComponent) {
  return {
    mount(rootContainer) {
      // component -> vnode
      const vnode = createVNode(rootComponent);
      // render 处理后续的 Component 开箱和 Element 渲染
      render(vnode, rootContainer);
    }
  }
}
// vnode.ts
export function createVNode(type, props?, children?) {
  const vnode = {
    type,
    props,
    children
  };
  return vnode;
}

render

render中调用了patch去处理ComponentElement,之所以调用patch是为了后续的递归处理。

// renderder.ts
export function render(vnode, container) {
  patch(vnode, container);
}

function patch(vnode, container) {
  // 根据 vnode.type 的类型判断做哪个处理
  if(typeof vnode.type === 'string') {
    processElement(vnode, container);
  } else if (isObject(vnode.type)) {
    processComponent(vnode, container);
  }
}
组件处理 processComponent

image.png

// renderer.ts
function processComponent(vnode, container) {
  mountComponent(vnode, container);
}

function mountComponent(vnode, container) {
  // 创建组件实例
  const instance = createComponentInstance(vnode);
  // 处理组件实例中的 props slots 以及处理调用 setup 的返回值,最后设置 render
  setupComponent(instance);
  // 调用组件实例上的 render
  setupRenderEffect(instance, container);
}
setupComponent

image.png

// component.ts
export function createComponentInstance(vnode) {
  const component = {
    vnode,
    type: vnode.type
  };
  return component;
}

export function setupComponent(instance) {
  // 这里先只处理 setup 的返回值和设置 render
  setupStatefulComponent(instance)
}

function setupStatefulComponent(instance) {
  const component = instance.type;
  const { setup } = component;
  if(setup) {
    const setupResult = setup();
    // 处理 setup 返回值
    handleSetupResult(instance, setupResult);
  }
}

function handleSetupResult(instance, setupResult) {
  if(typeof setupResult === "object") {
    instance.setupState = setupResult;
  }
  // 把 render 存到 组件实例的上下文中
  finishComponentSetup(instance);
}

function finishComponentSetup(instance) {
  comst Component = instance.type;
  
  instance.render = Component.render;
}
setupRenderEffect
renderder.ts
function setupRenderEffect(instance, container) {
  const subTree = instance.render();
  // 这里就是上面说到的递归处理 
  // subTree 是 Element 类型就挂载出来,是 Component ,继续进行开箱操作
  patch(subTree, container);
}
Element处理 processElement

image.png

// renderder.ts
function processElement(vnode, container) {
  mountElement(vnode, container);
}

function mountElement(vnode, container) {
  // 创建元素
  const el = document.createElement(vnode.type);
  const { children, props } = vnode;
  
  if(typeof children === 'string') {
    el.textContent = children;
  } else if(Array.isArray(children)) {
    mountChildren(vnode, el)
  }
  
  // 设置元素属性
  for(const key in props) {
    el.setAttribute(key, props[key]);
  }
  
  container.appendChild(el);
}

function mountChildren(vnode, container) {
  const { children } = vnode;
  
  children.forEach(v => {
    patch(v, container);
  })
}

roll up打包库

安装rollup

yarn add rollup --dev

安装rollup能识别typescript的插件

yarn add @rollup/plugin-typescript --dev

创建mini-vue出口

// src/index.ts
export * from "./runtime-core";

创建配置文件rollup.config.js

import pkg from "./package.json";
import typescript from "@rollup/plugin-typescript";
export default {
  input: "./src/index.ts",
  output: [
    {
      format: "cjs",
      file: pkg.main,
    },
    {
      format: "es",
      file: pkg.module,
    },
  ],

  plugins: [typescript()],
};

配置package.json

{
  "main": "lib/5c24-mini-vue.cjs.js",
  "module": "lib/5c24-mini-vue.esm.js",
  "scripts": {
    "test": "jest",
    "build": "rollup -c rollup.config.js"
  },
  "devDependencies": {
    ...
  }
}

源码地址戳这里