虚拟 DOM: 由JavaScript对象来描述真实 DOM。
- VUE 内部的虚拟 DOM 是改造了一个开源库 Snabbdom。
- 真实 DOM 的操作耗能太大了。
虚拟DOM的好处:
- 维护视图和状态的关系。跟踪状态的变化。状态改变后不需要立即更新真实DOM。而是创建虚拟DOM树 最终来映射到真实DOM。
- 复杂视图情况下提升渲染性能
- 跨平台:
- 浏览器平台渲染dom
- 服务端渲染 ssr
- 原生应用(Weex / React Native )
- 小程序( mpvue/ uni-app )
虚拟 DOM 实现的核心是 diff 算法:
- diff 的过程就是【调用名为 patch 的函数,】比较新旧节点,将差异更新到真实 DOM。
- 通过 h 函数来创建虚拟 DOM。
- demo:
假如有个数组[1,2,3],然后在开始位置插入100.
没有设置key:diff算法先从新旧开始节点对比,因为sel和key(undefined)一样,它们被认为是相同节点,那么会对比他们的子节点,1和100不同,更新text。重复这一对比过程更新直到旧的数组遍历结束,此时新节点还剩一项3,做插入操作
设置key:diff算法先从新旧开始节点对比,因为key不一样(1和100不等),从最后往前开始对比,因为sel和key一样,他们的text也一样,那么不更新。重复这一对比过程更新直到旧的数组遍历结束,此时新节点还剩一项100,做插入操作
patch( oldVnode: Element | VNode,newVnode:VNode ) {
return newVnode
}
Snabbdom 中源码分析:
- init 注册模块,返回 patch 函数
- h 函数用来创建节点Vnode . Vnode 的本质就是一个 JavaScript 对象。
函数重载:
- 参数个数或参数类型不同的函数
- 函数名相同
- JavaScript 中没有重载的概念
- typescript 中有重载。
- patch 函数比较新旧节点,状态变化时将差异更新到真实 DOM 树。返回 newVnode
import {
init,
classModule,
propsModule,
styleModule,
eventListenersModule,
h,
} from "snabbdom";
const patch = init([
classModule,
propsModule,
styleModule,
eventListenersModule,
])
let vnode = h('div#aaa.myclass','hi虚拟dom');
let dom = document.querySelector('#app');
let oldVnode = patch(dom , vnode);
vnode = h('div#bbb.mybbbclass',[
h('h1','创建子节点h1'),
h('p','创建子节点p')
]);
patch(oldVnode,vnode)
patch(oldvalue,h('!'))
let my_vnode = h('div',[
h('h1',
{
style: { backgroundColor: 'red' },
'hello h1'
}
),
h('p',
{
on: { click: Handle},
'hello p'
}
)
])
function Handle() {
console.log('点击p');
}
h 函数:创建Vnode节点
export interface VNode {
sel: String | undefined,
data: VNode | undefined,
children: Array <VNode | String> | undefined,
text: String | undefined,
key: Key | Number,
}
patch 的内部实现:
- 比较新旧节点,状态变化时将差异更新到真实 DOM 树。返回 newVnode
实现思路: - 对比新旧vnode是否是相同节点【key节点唯一标识, sel节点选择器 】
- 如果当前vnode不是相同节点,就没有必要继续比较下面的子节点,直接删除旧节点对应的DOM元素,将新节点内容渲染到真实DOM。【比较的是虚拟DOM 和真实 DOM 之间存在映射关系的两个节点】
- 如果是相同节点:[节点标识和节点选择器都相同]
- 判断新的vnode是否有text,有并且与旧vnode的text不同,更新文本内容
- 如果新的vnode有children【children text互斥】,判断子节点是否有变化。⭐
function patch(oldVnode: Vnode | Element, vnode:VNode) {
if(!isVnode(oldVnode)) {
oldVnode = emptyNodeAt(oldVnode);
}
if(sameVnode(oldVnode,vnode)) {
patchVnode(oldVnode,vnode,insertedVnodeQueue);
}
else {
olddom = oldVnode.elm!;
parentdom = api.parentNode(olddom) as Node;
createElm(vnode, insertedVnodeQueue);
if(parentdom !== null) {
api.insertBefore(parentdom, vnode.elm!, api.nextSibling(olddom));
removeVnodes(parentdom, [oldVnode], 0, 0);
}
}
for(i = 0;i<insertedVnodeQueue.length; ++i) {
insertedVnodeQueue[i].data!.hook!.inser!(insertedVnodeQueue[i])
}
for(i=0;i < cbs.post.length; ++i) cbs.post([i]() i = 0;
return vnode;
}
function isVnode(oldVnode) {
return oldVnode.sel;
}
function emptyNodeAt(elm: Element) {
const id = elm.id? '#'+id :'';
const cs = elm.className? '.'+ elm.className.split(' ').join('.') : '';
return vnode(api.tagName(elm).toLowerCase()+id+cs, {}, [], undefined, elm);
}
function sameVnode(oldVnode,vnode) {
return oldVnode.key === vnode.key && oldVnode.sel === vnode.sel;
}
function patchVnode(oldVnode: VNode|Element,vnode:VNode) {
if(oldvalue === vnode) return;
if(oldvnode.text !== vnode.text) {
}
if(oldVnode.children !== vnode.children) {
updateChilren();
}
}
function createElm(vnode, insertedVnodeQueue): Node {
const children = vnode.children;
const sel = vnode.sel;
if(sel === '!') {
if(vnode.text === undefined) {
vnode.text = ''
}
vnode.elm = api.createComment(vnode.text!);
}
else if(sel !== undefined) {
if(vnode.children) {
for(i = 0; i< vnode.children.length; ++i) {
if(vnode.children[i] !== null) {
api.appendChild(elm, createElm(vnode.children[i] as Vnode, insertedVnodeQueue))
}
}
}else if( typeof vnode.text === 'string' || typeof vnode.text === 'number' ) {
api.appendChild(elm, api.createTextNode(vnode.text))
}
}else {
vnode.elm = api.createTextNode(vnode.text!);
}
return vnode.elm
}
function removeVnodes(parentElm: Node, vnodes:VNode[], startIdx:Number, endIdx: Number): void {
for(; startIdx <= endIdx; ++startIdx) {
const ch = vnodes[startIdx];
if(ch!==null) {
if(ch.sel) {
}else {
api.removeChild(parentElm, ch.elm!)
}
}
}
}