农村的师傅的博客

一个迫于生计,无法放飞自我,导致喜欢上了前端开发,并即将成长为强者(指头发)的程序猿。

0%

vue.js设计与实现-开始

仍然是最近几个月闲来无事,把js和css又重新过了一遍之后,又把目光又放到了vue3上面,之前几年都是用的react比较多,vue2只在大学毕业那一两年用过,vue3还是最近的工作中才有机会去使用的,同样,为了较为体系的去了解vue3,除了官方文档之外,还找了本《Vue.js设计与实现》来作为参考,这本书不厚,干货确实不少,对于vue了解不深的人来说,会有不小的收获。同时在学习的过程中,也会动手去自行实现,最终跑出了一个感觉还不错的vue框架示例。

背景

  • 其实本身在开始学习深入学些vue时,也纠结过是选react还是学vue的,vue这边有一本好书(至少从其他方面看起来),但是react中我确实没有看到有什么好书在,只能去网上找一些博客或者教程,但是这些都是一个非系统性的一些教程或者寻找的难度偏大。
  • 这本《vue.js设计与实现》除了对vue的源码讲解之外,其实还包含了一些本身vue的一些设计和架构方面的知识,这部分可以让我们学习的框架设计思维。也许其中的内容,有很大一部分是相通的。
  • vue3的使用时间比较短,有些东西可能是知其然不知其所以然,且存在一些和自己较劲的疑问,你明知道它实际上就是这样,但是确不知道为什么是这样,有时候会让你在书写时产生一种滞涩感,并不流畅。
  • 避免vue的选项式API,其实现和组合式API很不一样,可能会让你混乱,优先组合式API的理解和学习。
  • 整个部分分为响应式、渲染器和组件以及编译器三个部分

《vue.js设计与实现》阅读思考

一些在阅读之前或者过程中想到的一些疑问。

  • 响应式语法的本质是signals,react和vue的响应式本质差异是什么?
  • vue的组合式API和react的hooks,它们解决的问题是什么?如何进行比较?
    • 我个人认为,它们都是为了解决代码组织和复用而被设计出来的(还记得react的高阶组件和vue的mixin吗)
    • hooks是为了解决react函数式组件功能不足而被设计出来的,从而让函数式组件作为主流
    • 但是hooks本身有一些限制,了解它是为了明白其运行机制,从而防止踩坑。
    • 而vue所谓的组合式API,他其实在底层中和react的hooks差别非常大,没有强制约定(不一定要use开头),也没有react中hooks的那些限制(不能写在if里面),它更符合从js的角度,将复用的代码抽离到一个函数中
    • 举个例子就是:js代码中如果你需要在多个地方复用一段代码,那么将这段代码抽离为一个函数,然后在不同的地方调用即可。而对于vue的组合式API,则是你在多个组件中想要复用一段逻辑,那么同样和js代码一样,将这段逻辑抽离为一个函数即可。这两种行为模式和他们的结果预期之间几乎没有任何差别。而在react中,如果你抽离出这样一个函数作为hooks,那么它就需要遵守hooks的一些限制。(参考后续的渲染器和组件相关的文章也许就能更好的理解vue的组合式API了)
  • fiber架构在vue中为何没有类似的?
    • 这个其实没有什么意义,因为fiber架构它本身的设计并非是为了所谓的单纯作为高级功能而提供,同样是为了解决react架构本身遇到的一些问题。即他的更新和渲染机制。
  • vue和react的技术选型:很多文章都提到,如果是大型前端项目,推荐react,而中小型项目推荐vue。在现代开发中(2026年了),vue真的不适合大型项目吗?
    • 2026 年最新结论:Vue 完全适合大型项目!「中小型用 Vue、大型用 React」的说法是过时偏见 + 历史遗留认知(深度剖析核心差异 + 选型本质)
    • Vue 3完全能胜任任何量级的大型前端项目
    • 「中小型用 Vue,大型用 React」的论调,根源是 Vue2 的缺陷 + 早期生态差异,这个说法在 Vue3 正式发布(2020 年)后就已经彻底失效,到 2026 年的今天,这个观点已经是「前端远古谣言」;
    • 两者的核心定位:React 和 Vue3 都是「顶级的、能覆盖全量级项目」的前端框架,没有「谁适合大、谁适合小」的绝对边界,只有「团队技术栈、工程化偏好、生态适配、心智模型」的差异;
    • 补充:Vue2 确实不适合超大型项目,但 2026 年的今天,几乎所有企业新项目都用 Vue3,Vue2 仅做维护,讨论「Vue 是否适合大型项目」,默认指的就是 Vue3。
    • 硬要说,vue在react方面在大型项目的短板,我个人认为是两方面的生态(antd企业组件库、服务端渲染nextjs比nuxtjs更完善)

react和vue的响应式本质差异是什么?

  • 响应式的本质是「状态变更 → 依赖重新执行」
    • 不管是 React 还是 Vue,响应式的目标都是:当某个状态(signal)变化时,自动让依赖这个状态的代码重新运行。但二者的核心差异,就在于「怎么追踪依赖」「什么时候触发更新」「更新范围有多大」。
    • 核心差异:React 是「无依赖追踪的重执行模式」(状态变 → 组件重跑),Vue 是「有依赖追踪的精准更新模式」(属性变 → 仅依赖项更新);
    • 信号载体:React 是「不可变快照」,Vue 是「可变更的响应式属性」;

差异表格:

维度 React 响应式(函数式) Vue 响应式(Proxy/Object.defineProperty)
核心原理 基于「状态不可变 + 重新执行」,无主动依赖追踪 基于「Proxy 拦截 + 依赖收集」,主动追踪依赖
信号(signal)载体 useState/useReducer 返回的「不可变状态」 响应式对象(reactive/ref)的「可变更属性」
依赖追踪方式 被动追踪:组件渲染时「快照式」读取状态,状态变了就重新渲染整个组件(或部分) 主动追踪:拦截属性的「读/写」,读取时收集依赖(effect/computed),写入时触发依赖重新执行
更新触发时机 状态变更时调用 setXxx,标记组件为「待更新」,由 React 调度后重新执行组件函数 响应式属性赋值时,直接触发依赖(如视图/计算属性)的更新,更新时机更即时
更新粒度 组件级/粒度由开发者控制(memo/useMemo/useCallback),默认组件整体重渲染 细粒度:仅更新依赖该属性的具体 DOM/effect/computed,不影响无关部分
状态变更方式 必须通过「替换式更新」(不可变),如 setCount(c => c+1),不能直接改值 可直接修改属性(可变),如 count.value++,拦截器自动感知

React 的响应式并不是「监听状态变化」,而是「状态变了 → 告诉 React 这个组件需要重新跑一遍」:

  • 你用 const [count, setCount] = useState(0) 时,count 是一个「不可变的快照」,每次 setCount 并不是修改 count,而是告诉 React:「这个状态变了,下次调度时重新执行这个组件函数」。
  • React 没有主动去追踪「组件里哪行代码用了 count」,而是「不管你有没有用这些状态,只要有一个状态变了,就重新执行整个组件函数,重新生成虚拟 DOM 对比」。
  • 为了优化粒度,React 提供了 memo(组件级缓存)、useMemo(计算结果缓存)、useCallback(函数缓存),本质是「手动缩小重新执行的范围」,而非 React 本身能精准追踪依赖。

Vue 的响应式是「主动监听状态变化 + 精准触发依赖」,核心是 ES6 Proxy(Vue3)/ Object.defineProperty(Vue2):

  • 你用 const count = ref(0) 或 const obj = reactive({ name: “” }) 时,Vue 会用 Proxy 拦截 count.value/obj.name 的「读取(get)」和「写入(set)」:
    • 读取时(get):收集依赖(比如模板里的 <p>{{count}}</p>);
    • 写入时(set):触发所有收集到的依赖重新执行。

权衡的艺术

框架设计里到处都体现了权衡的艺术。

命令式和声明式

  • 命令式:自然语言描述能够与代码产生一一对应的关系,代码本身描述的是“做事的过程”,这符合我们的逻辑直觉。
  • 与命令式框架更加关注过程不同,声明式框架更加关注结果。
    • 即他需要用户编写的是最终程序是什么样子的,至于如何做到这个样子,由框架在内部使用命令式的方式帮你做
    • 声明式框架内部是命令式的,而暴露给用户却是声明式的。
  • 命令式和声明式的性能与可维护性权衡
    • 声明式代码的性能不优于命令式代码的性能。
    • 声明式代码会比命令式代码多出找出差异的性能消耗,因此最理想的情况是,当找出差异的性能消耗为 0 时,声明式代码与命令式代码的性能相同,但是无法做到超越,毕竟框架本身就是封装了命令式代码才实现了面向用户的声明式。
    • 如果只是渲染本身(不做修改,那么声明式和命令式的性能可以视为是一致的),但是真的只会有只渲染不更新吗?
    • 既然在性能层面命令式代码是更好的选择,那么为什么 Vue.js 要选择声明式的设计方案呢?原因就在于声明式代码的可维护性更强。

虚拟dom的作用

  • 虚拟dom其实就是用 JavaScript 对象来描述真实的 DOM 结构。
  • 声明式代码的更新性能消耗 = 找出差异的性能消耗 + 直接修改的性能消耗,因此,如果我们能够最小化找出差异的性能消耗,就可以让声明式代码的性能无限接近命令式代码的性能。而所谓的虚拟 DOM,就是为了最小化找出差异这一步的性能消耗而出现的
  • 至此,相信你也应该清楚一件事了,那就是采用虚拟 DOM 的更新技术的性能理论上不可能比原生 JavaScript 操作 DOM 更高。
    • 这里我们强调了理论上三个字,因为这很关键,为什么呢?因为在大部分情况下,我们很难写出绝对优化的命令式代码,尤其是当应用程序的规模很大的时候,即使你写出了极致优化的代码,也一定耗费了巨大的精力,这时的投入产出比其实并不高。
    • 也许AI的出现,可能帮我们写出绝对优化的命令式代码,但是这里的点在于,命令式本身还有一个比较难以解决的问题在于可维护性,只需要还有人参与维护,那么其本身的可维护性就是需要考虑的一个重要因素
  • 虚拟dom的作用就是让我们不用付出太多的努力(写声明式代码),还能够保证应用程序的性能下限,让应用程序的性能不至于太差,甚至想办法逼近命令式代码的性能

这里有一个很有意思的比较,那就是虚拟dom、js原生和innerHTML来创建和更新页面之间的写法差异和性能差异。这本质上也是我们开发范式的一个变化。

通常创建时性能差别不会很大,重点在于更新时的性能

  • innerHTML:一把梭,直接将整个拼接的html片段插入到页面,更新时也是全量更新
    • 更新时性能:拼接html + html全量更新
    • 心智负担中等,性能差
  • js原生:使用js来操作dom API来创建和更新dom元素
    • 更新时性能:只需要进行必要的更新(需要自己写)
    • 心智负担比较高、可维护性比较差,性能最好
  • 虚拟dom:使用js对象来描述页面的dom元素,更新时按照需要进行必要的更新
    • 更新时性能:通过js创建虚拟dom + 必要更新
    • 心智负担小,性能中等,可维护性好

框架的运行时和编译时

注意:这里的运行和编译时,和传统语言中的编译和运行不一样,例如c这种编译和运行,c的编译是由c源码编译为操作系统运行的二进制代码(目标代码)这里的编译时和框架的编译时倒是类似的。不过c里面的运行是指编译后的目标代码的执行。框架里面的运行时因为还涉及到了描述页面的虚拟dom或者html字符串这一个中间层,他的目标代码其实是指js操作dom API,所以框架运行时不完全等于c里面中的运行时。稍微理解一下就好。

当设计一个框架的时候,我们有三种选择:纯运行时的、运行时 + 编译时的或纯编译时的。这需要你根据目标框架的特征,以及对框架的期望,做出合适的决策

  • 这里的运行时、编译时,应该需要先定义一个目标,即最终的目标代码是什么,这里其实就是指:创建dom元素并将其添加到页面上去。
  • 同时,还需要一个源数据,其源数据的不同,通常决定了你是需要运行时还是编译时

这里还是以更贴合vue的说法来举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 源数据:用户定义的数据
const domObj = {
tag: 'div',
children: [
{tag: 'span', children: 'hello world'}
]
}
// 渲染函数,基于domObj对象,将其转换为dom渲染到页面中
function render(obj, target) {}

// 源数据:html字符串
const html = `
<div>
<span>hello world</span>
</div>
`
// 编译函数,将html编译为domObj对象,此时就可以利用编译后的结果,调用render函数来渲染了。
function compiler() {}
  • 运行时:基于js对象来描述dom,并通过render来渲染到页面中区,此时可以算作是运行时。
  • 运行时 + 编译时:将另外一种数据经过compiler函数编译后,使其可以作用于运行时,然后再由render运行时运行
    • 注意:这里的编译时,并不作为运行时的一个前置步骤
    • 他也可以直接将内容全部编译为最终的目标代码
    • 也可以将其编译为render函数的前置数据,然后全部交由render去运行时渲染
    • 或者甚至可以编译一部分作为最终代码,一部分作为render函数的前置数据
    • 编译本身具有较大的灵活些
  • 编译时:我们可以直接将html编译为js的dom操作,这是在真正运行代码之前就做到的事,浏览器运行的是编译后的代码。此时连render函数都省掉了。

其实,上面的各种时(编译时、运行时)和各种函数以及数据,它们的定义,我个人感觉并不是固定的,或者说是由源数据(或者代码)来决定是否要引入所谓的运行时和编译时。因为对于使用者来说,这个框架它具有更高层次的抽象,我们编写的不是js操作dom这种所谓的”原始API”代码。

  • 只要你的源数据或者源代码本身不是最终的目标代码(js操作dom渲染页面),那么都需要引入运行时和编译时看来达到最终的目的
  • 如果你的代码是基于类似domObj的js对象来描写页面,那么可能需要一个render函数来作为运行时,以便达到和最终的目标代码一样的效果
  • 如果你的代码是基于html来描写页面
    • 那么可能需要一个编译时和运行时,或者将编译时的函数,也视为运行时的一部分,将其叫做运行时。
    • 或者直接将基于html的代码编译为纯粹的目标代码,此时就不需要再引用运行时的render函数了。

它们的优缺点

  • 首先是纯运行时的框架。由于它没有编译的过程,因此我们没办法分析用户提供的内容
  • 但是如果加入编译步骤,可能就大不一样了,我们可以分析用户提供的内容,看看哪些内容未来可能会改变,哪些内容永远不会改变,这样我们就可以在编译的时候提取这些信息,然后将其传递给 Render 函数,Render 函数得到这些信息之后,就可以做进一步的优化了。
  • 然而,假如我们设计的框架是纯编译时的,那么它也可以分析用户提供的内容。由于不需要任何运行时,而是直接编译成可执行的 JavaScript 代码,因此性能可能会更好

各大框架的运行时和编译时

  • react是一个编译时 + 运行时框架,但是这里的编译和vue的编译不一样,其本质上是指jsx这种语法层面的编译,其本身不会基于代码本身做太多的优化之类的
    • react新出的编译器增加了这个功能,所以现在也许也可以将react划分为编译 + 运行时框架了
    • 这里的运行时框架,其实就是指你的代码被编译后不会变成纯粹的dom操作,例如更新数据后,需要更新页面时,它需要经过一个虚拟dom、diff算法、fiber调度等过程才能真正操作相应的dom来更新页面,这些中间的部分和内容就如同render函数一样,是框架本身需要额外处理的运行时逻辑。
  • vue是编译时 + 运行时框架,其除了本身的SFC的编译之外,还会基于代码分析做一些编译优化
  • Svelte是一个编译时的框架,基本上其编译后的结果,是很纯粹的dom操作代码,并且其事件和响应式本身也是基于函调用后,触发相应的dom更新操作,没有额外的虚拟dom、diff算法等所谓的运行时。

框架设计的核心要素

框架设计要比想象得复杂,并不是说只把功能开发完成,能用就算大功告成了,这里面还有很多学问。比如,我们的框架应该给用户提供哪些构建产物?产物的模块格式如何?当用户没有以预期的方式使用框架时,是否应该打印合适的警告信息从而提供更好的开发体验,让用户快速定位问题?开发版本的构建和生产版本的构建有何区别?

  • 衡量一个框架是否足够优秀的指标之一就是看它的开发体验如何
    • 所以在框架设计和开发过程中,提供友好的警告信息至关重要。始终提供友好的警告信息不仅能够帮助用户快速定位问题,节省用户的时间,还能够让框架收获良好的口碑,让用户认可框架的专业性。
    • 浏览器允许我们编写自定义的 formatter,从而自定义输出形式。在 Vue.js 3 的源码中,你可以搜索到名为 initCustomFormatter 的函数,该函数就是用来在开发环境下初始化自定义 formatter 的
    • 浏览器勾选“Console”→“Enable custom formatters”选项
  • 框架的大小也是衡量框架的标准之一。在实现同样功能的情况下,当然是用的代码越少越好,这样体积就会越小,最后浏览器加载资源的时间也就越少。
    • 提供越完善的警告信息就意味着我们要编写更多的代码,这不是与控制代码体积相悖吗?
    • 如果我们去看 Vue.js 3 的源码,就会发现每一个 warn 函数的调用都会配合 __DEV__ 常量的检查
    • Vue.js 使用 rollup.js 对项目进行构建,这里的 __DEV__ 常量实际上是通过 rollup.js 的插件配置来预定义的,其功能类似于 webpack 中的 DefinePlugin 插件。
    • 且当某一段代码的判断条件始终为假,这段永远不会执行的代码称为 dead code,它不会出现在最终产物中,在构建资源的时候就会被移除
    • 但是调用方应该还是会触发调用,调用方的warn函数理论上也应该去除掉才行(需要配合tree-shanking)
  • 框架要做到良好的 Tree-Shaking
    • 我们知道 Vue.js 内建了很多组件,例如 Transition 组件,如果我们的项目中根本就没有用到该组件,那么它的代码需要包含在项目最终的构建资源中吗?答案是“当然不需要”
    • Tree-Shaking 中的第二个关键点——副作用。如果一个函数调用会产生副作用,那么就不能将其移除。
    • 因为静态地分析 JavaScript 代码很困难,所以像 rollup.js 这类工具都会提供一个机制,让我们能明确地告诉 rollup.js:“放心吧,这段代码不会产生副作用,你可以移除它。通常可以使用注释代码 /*#__PURE__*/来告诉rollup.js,对于相关函数的调用不会产生副作用,你可以放心地对其进行 Tree-Shaking。
    • 这会不会对编写代码造成很大的心智负担呢?其实不会,因为通常产生副作用的代码都是模块内函数的顶级调用。也就是说,对于其注释,只需要关注其顶层的调用即可。
  • 框架应该输出怎样的构建产物
    • Vue.js 的构建产物除了有环境上的区分之外,还会根据使用场景的不同而输出其他形式的产物。
    • 不同类型的产物一定有对应的需求背景,因此我们需要从需求来确定。
    • 给浏览器esm用的、给打包工具esm用的(yarn、pnpm项目)、直接script引用的、node环境应用的cjs(服务端渲染)
    • ESM 格式的资源有两种:用于浏览器的 esm-browser.js 和用于打包工具的 esm-bundler.js。它们的区别在于对预定义常量 __DEV__ 的处理,前者直接将 __DEV__ 常量替换为字面量 true 或 false,后者则将 __DEV__ 常量替换为 process.env.NODE_ENV !== ‘production’ 语句。
  • 特性开关
    • 在设计框架时,框架会给用户提供诸多特性(或功能),例如我们提供 A、B、C 三个特性给用户,同时还提供了 a、b、c 三个对应的特性开关,用户可以通过设置 a、b、c 为 true 或 false 来代表开启或关闭对应的特性,这将会带来很多益处。
      • 对于用户关闭的特性,我们可以利用 Tree-Shaking 机制让其不包含在最终的资源中。
      • 该机制为框架设计带来了灵活性,可以通过特性开关任意为框架添加新的特性,而不用担心资源体积变大。
      • 同时,当框架升级时,我们也可以通过特性开关来支持遗留 API,这样新用户可以选择不使用遗留 API,从而使最终打包的资源体积最小化。
    • 那怎么实现特性开关呢?其实很简单,原理和上文提到的 __DEV__ 常量一样,本质上是利用 rollup.js 的预定义常量插件来实现。
  • 错误处理(这里不仅仅是指用户调用接口时的错误处理方式,还包含异步的错误处理方式)
    • 错误处理是框架开发过程中非常重要的环节。框架错误处理机制的好坏直接决定了用户应用程序的健壮性,还决定了用户开发时处理错误的心智负担。
    • 例如用户提供的异步回调中抛出了异常,那么该用什么形式抛出,用户该用什么方式捕获呢?
    • 我们可以将错误处理程序封装为一个函数,假设叫它 callWithErrorHandling,然后再每一个需要捕获的异步回调中包装该callWithErrorHandling,然后在callWithErrorHandling中同一处理错误的抛出形式,例如事件、或者用户提供的错误处理方法
    • 实际上,这就是 Vue.js 错误处理的原理,你可以在源码中搜索到 callWithErrorHandling 函数。另外,在 Vue.js 中,我们也可以注册统一的错误处理函数:app.config.errorHandler = () => { // 错误处理程序 }
  • 良好的 TypeScript 类型支持
    • 使用 TS 编写代码与对 TS 类型支持友好是两件事。在编写大型框架时,想要做到完善的 TS 类型支持很不容易。疯狂的类型体操
    • 相比之下,纯粹的js代码的库,则ts类型写起来就要好多了。

vue的设计思路

Vue.js 3 是一个声明式的 UI 框架,意思是说用户在使用 Vue.js 3 开发页面时是声明式地描述 UI 的。

编写前端页面都涉及哪些内容,具体如下。

  • DOM 元素:例如是 div 标签还是 a 标签。
  • 属性:如 a 标签的 href 属性,再如 id、class 等通用属性。
  • 事件:如 click、keydown 等。
  • 元素的层级结构:DOM 树的层级结构,既有子节点,又有父节点。
  • 注意:样式可以由浏览器本身的层叠来直接应用,而style属性本质上也是前端元素的一个属性而已

如何声明式地描述上述内容呢?这是框架设计者需要思考的问题。其实方案有很多。拿 Vue.js 3 来说,相应的解决方案是:

  • 使用与 HTML 标签一致的方式来描述 DOM 元素,例如描述一个 div 标签时可以使用 <div></div>;
  • 使用与 HTML 标签一致的方式来描述属性,例如 <div id="app"></div>;
    • 使用 : 或 v-bind 来描述动态绑定的属性,例如 <div :id="dynamicId"></div>;
  • 使用 @ 或 v-on 来描述事件,例如点击事件 <div @click="handler"></div>;
  • 使用与 HTML 标签一致的方式来描述层级结构,例如一个具有 span 子节点的 div 标签 <div><span></span></div>。

除了上面这种使用模板来声明式(vue的模板)地描述 UI 之外,我们还可以用 JavaScript 对象来描述:

1
2
3
4
5
6
7
8
9
const title = {
tag: 'h3',
props: {
onClick: handle,
},
children: [
{ tag: 'sapn' }
]
}
  • 使用js描述UI和使用模板描述UI的最大差别在于:使用 JavaScript 对象描述 UI 更加灵活。。虚拟 DOM 要比模板更加灵活,但模板要比虚拟 DOM 更加直观
  • 使用 JavaScript 对象来描述 UI 的方式,其实就是所谓的虚拟 DOM
  • 我们在 Vue.js 组件中手写的渲染函数(h函数)就是使用虚拟 DOM 来描述 UI 的,而h函数返回的就是一个虚拟dom,其本质上是为了简化虚拟dom的编写方式。
    • h 函数就是一个辅助创建虚拟 DOM 的工具函数,仅此而已。

在vue中,一个组件要渲染的内容是通过渲染函数来描述的,也就是上面代码中的 render 函数,Vue.js 会根据组件的 render 函数的返回值拿到虚拟 DOM,然后就可以把组件的内容渲染出来了。

初识渲染器

  • 渲染器的作用就是把虚拟 DOM 渲染为真实 DOM
  • 渲染器的精髓都在更新节点的阶段
  • 也就是说,渲染器本身出了基于虚拟dom将其渲染为真实dom到页面上之外,其本身还需要精准识别他和现有页面dom之间的差异,并精确的找到需要更新的内容去更新,而不仅仅只是全量的重新创建dom
    • 而这里需要的就是diff算法了。

组件的本质

  • 虚拟 DOM 除了能够描述真实 DOM 之外,还能够描述组件。
  • 而组件的本质就是:组件就是一组 DOM 元素的封装,这组 DOM 元素就是组件要渲染的内容
    • 它可以是一个返回虚拟 DOM 的函数,也可以是一个对象,但这个对象下必须要有一个函数(render函数)用来产出组件要渲染的虚拟 DOM。
  • 因此我们可以定义一个函数来代表组件,而函数的返回值就代表组件要渲染的内容
  • 组件的返回值也是虚拟 DOM,它代表组件要渲染的内容。
  • 就像 tag: ‘div’ 用来描述 <div> 标签一样,tag: MyComponent 用来描述组件,只不过此时的 tag 属性不是标签名称,而是组件函数。
    • 为了能够渲染组件,需要渲染器的支持。渲染dom节点和渲染组件是两个不同的实现方式,需要分开。

模板的工作原理

  • 编译器和渲染器一样,只是一段程序而已,不过它们的工作内容不同。编译器的作用其实就是将模板编译为渲染函数
  • 对于编译器来说,模板就是一个普通的字符串,它会分析该字符串并生成一个功能与之相同的渲染函数:render() { return h('div', { onClick: handle }, children: [] )}
  • 编译器会把(vue文件中template)模板内容编译成渲染函数,并且同其他部分(例如script中的内容)一起组成一个组件对象(或者组件函数)

无论是使用模板还是直接手写渲染函数,对于一个组件来说,它要渲染的内容最终都是通过渲染函数产生的,然后渲染器再把渲染函数返回的虚拟 DOM 渲染为真实 DOM,这就是模板的工作原理,也是 Vue.js 渲染页面的流程。

Vue.js 是各个模块组成的有机整体

组件的实现依赖于渲染器,模板的编译依赖于编译器,并且编译后生成的代码是根据渲染器和虚拟 DOM 的设计决定的,因此 Vue.js 的各个模块之间是互相关联、互相制约的,共同构成一个有机整体。

例如如下模板:

1
<div id="a" v-bind:class="cls">123</div>

此时编译器会将其编译为渲染函数:

1
2
3
4
5
6
7
8
9
10
11
12
function render() {
// 为了简单,直接使用虚拟dom的方式
return {
tag: 'div',
props: {
id: 'a',
class: cls,
},
children: '123',
patchFlags: 1,
}
}
  • 我们知道渲染器的作用之一就是寻找并且只更新变化的内容,所以当变量 cls 的值发生变化时,渲染器会自行寻找变更点。
  • 对于渲染器来说,这个“寻找”的过程需要花费一些力气。那么从编译器的视角来看,它能否知道哪些内容会发生变化呢?如果编译器有能力分析动态内容,并在编译阶段把这些信息提取出来,然后直接交给渲染器,这样渲染器不就不需要花费大力气去寻找变更点了吗?
  • 所以,编译器在编译的过程中,就可以尝试分析那些是静态、那些是动态属性,并尝试引入了一个patchFlags属性来进行标记,假设上面的1表示属性class才是动态的,那么在后续渲染器读取改patchFlags时就可以知道,只有属性class是动态的,那么在寻找变更时就可以更加有效率,性能就提升了。
  • 通过这个例子,我们了解到编译器和渲染器之间是存在信息交流的,它们互相配合使得性能进一步提升,而它们之间交流的媒介就是虚拟 DOM 对象。在后面的学习中,我们会看到一个虚拟 DOM 对象中会包含多种数据字段,每个字段都代表一定的含义。