虚拟dom
概念
虚拟DOM(Virtual DOM)是对DOM的JS抽象表示,它们是JS对象,能够描述DOM结构和关系。应用的各种状态变化会作用于虚拟DOM,最终映射到DOM上。
js在操作数据时希望有个中间层,这个中间层就是虚拟dom操作,当我们对中间层进行操作的时候我希望可以直接转换成dom,那么这中间的两个操作分别是数据响应式和打补丁(patch)
工作机制: js操作=》dom 数据的变更可以直接通过响应式去修改虚拟dom set调用了更新,调用了watch.js中的run方法,其实就是重新计算了虚拟dom的值,这时候就得到了一个新的虚拟dom的值和一个老的虚拟dom的值,这时候就需要在这里面做个对比,通过对比得到最小的虚拟操作的数就可以了
dom =》 js操作 如果用户和界面有交互,想改变值,就会通过一些事件和交互来触发回调函数,到时候这些回调函数又可以影响数据状态
体验虚拟DOM
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>使用snabbdom实现更新</title>
</head>
<body>
<div id="app"></div>
<script src="node_modules/snabbdom/dist/snabbdom.js"></script>
<script src="node_modules/snabbdom/dist/snabbdom-style.js"></script>
<script>
const obj = {}
const {init, h} = snabbdom
// 1.获取patch函数,对比两个虚拟dom,执行dom操作
const patch = init([
snabbdom_style.default
])
let vnode;// 保存之前旧的虚拟dom
function defineReactive(obj, key, val) {
// 对传入obj进行访问拦截
Object.defineProperty(obj, key, {
get() {
console.log('get ' + key);
return val
},
set(newVal) {
if (newVal !== val) {
console.log('set ' + key + ':' + newVal);
val = newVal
// 更新函数
update()
}
}
})
}
// 使用虚拟dom做更新
function update() {
vnode = patch(vnode, h('div#app', {style:{color:'red'}},obj.foo))
}
defineReactive(obj, 'foo', new Date().toLocaleTimeString())
// 执行初始化
vnode = patch(app, h('div#app', obj.foo))
console.log(vnode);
setInterval(() => {
obj.foo = new Date().toLocaleTimeString()
}, 1000);
</script>
</body>
</html>
npm i snabbdom
优点
- 虚拟DOM轻量、快速:当它们发生变化时通过新旧虚拟DOM比对可以得到最小DOM操作量,从而提升性能
patch(vnode, h(‘div#app’, obj.foo)) - 跨平台:将虚拟dom更新转换为不同运行时特殊操作实现跨平台
const patch = init([snabbdom_style.default])
patch(vnode, h(‘div#app’, {style:{color:‘red’}}, obj.foo))
-兼容性:还可以加入兼容性代码增强操作的兼容性
必要性
vue 1.0中有细粒度的数据变化侦测,它是不需要虚拟DOM的,但是细粒度造成了大量开销,这对于大型项目来说是不可接受的。因此,vue 2.0选择了中等粒度的解决方案,每一个组件一个watcher实例,这样状态变化时只能通知到组件,再通过引入虚拟DOM去进行比对和渲染。
整体流程
mountComponent() core/instance/lifecycle.js
渲染、更新组件
// 定义更新函数
const updateComponent = () => {
// 实际调用是在lifeCycleMixin中定义的_update和renderMixin中定义的_render
vm._update(vm._render(), hydrating)
}
_render core/instance/render.js
生成虚拟dom
_update core\instance\lifecycle.js
update负责更新dom,转换vnode为dom (将虚拟dom变化成真实dom)
patch() platforms/web/runtime/index.js
__patch__是在平台特有代码中指定的
Vue.prototype.patch = inBrowser ? patch : noop
patch获取
patch是createPatchFunction的返回值,传递nodeOps和modules是web平台特别实现
export const patch: Function = createPatchFunction({ nodeOps, modules })
- platforms\web\runtime\node-ops.js
定义各种原生dom基础操作方法 - platforms\web\runtime\modules\index.js
modules 定义了属性更新实现
流程:watcher.run() => componentUpdate() => render() => update() => patch()
patch实现
patch core\vdom\patch.js
首先进行树级别比较,可能有三种情况:增删改。
- new VNode不存在就删;
- old VNode不存在就增;
- 都存在就执行diff执行更新
patchVnode(diff算法)
比较两个VNode,包括三种类型操作:属性更新、文本更新、子节点更新
具体规则如下:
- 新老节点均有children子节点,则对子节点进行diff操作,调用updateChildren
- .如果老节点没有子节点而新节点有子节点,先清空老节点的文本内容,然后为其新增子节点。
- 当新节点没有子节点而老节点有子节点的时候,则移除该节点的所有子节点。
- 当新老节点都无子节点的时候,只是文本的替换。
updateChildren
updateChildren主要作用是用一种较高效的方式比对新旧两个VNode的children得出最小操作补丁。执行一个双循环是传统方式,vue中针对web场景特点做了特别的算法优化,我们看图说话:
在新老两组VNode节点的左右头尾两侧都有一个变量标记,在遍历过程中这几个变量都会向中间靠拢。当oldStartIdx > oldEndIdx或者newStartIdx > newEndIdx时结束循环。
下面是遍历规则:
首先,oldStartVnode、oldEndVnode与newStartVnode、newEndVnode两两交叉比较,共有4种比较方法。
当 oldStartVnode和newStartVnode 或者 oldEndVnode和newEndVnode 满足sameVnode,直接将该VNode节点进行patchVnode即可,不需再遍历就完成了一次循环。如下图,
如果oldStartVnode与newEndVnode满足sameVnode。说明oldStartVnode已经跑到了oldEndVnode后面去了,进行patchVnode的同时还需要将真实DOM节点移动到oldEndVnode的后面。
如果oldEndVnode与newStartVnode满足sameVnode,说明oldEndVnode跑到了oldStartVnode的前面,进行patchVnode的同时要将oldEndVnode对应DOM移动到oldStartVnode对应DOM的前面。
如果以上情况均不符合,则在old VNode中找与newStartVnode满足sameVnode的vnodeToMove,若存在执行patchVnode,同时将vnodeToMove对应DOM移动到oldStartVnode对应的DOM的前面。
当然也有可能newStartVnode在old VNode节点中找不到一致的key,或者是即便key相同却不是sameVnode,这个时候会调用createElm创建一个新的DOM节点。
至此循环结束,但是我们还需要处理剩下的节点。
当结束时oldStartIdx > oldEndIdx,这个时候旧的VNode节点已经遍历完了,但是新的节点还没有。说明了新的VNode节点实际上比老的VNode节点多,需要将剩下的VNode对应的DOM插入到真实DOM中,此时调用addVnodes(批量调用createElm接口)。
但是,当结束时newStartIdx > newEndIdx时,说明新的VNode节点已经遍历完了,但是老的节点还有剩余,需要从文档中删 的节点删除。