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
},
两个关键:
- 用template标签作为container,是因为这个标签可以容纳所有标签
- 用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函数
- dom.style(node, 'color', 'red') 设置一个属性
- dom.style(node, 'color') 读一个属性
- 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 //表示没找到
}