温故知新:是时候封装一个DOM库了

1,579 阅读5分钟

由于原始的DOM提供的API过长,不方便记忆,
于是我采用对象风格的形式封装了一个DOM库->源代码链接
这里对新封装的API进行总结
同样,用增删改查进行划分,我们先提供一个全局的window.dom对象

创建节点

create (string){
        const container = document.createElement("template")//template可以容纳任意元素
        container.innerHTML = string.trim();//去除字符串两边空格
        return container.content.firstChild;//用template,里面的元素必须这样获取  
    }

首先,如果用原始的DOM API,我们想要创建一个div,div里面含有一个文本'hi',需要分为两步

  1. document.createElement('div')
  2. div.innerText = 'hi' 而这里,只需要一步就能完成dom.create('<div>hi</div>')
    它可以直接创建多标签的嵌套,如create('<div><span>你好</span></div>')
    为什么能这样写?
    因为我们用innerHTML直接把字符串写进了HTML里,字符串直接变成了HTML里面的内容
    为什么使用template?
    因为template可以容纳任意元素,如果使用div,div不能直接容纳<td></td>标签,但template就可以

新增哥哥

before(node,node2){
    node.parentNode.insertBefore(node2,node);
    }

这个比较简单,找到爸爸节点,然后使用insertBefore,新增一个node2即可

新增弟弟

after(node,node2){
    node.parentNode.insertBefore(node2,node.nextSibling); //把node2插到node下一个节点的前面,即使node的下一个节点为空,也能插入  
    }

由于原始的DOM只有insertBefore,并没有insertAfter,所以要实现这个功能我们需要一个曲线救国的方法:
node.nextSibling 表示node节点的下一个节点,
而想在node的后面插入一个节点,就等于说在node的下一个节点前插入一个新节点node2即可
如上代码就是实现了这个操作,而即使node的下一个节点为空,也能成功插入

新增儿子

 append(parent,node){
        parent.appendChild(node)
    }

找到爸爸节点,用appendChild即可

新增爸爸

 wrap(node,parent){
        dom.before(node,parent)
        dom.append(parent,node)
    }

思路如图:

image.png

分为两步走:

  1. 先把新增的爸爸节点,放到老节点的前面
  2. 再把老节点放入新增的爸爸节点的里面 这样就可以使新的爸爸节点包裹住老节点

使用示例:

const newDiv = dom.create('<div id="newParent"></div>')
dom.wrap(test, newDiv)

删节点

remove(node){
       node.parentNode.removeChild(node)
       return node
    }

找到爸爸节点,removeChild即可

删除所有子节点

empty(node){
    const array = []
    let x = node.firstChild
    while (x) {
        array.push(dom.remove(node.firstChild))
        x = node.firstChild//x指向下一个节点
    }
    return array
    }

其实一开始的思路,是用for循环

for(let i = 0;i<childNodes.length;i++){
    dom.remove(childNodes[i])
}

但这样的思路有一个问题:childNodes.length是会随着删除而变化的
所以我们需要改变思路,用while循环:

  1. 先找到该节点的第一个儿子赋值为x
  2. 当x是存在的,我们就把它移除,并放入数组里面(用于获取删除的节点的引用)
  3. 再把x赋值给它的下一个节点(当第一个儿子被删除后,下一个儿子就变成了第一个儿子)
  4. 反复操作,直到所有子节点被删完

读写属性

 attr(node,name,value){
        if(arguments.length === 3){
            node.setAttribute(name,value)
        }else if(arguments.length === 2){
            return node.getAttribute(name)
        }
    }

这里运用重载,实现两种不同的功能:

  1. 当输入的参数是3个时,就写属性
  2. 当如数的参数是2个时,读属性

使用示例:

//写:
//给 <div id="test">test</div> 添加属性
dom.attr(test,'title','Hi,I am Wang')
//添加之后:<div id="test" title="Hi,I am Wang">test</div>
//读:
const title = dom.attr(test,'title')
console.log(`title:${title}`)
//打印出:title:Hi,Hi,I am Wang

读写文本内容

text(node,string){
    if(arguments.length === 2){
        if('innerText' in node){
            node.innerText = string
        }else{
            node.textContent = string
        }
    }else if(arguments.length === 1){
        if('innerText' in node){
            return node.innerText
        }else{
            return node.textContent
        }
    }
}

为什么这里需要适配,innerText与textContent?
因为虽然现在绝大多数浏览器都支持两种,但还是有非常旧的IE只支持innerText,所以这里是为了适配所有浏览器

同时与读写属性思路相同:

  1. 当输入的参数是2个时,就在节点里写文本
  2. 当输入的参数是1个时,就读文本内容

读写HTML的内容

html(node,string){
    if(arguments.length === 2){
        node.innerHTML = string
    }else if(arguments.length === 1){
            return node.innerHTML
        }

同样,2参数写内容,1参数读内容

修改Style

style(node,name,value){
        if(arguments.length === 3){
            //dom.style(div,'color','red')
            node.style[name] = value
        }else if(arguments.length === 2){
            if(typeof name === 'string'){
             //dom.style(div,'color')
            return node.style[name]    
            }else if(name instanceof Object){
                //dom.style(div,{color:'red'})
                const Object = name
                for(let key in Object){
                    //key:border/color
                    //node.style.border = ...
                    //node.style.color = ...
                    node.style[key] = Object[key]
                } 
            }
        }
    }

思路:

  1. 首先判断输入的参数,如果为3个如:dom.style(div,'color','red')
  2. 就更改它的style
  3. 如果输入参数为2个时,先判断输入name的值的类型
  4. 如果是字符串,如dom.style(div,'color'),就返回style的属性
  5. 如果是对象,如dom.style(div,{border:'1px solid red',color:'blue'}),就更改它的style

增删查class

class:{
    add(node,className){
        node.classList.add(className)    
    },
    remove(node,className){
        node.classList.remove(className)
    },
    has(node,className){
        return node.classList.contains(className)
    }
}

注:查找一个元素的classList里是否有某一个class, 用的是contains

添加事件监听

on(node,eventName,fn){
       node.addEventListener(eventName,fn) 
    }

使用示例:

const fn = ()=>{
    console.log('点击了')
}
dom.on(test,'click',fn)

这样当点击id为test的div时,就会打印出'点击了'

删除事件监听

off(node,eventName,fn){
    node.removeEventListener(eventName,fn)
    }

获取单个或多个标签

find(selector,scope){
    return (scope || document).querySelectorAll(selector)
    }

可以在指定区域或者全局的document里找

使用示例:
在document中查询:

const testDiv = dom.find('#test')[0]
console.log(testDiv)

在指定范围内查询:

 <div>
        <div id="test"><span>test1</span>
        <p class="red">段落标签</p>
        </div>
        <div id="test2">
            <p class="red">段落标签</p>
            </div>
    </div>

我只想找test2里面的red,应该怎么做

const test2 = dom.find('#test2')[0]
 console.log(dom.find('.red',test2)[0])

注意:末尾的[0]别忘记写

获取父元素

parent(node){
    return node.parentNode
    }

获取子元素

children(node){
    return node.children 
}

获取兄弟姐妹元素

siblings(node){
    return Array.from(node.parentNode.children).filter(n=>n!==node) //伪数组变数组再过滤本身
    }

找到爸爸节点,然后过滤掉自己本身

获取弟弟

next(node){
    let x = node.nextSibling
    while(x && x.nodeType === 3){
        x = x.nextSibling
    }
    return x
    }

为什么这里需要while(x && x.nodeType === 3)
因为我们不想获取文本节点(空格回车等)
所以当读到文本节点时,自动再去读取下一个节点,直到读到的内容不是文本节点为止

获取哥哥

previous(node){
        let x = node.previousSibling
        while(x && x.nodeType === 3){
            x = x.previousSibling
        }
        return x
    }

与上面思路相同

遍历所有节点

each(nodeList,fn){
        for(let i=0;i<nodeList.length;i++){
            fn.call(null,nodeList[i])
        }
    }

注:null用于填充this的位置
使用示例:
利用fn可以更改所有节点的style

const t = dom.find('#travel')[0]
dom.each(dom.children(t),(n)=>dom.style(n,'color','red'))

遍历每个节点,把每个节点的style都更改

用于获取排行老几

index(node){
    const list = dom.children(node.parentNode)
    let i;
    for(i=0;i<list.length;i++){
        if(list[i]===node){
             break
        }
    }
    return i
    }

思路:

  1. 获取爸爸节点的所有儿子
  2. 设置一个变量i
  3. 如果i等于想要查询的node
  4. 退出循环,返回i值