Vue.js 源码剖析-Vue 首次渲染过程、响应式原理、虚拟 DOM、模板编译和组件化
本文主要从 Vue.js 源码进行分析,剖析 Vue 首次渲染的过程、Vue 响应式原理、渲染虚拟Dom中k的作用及好处、以及模板的编译过程。让你更深刻的了解 Vue.js 从初始化到渲染到页面的整个工作过程!
Vue源码地址:https://github.com/vuejs/vue
首先需要准备一份Vue.js的源码,这里主要看src目录下的源码,怎么从gitHub上把项目Clone到本地就不详细讲解了,不会的自行百度。下面我们从Vue 首次渲染的过程开始,探索Vue.js的整个运行过程;
Vue 首次渲染的过程
- 在首次渲染之前,首先进行Vue的初始化,初始化实例成员和静态成员;
- 当初始化结束之后,要调用 Vue 的构造函数 new Vue(),在构造函数中调用了_init() 方法,这个方法相当于我们整个Vue 的入口;
- 在 _init 方法中,最终调用了 this.$mount(),一共有两个$mount:第一个定义在 entry-runtime-with-compiler.js 文件中,也就是我们的入口文件 $mount,这个 $mount 方法的核心作用是帮我们把模板编译成 render 函数。此处的 $mount 方法首先会判断options中是否传入了 render 选项,如果没有传入,它会去获取我们的 template 选项,如果 template 选项也没有的话, 会把 el 中的内容作为我们的模板通过 compileToFunctions 函数编译成 render 函数,当把 render 函数编译好之后,会把 render 函数存储在我们的options.render 中。
if (!options.render) {
let template = options.template
if (template) {
...
}
}
Vue.compile = compileToFunctions
export default Vue
- 接着会调用 src/platforms/web/runtime/index.js 文件中的 $mount 方法,在这里首先重新获取 el ,因为如果是运行时版本的 Vue ,是不会在 entry-runtime-with-compiler.js 这个文件中获取 el 的,所以 这里为了更加的严谨会在 runtime/index.js 中的 $mount 中重新获取一下 el 。
- 判断完 el 之后接着会调用 mountComponent(this, el, hydrating) 这个方法。这个方法在src/core/instance/lifecycle.js 文件中定义。 在 mountComponent() 中首先会判断 vm.$options.render 。如果没有 render 选项,但是我们传入了模版,并且当前是开发环境的话会给我们发送一条警告,告诉我们运行时版本不支持编译器。接下来会出发 beforeMount 这个生命周期中的钩子函数,也就是开始挂在之前。
- 接下来定义 updateComponent(), 在这个方法中定义了_render 和 _update, _render 的作用是生成vnode, _update 的作用是将虚拟Dom 转换成真实Dom,并挂载到页面中来。
- 在接下来就是创建 Watcher 对象,在创建Watcher 时,传递了 updateComponent 这个函数,这个函数最终是在 Watcher 内部调用的。在 Watcher 创建完之后还调用了 get 方法,在 get 方法中,会调用 updateComponent() 这个方法。
- 最后触发 mounted 生命周期钩子函数,挂载结束, 最终返回 Vue 实例。
这就是 Vue 首次渲染的整个过程,以上过程可通过 Vue 源码进行调试加深印象。
Vue 响应式原理
官网用了一张图来表示Vue 响应式整个过程,现在咱们分析一下这张图:
Vue 响应式过程分为三个步骤:
- init 阶段: Vue 的 data 属性注册到Vue 实例上,所有属性都会变成响应式的,也就是加上了 getter/ setter函数。
- 发布通知阶段:当调用 defineReactive () 这个方法的 set方法时,最终会调用dep.notify()方法(派发更新(发布更改通知)),在 notify 这个方法中 会调用每个订阅者的update方法实现更新
// 将观察对象和 watcher 建立依赖
depend () {
if (Dep.target) {
// 如果 target 存在,把 dep 对象添加到 watcher 的依赖中
Dep.target.addDep(this)
}
}
// 发布通知
notify () {
// stabilize the subscriber list first
const subs = this.subs.slice()
if (process.env.NODE_ENV !== 'production' && !config.async) {
// subs aren't sorted in scheduler if not running async
// we need to sort them now to make sure they fire in correct
// order
subs.sort((a, b) => a.id - b.id)
}
// 调用每个订阅者的update方法实现更新
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
- 更新阶段:在调用 watcher 对象的 update() 方法 时会调用 queueWatcher(this) 这个方法,并触发 beforeUpdate 钩子函数,最后调用 watcher.run() 这个方法,接着会调用 watcher 里面的 getter() 方法, 之后调用updateComponent,触发updated 钩子函数。
下面我梳理了一份响应式原理的导图,方便理解 Vue 响应式原理的整个过程,它的作用是判断 watcher 是否被处理, 如果没有的话添加到 queue 队列中,并调用 flushSchedulerQueue(), 在flushSchedulerQueue 中
我们整个响应式是从 src/core/instance/init.js 的 initState 方法中开始的,调用 initState() 初始化 vue 实例的状态,在 initState 中调用 initData(vm),将 data 属性注册到 vue 实例上,并且调用 observe() 将data 对象转换成响应式对象。observe就是我们响应式的入口。
最后总结一下:
1、第一步:组件初始化的时候,先给每一个Data属性都注册getter,setter,也就是reactive化。然后再new 一个自己的Watcher对象,此时watcher会立即调用组件的render函数去生成虚拟DOM。在调用render的时候,就会需要用到data的属性值,此时会触发getter函数,将当前的Watcher函数注册进sub里。
2、第二步:当data属性发生改变之后,就会遍历sub里所有的watcher对象,通知它们去重新渲染组件。
虚拟 DOM 中 使用 Key 的作用和好处
在 patchVNode 比较新旧节点的差异时,通过源码调试,我们可以总结出:
当没有设置 Key 时,会多次更新 DOM 操作 和插入操作。但,当我们设置 Key 的时候, 在比较 updateChildren 中比较子节点的时候,因为 key 相同,没有更新 Dom操作, 只做了一次插入操作。所以设置Key 可以减少 dom 的操作,减少 diff 和渲染所需要的时间,提升了性能,大大的优化了 Dom 操作。
Vue 中模板编译的过程
先上一张图大致看一下整个流程
从图中可以看到 compile 是从 mount 后开始进行,整体逻辑大致分为三个部分
- 解析器(parse) - 将 模板字符串 转换成 AST 抽象语法树
- 优化器(optimize) - 对 AST 进行静态节点标记, 主要用来做虚拟 DOM 的渲染优化
- 代码生成器(generate) - 将 AST 抽象语法书转换为 render 函数的 js 字符串
这里我先解释一下什么是 AST 即 抽象语法树
AST(abstract syntax tree 抽象语法树), 是源代码的抽象语法结构的树状表现形式;
从代码上简单理解一下
<div class="name">AST</div>
//转成AST后会得到如下格式
[
{
"type": "tag",
"name": "div",
"attribs": {
"class": "name"
},
"children": [
{
"data": "AST",
"type": "text",
"next": null,
"startIndex": 18,
"prev": null,
"parent": "[Circular ~.0]",
"endIndex": 20
}
],
"next": null,
"startIndex": 0,
"prev": null,
"parent": null,
"endIndex": 26
}
]
AST会经过generate得到render函数,render的返回值是VNode;
解析器(parse)
parse 的目标是把 template 模板字符串转换成 AST 树,它是一种用 JavaScript 对象的形式来描述整个模板。那么整个 parse 的过程是利用正则表达式顺序解析模板,当解析到开始标签、闭合标签、文本的时候都会分别执行对应的回调函数,来达到构造 AST 树的目的;
优化器(optimize)
通过 optimize 把整个 AST 树中的每一个 AST 元素节点标记了 static 和 staticRoot, optimize 的过程,就是深度遍历这个 AST 树,去检测它的每一颗子树是不是静态节点,如果是静态节点则在每次重新渲染时不需要再重新生成节点
代码生成器(generate)
把抽象语法树生成字符串形式的 js 代码;
之后将 render 函数 通过 createFunction 函数 转换为 一个可以执行的函数,将 最后的 render 函数 挂载到 option 中,最后执行公共的 mount 函数。
总结: 首先通过parse将template解析成AST,其次optimize对解析出来的AST进行标记,最后generate将优化后的AST转换成可执行的代码;