snabbdom的学习
**在snabbdom不研究模板编译的部分,在此只研究snabbdom中的h函数渲染成虚拟dom对象以及patch函数以及节点的创建。
**
index.js
import h from './mysnabbdom/h';
import patch from './mysnabbdom/patch'
var myVnode = h('ul',{},[
h('li',{key:'a'},'a'),
h('li',{key:'b'},'b'),
h('li',{key:'c'},'c'),
h('li',{key:'d'},'d'),
h('li',{key:'e'},'e'),
])
var myVnode1 = h('ul',{},[
h('li',{key:'a'},'a'),
h('li',{key:'c'},'c'),
h('li',{key:'b'},'b'),
h('li',{key:'e'},'e'),
h('li',{key:'d'},'d'),
])
// var myVnode = h('ul',{},'111')
const container = document.getElementById("container");
patch(container, myVnode);
document.getElementById('btn').onclick = function() {
patch(myVnode, myVnode1);
}
h函数的实现
- 通过h函数创建出虚拟dom树
import vnode from "./vnode"
// 编写一个低配版本的H函数,这个函数必须接收三个参数,缺一不可
// 也就是说,调用的时候形态必须是下面的三种之一
// 实际中snabbdom的h函数中五个参数h(sel,data,children,key,elm),它会在使用时先判断参数个数,然后进行参数转换。
// 形态1 h('div',{},'文字')
// 形态2 h('div',{},[h('div',{},'文字'),h('div',{},'文字')])
// 形态3 h('div',{}, h('div',{},'文字'))
export default function(sel,data,c){
// 检测参数的个数
if(arguments.length!=3){
throw new Error('请传入三个参数')
}
// 检测c参数的类型
if(typeof c == 'string' || typeof c =='number'){
return vnode(sel,data,undefined,c,undefined)
} else if(Array.isArray(c)){
let children = []
for (let i = 0; i < c.length; i++) {
// c[i]必须是一个对象
if(!(typeof c[i] == 'object'&& c[i].hasOwnProperty('sel'))){
throw new Error('传入的数组参数中有项不是H函数')
}
children.push(c[i])
}
return vnode(sel,data,children,undefined,undefined)
} else if(typeof c == 'object' && c.hasOwnProperty('sel')){
// 说明现在调用H函数是形态3
// 即,传入的是一个h函数的时候
let children = [c]
return vnode(sel,data,children,undefined,undefined)
} else {
throw new Error('传入的第三个参数不对')
}
}
vnode的实现
- vode函数非常简单,它的作用就是返回一个虚拟节点的整合体
// 函数的功能非常简单,就是把传入的5个参数组合成对象返回
export default function(sel,data,children,text,elm){
return {
sel,data,children,text,elm
}
}
createElement函数(创建节点,但是在此函数中,不进行插入,是孤儿节点)
- createElement
// 真正创建节点,将vnode创建为dom,是孤儿节点 不进行插入
export default function createElement(vnode){
let domNode = document.createElement(vnode.sel)
if(vnode.text!=''&& (vnode.children == undefined || vnode.children.length ==0)){
// 内部是文字
domNode.innerText = vnode.text;
// 补充elm属性
} else if(Array.isArray(vnode.children) && vnode.children.length > 0){
console.log(vnode)
// 它内部是子节点,就是递归创建节点
for(let i = 0; i<vnode.children.length;i++){
let ch = vnode.children[i]
console.log(ch)
let chDom = createElement(ch)
domNode.appendChild(chDom)
}
}
vnode.elm = domNode
// 返回elm,elm是一个dom
return vnode.elm;
}
patchVnode函数的实现(diff算法在此函数中)
- 在pathc中会判断新旧节点,进行最小化更新,diff算法就在此函数中。
- 它接收两个参数(oldVnode,newVnode)
- 它的执行流程如下
- 首先patch被调用
- oldVnode是虚拟节点还是DOM节点 ,如果是DOM节点的话,将oldVnode包装成虚拟节点,如果是虚拟节点的话,进行下一步
- 判断新旧节点是不是同一个节点(新旧节点的选择器以及key相同为同一节点),不是同一个节点的话,暴力删除旧的,插入新的,如果是同一个节点的话,进行精细化比较(diff算法的核心)
import vnode from "./vnode";
import createElement from "./createElement";
// patch函数的作用
export default function (oldVnode,newVnode){
if(oldVnode.sel == '' || oldVnode.sel == undefined){
// 传入的第一个参数是Dom节点,此时要包装为虚拟节点
oldVnode = vnode(oldVnode.tagName.toLowerCase(),{},[],undefined,oldVnode)
}
if(oldVnode.key == newVnode.key && oldVnode.sel == newVnode.sel){
// 是同一个节点需要精细比较
patchVnode(oldVnode,newVnode)
console.log('同一个节点',oldVnode,newVnode);
} else {
// 不是同一个节点
let newVodeElm = createElement(newVnode)
if(oldVnode.elm.parentNode && newVodeElm){
// 虚拟DOM上树
oldVnode.elm.parentNode.insertBefore(newVodeElm,oldVnode.elm)
}
// 删除老节点
oldVnode.elm.parentNode.removeChild(oldVnode.elm)
}
}
patchVnode函数(oldVnode,newVnode)
- 该函数是对相同节点的新旧节点进行比较
- 它的步骤主要分为下面几步
- 区分新旧虚拟节点是不是统一对象(根据内存中的地址是否相等),如果不是进行下面判断
- 判断新节点有没有text属性(如果有text属性的话)
- 如果有text属性的话,判断新旧text是否相同,不相同的话就让新的text写入新的elm中,如果老的elm中是children的话,那么也会立即消失掉
- 相同的话就不做操作
- 新节点没有text属性的话,(就是有子节点情况)
- 判断旧节点有没有子节点,如果没有的话就是有text就根据新节点的children创建dom进行插入
- 如果旧节点有子节点,那就进行最复杂的算法(四指针算法,updateChildren函数)
import createElement from "./createElement";
import updateChildren from "./updateChildren";
export default function patchVode(oldVnode,newVnode){
// 判断新旧虚拟节点是不是同一个对象(内存中地址相等)
if(oldVnode === newVnode) return
// 判断newVnode有没有text属性
// 新节点有text属性
if(newVnode.text != undefined &&(newVnode.children == undefined || newVnode.children.length ==0)){
if(newVnode.text!=oldVnode.text){
// 如果新的虚拟节点中的text和老的虚拟节点的text不同,那么直接
// 让新的text写入新的elm中,如果老的elm中是children的话,那么也会立即消失掉
oldVnode.elm.innerText = newVnode.text
}
// 如果新旧text相等的话 什么都不做
} else {// 新节点没有text属性的话,那就是有子节点
// 判断老的有没有children
if(oldVnode.children!=undefined&&oldVnode.children.length>0){
// 如果老的有子节点的话,那就是最复杂的diff算法情况
updateChildren(oldVnode.elm,oldVnode.children,newVnode.children)
} else { // 老的没有children,有text
oldVnode.elm.innerHTML = '';
for (let i = 0; i < newVnode.children.length; i++) {
let dom = createElement(newVnode.children[i])
oldVnode.elm.appendChild(dom)
}
}
}
}
updateChildren函数
- 该函数是用来对都具有子节点的两个虚拟dom进行精细化比较的地方。
- diff最精细的diff算法就是在这里。
import patchVnode from "./patchVnode";
import createElement from "./createElement";
function checkSameVnode(a,b){
return a.sel == b.sel && a.key == b.key
}
export default function updateChildren(parenteElm,oldCh,newCh){
// 旧前
let oldStartIdx = 0;
// 新前
let newStartIdx = 0;
// 旧后
let oldEndIdx = oldCh.length - 1;
// 新后
let newEndIdx = newCh.length - 1;
// 旧前节点
let oldStartVnode = oldCh[0];
// 旧后节点
let oldEndVnode = oldCh[oldEndIdx];
// 新前节点
let newStartVnode = newCh[0];
// 新后节点
let newEndVnode = newCh[newEndIdx];
let keyMap = null;
//
while(oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx){
// 首先不是判断是否命中 而是要略过加了undefined的项
if(oldStartVnode == null||oldCh[oldStartIdx] == undefined) {
oldStartVnode = oldCh[++oldStartIdx]
} else if(oldEndVnode == null||oldCh[oldEndIdx] == undefined) {
oldEndVnode = oldCh[--oldEndIdx]
} else if(newStartVnode == null||newCh[newStartIdx] == undefined) {
newStartVnode = newCh[++newStartIdx]
} else if(newEndVnode == null||newCh[newEndIdx] == undefined) {
newEndVnode = newCh[--newEndIdx]
} else if(checkSameVnode(oldStartVnode,newStartVnode)){ // 再去判断命中
console.log('①命中');
patchVnode(oldStartVnode,newStartVnode)
oldStartVnode = oldCh[++oldStartIdx];
newStartVnode = newCh[++newStartIdx];
} else if(checkSameVnode(newEndVnode,oldEndVnode)) { // 新后与旧后命中②命中
console.log('②命中');
patchVnode(newEndVnode,oldEndVnode)
oldEndVnode = oldCh[--oldEndIdx];
newEndVnode = newCh[--newEndIdx];
} else if(checkSameVnode(newEndVnode,oldStartVnode)) { //③命中
console.log('③命中');
patchVnode(newEndVnode,oldStartVnode)
// 当新后与旧前命中的时候,此时要移动节点
// 移动新前指向的这个节点到老节点的旧后的后面
// 如果移动节点,只要插入一个已经在dom树上的节点,他就会移动
parenteElm.insertBefore(oldStartVnode.elm,oldEndVnode.elm.nextSibling)
oldStartVnode = oldCh[++oldStartIdx];
newEndVnode = newCh[--newEndIdx]
} else if (checkSameVnode(newStartVnode,oldEndVnode)){ //④命中
console.log('④命中');
// 新前与旧后命中的时候,此时也要移动节点
// 移动新前指向的这个节点到老节点的旧前的前面
parenteElm.insertBefore(oldEndVnode.elm,oldStartVnode.elm)
oldEndVnode = oldCh[--oldEndIdx];
newStartVnode = newCh[--newStartIdx]
} else {
console.log('都没有找到');
// 四种情况都没命中的话
// 寻找key的map
if(!keyMap){
keyMap = {}
for (let i = oldStartIdx; i <= oldEndIdx; i++) {
const key = oldCh[i]?oldCh[i].key:undefined
if(key != undefined){
keyMap[key] = i;
}
}
}
// 寻找当前这项newstartIdx在keyMap的位置序号,如果不存在的话,表示新增就把这项加入oldstartVnode之前
const idxInOld = keyMap[newStartVnode.key]
// 判断 如果idxInOld 是undefined的话表示他是全新的项 要添加
if(idxInOld == undefined){
// 被加入的项(就是newStartVnode)现在还不是dom节点
parenteElm.insertBefore(createElement(newStartVnode),oldStartVnode.elm)
} else {
// 如果不是全新的话,要移动
console.log('移动节点');
// 找到oldCh中对应的这项
const elmToMove = oldCh[idxInOld]
if(elmToMove.elm.nodeType==1){
// 进行比较,看是否一致
patchVnode(elmToMove,newStartVnode)
// 先把这项设置为undefined,表示已经处理了这项
oldCh[idxInOld] = undefined
//移动 把找到的这项oldVnode也移动到oldStartVnode之前 调用insertBefore
parenteElm.insertBefore(elmToMove.elm,oldStartVnode.elm)
}
}
// 指针下移
newStartVnode = newCh[++newStartIdx]
}
}
// 循环结束之后,如果存在没处理的
if(newStartIdx <= newEndIdx) {
// 新的节点多于旧的节点(有新增情况)
// const before = newCh[newEndIdx+1] == null ? null :newCh[newEndIdx+1].elm;
for (let i = newStartIdx; i <= newEndIdx; i++) {
parenteElm.insertBefore(createElement(newCh[i]),oldCh[oldStartIdx].elm)
}
} else if(oldStartIdx<=oldEndIdx) {
// 旧节点比新节点多(删除)
for (let i = oldStartIdx; i <= oldEndIdx; i++) {
if(oldCh[i]){
parenteElm.removeChild(oldCh[i].elm)
}
}
}
};