[译] Vue.js 内部原理浅析

本文涉及的产品
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: [译] Vue.js 内部原理浅析

原文:medium.com/js-imaginea…

说到 JavaScript 框架,Vue.js 绝对是个热门的 UI 框架(译注:截至本文翻译时其 Github 155k ⭐️ & 23k 🍴, 关注数已经超过了 React)。于我来说 Vue.js 最吸引人的地方在于 -- 其学习曲线,非常之低。个人角度来讲,我感觉就像正在做着 jQuery 一类的事情。鼓捣几天之后,你就能开始建立应用了。

一年前我开始探索 Vue.js 并建立了一些应用。但是几天前,一股深入了解 Vue.js 代码的渴望在我心中升腾。我翻阅了 Github 上的源码并进行了多轮调试以了解其底层运行机制。这也是本文中我要写的东西。

所以,让我们来点干货,本文将尝试给你如下 4 个问题的答案:

  1. 当你创建一个 Vue.js 实例时发生了什么?
  2. 模板内部都在发生着什么?
  3. Virtual DOM 有何意义?
  4. 当一个属性改变时模板是如何再次渲染的?

Vue 组件中包含一个模板(template),而模板在出现在浏览器里之前必须经历多个阶段。我们来编写一个短小的模板,并以之作为一个例子驱动本文的进行。

<div id="app">
  <span v-if="dynamic">Dynamic text</span>
  <span><p>Static text</p></span>
  <button @click="toggleFlag">Toggle Dynamic</button>
</div>

组件的 JS logic 就不写出来了,因为模板本身已经可以自解释。

编译阶段

Vue compiler 读取一个组件的模板,使之经历下图所示的 parsing、optimizing、codegen 阶段并最终创建一个渲染函数。该渲染函数的职责就是创建一个 VNode,而该 VNode 会被 Virtual DOM 的 patch 过程用来创建真实 DOM。

解析阶段

在编译的这个阶段对特定组件中的置标语言模板进行解析。正如你能在下图中见到的,首先 parser 会将模板解析成 HTML parser,随后转成 AST(即 抽象语法树)。

parsing 阶段之后的 AST

AST 包含了诸如 attributes、parent、children、tag 等等的信息。解析过程中也会将 directives 以类似元素的方式处理。诸如 v-forv-ifv-once 等结构化的 directives 会被表现为一个特定元素 AST 中的 key-value 对。如我们模板中的 v-if,在解析后将被推入 attrsMap 中变成形如 {v-if: “dynamic”} 的对象。

优化阶段

optimizer 的目标就是遍历生成的 AST 并探测纯静态的子树,即 DOM 中不会改变的那些部分。如下图所示,这些元素将被标记为 static。

优化后的 AST

一旦检测到静态子树,Vue 便将其提升为常量,从而不会在每次重新渲染时为其生成新鲜的节点。这些节点也会在 Virtual DOM 的 patch 过程中被完全地跳过。

Codegen 阶段

编译的最后一个阶段就是 Codegen,该阶段将创建真正的渲染函数以用于 patch 过程。

render function 的层次结构

在上图中,可以看到模板的层次结构已经被转换成了渲染函数的层次结构。基于 optimizer 打过的 static 标记,Codegen 将渲染函数分叉为两个独立的函数。一个是普通的渲染函数,另一个是静态渲染函数。

最后,当真正的渲染过程触发时,渲染函数将被用于创建 VNode。

注意:如果你使用了一个构建步骤,如单文件组件时,模板的编译将提前发生。

observer 和 watcher — 反应式组件

Observer

Vue 会在底层遍历所有我们定义在 data 中的属性,并通过 Object.defineProperty 将它们转换为 getter/setters。

当任何 data 属性得到一个新值时,set 函数将会通知 Watchers

Watcher

当一个 Vue 应用被初始化时,会为每个组件创建一个 Watcher。Watcher 会解析一个表达式,收集订阅者并在表达式的值变化时触发回调。这个做法被同时用在了 $watch API 和 directives 上。每个组件实例都有一个相应的 watcher 实例,用以将渲染组件期间“触及”的任何属性记录为依赖项(译注:在 getter 里收集会访问到的依赖数据)。其后,当一个依赖项的 setter 被触发,它就会通知到 watcher,并最终触发 patch 过程。

无论何时,当一个数据的改变被观察到,就会开启一个队列并缓存本轮事件循环中发生的所有数据改变。所有 watchers 都被添加到此队列中。每个 watcher 有一个独特的自增 Id,这样如果相同的 watcher 被触发多次,它只会在被使用前被推送到队列中一次。因为 watchers 要以从 parent 到 child 的顺序运行,所以队列也会被排序。

在内部,Vue 会为异步排队尝试使用原生的 Promise.thenMessageChannel,实在不行就用 setTimeout(fn, 0)

nextTick 函数会消耗掉队列中的所有 watchers。在那之后,渲染过程将通过 watcher 的 run() 函数被初始化。

patch 过程

patch 过程基本上就是一个使用 Virtual DOM 和真实 DOM 高效交互的过程。一个 Virtual DOM 就是表示一个 DOM(文档对象模型 - Document Object Model) 的 JavaScript 对象。Vue.js 在内部使用了 snabbdom 库。所以,让我们看看 patch 过程中到底发生了什么。

整个过程就是个关于两相对比新旧 VNode (Virtual DOM Node) 的游戏。

其算法将以如下方式运行 --

  1. 首先检查旧 VNode 是否存在,若不存在则为每个 VNode 创建 DOM 元素。当你首次登录到应用中并且第一次渲染过程初始化时,就是旧 VNode 不存在的时候。
  2. 反过来说,如果旧 VNode 存在的话,比较新旧 VNode 的 children 的过程就将启动 -- 普通的节点将在 DOM 中保持原状,新节点将被添加,而旧的且不匹配的节点将从 Virtual DOM 和真实 DOM 中同时移除。
  3. 另外如果有必要的话,匹配节点的样式、class、dataset 和事件监听器也会被更新或删除。

相同的过程会递归式地应用到所有节点上。

此外,我得提醒你一些事情 -- 静态节点,我们在优化阶段讨论过的。静态节点树并不会被触及,并被原样使用。这意味着 -- 我们并不需要对这种树与真实 DOM 交互。

生命周期钩子

让我们来讨论一下特定组件的生命跨度,并尝试把它们带入本文讨论的话题。

组件生命周期可被分为四个节段 --

  • 创建
  • 加载
  • 更新
  • 销毁

一旦 Vue 的新实例被执行,创建组件的过程就启动了。

beforeCreation:  收集组件所需的事件、数据之前。换句话说 -- 在收集 watchers/dependencies 的过程中。

created:  当 Vue 设置好 data 和 watchers 的时候。

beforeMount:  早于 patch 过程。VNode 正在基于 data 和 watchers 被创建。

mount: patch 过程之后。

beforeUpdate:  如果数据改变,watcher 会更新 VNode 并重新开始一次 patch 过程。

update:  patch 过程完成时。

beforeDestroy: 卸载组件之前。此时,组件仍是全须全尾的。

destroyed: 销毁 watchers 并删除附加其上的事件监听器或子组件时。



相关文章
|
2月前
|
JavaScript 算法 编译器
vue3 原理 实现方案
【8月更文挑战第15天】vue3 原理 实现方案
32 1
|
6天前
|
缓存 JavaScript 前端开发
「offer来了」从基础到进阶原理,从vue2到vue3,48个知识点保姆级带你巩固vuejs知识体系
该文章全面覆盖了Vue.js从基础知识到进阶原理的48个核心知识点,包括Vue CLI项目结构、组件生命周期、响应式原理、Composition API的使用等内容,并针对Vue 2与Vue 3的不同特性进行了详细对比与讲解。
「offer来了」从基础到进阶原理,从vue2到vue3,48个知识点保姆级带你巩固vuejs知识体系
|
8天前
vue2的响应式原理学“废”了吗?继续观摩vue3响应式原理Proxy
该文章对比了Vue2与Vue3在响应式原理上的不同,重点介绍了Vue3如何利用Proxy替代Object.defineProperty来实现更高效的数据响应机制,并探讨了这种方式带来的优势与挑战。
vue2的响应式原理学“废”了吗?继续观摩vue3响应式原理Proxy
|
12天前
|
前端开发 JavaScript Java
JavaScript的运行原理
JavaScript 的运行原理包括代码输入、解析、编译、执行、内存管理和与浏览器交互几个步骤。当打开网页时,浏览器加载 HTML、CSS 和 JavaScript 文件,并通过 JavaScript 引擎将其解析为抽象语法树(AST)。接着,引擎将 AST 编译成字节码或机器码,并在执行阶段利用事件循环机制处理异步操作,确保单线程的 JavaScript 能够高效运行。同时,JavaScript 引擎还负责内存管理和垃圾回收,以减少内存泄漏。通过与 DOM 的交互,JavaScript 实现了动态网页效果,提供了灵活且高效的开发体验。
|
17天前
|
存储 JavaScript 前端开发
[JS] ES Modules的运作原理
【9月更文挑战第16天】ES Modules(ECMAScript Modules)是 JavaScript 中的一种模块化开发规范,适用于浏览器和 Node.js 环境。它通过 `export` 和 `import` 关键字实现模块的导出与导入。模块定义清晰,便于维护和测试。JavaScript 引擎会在执行前进行静态分析,确保模块按需加载,并处理循环依赖。ES Modules 支持静态类型检查,现代浏览器已原生支持,还提供动态导入功能,增强了代码的灵活性和性能。这一规范显著提升了代码的组织和管理效率。
|
8天前
|
开发框架 JavaScript 前端开发
手把手教你剖析vue响应式原理,监听数据不再迷茫
该文章深入剖析了Vue.js的响应式原理,特别是如何利用`Object.defineProperty()`来实现数据变化的监听,并探讨了其在异步接口数据处理中的应用。
|
1月前
|
缓存 JavaScript 容器
vue动态组件化原理
【9月更文挑战第2天】vue动态组件化原理
34 2
|
2月前
|
JavaScript 前端开发 安全
JS 混淆解析:JS 压缩混淆原理、OB 混淆特性、OB 混淆JS、混淆突破实战
JS 混淆解析:JS 压缩混淆原理、OB 混淆特性、OB 混淆JS、混淆突破实战
49 2
|
2月前
|
JavaScript 前端开发 开发者
Vue学习之--------深入理解Vuex、原理详解、实战应用(2022/9/1)
这篇文章详细介绍了Vuex的基本概念、使用场景、安装配置、基本用法、实际应用案例以及注意事项,通过一个数字累加器的实战示例,帮助开发者深入理解Vuex的原理和应用。
|
2月前
|
JavaScript API
Vue学习之--------列表排序(ffilter、sort、indexOf方法的使用)、Vue检测数据变化的原理(2022/7/15)
这篇博客文章讲解了Vue中列表排序的方法,使用`filter`、`sort`和`indexOf`等数组方法进行数据的过滤和排序,并探讨了Vue检测数据变化的原理,包括Vue如何通过setter和数组方法来实现数据的响应式更新。
Vue学习之--------列表排序(ffilter、sort、indexOf方法的使用)、Vue检测数据变化的原理(2022/7/15)
下一篇
无影云桌面