一个以 Vue3 思想为蓝本的最小实现,覆盖响应式系统、运行时(核心与 DOM)、组件系统、调度与部分编译器能力。项目采用 monorepo 结构(pnpm workspace),用 esbuild 做开发期打包与 watch。
- 响应式系统(packages/reactivity)
reactive/ref/computed/effect/watch/watchEffect- 依赖收集与触发:WeakMap(target→Map(key→dep)),双向记忆(dep ↔ effect)优化
- 计算属性懒执行与脏值标记(
DirtyLevels),proxyRefs自动脱 ref
- 运行时核心(packages/runtime-core)
- 虚拟节点与
h、createVnode、Fragment/Text、ShapeFlags - 组件实例/
setup/props/attrs/slots/emit/expose - 生命周期:
onBeforeMount/onMounted/onBeforeUpdate/onUpdated provide/inject- 异步组件:
defineAsyncComponent(支持loader+delay+timeout+error/loading组件与onError(retry/fail)) - 内置组件:
Teleport、Transition - 渲染与 Diff:带
key的子节点比较 + 最长递增子序列(LIS)移动优化 - Block/动态节点:
openBlock/createElementBlock/patchFlag(支持TEXT/CLASS/STYLE等) - 微任务调度:
queueJob(去重 + Promise 微任务批量刷新)
- 虚拟节点与
- 运行时 DOM(packages/runtime-dom)
nodeOps封装 DOM 增删改查patchProp针对class/style/事件/普通属性分别处理- 事件采用 invoker 机制,减少解绑/重绑成本
- 编译器核心(packages/compiler-core)
parse:模板 → AST(支持元素、文本、插值{{}},保留位置信息loc)transform:表达式前缀_ctx.、TEXT + INTERPOLATION 合并为COMPOUND_EXPRESSION、生成VNODE_CALLcodegen:输出render(_ctx){ return createElementVNode(...) }字符串代码,自动注入 helpers(如toDisplayString)
- 公共工具(packages/shared)
ShapeFlags/PatchFlags及常用类型守卫
- packages/shared:通用工具与位运算标识
- packages/reactivity:响应式核心与 watch 系列
- packages/runtime-core:平台无关渲染器、组件系统、Diff、内置组件
- packages/runtime-dom:DOM 平台适配(nodeOps + patchProp 模块化)
- packages/compiler-core:解析/转换/代码生成
- script/dev.js:基于 esbuild 的按包打包与 watch 脚本
- 环境:Node 18+,pnpm(本项目记录为
pnpm@10.17.0) - 安装依赖:
pnpm i
- 开发打包(默认打包 compiler-core,为 ESM):
pnpm dev # 等价于:node script/dev.js compiler-core -f esm - 指定打包目标与格式(常用:esm/global/cjs):
# 运行时(DOM) node script/dev.js runtime-dom -f esm node script/dev.js runtime-dom -f global # 生成 UMD,全局名见各包 package.json 的 buildOptions.name # 响应式 node script/dev.js reactivity -f esm # 运行时核心 node script/dev.js runtime-core -f esm # 编译器核心 node script/dev.js compiler-core -f esm
提示:脚本会输出到 packages/<target>/dist/<target>.js,并处于 watch 模式。
-
在浏览器用运行时渲染(global 构建后):
<!-- 先执行:node script/dev.js runtime-dom -f global --> <div id="app"></div> <script src="./packages/runtime-dom/dist/runtime-dom.js"></script> <script> const { h, render, ref } = RuntimeDom const count = ref(0) const vnode = h('button', { onClick(){ count.value++ } }, 'count: ' + count.value) render(vnode, document.getElementById('app')) // 简单更新:再次 render 新 vnode(本项目演示用) setInterval(() => { render(h('button', { onClick(){ count.value++ } }, 'count: ' + count.value), document.getElementById('app')) }, 1000) </script>
-
使用编译器把模板编译为渲染函数代码:
import { compile } from './packages/compiler-core/src/index' const code = compile('<div>hello {{name}}</div>') console.log(code) // 输出形如: // const {toDisplayString:_toDisplayString,createElementVNode:_createElementVNode} = Vue // return function render(_ctx) { // return _createElementVNode("div", null, _toDisplayString(_ctx.name)) // }
-
异步组件/Teleport/Transition(API 与 Vue3 类似,适合在 runtime 层做交互演示),可参考:
packages/runtime-core/src/defineAsyncComponent.tspackages/runtime-core/src/components/Teleport.tspackages/runtime-core/src/components/Transition.ts
- 响应式
- 依赖收集:
track(target,key)→targetMap[target].get(key)→dep(Map);trackEffect(effect, dep)双向关联并按次序清理失效依赖 - 触发更新:
trigger(target,key)→triggerEffects(dep),计算属性通过dirty标记懒求值 - watch:统一走
doWatch,支持deep、immediate、onCleanup
- 依赖收集:
- 渲染与 Diff
patch按type/shapeFlag分派:元素/组件/Fragment/Text/Teleport- 儿子比较:头尾指针 + 中间映射 + LIS 移动优化
- Block/动态节点:
openBlock/createElementBlock/setupBlock收集dynamicChildren,结合PatchFlags做定向更新(如只更TEXT/CLASS/STYLE) - 事件:invoker 复用函数引用,减少 add/removeEventListener 次数
- 组件系统
- 实例化:
createComponentInstance,上下文代理访问data/props/setupState/$attrs/$slots setup(props, { slots, attrs, emit, expose });proxyRefs让setupState/template使用更自然- 生命周期执行前后通过
currentInstance校准上下文 provide/inject通过原型链式provides继承 + 首次写时拷贝
- 实例化:
- 编译器
parse记录loc位置;transformExpression给插值加_ctx.前缀transformText合并相邻 TEXT/INTERPOLATION,必要时包一层TEXT_CALLcodegen输出 helpers 导入前导码 +render函数体
- 编译器仅覆盖最小子集:元素、文本、插值与基础文本合并;属性/指令/条件/循环等尚未实现
patchProps目前只处理class/style/事件/通用 attr,不含复杂 DOM Props- 组件更新判断
shouldComponentUpdate和props/slots逻辑为教学取舍,未覆盖所有边界 - LIS 实现(
packages/runtime-core/src/seq.ts)里二分右边界使用了length(未定义),应为result.length;在复杂 keyed diff 下可能影响顺序优化 - demo 代码为演示性质,实际项目需配合 bundler(Vite/Rollup/Webpack)与 TS 构建
- 开发脚本:
script/dev.js(esbuild watch)- 入口:
packages/<target>/src/index.ts - 输出:
packages/<target>/dist/<target>.js -f指定打包格式;全局名取自各包package.json.buildOptions.name
- 入口:
本项目用于学习与实验,未附带正式开源许可证。请勿用于生产环境。