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 文件
- 读取 App.vue 文件内容
- 实现大胡子模版语法的填充
- 实现 v-model 指令的填充(待实现...)
- core/util.js 工具方法
- 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);
});
};