snabbdom
是著名的虚拟DOM库,是diff算法
的鼻祖,vue源码借鉴了snabbdom
官方git
:https://github.com/snabbdom/snabbdom
TypeScript
写的,git上并不提供编译好的javascript
版本真实DOM:
<div class="box">
<h3>标题h3>
<ul>
<li>牛奶li>
<li>香蕉li>
ul>
div>
虚拟DOM:
const dom = {
"sel":"div",
"data":{
"class":{"box":true}
},
"children":[
{
"sel":"h3",
"data":{}
"text":"标题"
}
{
"sel":"ul",
"data":{},
"children":[
{"sel":"li","data":{},"text":"牛奶"}
{"sel":"li","data":{},"text":"香蕉"}
]
}
]
}
diff是发生在虚拟DOM上的
新虚拟DOM和老虚拟DOM进行diff(精细化比较),算出应该如何最小量更新,最后反应到真正的DOM上
DOM如何变为虚拟DOM,属于模板编译原理范畴。请看上一节内容
使用h函数来产生虚拟节点
//调用
h('a',{props:{href:'http://www.baidu.com'}},'百度')
//获得的虚拟节点
let dom = {"sel":"a","data":{props:{href:'http://www.baidu.com'}},"text":"百度"}
//真正的虚拟dom节点
<a herf ="http://www.baidu.com">百度</a>
虚拟节点需要有的属性
{
children:undefind
data:{}
elm:undefined
key:undefined
sel:"div"
text:"我是一个盒子"
}
使用snabbdom需要下载 npm i snabbdom -D
运用snabbdom
//引入snabbdom的方法
import { init } from 'sanbbdom/init'
import { classModule } from 'snabbdom/modules/class'
import { propsModule } from 'snabbdom/modules/props'
import { styleModule } from 'snabbdom/modules/style'
import { eventListenersModule } from 'snabbdom/modules/eventListeners'
import { h } from 'snabbdom/h'
//创建出patch函数
const patch = init([classModule,propsModule,styleModule,eventListenersModule])
//创建虚拟节点
const myVnode1 = h('a',{props:{href:'http://www.baidu.com'}},'百度')
//让虚拟节点上树
const container = document.getElementById('container')
patch(container,myVnode1)
h函数可以嵌套使用,从而得到虚拟DOM
例如这样:
h('ul',{},[
h('li',{},'苹果'),
h('li',{},'香蕉'),
h('li',{},'雪梨')
])
从而转换为下面的形式
{
"sel":"ul",
"data":{},
"children":[
{"sel":"li","text":"苹果"},
{"sel":"li","text":"香蕉"},
{"sel":"li","text":"雪梨"}
]
}
创建一个index.js作为入口文件
import h from './h.js'
let myVode = h('div',{},[
h('p',{},'哈哈哈'),
h('p',{},'哈哈哈'),
h('p',{},'哈哈哈')
])
创建一个vnode.js
//这个函数的功能非常简单,就是将传入的数据返回为对象
export default function(sel,data,children,text,elm){
return {
sel,data,children,text,elm
}
}
创建h.js
//编写的是一个低版本的h函数,必须接收3个参数缺一不可
//形态《1》 h('div',{},'文字')
//形态《2》 h('div',{},[])
//形态《3》 h('div',{},h())
import vnode from './vnode.js'
export default function (sel,data,c){
//检查参数的个数
if(arguments.length != 3){
throw new Error('传入的参数不符合条件')
}
//检测c的类型
if(typeof c == 'string' || typeof c == 'number'){
//形态1
return vnode(sel,data,undefined,c,undefined)
}else if(Array.isArray(c)){
//形态2
let children = []
//遍历c
for(let i = 0 ; i<c.length;i++){
//检查c[i]必须是一个对象,如果不满足
if(!(typeof c == 'object' && c.hasOwnProperty('sel'))){
throw new Error('传入的数组参数中有项不是h函数')
}
//此时只需要收集好就好了
children.push(c[i])
}
//循环结束了,就说明children收集完毕了,此时可以返回虚拟节点了,他有children属性
return vnode(sel,data,children,undefined,undefined);
}else if(typeof c == 'object' && c.hasOwnProperty('sel')) {
//形态3
let children = [c]
return vnode(sel,data,children,undefined,undefined);
}else{
throw new Error('传入的第三个类型错误')
}
}
如何判断“是否为同一节点呢”
创建节点时,所有子节点需要递归创建的
新建一个index.js作为入口文件
import h from './h.js'
import patch from './patch.js'
const container = document.getElementById('container')
const myVnode1 = h('h1',{},'你好')
patch(container,myVnode1)
新建一个patch.js,编写上树函数
import vnode from './vnode.js'
import patchVnode from './patchVnode.js'
export default function (oldVnode,newVnode){
//判断传入的第一个参数,是DOM节点还是虚拟节点
if(oldVnode.sel == '' || oldVnode.sel == undefined){
//此时传入的为dom节点,此时要包装为虚拟节点
oldVnode = vnode(oldVnode.tagName.toLowerCase(),{},[],undefined,oldVnode)
}
//判断oldVnode和newVnode是不是同一个节点
if(oldVnode.key == newVnode.key && oldVnode.sel == newVnode.key){
patchVnode(oldVnode,newVnode)
}else{
//不是同一个节点
let newVnodeElm = createElement(newVnode)
if(oldVnode.elm.parentNode && newVnodeElm){
oldVnode.elm.parentNode.inserBefore(newVnodeElm,oldVnode.elm)
}
//删除老节点
oldVnode.elm.parentNode.removeChild(oldVnode.elm)
}
}
新建一个createElement.js用于创建新节点
//真正创建节点,将vnode创建为DOM,是孤儿节点就不进行插入
export default function createElement(vnode){
//创建一个dom节点,这个节点现在还是孤儿节点
let domNode = document.createElement(vnode.sel)
//判断有子节点还是文本
if(vnode.text != '' && (vnode.children == undefined || vnode.children.length == 0)){
//内部是文字
domNode.innerText = vnode.text
}else if(Array.isArray(vnode.children) && vnode.children.length > 0){
//如果是子节点,需要递归创建节点
for(let i = 0;i<vnode.children.length;i++){
let ch = vnode.children[i]
//创建他的dom,一旦调用了createElement意味着:创建出dom;并且他的elm属性指向了创建出DOM,但还没上树,是个孤儿节点
let chDom = createElement(ch)
domNode.appendChild(chDom)
}
}
//补充elm属性
vnode.elm = domNode
return vnode.elm
}
经典的diff算法优化策略
四种命中查找:
是按顺序命中查找的。需要准备四个指针。
新前<=新后 && 旧前 <= 旧后
undefined
,移动新前的指针新建一个patchVnode.js用于处理是同一个节点的清况
import createElement from './createElement.js'
import updateChildren from './updateChildren.js'
export default patchVnode(oldVnode,newVnode){
//是同一个节点
//newVnode 和 oldVnode 如果是内存中的同一对象,啥也不用做,否则下一步
if(oldVnode === newVnode) return;
//判段newVnode有没有text属性
if(newVnode.text != undefined && (newVnode.children == undefined || newVnode.children.length == 0)){
//此时有text属性
//如果新旧node的text相同,则啥也不用做
//不同则进行下一步
if(newVnode.text != oldVnode.text){
//如果新虚拟节点中的text和老虚拟节点的text不同,那么直接让新的text写入老的elm中即可。
//如果老的elm中是children,那么也会直接消失了
oldVnode.elm.innerText = newVnode.text
}
}else{
//此时没有text属性
//判断老的有没有children
if(oldVnode.children != undefined && oldVnode.children.length > 0){
//此时老的有children,较为复杂。新老都有children
updateChildren(oldVnode.elm,oldVnode.children,newVnode.children)
}else{
//此时老的没有children,新的有
//先把老的节点的内容清空
oldVnoed.elm.innerHTML = ''
//遍历新的子节点,创建dom,上树
for(let i = 0;i<newVnode.children;i++){
let dom = createElement(newVnode.children[i])
oldVnode.elm.appendChild(dom)
}
}
}
}
新建一个updateChildren.js用于更新子节点的函数
updateChildren
和 patchVnode
是个互相嵌套的过程
import patchVnode from './patchVnode.js'
import createElement from './createElement.js'
function checkSameVnode(a,b){
return a.sel == b.sel && a.key == b.key
}
export default function updateChildren(parentElm,oldCh,newCh){
//旧前
let oldStartIdx = 0;
//新前
let newStartIdx = 0;
//旧后
let oldEndIdx = oldCh.length - 1
//新后
let newEndIdx = newCh.length - 1
//旧前节点
let oldStartVnode = oldCh[0]
//新前节点
let newStartVnode = newCh[0]
//旧后节点
let oldEndVnode = oldCh[oldEndIdx]
//新后节点
let newEndVnode = newCh[newEndIdx]
let keyMap = null
//开始循环
while( oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx ){
//略过是undefined的值
if(oldStartVnode == undefined){
oldStartVnode = oldCh[++oldStartIdx]
}else if(oldEndVnode == undefined){
oldEndVnode = oldCh[--oldEndIdx]
}else if(newEndVnode == undefined){
newEndVnode = newCh[--newEndIdx]
}else if(newStartVnode == undefined){
newStartVnode = newCh[++newStartIdx]
}
//如果 新前 和 旧前 相同
if(checkSameVnode(oldStartVnode,newStartVnode)){
//相同则放回patchVnode中进行比较处理
patchVnode(oldStartVnode,newStartVnode)
//移动指针,和指针指向
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
}else if(checkSameVnode(oldEndVnode,newEndVnode)){
//如果 新后 和 旧后 相同
patchVnode(oldEndVnode,newEndVnode)
//移动指针,和指针指向
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
}else if(checkSameVnode(oldStartVnode,newEndVnode)){
//如果 新后 和 旧前 相同
patchVnode(oldStartVnode,newEndVnode)
//新前指向的节点,移动到旧后之后,旧前所对应的值变为undefined
parentElm.insertBefore(oldStartVode.elm,oldEndVode.elm.nextSibling)
//移动指针,和指针指向
oldStartVode = oldCh[++oldStartIdx]
newEndVode = newCh[--newEndIdx]
}else if(checkSameVnode(oldEndVnode,newStartVnode)){
//如果 新前 和 旧后 相同
patchVnode(oldEndVnode,newStartVnode)
//新前指向的节点,移动到旧前之前
parentElm.insertBefore(oldEndVode.elm,oldStartVode.elm.nextSibling)
//移动指针,和指针指向
oldEndVode = oldCh[--oldEndIdx]
newStartVode = newCh[++newStartIdx]
}else{
//都没有命中
//寻找key的map
if(!keyMap){
keyMap = {}
//创建keymap映射对象,这样不用每次都遍历了
for(let i = oldStartIdx;i<=oldEndIdx;i++){
const key = oldCh[i].key
if(key!=undefined){
keyMap[key] = i
}
}
}
//寻找当前项newStartIdx 在keyMap中映射的位置序号
const idxInOld = keyMap[newStartVnode.key]
if(idxInOld == undefined){
//表示是全新的项
parentElm.insertBefore(createElement(newStartVnode),oldStartVnode.elm)
}else{
//如果不是undefined 不是全新的项,要移动
const elmToMove = oldCh[idxInOld]
patchVnode(elmToMove,newStartVnode)
//把这项设置为undefined
oldCh[idxInOld] = undefined
//移动到旧前前面
parentElm.insertBefore(elmToMove.elm,oldStartVode.elm)
}
//完成后指针下移
newStartVnode = newCh[++newStartIdx]
}
}
//判断是否有剩余的节点
if(oldStartIdx <= oldEndIdx){
//证明旧的比新的多,要进行删除
for(let i = oldStartIdx ; i<=oldEndIdx;i++){
//当他不是undefined时
if(oldCh[i]){
parentElm.removeChild(oldCh[i].elm)
}
}
}
if(newStartIdx <= newEndIdx){
//证明新的比旧的多,要进行插入
//要插入的标杆
const before = oldCh[oldStarIdx] == null ? null : oldCh[oldStarIdx].elm
for(let i = newStartIdx ; i <= newEndIdx ; i++){
//insertBefore方法可以自动识别null,如果是null就会自动排到队尾去。和appendChild是一致了
//newCh[i]不是真正的dom,需要使用createElement创建实际的dom
parentElm.insertBefore(createElement(newCh[i]),before)
}
}
}
//移动到旧前前面
parentElm.insertBefore(elmToMove.elm,oldStartVode.elm)
}
//完成后指针下移
newStartVnode = newCh[++newStartIdx]
}
}
//判断是否有剩余的节点
if(oldStartIdx <= oldEndIdx){
//证明旧的比新的多,要进行删除
for(let i = oldStartIdx ; i<=oldEndIdx;i++){
//当他不是undefined时
if(oldCh[i]){
parentElm.removeChild(oldCh[i].elm)
}
}
}
if(newStartIdx <= newEndIdx){
//证明新的比旧的多,要进行插入
//要插入的标杆
const before = oldCh[oldStarIdx] == null ? null : oldCh[oldStarIdx].elm
for(let i = newStartIdx ; i <= newEndIdx ; i++){
//insertBefore方法可以自动识别null,如果是null就会自动排到队尾去。和appendChild是一致了
//newCh[i]不是真正的dom,需要使用createElement创建实际的dom
parentElm.insertBefore(createElement(newCh[i]),before)
}
}
}