snabbdom的学习

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属性的话)
      1. 如果有text属性的话,判断新旧text是否相同,不相同的话就让新的text写入新的elm中,如果老的elm中是children的话,那么也会立即消失掉
      2. 相同的话就不做操作
    • 新节点没有text属性的话,(就是有子节点情况)
      1. 判断旧节点有没有子节点,如果没有的话就是有text就根据新节点的children创建dom进行插入
      2. 如果旧节点有子节点,那就进行最复杂的算法(四指针算法,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)
            }
        }
    }
};

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值