手把手搭建Vue轮子从0到1:1. 前期准备

176 阅读9分钟

产出:Vue3源码库 MVP 版~ vue-mini

Vue3核心模块

  1. reactivity:响应性
  2. runtime:运行时
  3. compiler:编译器

vue3源码GitHub:github.com/vuejs/core

www.yuque.com/g/daiyubuzh… 《Vue3源码》

1. 编程范式:声明式、命令式

区别:

  • 声明式关注“如何做”,需手动控制每一步操作流程(关注过程 - 详细逻辑与步骤)。
  • 命令式关注“做什么”,描述目标状态而非具体操作(关注结果 - 过程隐藏)。
维度命令式声明式
关注点如何实现(步骤)目标状态(结果)
代码量较多(需手动操作)较少(框架自动化)
维护性(对代码方便的:阅读、修改、删除、增加)低(易产生冗余代码)高(逻辑与视图解耦)
性能(耗时越少,性能越强)11+n
命令式 > 声明式
典型框架jQuery、原生JSVue、React、ArkUI

示例:

  1. 命令式编程
// 命令式:手动创建元素、设置属性、绑定事件
const button = document.createElement('button');
button.textContent = '点击我';
button.style.color = 'blue';
button.addEventListener('click', () => {
  button.textContent = '已点击';
  button.style.color = 'red';
});
document.body.appendChild(button);
  • 开发者需明确写出创建元素、修改样式、绑定事件等步骤。
  • 代码冗长且与DOM强耦合,维护成本高
  1. 声明式编程(Vue示例)
<!-- 声明式:通过模板描述UI状态 -->
<template>
  <button 
    :style="{ color: textColor }" 
    @click="handleClick">
    {{ buttonText }}
  </button>
</template>

<script>
export default {
  data() {
    return {
      buttonText: '点击我',
      textColor: 'blue'
    };
  },
  methods: {
    handleClick() {
      this.buttonText = '已点击';
      this.textColor = 'red';
    }
  }
};
</script>
  • 开发者仅需声明按钮的文本、样式及交互逻辑,无需操作DOM。
  • Vue内部自动处理状态到视图的映射,代码更简洁
  1. 混合使用示例(Vue)
// Vue中命令式访问DOM
const inputRef = ref(null);
onMounted(() => {
  inputRef.value.focus(); // 手动聚焦输入框
});

2. 框架实现概念

2.1. 企业应用的开发与设计原则:项目成本+开发体验

  • 项目成本 =》 开发周期 =》可维护性
  • 开发体验 =》心智负担更低

企业应用开发需平衡‌项目成本‌(预算、资源、时间)与‌开发体验‌(效率、可维护性、团队协作),核心原则包括:

  1. 模块化设计‌:通过组件化降低重复开发成本,提升代码复用率(如微服务架构);
  2. 技术选型适配性‌:选择成熟框架(如Spring Boot、Vue)减少学习成本,避免过度追求新技术;
  3. 自动化工具链‌:CI/CD流水线(如Jenkins)和低代码平台可压缩人力成本,加速交付;
  4. 开发者体验优化‌:完善的文档、调试工具和标准化规范能减少沟通损耗。

典型案例‌:

  • 成本控制:采用云服务(AWS/Aliyun)按需付费,避免基础设施过度投入;
  • 体验提升:使用React+TypeScript增强代码可读性,降低维护难度。

最终目标是通过技术决策与流程设计,实现‌成本可控‌与‌开发高效‌的双赢。

2.2. 框架设计的过程:不断取舍

尤大大在Vue框架设计中的取舍实践:找到可维护性和性能之间的一个平衡点,封装了命令式的逻辑,对外则暴露声明式的接口。

2.3. 编译时、运行时

在Vue框架中,‌编译时‌与‌运行时‌是两个核心阶段,它们共同协作完成从模板到最终DOM渲染的全过程。以下通过Vue模板的处理流程具体说明:

  1. 编译时(Compile Time)‌:

当开发者编写Vue单文件组件(Single File Component,SFC)时,模板部分会被编译器处理

<template>
  <div>{{ message }}</div>
</template>
  • 编译器作用‌:将类HTML的模板语法转换为 JavaScript 渲染函数。例如上述模板会被编译为类似render(h) { return h('div', this.message) }的代码。

  • 关键点‌:

    • 编译时在构建阶段(如vite buildwebpack打包)完成,生成浏览器可执行的JS代码。
    • 若使用仅运行时版本的Vue(如vue.runtime.js),则需预编译模板,否则会报错。

Vue编译时核心代码:编译器 compiler-core

github.com/vuejs/core/…

可以用‌乐高说明书‌的比喻来理解Vue3编译时将HTML节点转为render函数的过程:

① 原始乐高图纸(HTML模板)
就像小朋友画的乐高搭建草图:

<div class="house">
  <p>{{ message }}</p>
</div>

这种带{{}}的模板浏览器无法直接执行。

② 翻译成步骤说明书(生成AST)
编译器像乐高设计师,把图纸拆解成树形结构说明书(AST):

{
  type: "div",
    props: { class: "house" },
    children: [{
      type: "p",
      expression: "message" // 动态内容标记
    }]
}

这个结构记录了所有零件关系和组装要点。

③ 优化说明书(静态标记)
设计师用黄色荧光笔标出永不改动的部分(如class="house"),后续拼装时可直接跳过检查。

④ 生成拼装指南(render函数)
最终输出机器可执行的组装步骤:

function render() {
  return h('div', { class: 'house' }, [
    h('p', this.message)
  ])
}

就像乐高说明书用编号和箭头表示拼装顺序。

完整比喻流程‌:
原始图纸 → 设计师拆解 → 标记重点 → 生成步骤指南
对应Vue3的:
模板 → AST → 优化 → render函数

这种转换让浏览器能像拼乐高一样,按照明确步骤动态构建DOM

总结:Vue编译时可以把 html 的节点,编译成 render 函数。

  1. ‌运行时(Runtime)‌
    运行时阶段发生在浏览器执行编译后的代码时:
  • 渲染函数执行‌: 编译生成的渲染函数被调用,生成虚拟DOM(VNode)树。
  • 响应式更新‌: 组件实例化后,数据变化触发渲染函数重新执行,通过 Diff 算法更新真实DOM。
  • 轻量化优势‌: 运行时版本(不含编译器)体积更小,适合生产环境。
<!-- 开发阶段:SFC文件 -->
<template>
  <button @click="count++">{{ count }}</button>
</template>

<!-- 编译后:生成渲染函数 -->
<script>
export default {
  render() {
    return h('button', { onClick: () => this.count++ }, this.count);
  }
}
</script>

<!-- 运行时:浏览器执行渲染函数并绑定事件 -->

Vue运行时核心代码:渲染函数 render

core/packages/runtime-core at main · vuejs/core

cn.vuejs.org/api/options…

可以用‌搭积木‌的比喻来理解Vue3运行时将vnode转为真实DOM的过程:

① 设计图纸(vnode)‌

就像小朋友画积木搭建草图({ type: 'div', children: 'Hello' }),vnode是用JS对象描述DOM该长什么样。

② ‌挑选积木块(createElement)‌

render函数像小朋友的手,根据图纸找到对应积木(调用document.createElement创建div)。

③ ‌组装积木(挂载DOM)‌

把文字积木"Hello"塞进div积木(el.textContent = vnode.children),最后放到展示架(container.appendChild(el))上。

// 设计图纸(vnode)
const toyHouse = {
  type: 'div',       // 要搭个方盒子
  props: { class: 'house' }, // 涂成红色
  children: '我的小房子' // 盒子上贴的字
}

// 搭积木过程(render实现)
function buildToy(vnode, container) {
  const block = document.createElement(vnode.type) // 1.找积木
  block.className = vnode.props.class              // 2.涂颜色
  block.textContent = vnode.children               // 3.贴文字
  container.appendChild(block)                     // 4.放展示架
}

buildToy(toyHouse, document.body)

就像积木最终变成实物玩具,Vue运行时通过类似步骤把JS对象变成页面真实元素

总结:Vue运行时可以利用 render 把 vnode 渲染成真实的 dom 节点。

2.4. 编译时+运行时

Vue 先通过 compiler 解析 html 模板,生成 render 函数,然后通过 runtime 解析render,从而挂着真实 dom。

问:那么 Vue 为什么要采用 编译时+运行时 的方式来设计呢?

  1. 纯运行时:因不存在编译器,所以只能提供一个复杂的 JS 对象。
  2. 纯编译时:因缺少运行时,所以只能把分析差异的操作,放到编译时执行。同样为了省略运行时,所以速度可能会更快。但这种方式损失灵活性,比如 svelte(纯编译时框架),其实际运行速度可能达不到理论上的速度。
  3. 运行时 + 编译时:比如 Vue、React。保持灵活性的基础上,尽量的进行性能的优化,从而达到一种平衡。

Dom 渲染过程:

① 初次渲染(挂载)

② 更新渲染(打补丁)

2.5. Vue 编译时+运行时混合设计核心原因:

  1. ‌性能与灵活性的平衡‌
  • 编译时优化‌:通过将HTML模板预先编译为渲染函数,可以标记静态节点、优化指令处理,减少运行时计算量。例如v-if会被编译为条件判断语句而非运行时解析。
  • ‌运行时动态性‌:保留运行时能力允许动态生成模板(如通过API返回的HTML字符串),这是纯编译时框架(如Svelte)无法实现的。
  1. ‌开发体验提升‌
  • 声明式模板‌:开发者可直接编写类HTML的模板(编译时特性),而无需手动编写命令式渲染函数(如React的JSX)。编译器将其转换为高效的JavaScript代码。
  • 渐进式适配‌:支持直接使用未编译的模板(完整版)或预编译模板(运行时版),适应不同项目需求。
  1. ‌架构分层优势‌
  • 编译时职责‌:

    • 解析模板为AST并优化(如静态节点标记);
    • 处理指令(如v-for转换为循环逻辑)。
  • 运行时职责‌:

    • 执行渲染函数生成虚拟DOM;
    • 响应式数据更新与补丁(Patch)。
  1. ‌体积与效率权衡‌
  • 生产环境优化‌:通过构建时预编译(如vue-loader),可移除编译器代码,减少30%+体积。
  • ‌开发环境便利‌:保留运行时编译支持快速迭代。

对比其他方案:
‌纯运行时‌(如早期React):需手动优化,心智负担高。
‌纯编译时‌(如Svelte):灵活性受限,无法处理动态模板。
这种混合设计使Vue在性能、开发体验和适应性上达到最佳平衡。

3. 副作用

副作用指的是:当我们 对数据进行 setter 或 getter 操作时,所产生的一系列后果。

3.1. setter

setter 表示的是赋值操作

msg = 'hi'

假如 msg 是一个响应性数据,那么这样的一次数据改变,就会影响到对应的视图改变。

那么就可以说,msg 的 setter 行为,触发了一次副作用,导致视图跟随发生了变化。

3.2. getter

getter 表示的是取值操作

element.innerText = msg

对于变量 msg 而言,触发了一次 getter 操作,这样的一次取值操作,同样会导致 element 的 innerText 发生改变。

所以就可以说,msg 的 getter 行为触发了一次副作用,导致 element 的 innerText 发生了变化。

3.3. 思考:副作用会有多个吗

会。

举例:

<template>
  <div>
    <p>姓名:{{ obj.name }}</p>
    <p>年龄:{{ obj.age }}</p>
  </div>
</template>

<script>
  const obj = ref({
    name:'demo',
    age:10
  })
  obj.value = {
    name:'demo1',
    age:11
  }
</script>

obj.value 触发了一次 setter 行为,但是会导致两个 p 标签内容发生改变,也就是产生了两次副作用。

4. 良好的 TS 支持是如何提供的?

初识 Typescript

错误认识:vue3 对 ts 支持比较好,因为 vue3 本身是使用 ts 编写的。

正确认识:ts 编写的程序 和 ts 类型支持友好是两回事。想要让我们的程序拥有更好地 ts 支持,需要做很多额外的事情。比如 vue 内部其实也做了非常多的事情:packages\runtime-core\src\componentPublicInstance.ts