美文网首页
自己实现一个简单的虚拟 DOM

自己实现一个简单的虚拟 DOM

作者: astak3 | 来源:发表于2019-03-14 21:15 被阅读0次

    自己实现虚拟 DOM

    从 HTML 中提炼数据结构

    先来看下我们的 HTML

    <div class='root'>
        <h1><span>傅雷家书</span></h1>
        <span>读家书,想付雷</span>
    </div>
    

    从 HTML 中我们可以抽离出它的数据结构:

    1. 首先页面中只需要一个根节点root,定义为:nodesDate数组
    2. root内有两个子元素h1span,数组有两项,每项为内容为tagchildren
    3. 接下来内部所有元素都是如此定义,直到遇到文本元素,将他定义为text
    nodesDate = {
        tag:"div",
        children:[{
            tag:'h1',
            children:[{
                tag:'span',
                children:[{
                    tag:'#text',
                    text:'傅雷家书'
                }]
            }]
        },{
            tag:'span',
            children:[{
                tag:'#text',
                text:'读家书,想傅雷'
            }]
        }]
    }
    

    用这种视野在看 HTML 的话,就不是单纯的 HTML 了,而是一堆hash

    从上面数据结构中我们可以提炼出3个有用的属性,分别是tagchildrentext,那我们是不是可以定义一个方法,传递这三个参数,就能满足我们的需求呢?

    构造 HTML 方法

    想一下我们拿到这三个参数后,要干什么呢?

    当然是在页面中生成 DOM 元素啊!

    对,这三个参数是我们各自私有属性,通过这三个属性能生成各自的 DOM,生成 DOM 的方法是不是公用的呢?

    所以可以用用构造函数模式创建我们要的方法,(PS:之前讲过new操作符的背后的逻辑,不理解的可移步:使用 new 操作符内部到底在做什么

    function vNode(tag,children,text){
        this.tag = tag
        this.children = children
        this.text = text
    }
    
    vNode.prototype.render = function(){
        //如 tag 为文本的话,创建一个文本节点
        if(this.tag === '#text'){
            return document.createTextNode(this.text)   // 返回文本
        }
        
        //tag 不是文本的话,创建一个 DOM 节点,并且遍历 children,每次遍历都是调用自身的 render 方法
        let element = document.createElement(this.tag)
        this.children.forEach((vChild)=> {
            element.appendChild(vChild.render())    //在遍历 h1 时,没有直接跳出,而是在其内部不断循环。直到 h1 内部的 children 全部循环结束为止,才进入下一个元素 span,当 h1 循环结束时,h1 内部的节点都已经生成好了。
        })
        return element      //返回节点
    }
    
    function v(tag,children,text){
        //如果 chilren 为字符串,那么就把 children 赋值给 text,并把 children 初始化为 [],不然后面会报错
        if(typeof children === 'string'){
            text = children
            children = []
        }
        return new vNode(tag,children,text)
    }
    
    //格式参见 nodesData,vNode 的实例化
    let vNodes = v('div',[
        v('h1',[
            v('span',[
                v('#text','傅雷家书')])
        ]),
        v('span',[
            v('#text','——傅敏')])
    ])
    
    const root = document.querySelector('.root')    //获取 root 节点
    root.appendChild(vNodes.render())   //这里只运行一次,把最终的 DOM 添加进页面中
    

    实现增删改

    如果此时一个数据变动比如,按照以前的逻辑

    root.innerText = ''
    root.appendChild(vNodes.render())
    

    如果数据非常大,用这种方法根本没啥意义,每一次改动 DOM 树都要重新渲染一遍,造成性能低下,有什么好的方法可以实现呢?

    function patchElement(parent, newVNodes, oldVNodes, index = 0) {
        //如果没有传递老的 VNodes,默认就是新的
        if(!oldVNodes) {
            parent.appendChild(newVNodes.render())
        } else if(!newVNodes) {
            parent.removeChild(parent.childNodes[index])
        } else if(newVNodes.tag !== oldVNodes.tag || newVNodes.text !== oldVNodse.text) {   
            //如果元素不一样或者文本不一样,走这边
            //当有走这边时,newVNodes 是和 oldVNodes 不同的那个值,这里的 parent 是当前元素或文本的 parent
            //replaceChild(sp1,sp2),是将 sp2 换成 sp1
            parent.replaceChild(newVNodes.render(), parent.childNodes[index])
        } else {
            for(let i = 0; i < newVNodes.children.length || i < oldVNodes.children.length; i++) {
            //取值永远是 newVNodes.length,除非不传 newVNodes
            //这里 index 只有当 i 变化时,下一次才是 index 才等于 i 的值
            //当 i = 0 时,这次的 parent.childNode[index],是下一次的 parent,所以这里要用 index
            //当循环走完,发现元素或者文本不一样时,才走第三个逻辑
                patchElement(parent.childNodes[index], newVNodes.children[i], oldVNodes.children[i], i)
            }
        }
    }
    

    相关文章

      网友评论

          本文标题:自己实现一个简单的虚拟 DOM

          本文链接:https://www.haomeiwen.com/subject/nwdhmqtx.html