Vue源码解析-数据驱动

资源列表:
慕课网:Vue.js 源码全方位深入解析
Vue.js 技术揭秘
知乎:染陌同学 VirtualDOM与diff(Vue实现)

Vue源码解析-数据驱动

Introduction

现代前端框架抛弃里如使用 JQuery 等前端库直接修改 DOM,而是由数据驱动视图。
本章节主要研究 Vue 中模板和数据如果渲染成最红的 DOM

1
2
3
4
5
6
7
8
9
10
<div id="app">
{{ message }}
</div>

var app = new Vue({
el: '#app',
data: {
message: 'Hello Vue!'
}
})

new Vue 发生了什么

new 关键字在 JavaScript 中实例化一个对象,而 Vue 本质是一个 Function 模拟的类,在其上扩展静态方法和原型方法。

源码,在src/core/instance/index.js

1
2
3
4
5
6
7
8
function Vue (options) {
if (process.env.NODE_ENV !== 'production' &&
!(this instanceof Vue)
) {
warn('Vue is a constructor and should be called with the `new` keyword')
}
this._init(options)
}

最后 new Vue() 调用的是 this._init 方法。

源码:src/core/instance/init.js

Vue 初始化主要做了几件事

  • 合并配置
  • 初始化生命周期
  • 初始化事件中心
  • 初始化渲染
  • 初始化 datapropscomputedwatcher

最后判断是否有 el 属性,如果有则调用 vm.$mount 方法挂载 vm,挂载的目标就是把模板渲染成最终的 DOM

1
2
3
4
// 68
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}

el、template

Vue 文档

el

template

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<body>
<div id="app">
<hello-world></hello-world>
</div>

<script>
Vue.component('hello-world', {
template: "<h1>hello world</h1>"
})
var app = new Vue({
el: '#app',
data: {
}
})
</script>
</body>

Vue 实例挂载的实现

$mount 方法的实现是和平台、构建方式相关,不同平台如 weex 有不同的 $mount 实现,底层使用的 API 也不尽相同,这里研究 compiler 的实现。

源码: src/platform/web/entry-runtime-with-compiler.js

  • el 做了限制,不能挂载到 bodyhtml 根节点上
  • 如果没有render 方法,则会把 el 或者 template 字符串转成 render 方法,在 Vue 2.0 版本中,所有 Vue 的组件的渲染最终都需要 render 方法

$mount 方法实际上会去调用 mountComponent 方法,

源码:src/core/instance/lifecycle.js

  • 先调用 vm._render 方法生成虚拟 Node
  • 再实例化一个渲染 Watcher,在它的回调函数中调用了 updateComponent
  • 最后调用 vm._update 更新 DOM
  • 最后的判断,vm._isMountedtrue, 表示已经挂载,同时执行 mounted 钩子函数。

⚠️ 这里注意 vm.$vnode 表示 Vue 实例的父虚拟 Node,所以它为 Null 则表示当前是根 Vue 的实例。

render

它用来把实例渲染成一个虚拟 Node

源码: src/core/instance/render.js

Vue 文档中的 render 方法,第一个参数 createElement,用来创建 Vnode,模板中的写法:

1
2
3
<div id="app">
{{ message }}
</div>

等同于 render 函数的写法

1
2
3
4
5
6
7
render: function (createElement) {
return createElement('div', {
attrs: {
id: 'app'
},
}, this.message)
}

📌 Vue 2.0 相比 Vue 1.0 最大的升级就是利用了 Virtual DOM

Virtual DOM

为什么要使用虚拟 DOM
在文档中,一个真实的 DOM 元素是很庞大的,直接修改该元素成本很高,使用 虚拟 DOM,只需要维护一个真实 DOM 映射的对象,这要比维护真实 DOM 成本小得多。

Virtual DOM 是用 VNode 这么一个 Class 去描述

定义在 src/core/vdom/vnode.js

VNode 是对真实 DOM 的一种抽象描述,核心无非就是几个关键属性,标签名、数据、子节点、键值等。

映射到真实的 DOM 实际上要经历 VNodecreatediffpatch 等过程。

createElement

Vue.js 利用 createElement 方法创建 VNode

它定义在 src/core/vdom/create-elemenet.js 中:

每个 VNode 都有 children,每个 children 也是一个 VNode,这样就形成了 VNode tree

最后通过 vm._update, 将 VNode 渲染成真实的 DOM 并渲染出来。

update

该方法被调用的时机有两次,一次是首次渲染,一次是在数据更新的时候。

源码: src/core/instance/lifecycle.js

核心是调用了 vm.__patch__,该方法根据平台的不同有区别。

在该方法中实现了 diff 算法,具体参考: Vue.js 技术揭秘

diff算法

diff 算法是通过同层的树节点进行比较而非对树进行逐层搜索遍历,所以时间复杂度只有 O(n),是一种高效的算法。

判断为同一个 VNode的依据

1
2
3
4
5
6
7
8
9
10
11
12
13
/*
判断两个VNode节点是否是同一个节点,需要满足以下条件
key相同
tag(当前节点的标签名)相同
isComment(是否为注释节点)相同
是否data(当前节点对应的对象,包含了具体的一些数据信息,是一个VNodeData类型,可以参考VNodeData类型中的数据信息)都有定义
当标签是<input>的时候,type必须相同
*/

/*
判断当标签是<input>的时候,type是否相同
某些浏览器不支持动态修改<input>类型,所以他们被视为不同类型
*/

如何进行对比?

  • 1.如果新旧 VNode 都是静态的,同时它们的 key 相同(代表同一节点),并且新的 VNodeclone 或者是标记了once(标记 v-once 属性,只渲染一次),那么只需要替换 elm 以及 componentInstance 即可。

  • 2.新老节点均有 children 子节点,则对子节点进行diff 操作,调用 updateChildren这个updateChildren 也是 diff 的核心。

  • 3.如果老节点没有子节点而新节点存在子节点,先清空老节点 DOM 的文本内容,然后为当前 DOM 节点加入子节点。

  • 4.当新节点没有子节点而老节点有子节点的时候,则移除该 DOM 节点的所有子节点。

  • 5.当新老节点都无子节点的时候,只是文本的替换。

其他对比细节参考博客文章。

DOM操作

我们只是将虚拟 DOM 映射成了真实的 DOM。那如何给这些 DOM 加入 attrclassstyleDOM 属性呢?

需要在 create 以及 update 钩子被调用时更新 DOMattr 属性即可

总结

模板和数据如何渲染成最终的 DOM 的过程分析完毕了,我们可以通过下图更直观地看到从初始化 Vue 到最终渲染的整个过程。