练习:手写封装 DOM 库之删改节点

175 阅读4分钟

这里是手写 DOM 库第二篇~

1. 配置环境

在终端用 yarn 安装 parcel: yarn global add parcel 启动服务器: parcel src/index.html

2. 对象风格(命名空间风格)封装 DOM 操作

window.dom 是我们提供的全局对象,在src目录中创建index.html, dom.js 和 main.js

3. 功能:删除节点

(1) 删除节点本人

<!--index.html-->
<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>手写 DOM 封装</title>
</head>
<body>
Halo
<div id="test">test</div>
<script src="dom.js"></script>
<script src="main.js"></script>
<!--引入dom和main.js-->
</body>
</html>
// ...在dom.js中添加
    remove(node){
        node.parentNode.removeChild(node)
        return node
    }

main.js这里使用node.parentNode.removeChild(node),也可以写成node.remove(node),但是由于直接.remove的这个接口比较新,IE可能不支持,所以使用删除节点爸妈的孩子这个方法删除~

// main.js
const div = dom.create('<div>test的弟弟</div>');
const div2 = dom.create('<div>test的哥哥</div>');
const div3 = dom.create('<div>test的孩子</div>');
const div4 = dom.create('<div>test的妈妈</div>');

dom.after(test, div);
dom.before(test,div2);
dom.append(test,div3);
dom.wrap(test,div4);
dom.remove(div3)

得到结果如图,test的孩子被删除了⬇️

删除之前是这样的⬇️

(2) 删除节点后代

  • 在index.html中添加节点<emptyTest>和其后代
<div id="emptyTest">
    <div id="小刘">贝贝</div>
    <div id="小关">鱼鱼</div>
    <div id="小张">菲菲</div>
</div>
  • 在dom.js中,声明空数组array,取节点中的第一个子节点(老大),用dom.remove(node)删除节点中的这个孩子,并将他添加到array的最后一位;以此类推,直到最后一个子节点/最小的孩子也没了,返回array
    empty(node){
        const array = []
        let x = node.firstChild
        while (x){
            array.push(dom.remove(node.firstChild))
            x = node.firstChild
        }
        return array
    }
  • 然后在main.js中运行封装好的dom.empty得到如下图所示
// main.js
const nodes = dom.empty(window.emptyTest)
console.log(nodes)


⚠️ 注意这里每个子节点之间有回车,所以会返回四个text

4. 功能:修改节点

(1) 读写属性

  • 在dom.js中,令 attr() 函数接受节点,属性名和属性值三个参数;然后使用API .setAttribute 设定属性
    attr(node, name, value){ 
        node.setAttribute(name, value) 
    }
  • 在main.js中,运行dom.attr,更改title属性
dom.attr(test, 'title', '这是一个新名字')

效果如下⬇️

  • 这里的 attr 还可以只接受两个参数,可将其改写为,如果长度为3就set,如果长度为2就get ⬇️
// dom.js
    attr(node, name, value){ 
        if(arguments.length === 3){
            node.setAttribute(name, value)
        }else if(arguments.length === 2){
            return node.getAttribute(name)
        }
    }
  • main.js更改为:
dom.attr(test, 'title', '这是一个新名字')
const title = dom.attr(test, 'title')
console.log(`title: ${title}`)

控制台打印出新的title的值如图 ⬇️

(2) 读写文本内容

  • dom.js
    text(node, string){
        node.innerText = string
    }

这里还可以优化成 ⬇️ ,兼容 innerText(展示给人看的元素) 和 textContent (所有元素内容),这个操作叫做适配
关于 Node.textContent 和 HTMLElement.innerText 的区别,请详见 MDN 文档

    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
            }
        }
    }
  • main.js
dom.text(test, '哈喽,这是新的内容~')

此时test文本内容有了新的内容更改⬇️

(3) 读写HTML内容

  • dom.js 中添加有适配功能的 dom.html 函数,根据参数的长度(1/2)实现不同的效果(读/写)
    html(node, string){
        if(arguments.length === 2){
            node.innerHTML = string
        }else if(arguments.length === 1){
            return node.innerHTML
        }
    }

(4) 修改style样式

  • dom.js
    style(node, object){
        for(let key in object){
            node.style[key] = object[key]
        }
    }
  • main.js
dom.style(test, {border: '2px solid red', color: 'blue', background: 'yellow'})

得到效果如图

  • dom.style 的适配
// 改写dom.js的dom.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'})
                // 如果 name 是 Object 的实例
                for(let key in name){
                    node.style[key] = name[key]
                }
            }
        }
    }
// 改写 main.js
dom.style(test, {border: '2px solid red', color: 'blue', background: 'yellow'})
console.log(dom.style(test, 'border'))
dom.style(test, 'border', '1px solid black')

控制台先打印出第一次设置的 border 样式,浏览器渲染出最终设置的 border 样式如图 ⬇️

(5) 添加、修改和查看 class

添加 class
  • 在 index.html的 <head> 中添加 <style>
    <style>
        .pink{
            background: pink;
        }
    </style>
  • dom.js
    class: {
        add(node, className){
            node.classList.add(className)
        },
        remove(node, className){
            node.classList.remove(className)
        },
        has(node, className){
            return node.classList.contains(className)
        }
    }
  • 在main.js中运行dom.class.add
dom.style(test, {border: '2px solid red', color: 'grey'})
dom.style(test, 'border', '3px dashed purple')
dom.class.add(test, 'pink')

此时背景色被设置为粉色如图

修改 class
  • 在main.js中运行dom.class.remove
dom.class.remove(test, 'pink')

此时页面和class中的粉色均被删去如图 ⬇️

查找 class
  • 在 main.js 中运行打印 dom.class.has 会返回一个布尔值
console.log(dom.class.has(test, 'pink')) 

得到控制台如下,所以 pink 不存在于 class

(6) 添加 / 删除事件监听

  • dom.js 中添加 onoff
    on(node, eventName, fn){
        node.addEventListener(eventName, fn)
    },
    off(node, eventName, fn){
        node.removeEventListener(eventName, fn)
    }
  • 在main.js中运行 dom.on
dom.on(test, 'click', ()=>{
    console.log('test被点击了!')
})

点击前 ⬇️
点击后~ ⬇️

  • 在 main.js 中运行 dom.off
const fn = () => {
    console.log('被点击了!')
} // 命名一下这个函数,方便之后调用
dom.on(test, 'click', fn)
dom.off(test, 'click', fn)

现在不管怎么点击都没有效果,因为事件监听在添加之后就被删除了~
~本系列未完待续~

Reference List | 参考资料
MDN, Element.setAttribute: developer.mozilla.org/en-US/docs/…
MDN, textContent VS innerText: wiki.developer.mozilla.org/zh-CN/docs/…