Skip to content
📈0️⃣

Vue3 mount 挂载实例

本节实现内容

  • mount 挂载实例
  • 组件模版内容替换
  • 下一节实现 ref 响应式

实现效果

  • 实现 mount 挂载实例
  • 实现组件模版内容替换
  • 实现 ref 响应式

效果展示

实现思路

  • 实现 mount 挂载实例
    • 创建 Vue 实例
    • 解析模版
    • 替换模版
    • 渲染模版
  • 实现组件模版内容替换
    • 实现大胡子语法解析器,解析模版内容,替换成对应的 HTML 结构
  • 实现 ref 响应式
    • 实现 ref 响应式
    • 实现 ref 依赖收集
    • 实现 ref 依赖更新

源码目录结构

新建文件夹 simple-vue ,然后新建如下文件:

bash
├── compiler # 编译相关
├── core # 核心代码
   ├── util # 工具方法
├── index.js # 入口文件

源码实现

  • index.js 文件内容用于创建 Vue3 实例
  • compiler 简易版编译器, 用来解析 Vue 文件
    1. 读取 App.vue 文件内容
    2. 实现大胡子模版语法的填充
    3. 实现 v-model 指令的填充(待实现...)
  • core/util.js 工具方法
    1. nextTick 方法, 强制代码在下个宏任务执行, 避免获取不到 dom 元素
  • core/observer.js 响应式: 核心 2. observer 响应式: 核心方法 { ref() computed() reactive() readonly() }

demo 文件结构如下

bash
├── App.vue # 简易版组件
├── index.html # index.html
├── main.js # 简易版Vue3实例
├── simple-vue
   ├── core
   ├── compiler # 编译相关
   └── utile # 核心代码
   ├── index.js # 入口文件

demo 代码

html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>simple-vue.demo1</title>
  </head>
  <body>
    <div id="app"></div>
    <script type="module" src="./main.js"></script>
  </body>
</html>
js
// 导入 simpleVue
import { createApp } from "./simple-vue/index.js";
// 导入 App.vue 根组件, 这里没有用脚手架工具, 直接用fetch
const App = fetch("./App.vue").then((res) => res.text());
// 创建实例并挂载
createApp(App).mount("#app");
vue
<script setup>
import { ref } from "vue";
const pname = ref("张三");
const age = ref(18);
function changepname() {
  pname.value = ["李四", "王五", "赵六"][Math.floor(Math.random() * 3)];
}
function changeAge() {
  age.value++;
}
</script>

<template>
  <div class="app-container">
    <span>{{ pname }}</span>
    <span>{{ age }}</span>
    <button onclick="changepname()">修改名称</button>
    <button onclick="changeAge()">修改年龄</button>
  </div>
</template>

<style>
.app-container {
  padding: 20px;
  border: 1px solid silver;
}
</style>

demo/simple 代码:

js
import compiler from "./compiler.js";
import { nextTick } from "./core/util.js";

// 创建 Vue3 实例, 传入一个 <script setup> 模版组件 App.vue
export const createApp = (props) => {
  // 返回一个 app 根组件, 挂载组件
  return {
    async mount(el) {
      const appStr = await props;
      await nextTick();
      const vm = document.querySelector(el);
      compiler(appStr, vm);
    },
  };
};
js
/**
 * 简易版编译器, 用来解析 Vue 文件
 * 它把 Vue 文件拆分成 template、script、style 三部分
 * template: 通过 updateTemplate() 函数更新 dom
 * script: 通过 setScript() 添加到 html 文件中
 * style: 通过 setStyle() 添加到 html 文件中
 */
import { nextTick } from "./core/util.js";
import { ref } from "./core/observer.js";
window.$ref = ref;

// 解析模版数据
export const APP_DATA = {
  vm: null,
  templateStr: "",
  scriptStr: "",
  styleStr: "",
  proxyMap: {},
};

// 读取 vue 组件内容并提取 template、script、style 对应的内容
const compilerVue = (param, vm) => {
  const getContent = (regex) => {
    const matches = regex.exec(param);
    if (matches && matches.length > 1) {
      return matches[1].trim();
    }
    return "";
  };
  const templateStr = getContent(/<template>([\s\S]+)<\/template>/);
  const scriptStr = getContent(/<script setup>([\s\S]+)<\/script>/);
  const styleStr = getContent(/<style>([\s\S]+)<\/style>/);
  // 给 script 中的 ref 方法添加变量参数, 让变量名和值一一对应
  const setRefFunc = (scriptStr) => {
    const regex = /const\s+(\w+)\s*=\s*ref\([^\)]*(?=\))/g;
    return scriptStr.replace(regex, ($0, $1) => {
      return `${$0}, '${$1}'`;
    });
  };
  return Object.assign(APP_DATA, {
    vm,
    templateStr,
    scriptStr: setRefFunc(scriptStr),
    styleStr,
  });
};

/**
 * 1. 提取 script 中的 import 导入代码, 替换成 ref 方法;
 * 2. 把 script 添加到 head
 * 3. 更新 script 后, 会初始化 window.$ref(), 通过 ref 代理对象触发 updateTemplate() 函数更新 dom
 */
export const setScript = ({ scriptStr }) => {
  if (!scriptStr) return;
  const regex = /import\s+{.*ref.*}\s+.*;/g;
  const str = scriptStr.replace(regex, "const ref = window.$ref;");
  const vScript = document.querySelector("head #app-script");
  if (vScript) {
    vScript.innerHTML = str;
  } else {
    const script = document.createElement("script");
    script.id = "app-script";
    script.innerHTML = str;
    document.querySelector("head").append(script);
  }
};

// 把组件中的 style 添加到 head
export const setStyle = async ({ styleStr }) => {
  const vStyle = document.querySelector("head #app-style");
  if (vStyle) {
    vStyle.innerHTML = styleStr;
  } else {
    const style = document.createElement("style");
    style.id = "app-style";
    style.innerHTML = styleStr;
    document.querySelector("head").append(style);
  }
};

// 1. 更新 template 模板中的内容; 2. 把组件中的 html 标签添加到 dom 挂载点
export const updateTemplate = async ({ templateStr, vm, proxyMap }) => {
  if (!templateStr) return;
  await nextTick();
  let str = templateStr;
  Object.entries(proxyMap).forEach(([key, value]) => {
    const reg = new RegExp(`{{ ${key} }}`, "g");
    str = str.replace(reg, value);
  });
  vm.innerHTML = str;
};

export const trigger = (key, value) => {
  console.log("trigger", key, value);
  APP_DATA.proxyMap[key] = value;
  return updateTemplate(APP_DATA);
};

const init = (param, vm) => {
  const data = compilerVue(param, vm);
  setScript(data); // 更新 script 后, 触发 updateTemplate() 函数更新 dom
  setStyle(data);
};

export default init;
js
/**
 * 响应式: 核心
 * ref() computed() reactive() readonly()
 * watchEffect() watchPostEffect() watchSyncEffect() watch()
 */

import { trigger } from "./../compiler.js";

export const ref = (value, key) => {
  trigger(key, value);
  const target = { value, key }; // 目标对象
  const handler = {
    get(target, propKey, receiver) {
      return Reflect.get(target, propKey, receiver);
    },
    set(target, propKey, value, receiver) {
      trigger(key, value);
      return Reflect.set(target, propKey, value, receiver);
    },
  }; // 处理器对象
  return new Proxy(target, handler); // 创建代理对象
};
js
// 简易版, 强制代码在下个宏任务执行, 避免获取不到 dom 元素
export const nextTick = (callback) => {
  return new Promise((resolve) => {
    setTimeout(() => {
      callback && callback();
      resolve();
    }, 0);
  });
};

查看地址

http://vue.zichin.com/demo1/index.html