随手一丢 就是一个DOM库

708 阅读5分钟

DOM提供的接口实在是不好用,于是今天我来手写一个DOM库
源代码链接:
github.com/shawnjoncha…

我们提供一个全局的window.dom对象
接下来分别就“增删改查”进行说明

一、增加元素、增加节点

dom.create(<div>hi</div>) 创建节点

dom api需要用document.createElement('div'), div.innerText = 'hi' 两句话
这里我们只需要一句话即可,dom.create(<div>hi</div>)

 //目标效果
    // 输入create("<div><span>你好</span></div>")
    // 自动创建好div和span
    //实现思路,直接把字符串写进InnerHTML
    // 用template是因为这个标签里可以容纳所有标签,
    // div标签里就不能放<tr></tr>标签,而template可以
    create(string) {
        const container = document.createElement("template")
        container.innerHTML = String.prototype.trim.call(string) // 去掉多余空格
        return container.content.firstChild
    },

两个关键:

  1. 用template标签作为container,是因为这个标签可以容纳所有标签
  2. 用trim()去掉多余的空格,因为空格也算做节点,因此可能会干扰结果

dom.after(node, node2) 用于新增弟弟

思路:由于dom只提供了Insertbefore操作,没有提供insertAfter操作, 所以我们需要使用一点黑魔法
假设有两个节点 div1 --- > div3
我们想要再div1后面加一个div2, 这就等价于在div3前面插入div2
所以我们用现有的node.nextSibling获取当前节点的下一个节点,再insertBefore

after(node, node2) {
        // 目标是在node节点后面插入node2节点
        // 但是DOM只提供了insertBefore接口
        // 1 -> 3
        // 在1后面插入2, 等价于在3的前面插入2
        // 所以我们转换为在node的下一个节点的前面插入node2
        node.parentNode.insertBefore(node2, node.nextSibling)
    },

dom.before(node, node2) 用于新增哥哥

思路:找到爸爸节点,然后insertBefore

before(node, newNode) {
        node.parentNode.insertBefore(newNode, node)
},

dom.append(parent, child) 用于新增儿子

思路:找到爸爸节点,appendChild

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

dom.wrap(<div></div>)用于新增爸爸

wrap(node, newParent) {
        // 把Newparent 放到node前面
        // 把node append到newparent里
        // 目标: div1
        //        ↓----> div2
        // 变成  div1
        //        ↓----> div3
        //                ↓----> div2
            // 先把div3 放到div2的前面,再div3.appendChild(div2)
        node.before.call(null, node, newParent)
        newParent.append(node)
},

使用方法:

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

二、删除节点

dom.remove(node) 用于删除节点

思路:找到爸爸节点, removeChild

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

dom.empty(parent) 用于删除后代

很容易想到的一个思路是,用for循环遍历Parent.childNodes, 然后每次都remove
但是这里有一个坑是childNodes.length每次的Length会实时变化
所以这里我们用while循环
只要node还有子节点,就不断遍历

 // empty 把所有子节点删掉
    // 坑:childNodes.length每次的Length会变化
    empty(node) {
        // const {childNodes} = node 等价于const childNodes = node.childNodes
        const array = []
        let x = node.firstChild
        while (x) {
            array.push(dom.remove(node.firstChild))
            x = node.firstChild
        }
        return array
},

三、修改

dom.attr(node, 'title', ?) 用于读写属性

这里用到了重载,即当函数的参数不一样时,做不一样的处理
当只有两个参数时,就读属性,返回属性
当有三个参数时,就修改属性
这里由于同时可以读写属性,用到了getter和setter设计模式

// 根据参数的个数,实现不同的函数,这叫函数的重载
    attr(node, name, value) {
        if (arguments.length === 3) {
            node.setAttribute(name, value)
        } else if (arguments.length === 2) {
            return node.getAttribute(name)
        }
},

使用方法

// 修改 <div id="test" title="hi">test</div>
// #test的title属性值为 hello world
dom.attr(test, 'title', 'hello world')  //修改属性
const title = dom.attr(test, 'title')   //读属性
console.log(`title: ${title}`)

dom.text(node, ?) 用于读写文本内容

如果只有一个参数就读文本内容,返回node.innerText
如果有两个参数,就写文本内容 node.innerText = string
同时还对不同浏览器做了适配,这里运用了适配器模式

text(node, string) {
        if (arguments.length === 2) {
            // 适配不同浏览器
            if ('innerText' in node) { //ie
                node.innerText = string
            } else { // firefox / chrome
                node.textContent = string
            }
        } else if (arguments.length === 1) {
            if ('innerText' in node) { //ie
                return node.innerText
            } else { // firefox / chrome
                return node.textContent
            }
        }
},

dom.html(node, ?) 用于读写HTML内容

 html(node, string) {
        if (arguments.length === 2) {
            //修改
            node.innerHTML = string
        } else if (arguments.length === 1) {
            // 获取内容
            return node.innerHTML
        }
},

dom.style(node, {color:'red'}) 用于修改style

大体思路:修改 node.style.color = 'red'
需要注意的是,这里会有三种情况使用style函数

  1. dom.style(node, 'color', 'red') 设置一个属性
  2. dom.style(node, 'color') 读一个属性
  3. dom.style(node, {'color': 'red', 'border': '1px solid black'}) 传一个对象,同时设置多个属性 所以我们需要对不同的参数进行处理,再次用到了适配器模式
    首先我们需要判断参数的个数,
  • 如果是3,则是第一种情况,直接node.style.color = 'red'
  • 如果是2
    • 如果第二个参数是字符串
      • 判断一个东西是字符串的方法: 用typeof xxxxx === 'string'
typeof name === 'string'  
  • 然后直接return node.style[name]
    • 如果第二个参数是一个对象
    • 判断一个东西的方法:用instanceof : XXX instanceof Object === true
    • 然后遍历这个对象
    • 遍历对象的写法:
for (let key in object) {
   ......                  
}

实现代码:

//改样式
    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(test, 'border')
                // 获取某个css属性
                return node.style[name]
            }
            if (name instanceof Object) {
                //dom.style(test, {border: '1px solid red', color: 'blue'})
                let object = name
                for (let key in object) {
                    // key : border / color
                    // node.style.border = ....
                    // node.style.color = ...
                    node.style[key] = object[key]
                }
            }
        }
},

dom.class.add(node, 'blue') 用于添加class

大致思路:node.classList.add(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 方法

dom.on(node, 'click', fn) 用于添加事件监听

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

dom.off(node, 'click', fn) 用于删除事件监听

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

四、查

dom.find('选择器', scope) 用于获取标签或标签们

在指定区域或者全局document里找HTML标签
大致思路:document.querySelector('selector')

 // 根据选择器获取元素
    find(selector, scope) {
        return (scope || document).querySelectorAll(selector)
    },

dom.parent(node) 用于获取父元素

parent(node) {
        return node.parentNode
},

dom.children(node) 用于获取子元素

children(node) {
        return node.children
},

dom.siblings(node) 用于获取兄弟姐妹元素

找到爸爸节点,然后删掉是我自己的元素

siblings(node) {
  // 我父母所有孩子里不是我的,就是我的兄弟姐妹
  return Array.from(node.parentNode.children).filter(n => n !== node)
},

dom.next(node) 用于获取弟弟

注意: while (x && x.nodeType !== 1) 表示如果x存在,且x不是HTML元素节点,就继续找下一个,直到找到一个节点是HTML元素节点
这样写的原因是,节点包括HTML标签、 文本、注释等,所以需要这样处理一下,保证我们找到的是一个HTML标签

next(node) {
     let x = node.nextSibling
     while (x && x.nodeType !== 1) {
       // x 不是想要的HTML标签
       x = x.nextSibling
     }
 return x
},

dom.previous(node) 用于获取哥哥

prev(node) {
   let x = node.previousSibling
   while (x && x.nodeType !== 1) {
      // x 不是想要的HTML标签
      x = x.previousSibling
    }
  return x
}

dom.each(nodes, fn) 用于遍历所有节点

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

dom.index(node) 用于获取排行老几

index(node) {
        let nodeList = dom.children(dom.parent(node))
        let i
        for (i = 0; i < nodeList.length; i++) {
            console.log(nodeList[i])
            if (nodeList[i] === node) {
                return i
            }
        }
       return -1 //表示没找到
    }