什么是虚拟 DOM
虚拟 DOM(Virtual DOM) 是使用 JavaScript 对象来描述 DOM,虚拟 DOM 的本质就是 JavaScript 对 象,使用 JavaScript 对象来描述 DOM 的结构。应用的各种状态变化首先作用于虚拟 DOM,最终映射 到 DOM。Vue.js 中的虚拟 DOM 借鉴了 Snabbdom,并添加了一些 Vue.js 中的特性,例如:指令和组 件机制。
Vue 1.x 中细粒度监测数据的变化,每一个属性对应一个 watcher,开销太大Vue 2.x 中每个组件对应一 个 watcher,状态变化通知到组件,再引入虚拟 DOM 进行比对和渲染
为什么要使用虚拟 DOM
使用虚拟 DOM,可以避免用户直接操作 DOM,开发过程关注在业务代码的实现,不需要关注如 何操作 DOM,从而提高开发效率
作为一个中间层可以跨平台,除了 Web 平台外,还支持 SSR、Weex。 关于性能方面,在首次渲染的时候肯定不如直接操作 DOM,因为要维护一层额外的虚拟 DOM, 如果后续有频繁操作 DOM 的操作,这个时候可能会有性能的提升,虚拟 DOM 在更新真实 DOM 之前会通过 Diff 算法对比新旧两个虚拟 DOM 树的差异,最终把差异更新到真实 DOM
Vue.js 中的虚拟 DOM
- 演示 render 中的 h 函数
- h 函数就是 createElement()
const vm = new Vue({
el: '#app',
render (h) {
// h(tag, data, children)
// return h('h1', this.msg)
// return h('h1', { domProps: { innerHTML: this.msg } }) // return h('h1', { attrs: { id: 'title' } }, this.msg) const vnode = h(
'h1', {
attrs: { id: 'title' }
},
this.msg )
console.log(vnode)
return vnode
},
data: {
msg: 'Hello Vue'
} })
虚拟 DOM 创建过程
createElement
功能
createElement() 函数,用来创建虚拟节点 (VNode),我们的 render 函数中的参数 h,就是 createElement()
render(h) {
// 此处的 h 就是 vm.$createElement return h('h1', this.msg)
}
定义
在 vm._render() 中调用了,用户传递的或者编译生成的 render 函数,这个时候传递了
- createElement src/core/instance/render.js
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
// normalization is always applied for the public version, used in
// user-written render functions.
vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
vm.c vm.
c
r
e
a
t
e
E
l
e
m
e
n
t
c
r
e
a
t
e
E
l
e
m
e
n
t
v
m
.
c
在
编
译
生
成
的
r
e
n
d
e
r
函
数
内
部
会
调
用
,
v
m
.
createElement createElement vm.c 在编译生成的 render 函数内部会调用,vm.
createElementcreateElementvm.c在编译生成的render函数内部会调用,vm.createElement 在用户传入的 render 函数内部调用。当用户传入 render 函数的时候,要对用户传入的参数做处理
- src/core/vdom/create-element.js
执行完 createElement 之后创建好了 VNode,把创建好的 VNode 传递给 vm._update() 继续处理
update
功能
内部调用 vm.patch() 把虚拟 DOM 转换成真实 DOM
定义
src/core/instance/lifecycle.js
patch 函数初始化 功能
对比两个 VNode 的差异,把差异更新到真实 DOM。如果是首次渲染的话,会把真实 DOM 先转换成 VNode
Snabbdom 中 patch 函数的初始化
src/snabbdom.ts
export function init (modules: Array<Partial<Module>>, domApi?: DOMAPI) {
return function patch (oldVnode: VNode | Element, vnode: VNode): VNode {
}
}
vnode
export function vnode (sel: string | undefined,
data: any | undefined,
children: Array<VNode | string> | undefined,
text: string | undefined,
elm: Element | Text | undefined): VNode {
const key = data === undefined ? undefined : data.key return { sel, data, children, text, elm, key }
}
Vue.js 中 patch 函数的初始化
src/platforms/web/runtime/index.js
patch 函数执行过程
createElm
把 VNode 转换成真实 DOM,插入到 DOM 树上
patchVnode
updateChildren
updateChildren 和 Snabbdom 中的 updateChildren 整体算法一致,这里就不再展开了。我们再来看 下它处理过程中 key 的作用,再 patch 函数中,调用 patchVnode 之前,会首先调用 sameVnode()判 断当前的新老 VNode 是否是相同节点,sameVnode() 中会首先判断 key 是否相同。
通过下面代码来体会 key 的作用
div id="app">
<button @click="handler">按钮</button>
<ul>
<li v-for="value in arr">{{value}}</li>
</ul>
</div>
<script src="../../dist/vue.js"></script> <script>
const vm = new Vue({
el: '#app',
data: {
arr: ['a', 'b', 'c', 'd']
},
methods: {
handler () {
this.arr = ['a', 'x', 'b', 'c', 'd'] }
} })
</script>
当没有设置 key 的时候
在 updateChildren 中比较子节点的时候,会做三次更新 DOM 操作和一次插入 DOM 的操作
当设置 key 的时候
在 updateChildren 中比较子节点的时候,因为 oldVnode 的子节点的 b,c,d 和 newVnode 的 x,b,c 的 key 相同,所以只做比较,没有更新 DOM 的操作,当遍历完毕后,会再把 x 插入到 DOM 上DOM 操 作只有一次插入操作。