配电脑的比喻

目标

对于BOM/DOM/Canvas/jQuery/Date/LocalStorage/RegExp/Lodash/Underscore/URL 处理的封装,先实现部分 DOM


目录

  • 术语
  • 两种风格封装DOM操作
  • 命名风格
  • 细节
  • 增删改查
  • 后续
  • 设计模式
  • 正确地缩写 document.querySelector

术语

  • 库:工具代码
  • API:应用编程接口(中文翻译)(外部可使用的库的函数或属性)
  • 框架:framework(英文翻译)
  • 封装(encapsulation)
  • 意会即可

两种风格封装DOM操作

对象风格

也叫命名空间风格

各功能接口

接口

1
2
3
4
5
6
dom.create(`<div>hi</div>`) // 用于创建节点
dom.after(node, node2) // 用于向后追加兄弟节点
// 原生的提供了一个兼容性不佳的实验性接口`ChildNode.after() MDN`
dom.before(node, node2) // 用于向前追加兄弟节点
dom.append(parent, child) // 用于创建子节点
dom.wrap(`<div></div>`) //  用于创建父节点

index.html

1
2
3
4
5
6
7
8
<body>
    示例
    <div id="dad">
        <div id="test">test</div>
    </div>
    <script src="dom.js"></script>
    <script src="main.js"></script>
</body>

dom.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 相当于 dom.create = function() {}
window.dom = {
    // create: function() {} // 可简化为
    create(tagName /* 语义化 形参 */ ) {
        // return document.createElement(tagName) // 不能创建带有结构的 HTML 元素`<div><span>1</span></div>`

        // const container = document.createElement("div")
        const container = document.createElement("template")
        container.innerHTML = tagName.trim() // 除去空格
            // return container.children[0]
        return container.content.firstChild
            /* 存在 不可识别元素(<td></td>)的 bug
             ** <td</td>> 不能单独存在 只能放在<table></table> 里<tr></tr>或<tbody></tbody> 里,放在 div 里不符合 HTML 语法
             ** 可以放任意元素,不出 bug 的标签是 <template></template>
             ** <template></template> 是专门用来容纳任意标签的
             ** <template></template> 用template.content.firstChild拿到
             */
    },
    after(node, node2) { // 在后面插入节点,就相当于在此 node 后面的节点的前面插 // 必须调用父节点的 insertBefore() 方法
        console.log(node.siblings) // null ?
        node.parentNode.insertBefore(node2, node.nextSibling)
            /* 判断 排除最后一个节点 没有下一个节点 null 也符合 */
    },
    before(node, node2) {
        node.parentNode.insertBefore(node2, node)
    },
    append(parent, node) {
        parent.appendChild(node)
    },
    wrap(node, parent) {
        dom.before(node, parent) // 将要包裹的“父节点”先插到目标节点的前面
        dom.append(parent, node) // 再把目标节点用 append 移至将要包裹的父节点的下面
    }
}

main.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// 对比 document.createElement("div") 简化代码
// const div = dom.create("div")
/* 直接写出 HTML 结构 */
const div = dom.create("<div><span>newDiv</span></div>")
const td = dom.create("<tr><td>TD</td></tr>")
console.log(div)
console.log(td)
/* after */
dom.after(test, div)
const div3 = dom.create('<div id="wrapper">DIV3</div>') // 父节点
dom.wrap(test, div3)

接口

1
2
dom.remove(node) //用于删除节点
dom.empty(parent) //用于删除后代节点

dom.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
window.dom = {
    remove(node) {
        // node.remove() // IE 不支持 兼容性不好
        node.parentNode.removeChild(node);
        return node; // 仍然需要获取此节点的引用
    },
    empty(node) {
        // 清空 node 里面的所有子元素
        // node.innerHTML = ''
        // const childNodes = node.childNodes 可以改写成以下的写法
        /*
         ** const {childNodes} = node // 解构赋值
         */
        const array = [];
        /*
         **    for (let i = 0; i < childNodes.length; i++) { // 不需要i++的循环就用 while 循环代替
         **        console.log(childNodes)
         **        console.log(childNodes.length)
         **        dom.remove(childNodes[i]) // remove( nodes) 会实时改变 nodes 的长度每次减一 导致循环的长度不固定 出现 bug
         **        array.push(childNodes[i])
         **    }
         */
        //  不需要i++的循环就用 while 循环代替for 循环

        /* 获取第一个子节点 并 push 进数组 */
        let x = node.firstChild;
        while (x) {
            // 如果 x 存在
            array.push(dom.remove(node.firstChild));
            x = node.firstChild; // 第一个子节点已经移除 原先第二节点就变为现在的第一个节点
        }
        return array; // 仍然需要获取此节点的引用
    }
}

main.js

1
2
3
4
5
dom.class.add(test, "ftSize");
dom.class.remove(test, "ftSize");
/* empty test */
const nodes = dom.empty(window.empty);
console.log(nodes);

接口

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
dom.attr(node, 'title', ?) // 用于读写属性
dom.text(node, ?) // 用于读写文本内容
dom.html(node, ?) // 用于读写HTML内容
dom.style(node, "color", "red") // 用于修改style
dom.style(node, {color: "red"}) // 用于修改style
dom.style(node, "border") // 用于读取style
dom.class.add(node, 'blue') // 用于添加class
dom.class.remove(node, 'blue') // 用于删除class

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

/* 是否能有效删除事件监听? */
dom.off(node, 'click', fn) // 用于删除事件监听
dom.toggle(node,'click',fn) // 切换开关

dom.js

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
window.dom = {
    /* 改 用于读写属性 */
    /* 用判断 arguments 的个数来重载函数 */
    /* 重载
     ** 有三个形参时,就是设置;
     ** 第二个形参时,就是读取
     */
    attr(node, name, value) {
        // 组合
        if (arguments.length === 3) {
            node.setAttribute(name, value); // 原生DOM setAttribute(name, value)
        } else if (arguments.length === 2) {
            return node.getAttribute(name); // 原生DOM getAttribute(name) 并返回值
        }
    },
    /* 用于读/写文本内容 */
    /* 重载
     ** 有两个形参时,就是设置;
     ** 第一个形参时,就是读取
     */
    text(node, string) {
        // 设计模式 之 适配
        // console.log('innerText' in node) //true
        if (arguments.length === 2) {
            /* 写 */
            if ("innerText" in node) {
                node.innerText = string; // IE // 会将节点原本的所有内容,包括标签全部改变
            } else {
                node.textContent = string; // Chrome/ Firefox // 会将节点原本的所有内容,包括标签全部改变
            }
        } else if (arguments.length === 1) {
            /* 读 */
            if ("innerText" in node) {
                return node.innerText; // IE // 会将节点原本的所有内容,包括标签全部改变
            } else {
                return node.textContent; // Chrome/ Firefox // 会将节点原本的所有内容,包括标签全部改变
            }
        }
    },
    /* 用于读/写HTML内容 */
    html(node, string) {
        if (arguments.length === 2) {
            node.innerHTML = string;
        } else if (arguments.length === 1) {
            return node.innerHTML;
        }
    },
    /* 用于修改style */
    /* 重载
     ** 第二个形参是对象时,就是设置;dom.style(div, {color: "red"})
     ** 有三个形参时,也是设置;dom.style(div, 'color', 'red')
     ** 第二个形参是字符串时,就是读取 dom.style(div, 'color')
     */
    style(node, name, value) {
        if (arguments.length === 3) {
            // dom.style(div, "color", "red"')
            return node.style[name] = value;
            // 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 /* true */ ) {
                // dom.style(div, {color:'red'})
                const object = name;
                for (let key in object) {
                    // 遍历读取所有对应的key
                    // key: border | color | ···
                    // node.style.border = ...
                    // node.style.color = ...
                    // 调用属性值 []方法 读取的时变量;点方法 读取的是字符串
                    // node.style.key; // 字符串
                    node.style[key] = object[key];
                }
                return object
            }
        }
    },
    class: {
        /* 用于添加class */
        add(node, className) {
            node.classList.add(className)
        },
        /* 用于删除class */
        remove(node, className) {
            node.classList.remove(className)
        },
        has(node, className) {
            return node.classList.contains(className)
        }
    },
    /* 事件相关 */
    on(node, eventName, fn) {
        node.addEventListener(eventName, fn)
    },
    off(node, eventName, fn) {
        console.log(`${eventName}取消事件`)
        node.removeEventListener(eventName, fn)
    },
    toggle(node, eventName, fn) {
        node.addEventListener("mousedown", function() {
            console.log("鼠标按下了");
            node.addEventListener(eventName, fn);
            node.addEventListener("mouseup", function() {
                console.log("鼠标抬起了");
                 node.removeEventListener(eventName, fn)
            })
        });
    }
}

main.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
/* 改 */
/* 用于读写属性  attr(node, attributeName, value) */
dom.attr(test, "title", "Hi, Jack");
const title = dom.attr(test, "title");
console.log(`title: ${title}`);
/* 用于读写文本内容 */
dom.text(test, "Hello,this is a new text");

/* 用于读写HTML内容 */
const Dad = dom.html(dad);
console.log(`Dad: ${Dad}`);

/* 用于修改style */
dom.style(test, {
    border: "1px solid cyan",
    color: "red"
});
console.log(dom.style(test, "border"));
/* 重载 修改 style */
dom.style(test, "border", "5px solid olive");
/* 重载 读取 style */
const styleBorder = dom.style(test, "border");
console.log(styleBorder);
/* 用于添加class */
dom.class.add(test, "bgColor");
dom.class.add(test, "ftSize");
dom.class.remove(test, "ftSize");
console.log(dom.class.has(test, "bgColor"));
console.log(dom.class.has(test, "ftSize"));
/* 用于添加事件监听 */
function addFn() {
    console.log('指到这里')
    console.log(`点击 ${newDiv} 取消事件`)
    console.log(newDiv)
}

// test.addEventListener('click') // TL,DR
dom.on(test, "mouseenter", addFn);
/* 用于删除事件监听 */
// test.removeEventListener(eventName, fn) // TL,DR
dom.off(test, "click", addFn);
dom.toggle(test, "click", addFn);

用到了设计模式:重载 适配

接口

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
/* 用 ID 来返回元素 */
dom.find('选择器') // 用于获取标签(们)
/* 无法动态地查找 因为用了 querySelector */
dom.parent(node) // 用于获取父元素
dom.children(node) // 用于获取子元素
dom.siblings(node) // 用于获取兄弟元素
dom.next(node) // 用于获取前一个元素
dom.previous(node) // 用于获取后一个元素
dom.each(node, fn) // 用于遍历所有节点
dom.index(node) // 用于获取排列下标

dom.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
window.dom = {
    /* 查 */
    /* scope 为查找的范围 节点对象 */
    find(selector, scope) {
        /* 如果有 scope 节点 就找 scope 里的;没有就找 document 里的 */
        return (scope || document).querySelectorAll(selector)
            /* 返回的是 NodeList 伪数组 取用加 NodeList[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
    },
    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])
        }
    },
    index(node) {
        const list = dom.children(node.parentNode)
        let i
        for (i = 0; i < list.length; i++) {
            if (list[i] === node) {
                break
            }
        }
        return i
    }
}

main.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/* 查 */
/*
<div id="siblings">
    <div id="s1"></div>
    <div id="s2"></div>
    <div id="s3"></div>
</div>
<div id="travel">
    <div id="t1">t1</div>
    <div id="t2">t2</div>
    <div id="t3">t3</div>
</div>
*/

console.log(`=== 找父、兄节点 ===`)
console.log('parent')
console.log(dom.parent(test))
console.log('children')
console.log(dom.children(test2))
console.log('siblings')
const s2 = dom.find('#s2')[0]
console.log(dom.siblings(s2))
console.log('next')
console.log(dom.next(s2))
console.log('previous')
console.log(dom.previous(s2))
console.log(`=== ===`)
    /* 遍历 */
const t = dom.find('#travel')[0]
dom.each(dom.children(t), (n) => dom.style(n, 'color', 'red'))
    /* 下标 */
console.log(dom.index(s2))

看思路,而不是源代码


正确地缩写 document.querySelector

错误的

1
2
var $ = document.querySelectorAll;
console.debug($('body'));

这里报错的原因是 querySelectorAll 所需的执行上下文必需是 document

而赋值到 $ 调用后上下文变成了全局 window。

  • 正确的姿势去 alias

    1
    2
    3
    4
    5
    6
    
    let $ = document.querySelectorAll.bind(document);
    let query = document.querySelector.bind(document);
    let queryAll = document.querySelectorAll.bind(document);
    let fromId = document.getElementById.bind(document);
    let fromClass = document.getElementsByClassName.bind(document);
    let fromTag = document.getElementsByTagName.bind(document);
    

需要注意的地方是,这些方法返回的要么是单个 Node 节点,要么是 NodeList 而 NodeLis 是类数组的对象,但并不是真正的数组,所以拿到之后不能直接使用 map,forEach 等方法。

  • 正确的操作姿势应该是:

    1
    2
    3
    4
    5
    
    Array.prototype.map.call(document.querySelectorAll('button'),function(element,index){
    element.onclick = function(){
    }
    })
    // Set、Array.from等
    

·未完待续·

参考文章

相关文章