实现虚拟dom

227 阅读6分钟

虚拟dom实现

通过转换为对象去处理

定义类型


// 节点类型
const vnodeType = {
    HTML: 'HTML',
    TEXT: 'TEXT',
    COMPONENT: 'COMPONENT',
    CLASS_COMPONENT: 'CLASS_COMPONENT',
};

// 子节点的个数
const childType = {
    EMPTY: 'EMPTY',
    SINGLE: 'SINGLE',
    MULTIPLE:"MULTIPLE"
}

创建虚拟dom

  • 用对象去存储虚拟dom,(标签,属性,子元素)
  • 返回值代表的是存储本节点的属性
// create vdom
function createElement(tag, data, children) {

    let flag;
    if(typeof tag == 'string'){
        // 普通标签
        flag = vnodeType.HTML;
    }else if(typeof tag == 'function'){
        flag = vnodeType.COMPONENT;
    }else {
        flag = vnodeType.TEXT;
    }

    
    let childrenFlag;
    if(children == null){
        childrenFlag = childType.EMPTY;
    }else if(Array.isArray(children)){ // 元素
        let len = children.length;
        if(len ==0){
            childrenFlag = childType.EMPTY;
        }else {
            childrenFlag = childType.MULTIPLE;
        }
    }else {
        // 认为是文本
        childrenFlag = childType.SINGLE;
        children = createTextVnode(children + '');
    }


    return {
        flag, // vnode 类型
        tag, // 标签
        data, // 属性
        key:data && data.key, // 唯一关键字
        children, // 子元素
        childrenFlag, // 标识子元素的个数
        el:null, // 存放子元素的dom
    }
}

渲染


if(container.vnode){	
	// 更新
	patch(container.vnode, vnode, container )
}else { //首次
	mount(vnode, container);
}

container.vnode = vnode;// 为了判断是二次渲染

首次挂载

function mount(vnode, container, flagNode){
    let {flag} = vnode;

    if(flag == vnodeType.HTML){ // 普通标签
        mountElement(vnode, container, flagNode);
    }else if(flag == vnodeType.TEXT){ // 文本标签
        mountText(vnode, container);
    }
}

元素挂载

* 属性的复制

  • 子元素的挂载


function mountElement(vnode, container, flagNode){
    let dom = document.createElement(vnode.tag);
    vnode.el = dom;

    let { data, children, childrenFlag } = vnode;

    //挂载 data属性
    if(data) {
        for(let key in data) {
            // 节点 名字 老值, 新值
            patchDate(dom, key, null, data[key]);
        }
    }

    if(childrenFlag !== childType.EMPTY){ // 挂载子元素
        if(childrenFlag == childType.SINGLE){
            mount(children, dom);
        }else if(childrenFlag == childType.MULTIPLE) {
            for(let i=0; i<children.length; i++){
                mount(children[i], dom);
            }
        }
    }

    flagNode? container.insetBefore(dom, flagNode) : container.appendChild(dom);

}

文本的挂载

function mountText(vnode, container) {
    let dom = document.createTextNode(vnode.children);
    container.appendChild( dom );
}

更新

/**
 * 
 * 元素的更新
 * 
 * @param { 先前渲染的节点 } prev 
 * @param { 当前节点 } next 
 * @param { 容器 } container 
 */
function patch(prev, next, container) {
    console.log(prev, next, container);

    let nextFlag = next.flag;
    let prevFlag = next.flag;
 
    if(nextFlag !== prevFlag){ //  pre 是text next是p 直接替换
        replaceVnode(prev, next, container);
    }else if(nextFlag == vnodeType.HTML){ // 同为元素节点
        patchElement(prev, next, container);
    }else if(nextFlag == vnodeType.TEXT) {
        // patchText(prev, next);
    }

}

元素的更新

  • 这里分很多情况去处理
    1. 老的是单独的 老的是多个 老的是空的

    2. 新的是单独的 新的是空的 新的是多个


function patchChildren(
    prevChildrenFlag,
    nextChildrenFlag,
    prevChildren,
    nextChildren,
    container, //当前的容器
) {
    //更新子元素
    /**
     * 1. 老的是单独的
     *   老的是多个
     *   老的是空的
     * 
     * 2. 新的是单独的
     *    新的是空的
     *    新的是多个
     */
    switch(prevChildrenFlag) {
        case childType.SINGLE :

            switch(nextChildrenFlag) {
                case childType.SINGLE:
                    patch(prevChildren, nextChildren, container);
                    break;
                case childType.EMPTY:
                    container.removeChild(prevChildren);
                    break;
                case childType.MULTIPLE:
                    container.removeChild(prevChildren.el);
                    for(let i=0; i<nextChildren.length;i++){
                        mount(nextChildren[i], container);
                    }
                    break;
            }

            break;

        case childType.EMPTY:
            switch(nextChildrenFlag) {
                case childType.SINGLE :
                    console.log(nextChildren, container);

                    console.log('+++');
                    mount(nextChildren, container);
                    break;
                case childType.EMPTY :
                    break;
                case childType.MULTIPLE :
                    for(let i=0; i<nextChildren.length;i++){
                        mount(nextChildren[i], container);
                    }
                    break;
            }
            break;

        case childType.MULTIPLE:
            switch(nextChildrenFlag) {
                case childType.SINGLE :
                    for(let i=0; i<prevChildren.length;i++){
                        container.removeChild(prevChildren[i].el);
                    }
                    mount(nextChildren, container);
                    break;
                case childType.EMPTY :
                    for(let i=0; i<prevChildren.length;i++){
                        container.removeChild(prevChildren[i].el);
                    }
                    break;
                case childType.MULTIPLE :
                    let lastIndex = 0;
                    for(let i=0;i<nextChildren.length; i++) {
                        let nextVnode = nextChildren[i];
                        let j = 0;
                        let find = false;
                        for(;j<prevChildren.length;j++) {
                            // key 相同, 认为同一个元素
                            let prevVnode = prevChildren[j];
                            if(prevVnode.key === nextVnode.key) {
                                find = true;
                                patch(prevVnode, nextVnode, container);
    
                                if(j< lastIndex) {
                                    // 需要移动
                                    // abc  a移动到b之后
                                    let flagNode = nextChildren[i-1].el.nextSibling;
                                    // console.log(container);
                                    // console.log('===')
                                    container.insertBefore(prevVnode.el, flagNode);
                                }else {
                                    lastIndex = j;
                                }
                            }   
                        }

                        if(!find){ // 需要新增
                            let flagNode = i==0 ? prevChildren[0].el : nextChildren[i-1].el.nextSibling;
                            mount(nextVnode, container, flagNode);
                        }
                    }
                     // 移除不需要的元素
                     for(let i=0;i<prevChildren.length; i++){
                        const prevVnode = prevChildren[i];
                        const has = nextChildren.find(next=> next.key == prevVnode.key)
                        if(!has){
                            container.removeChild(prevVnode.el);
                        }
                    }
                   
                    break;
            }
        
        break;
        
        
            
    } 
    


}

完整代码

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>虚拟dom</title>
    <script src="./vdom.js"></script>

    <style>
        .item {
            font-size: 50px;
            color: orange;
        }

    </style>
</head>
<body>

    <div id="app">
    
    </div>
    <script>
        let vnode = createElement('div', {id: 'test'},[
            createElement('p', {key: 'a', style:{color: 'blue'}}, '节点1'),
            createElement('p', {key: 'b', onClick:()=>console.log('alert') }, '节点2'),
            createElement('p', {key: 'c', class:"item"}, '节点3'),
            createElement('p', {key: 'd'}, '节点4'),
         ]);

        let vnode1 = createElement('div', {id: 'test'},[
            createElement('p', {key: 'd'}, '节点4'),
            createElement('p', {key: 'a', style: {color: 'blue'}}, '节点1'),
            createElement('p', {key: 'b'}, '节点2'),
            createElement('p', {key: 'e'}, '节点5'),
            createElement('p', {key: 'f', style: {color: 'gray', 'font-size': '100px'}}, '节点6'),
        ]);

        console.log(vnode1)
        // console.log( JSON.stringify(div, null, 2) );
        render(vnode, document.getElementById('app'));

        setTimeout(()=>{
            render(vnode1, document.getElementById('app'));
        },1000)
    </script>

</body>
</html>

vdom.js



const vnodeType = {
    HTML: 'HTML',
    TEXT: 'TEXT',
    COMPONENT: 'COMPONENT',
    CLASS_COMPONENT: 'CLASS_COMPONENT',
};
const childType = {
    EMPTY: 'EMPTY',
    SINGLE: 'SINGLE',
    MULTIPLE:"MULTIPLE"
}


// create vdom
function createElement(tag, data, children) {

    let flag;
    if(typeof tag == 'string'){
        // 普通标签
        flag = vnodeType.HTML;
    }else if(typeof tag == 'function'){
        flag = vnodeType.COMPONENT;
    }else {
        flag = vnodeType.TEXT;
    }

    
    let childrenFlag;
    if(children == null){
        childrenFlag = childType.EMPTY;
    }else if(Array.isArray(children)){ // 元素
        let len = children.length;
        if(len ==0){
            childrenFlag = childType.EMPTY;
        }else {
            childrenFlag = childType.MULTIPLE;
        }
    }else {
        // 认为是文本
        childrenFlag = childType.SINGLE;
        children = createTextVnode(children + '');
    }


    return {
        flag, // vnode 类型
        tag, 
        data,
        key:data && data.key,
        children,
        childrenFlag,
        el:null, // 存放子元素的dom
    }
}

// 节点类型不一致 直接替换
function replaceVnode(prev, next, container) {
    container.removeChild(prev.el);
    mount(next, container);
}


function patchText(prev, next) {
    let el = (next.el = prev.el);
    if(next.children !== prev.children){
        el.nodeValue = next.children;
    }
}

function patchChildren(
    prevChildrenFlag,
    nextChildrenFlag,
    prevChildren,
    nextChildren,
    container, //当前的容器
) {
    //更新子元素
    /**
     * 1. 老的是单独的
     *   老的是多个
     *   老的是空的
     * 
     * 2. 新的是单独的
     *    新的是空的
     *    新的是多个
     */
    switch(prevChildrenFlag) {
        case childType.SINGLE :

            switch(nextChildrenFlag) {
                case childType.SINGLE:
                    patch(prevChildren, nextChildren, container);
                    break;
                case childType.EMPTY:
                    container.removeChild(prevChildren);
                    break;
                case childType.MULTIPLE:
                    container.removeChild(prevChildren.el);
                    for(let i=0; i<nextChildren.length;i++){
                        mount(nextChildren[i], container);
                    }
                    break;
            }

            break;

        case childType.EMPTY:
            switch(nextChildrenFlag) {
                case childType.SINGLE :
                    console.log(nextChildren, container);

                    console.log('+++');
                    mount(nextChildren, container);
                    break;
                case childType.EMPTY :
                    break;
                case childType.MULTIPLE :
                    for(let i=0; i<nextChildren.length;i++){
                        mount(nextChildren[i], container);
                    }
                    break;
            }
            break;

        case childType.MULTIPLE:
            switch(nextChildrenFlag) {
                case childType.SINGLE :
                    for(let i=0; i<prevChildren.length;i++){
                        container.removeChild(prevChildren[i].el);
                    }
                    mount(nextChildren, container);
                    break;
                case childType.EMPTY :
                    for(let i=0; i<prevChildren.length;i++){
                        container.removeChild(prevChildren[i].el);
                    }
                    break;
                case childType.MULTIPLE :
                    let lastIndex = 0;
                    for(let i=0;i<nextChildren.length; i++) {
                        let nextVnode = nextChildren[i];
                        let j = 0;
                        let find = false;
                        for(;j<prevChildren.length;j++) {
                            // key 相同, 认为同一个元素
                            let prevVnode = prevChildren[j];
                            if(prevVnode.key === nextVnode.key) {
                                find = true;
                                patch(prevVnode, nextVnode, container);
    
                                if(j< lastIndex) {
                                    // 需要移动
                                    // abc  a移动到b之后
                                    let flagNode = nextChildren[i-1].el.nextSibling;
                                    // console.log(container);
                                    // console.log('===')
                                    container.insertBefore(prevVnode.el, flagNode);
                                }else {
                                    lastIndex = j;
                                }
                            }   
                        }

                        if(!find){ // 需要新增
                            let flagNode = i==0 ? prevChildren[0].el : nextChildren[i-1].el.nextSibling;
                            mount(nextVnode, container, flagNode);
                        }
                    }
                     // 移除不需要的元素
                     for(let i=0;i<prevChildren.length; i++){
                        const prevVnode = prevChildren[i];
                        const has = nextChildren.find(next=> next.key == prevVnode.key)
                        if(!has){
                            container.removeChild(prevVnode.el);
                        }
                    }
                   
                    break;
            }
        
        break;
        
        
            
    } 
    


}

function patchElement(prev, next, container) {

    if(prev.tag !== next.tag){ // pre 是div next是p 直接替换
        replaceVnode(prev, next, container);
        return ;
    }

    let el = (next.el = prev.el);
    let prevData = prev.data;
    let nextData = next.data;
    if(nextData) {
        for(let key in nextData) {
            let prevVal = prevData[key];
            let nextVal = nextData[key];
            patchDate(el, key, prevVal, nextVal);
        }
    }

    if(prevData) {
        for(let key in prevData) {
            let prevVal = prevData[key];
            if(prevVal && !nextData.hasOwnProperty(key)){
                patchDate(el, key, prevVal, null);
            }
        }
    }

    //更新完毕 更新子元素
    patchChildren(
        prev.childrenFlag,
        next.childrenFlag,
        prev.children,
        next.children,
        el, //当前的容器
    );


}

/**
 * 
 * 元素的更新
 * 
 * @param { 先前渲染的节点 } prev 
 * @param { 当前节点 } next 
 * @param { 容器 } container 
 */
function patch(prev, next, container) {
    console.log(prev, next, container);

    let nextFlag = next.flag;
    let prevFlag = next.flag;

    // pre 是text next是p 直接替换
    if(nextFlag !== prevFlag){
        replaceVnode(prev, next, container);
    }else if(nextFlag == vnodeType.HTML){
        patchElement(prev, next, container);
    }else if(nextFlag == vnodeType.TEXT) {
        // patchText(prev, next);
    }

}



/**
 *  虚拟dom
 *  父容器
 *  */
function render(vnode, container) {
    // 首次渲染和再次渲染

    if(container.vnode){
        // 更新
        patch(container.vnode, vnode, container )
    }else { //首次
        mount(vnode, container);
    }

    container.vnode = vnode;
}


// 首次挂在元素
function mount(vnode, container, flagNode){
    let {flag} = vnode;

    if(flag == vnodeType.HTML){ // 普通标签
        mountElement(vnode, container, flagNode);
    }else if(flag == vnodeType.TEXT){ 
        mountText(vnode, container);
    }
}

function patchDate(el, key, prev, next) {
    switch(key) {
        case 'style': 
            for(let k in next) {
                el.style[k] = next[k];
            }
             
            for(let k in prev) { //删除没有出现的
                if(!next.hasOwnProperty(k)) {
                    el.style[k] = '';
                }
            }
            break;
        case 'class':
            el.className = next;
            break;
        default:
            if(key === 'onClick'){
                if(prev) {
                    el.removeEventListener(key.slice(2).toLocaleLowerCase(), prev);
                }
                if(next){
                    el.addEventListener(key.slice(2).toLocaleLowerCase(), next);
                }
            }else {
                el && el.setAttribute(key, next);
            }
            break;
    }
}

function mountElement(vnode, container, flagNode){
    let dom = document.createElement(vnode.tag);
    vnode.el = dom;

    let { data, children, childrenFlag } = vnode;

    //挂载 data属性
    if(data) {
        for(let key in data) {
            // 节点 名字 老值, 新值
            patchDate(dom, key, null, data[key]);
        }
    }

    if(childrenFlag !== childType.EMPTY){ // 挂载子元素
        if(childrenFlag == childType.SINGLE){
            mount(children, dom);
        }else if(childrenFlag == childType.MULTIPLE) {
            for(let i=0; i<children.length; i++){
                mount(children[i], dom);
            }
        }
    }

    flagNode? container.insetBefore(dom, flagNode) : container.appendChild(dom);

}

function mountText(vnode, container) {
    let dom = document.createTextNode(vnode.children);
    container.appendChild( dom );
}


// 文本类型的vnode
function createTextVnode(text){
    return {
        flag: vnodeType.TEXT,
        tag: null,
        data: null,
        children: text,
        childrenFlag: childType.EMPTY,
    }
}