什么是虚拟 DOM
- 虚拟 DOM(Virtual DOM) 是使用 JavaScript 对象来描述真实 DOM
- Vue.js 中的虚拟 DOM 借鉴了 Snabbdom,并添加了一些 Vue.js 中的特性
- 例如:指令 和 组件机制 虚拟 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以及DOM的浏览器兼容问题,从而提高开发效率
- 作为一个中间层可以跨平台,除了 Web 平台外,还支持 SSR(服务端渲染)、Weex(跨移动端平台)
- 虚拟 DOM不一定可以提供性能
- 首次渲染的时候回增加开销,因为要维护一层额外的虚拟 DOM
- 复杂视图下可提升渲染性能 关于性能方面,在首次渲染的时候肯定不如直接操作 DOM,因为要维护一层额外的虚拟 DOM, 如果后续有频繁操作 DOM 的操作,这个时候可能会有性能的提升,虚拟 DOM 在更新真实 DOM 之前会通过 Diff 算法对比新旧两个虚拟 DOM 树的差异,最终把差异更新到真实 DOM
Vue.js 中的虚拟 DOM
- render 中的 h 函数用法
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'
}
})
-
h 函数 vm.$createElement(tag, data, children, normalizeChildren)
- tag 标签名称或组件对象
- data 描述tag,可以设置 DOM 的属性或者标签的属性
- children tag中的文本内容或者子节点
-
h函数返回结果 - VNode对象 核心特性
- tag
- data
- children
- text
- elm(真实DOM)
- key
VNode 创建过程
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)
vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
vm.c 和 vm.$createElement 内部都调用了createElement,不同的是 vm.c 在编译生成的 render 函数内部会调用,vm.$createElement 在用户传入的 render 函数内部调用。
当用户传入 render 函数的时候,要对用户传入的参数做处理
- src/core/vdom/create-element.js
export function createElement (
context: Component,
tag: any,
data: any,
children: any,
normalizationType: any,
alwaysNormalize: boolean
): VNode | Array<VNode> {
// 判断第三个参数
// 如果 data 是数组或者原始值的话就是 children,实现类似函数重载的机制
if (Array.isArray(data) || isPrimitive(data)) {
normalizationType = children
children = data
data = undefined
}
if (isTrue(alwaysNormalize)) {
normalizationType = ALWAYS_NORMALIZE
}
return _createElement(context, tag, data, children, normalizationType)
}
在_createElement函数中创建VNode对象
执行完 createElement 之后创建好了 VNode,把创建好的 VNode 传递给 vm._update() 继续处理
update
功能
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
_update方法作用是把VNode渲染成真实DOM,首次渲染会调用,数据更新会调用
内部调用 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 {
}
}
Vue.js 中 patch 函数的初始化
- src/platforms/web/runtime/index.js
import { patch } from './patch'
Vue.prototype.__patch__ = inBrowser ? patch : noop
- src/platforms/web/runtime/patch.js
import * as nodeOps from 'web/runtime/node-ops'
import { createPatchFunction } from 'core/vdom/patch'
import baseModules from 'core/vdom/modules/index'
import platformModules from 'web/runtime/modules/index'
// the directive module should be applied last, after all
// built-in modules have been applied.
const modules = platformModules.concat(baseModules)
// nodeOps 操作dom的API
// modules 操作属性样式事件、指令、ref等
export const patch: Function = createPatchFunction({ nodeOps, modules })
- src/core/vdom/patch.js 函数柯里化
export function createPatchFunction (backend) {
......
......
......
return function patch (oldVnode, vnode, hydrating, removeOnly) {
}
}
patch 函数执行过程
createElm
把 VNode 转换成真实 DOM,插入到 DOM 树上
patchVnode
updateChildren
updateChildren 和 Snabbdom 中的 updateChildren 整体算法一致。它处理过程中 key 的作用,在 patch 函数中,调用 patchVnode 之前,会首先调用 sameVnode()判 断当前的新老 VNode 是否是相同节点,sameVnode() 中会首先判断 key 是否相同。