资源列表:
慕课网:Vue.js 源码全方位深入解析
Vue.js 技术揭秘
知乎:染陌同学 VirtualDOM与diff(Vue实现)
Vue源码解析-数据驱动
Introduction
现代前端框架抛弃里如使用 JQuery
等前端库直接修改 DOM
,而是由数据驱动视图。
本章节主要研究 Vue
中模板和数据如果渲染成最红的 DOM
。
1 | <div id="app"> |
new Vue 发生了什么
new
关键字在 JavaScript
中实例化一个对象,而 Vue
本质是一个 Function
模拟的类,在其上扩展静态方法和原型方法。
源码,在
src/core/instance/index.js
1 | function Vue (options) { |
最后 new Vue()
调用的是 this._init
方法。
源码:
src/core/instance/init.js
。
Vue
初始化主要做了几件事
- 合并配置
- 初始化生命周期
- 初始化事件中心
- 初始化渲染
- 初始化
data
、props
、computed
、watcher
等
最后判断是否有 el
属性,如果有则调用 vm.$mount
方法挂载 vm
,挂载的目标就是把模板渲染成最终的 DOM
1 | // 68 |
el、template
el
template
1 | <body> |
Vue 实例挂载的实现
$mount
方法的实现是和平台、构建方式相关,不同平台如 weex
有不同的 $mount
实现,底层使用的 API
也不尽相同,这里研究 compiler
的实现。
源码: src/platform/web/entry-runtime-with-compiler.js
- 对
el
做了限制,不能挂载到body
、html
根节点上 如果没有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._isMounted
为true
, 表示已经挂载,同时执行mounted
钩子函数。
⚠️ 这里注意
vm.$vnode
表示Vue
实例的父虚拟Node
,所以它为Null
则表示当前是根Vue
的实例。
render
它用来把实例渲染成一个虚拟 Node
,
源码: src/core/instance/render.js
Vue 文档中的 render 方法,第一个参数 createElement,用来创建 Vnode,模板中的写法:
1 | <div id="app"> |
等同于 render
函数的写法
1 | render: function (createElement) { |
📌 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
实际上要经历 VNode
的 create
、diff
、patch
等过程。
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 | /* |
如何进行对比?
1.如果新旧
VNode
都是静态的,同时它们的key
相同(代表同一节点),并且新的VNode
是clone
或者是标记了once
(标记v-once
属性,只渲染一次),那么只需要替换elm
以及componentInstance
即可。2.新老节点均有
children
子节点,则对子节点进行diff
操作,调用updateChildren
,这个updateChildren
也是diff
的核心。3.如果老节点没有子节点而新节点存在子节点,先清空老节点
DOM
的文本内容,然后为当前DOM
节点加入子节点。4.当新节点没有子节点而老节点有子节点的时候,则移除该
DOM
节点的所有子节点。5.当新老节点都无子节点的时候,只是文本的替换。
其他对比细节参考博客文章。
DOM操作
我们只是将虚拟 DOM
映射成了真实的 DOM
。那如何给这些 DOM
加入 attr
、class
、style
等 DOM
属性呢?
需要在 create
以及 update
钩子被调用时更新 DOM
的 attr
属性即可
总结
模板和数据如何渲染成最终的 DOM
的过程分析完毕了,我们可以通过下图更直观地看到从初始化 Vue
到最终渲染的整个过程。