虚拟DOM(Virtual DOM)是对DOM的JS抽象表示,它们是JS对象,能够描述DOM结构和关系。应⽤
的各种状态变化会作⽤于虚拟DOM,最终映射到DOM上。
优点:
- 虚拟DOM轻量、快速:当它们发⽣变化时通过新旧虚拟DOM⽐对可以得到最⼩DOM操作量,配合异步更新策略减少刷新频率,从⽽提升性能
- 跨平台:将虚拟dom更新转换为不同运⾏时特殊操作实现跨平台
- 兼容性:还可以加⼊兼容性代码增强操作的兼容性
<div id="app"></div>
<!--安装并引⼊snabbdom-->
<script src="../../node_modules/snabbdom/dist/snabbdom.js"></script>
<script>
// 之前编写的响应式函数
function defineReactive(obj, key, val) {
Object.defineProperty(obj, key, {
get() {
return val
},
set(newVal) {
val = newVal
// 通知更新
update()
}
})
}
// 导⼊patch的⼯⼚init,h是产⽣vnode的⼯⼚
const { init, h } = snabbdom
// 获取patch函数
const patch = init([])
// 上次vnode,由patch()返回
let vnode;
// 更新函数,将数据操作转换为dom操作,返回新vnode
function update() {
if (!vnode) {
// 初始化,没有上次vnode,传⼊宿主元素和vnode
vnode = patch(app, render())
}
else {
// 更新,传⼊新旧vnode对⽐并做更新
vnode = patch(vnode, render())
}
}
// 渲染函数,返回vnode描述dom结构
function render() {
return h('div', obj.foo)
}
// 数据
const obj = {}
// 定义响应式
defineReactive(obj, 'foo', '')
// 赋⼀个⽇期作为初始值
obj.foo = new Date().toLocaleTimeString()
// 定时改变数据,更新函数会重新执⾏
setInterval(() => {
obj.foo = new Date().toLocaleTimeString()
}, 1000);
</script>
</body>
整体流程
core/instance/lifecycle.js
// 定义更新函数
const updateComponent = () => {
// 实际调⽤是在lifeCycleMixin中定义的_update和renderMixin中定义的_render
vm._update(vm._render(), hydrating)
}
core/instance/render.js
_render() ⽣成虚拟dom
core\instance\lifecycle.js
_update()负责更新dom,转换vnode为dom
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
const vm: Component = this
const prevEl = vm.$el
const prevVnode = vm._vnode
const restoreActiveInstance = setActiveInstance(vm)
vm._vnode = vnode
// Vue.prototype.__patch__ is injected in entry points
// based on the rendering backend used.
if (!prevVnode) {
// initial render
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
} else {
// updates
vm.$el = vm.__patch__(prevVnode, vnode)
}
restoreActiveInstance()
// update __vue__ reference
if (prevEl) {
prevEl.__vue__ = null
}
if (vm.$el) {
vm.$el.__vue__ = vm
}
// if parent is an HOC, update its $el as well
if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
vm.$parent.$el = vm.$el
}
// updated hook is called by the scheduler to ensure that children are
// updated in a parent's updated hook.
}
platforms/web/runtime/index.js
patch是在平台特有代码中指定的
Vue.prototype.__patch__ = inBrowser ? patch : noop
patch是createPatchFunction的返回值,传递nodeOps和modules是web平台特别实现
// 传入web平台特有节点操作和属性操作
export const patch: Function = createPatchFunction({ nodeOps, modules })
- nodeOps 定义各种原⽣dom基础操作⽅法
- modules 定义了属性更新实现
core\vdom\patch.js
⾸先进⾏树级别⽐较,可能有三种情况:增删改
- new VNode不存在就删;
- old VNode不存在就增;
- 都存在就执⾏diff执⾏更新
patchVnode
⽐较两个VNode,包括三种类型操作:属性更新、⽂本更新、⼦节点更新
具体规则如下:
1. 新⽼节点均有children⼦节点,则对⼦节点进⾏diff操作,调⽤updateChildren
2. 如果新节点有⼦节点⽽⽼节点没有⼦节点,先清空⽼节点的⽂本内容,然后为其新增⼦节点。
3. 当新节点没有⼦节点⽽⽼节点有⼦节点的时候,则移除该节点的所有⼦节点。
4. 当新⽼节点都⽆⼦节点的时候,只是⽂本的替换。
updateChildren
updateChildren主要作⽤是⽤⼀种较⾼效的⽅式⽐对新旧两个VNode的children得出最⼩操作补丁。执
⾏⼀个双循环是传统⽅式,vue中针对web场景特点做了特别的算法优化
在新⽼两组VNode节点的左右头尾两侧都有⼀个变量标记,在遍历过程中这⼏个变量都会向中间靠拢。
当oldStartIdx > oldEndIdx或者newStartIdx > newEndIdx时结束循环。