虚拟DOM和diff算法
snabbdom简介:
snabbdom是瑞典语单词,单词原意:"速度";
snabbdom是著名的虚拟DOM库,是diff算法的鼻祖,Vue源码借鉴了snabbdom;
snabbdom库是DOM库,当然不能在node.js环境运行,所以我们需要搭建webpack和web
虚拟DOM和H函数
虚拟DOM:
用javaScript对象描述DOM的层次结构。DOM中的一切属性都在虚拟DOM中有对应的属性
真实DOM
<div class="box">
<h3>我是一个标题</h3>
<ul>
<li>牛奶</li>
<li>咖啡</li>
<li>可乐</li>
</ul>
</div>
虚拟DOM
{
"sel":"div", // 对应真实DOM的div标签
"data":{ // 对应真实DOM的div的属性
"class":{"box":true}
},
"children":[ // 对应真实DOM的div的children子元素
{
"sel":"h3", // 对应真实DOM的h3标签
"data":{}, // 对应真实DOM的h3的属性
"text":"我是一个标题" // 对应真实DOM的h3的内容
},
{
"sel":"ul", // 对应真实DOM的ul标签
"data":{}, // 对应真实DOM的ul的属性
"children":[ // 对应真实DOM的ul的children元素
{"sel":"li","data":{},"text":"牛奶"},
{"sel":"li","data":{},"text":"咖啡"},
{"sel":"li","data":{},"text":"可乐"},
]
}
]
}
diff是发生在虚拟DOM上的
新虚拟DOM和老虚拟DOM进行diff(精细化比较),算出应该如何最小量更新,最后反映到真正的DOM上。
旧虚拟DOM
{
"sel":"div",
"data":{
"class":{"box":true}
},
"children":[
{
"sel":"h3",
"text":"我是一个标题"
},
{
"sel":"ul",
"data":{},
"children":[
{"sel":"li","data":{},"text":"牛奶"},
{"sel":"li","data":{},"text":"咖啡"},
{"sel":"li","data":{},"text":"可乐"},
]
}
]
}
新的虚拟DOM和旧的虚拟DOM进行对比,这就是diff算法,最后反映到真实的DOM上
新虚拟DOM
{
"sel":"div",
"data":{
"class":{"box":true}
},
"children":[
{
"sel":"h3",
"text":"我是一个标题"
},
{
"sel":"span",
"text":"我是一个新的span"
},
{
"sel":"ul",
"data":{},
"children":[
{"sel":"li","data":{},"text":"牛奶"},
{"sel":"li","data":{},"text":"咖啡"},
{"sel":"li","data":{},"text":"可乐"},
{"sel":"li","data":{},"text":"雪碧"},
]
}
]
}
在diff算法的时候,他会发现新的虚拟DOM和旧的虚拟DOM的不同,然后找到不同的地方,发现新的虚拟DOM内的div内新建了个span,他就会命令DOM操作createElement创建节点,然后appendChild添加节点。下面的ul内的li也是一样
DOM如何变为虚拟DOM
这个属于模板编译原理范畴
虚拟DOM如何被渲染函数(h函数)生产
h函数用来产生虚拟节点(模板编译)
h 函数用来产生虚拟节点(vnode)v:virtual表示虚拟的意思,node:节点
比如这样调用h函数:
h('a',{props:{href:'https://www.baidu.com'}},'百度')
a:表示需要创建的节点标签
props:表示标签的属性
百度:表示显示的文本
将得到这样的虚拟节点:
{“sel":"a","data":{"props":{href:'https://www.baidu.com'}},"text":"百度"}
真实的DOM节点
<a href="https://www.baidu.com">百度</a>
一个虚拟节点的属性
children:表示子元素,如果是undfined就是表示他没有子元素
data:表示他的属性 ,calss style...
elm:表示这个元素对应的真实DOM节点,如果是undfined就说明虚拟DOM还没上树
key:表示这个节点的唯一标识
sel:表示选择器 select
text:表示这个虚拟节点的文本内容
h函数可以嵌套使用,从而得到虚拟DOM数(重要)
h函数是可以嵌套的比如
h('ul',{},[
h('li',{},'牛奶'),
h('li',{},'咖啡'),
h('li',{},'可乐')
])
虚拟DOM树
{
"sel":"ul",
"data":{},
"children":[
{
"sel":"li",
"text":"牛奶"
},
{
"sel":"li",
"text":"咖啡"
},
{
"sel":"li",
"text":"可乐"
},
]
}
使用h函数简单实现真实DOM树
第一步:先引入snabbdom里面的方法
import {
init,
classModule,
propsModule,
styleModule,
eventListenersModule,
h,
} from "snabbdom";
// 创建patch函数
var patch = init([classModule, propsModule, styleModule, eventListenersModule])
//创建虚拟节点
const myVnode3 = h('ul', [
h('li', '牛奶'),
h('li', [
h('div', [
h('p', '咖啡'),
h('p', '雪碧')
])
]),
h('li', [h('p', '可乐')])
])
// 让虚拟节点上树
const container = document.getElementById('container')
patch(container, myVnode3)
diff算法原理
手写一个简单的diff算法
1、先创建一个h.js
import vnode from "./vnode"
// 编写一个低配版本的h函数,这个函数必须接受3个参数,缺一不可,
// 相当于它的重载功能弱
// 也就是说,调用的时候形态必须是下面的三种之一
// 形态一:h('div',{},'文字')
// 形态二:h('div',{},[])
// 形态三:h('div',{},h())
/**
*这里只写了三种情况,因为我们只是简单的使用一个h函数
*
*/
export default function (sel, data, c) {
// 这个函数用到了递归,自己调用自己
/**
*sel必须是选择器
*data必须是一个对象
*c可以是string,array,object
*/
// console.log(Array.isArray(c));
// 检查参数的个数
if (arguments.length != 3)
/**
*先判断传过来的数据长度是否等于3位 因为我们只判断3位
*/
throw new Error('对不起,h函数必须传入3个参数,我们是低配版 的h 函数');
// 检查c的类型
if (typeof c === 'string' || typeof c === 'number') {
// 说明现在调用h函数是形态一
// 如果c的类型是string 或者number 就说明传过来的是文本
return vnode(sel, data, undefined, c, undefined);
} else if (Array.isArray(c)) {
// 说明现在调用的是h函数形态2
// 如果这里为true 说明c是一个数组,是sel的子元素
// 是Array我们就需要循环遍历,判断子元素内的情况。
let children = []
// 遍历数组C
for (let i = 0; i < c.length; i++) {
// 检查c[i]必须是一个对象
// 如果c[i]的每一项有一项不是对象,并且不是一个h函数,
// 不拥有选择器sel属性,这里要报错,[13131]如果你这样写 他虽然是个数组不错
// ,但是他不是一个对象 并且不拥有sel选择器所以需要报错!!!!
if (!(typeof c[i] === 'object' && c[i].hasOwnProperty('sel')))
throw new Error('传入的数组有项不是h函数');
// 这里不是c[i]执行
// 这里如果他是一个h函数,就把每一项push进对应的children内
children.push(c[i])
}
return vnode(sel, data, children, undefined, undefined)
} else if (typeof c == 'object' && c.hasOwnProperty('sel')) {
// 说明现在调用的是h函数形态3
// 如果c[i]的每一项有一项是对象,并且是一个h函数,拥有选择器sel属性,这里要报错,[h('sel',{},'文本')]或者h('sel',{},'text')如果你这样写 他是个数组, 并且拥有sel选择器
let children = [c]
return vnode(sel, data, children, undefined, undefined)
} else {
throw new Error('传入的第三个参数不对')
}
}
2、创建一个vnode.js
export default function (sel, data, children, text, elm) {
return {
sel, data, children, text, elm
}
}
sel:选择器
data:属性
children:子元素
text:文本
elm:表示这个元素对应的真实DOM节点,如果是undfined就说明虚拟DOM还没上树
创建一个index.js
import h from "./mysnabbdom/h"
var myVnode1 = h('div', {}, [
h('p', {}, '哈哈'),
h('p', {}, '哈哈'),
h('p', {}, h('span', {}, 'A'))
])
var myVnode2 = h('ul', {}, [
h('li', {}, '西瓜'),
h('li', {}, '苹果'),
h('li', {}, [
h('div', {}, [
h('span', {}, '香蕉')
])
]),
h('li',{},h('span',{},'文字'))
])
console.log(myVnode2);
diff算法心得
1、只有同一层时才会最小量更新,当然key是必须得,key是这个节点的唯一标识,告诉diff算法,在更改前后
它们是同一个DOM节点
2、只有是同一个虚拟节点,才进行精细化比较,否则就是暴力删除旧的、插入新的节点。如何定是同一个虚拟节点?
选择器相同且key相同。
import {
init,
classModule,
propsModule,
styleModule,
eventListenersModule,
h,
} from "snabbdom";
// 创建patch函数
const patch = init([classModule, propsModule, styleModule, eventListenersModule])
var btn = document.getElementById('btn')
const vnode1 = h('ul', {}, [
h('li', {
key: "A"
}, 'A'),
h('li', {
key: "B"
}, 'B'),
h('li', {
key: "C"
}, 'C'),
h('li', {
key: "D"
}, 'D')
])
patch(container, vnode1)
const vnode2 = h('ol', {}, [
h('li', {
key: 'E'
}, 'E'),
h('li', {
key: 'A'
}, 'A'),
h('li', {
key: 'B'
}, 'B'),
h('li', {
key: 'C'
}, 'C'),
h('li', {
key: 'D'
}, 'D'),
])
btn.onclick = function () {
patch(vnode1, vnode2)
}
3、只进行同层比较,不会进行跨层比较。即使是同一个虚拟节点,但是跨层了,对不起,精细化比较不会diff你。
而是暴力删除旧的,然后插入新的。
const vnode1 = h('div', {}, [
h('p', {
key: "A"
}, 'A'),
h('p', {
key: "B"
}, 'B'),
h('p', {
key: "C"
}, 'C'),
h('p', {
key: "D"
}, 'D')
)
DOM树<div>
<p>A</p>
<p>B</p>
<p>C</p>
<p>D</p>
</div>
const vnode2 = h('div', {}, h('section', {}, [
h('p', {
key: 'A'
}, 'A'),
h('p', {
key: 'B'
}, 'B'),
h('p', {
key: 'C'
}, 'C'),
h('p', {
key: 'D'
}, 'D'),
]))
DOM树<div>
<section>
<p>A</p>
<p>B</p>
<p>C</p>
<p>D</p>
</section>
</div>
只能比同层级,如果不是就是把旧的删除,创建新的节点,不是同层级,只要父级更新,子级也会更新。把上面的四
个拆除,创建一个新的祖级是div,父级是section,子级是p