被人忽视的 DOM API

在框架盛行的年代,还有多少人记得在没有框架时我们如何控制 dom 的行为呢?作者本人也一直忽视了这方面的学习,直到面试问到这个问题,下决心好好认识认识这个 dom api。

node、document 和 element

在学习 dom api 时对这三者还是挺混乱的。理一下他们之间的关系。

node

node 是一个接口,像 document 和 element 都是继承这个接口的。这个接口提供了 dom 节点的获取和操作方法。
node 有许多类型,下图列出了一些 node 的类型码。由图可见 element 的类型码为 1,文本节点类型码为 3,注释节点类型码为 8,document 的类型码为 9。

node type

这让我想到了 vue.js 中出现多次的代码:if (child.nodeType === 3) { ... } 其实就是判断当前节点是否为文本节点。

element

从上面的内容可知,element 就是一个特殊的 node(nodeType == 1),其实 element 就是 HTML 各类标签,如 <p><div> 这类有特殊含义的,能够携带一些特殊属性的节点。所以说 element 可以用 node 的所有 api。

document

同理,document 也是一个特殊的 node,它与 element 的不同之处在于 document 通常是 DOM 节点,即包含有 head 和 body 元素的一个 node。

参考:Difference between Node object and Element object? - Stack Overflow

api 学习

首先我发现一点:所有 dom 操作的起点都是使用 document 去获取各种类型的 node (集合)然后再去执行各类 dom 操作的行为!
由于 document 操作的 api 真的很多,所以我选取我想了解的部分学习了。在这里我学习 DOM API 的目的是:

学习 document 对象操作 dom 的方式,拥有脱离框架(jquery、vue等)来操作 dom 的能力。

获取节点

  • Document.documentElement 返回 document 的直属后代元素。
  • Document.activeElement 返回当前正在操作的元素
  • Document.body 返回当前文档的 <body> 元素。与此类似的还有 Document.head 和 Document.scripts 两个属性返回当前文档的 <head><script> 元素。
  • Document.getElementByClassName() 返回有给定样式名的元素列表
  • Document.getElementByTagName() 返回有给定标签名的元素列表
  • document.getElementById() 返回一个对识别元素的对象引用
  • document.querySelector() 返回文档中第一个匹配指定选择器的元素
  • document.querySelectorAll() 返回一个匹配指定选择器的元素节点列表
  • Node.childNodes 返回一个包含了该节点所有子节点的实时的 NodeList 是“实时的”意思是,如果该节点的子节点发生了变化,NodeList 对象就会自动更新。
  • Node.firstChild & Node.lastChild 返回该节点的第一个子节点或最后一个子节点,如果该节点没有子节点则返回 null。
  • Node.previousSibling & Node.nextSibling 返回与该节点同级的上一个或下一个节点,如果没有返回null。
  • Node.ownerDocument 返回这个元素属于的 Document 对象 。
  • Node.parentNode 返回一个当前结点 Node 的父节点 。
  • Node.parentElement 返回一个当前节点的父节点 Element

操作节点

  • Document.createComment() 创建一个新的注释节点并返回它
  • Document.createDocumentFragment() 创建一个新的文档片段
  • Document.createElement() 用给定的标签名创建一个新的元素。
  • Document.createTextNode() 创建一个文字节点
  • Document.write() 向文档中写入内容(与之有类似功能的是 Document.writeln() 不同之处在于后面多了个换行符。)
  • Element.innerHTML 设置或返回元素的内容
  • Node.textContent 获取或设置一个标签内所有子结点及其后代的文本内容。
  • Node.appendChild() 向元素添加新的子节点,作为最后一个子节点。
  • Node.cloneNode() 克隆元素(方法中传参为deep,如果deep为true则深拷贝。)
  • Node.insertBefore() 在指定已有节点前插入新节点(没有 insertAfter 方法。可以使用 insertBefore 方法和 nextSibling 来模拟它。)
  • Node.normalize() 合并元素中相邻文本节点
  • Node.removeChild() 从元素中移除子节点
  • Node.replaceChild() 替换元素中的子节点

其中 Document 的 createXXX 方法还有一些其他不常用的,如需使用请查阅 MDN。

其他常用属性和方法

  • Element.classList 返回元素的 class 集合。
  • EventTaget.addEventListener() 注册监听事件
  • Node.nodeType 返回该节点的类型码
  • Node.nodeValue 返回或设置当前节点的值。
  • Node.compareDocumentPosition() 比较当前节点与任意文档中的另一个节点的位置关系。
  • Node.contains() 传入的节点是否为该节点的后代节点。
  • Node.hasChildNodes() 是否拥有子节点
  • Node.isEqualNode() 检查两个元素是否相等
  • Node.isSameNode() 检查两个元素是否为相同的节点

以上内容均参考了 MDN 上的内容:

写个操作 DOM 的例子

接下来就使用这些 API 来进行一些 DOM操作。

获取各个位置的节点。

这里写了个小demo:

<div id="container">
    <div>
        <h1>get dom</h1>
        <ul id="list">
            <li><span>hello world 1</span></li>
            <li><span>hello world 2</span></li>
            <li><span>hello world 3</span></li>
            <li><span>hello world 4</span></li>
            <li><span>hello world 5</span></li>
        </ul>
    </div>
    <br/>
    <div>
        <button>commit</button>
    </div>
</div>

<script>
    var container = document.getElementById('container')
    console.log('列出所有node', container.childNodes)
    var h1 = document.getElementsByTagName('h1')[0]
    console.log('获取h1后的元素', h1.nextSibling)
    var uldiv = container.firstChild
    while (uldiv && uldiv.nodeType != 1) {
        uldiv = uldiv.nextSibling
    }
    var ul = uldiv.lastChild
    while (ul && ul.nodeType == 3) {
        ul = ul.previousSibling
    }
    console.log('获取ul中第一个元素内容', ul.firstChild)
    var doc = h1.ownerDocument
    console.log('获取当前 Document 对象', doc)
    var li1 = ul.firstChild
    console.log('获取li的父级节点', li1.parentElement)
    var button = document.getElementsByTagName("button")[0]
    button.onclick = log
    button.focus()
    button.click()
    console.log('获取正在操作的元素', document.activeElement)

    function log(){
        console.log('button is clicked')
    }
</script>

最后返回结果如图:

返回结果

由于在 chrome 中空格、换行算是文本节点。所以获取最后元素的时候总是会获取到那些文本节点上去。这个要注意的。所以我在代码中使用 nodeType == 1 来区分是否为元素。
在上面例子中查找了各种关系的元素,解决日常元素获取问题应该不难了。

实践创建node、插入node和删除node。

<div>
    <div id="container">
        <h2 id="child">Hello Child</h2>
    </div>
    <br/>
    <div id="buttonGroup"></div>
</div>

<script>
    // 父节点向子节点插入元素
    function appendChild(){
        var container = document.getElementById("container")
        var text = document.createElement("h2")
        text.textContent = 'Hello New Child'
        container.appendChild(text)
    }
    // 子节点获取父节点,在父节点后插入元素
    function appendParent(){
        var child = document.getElementById('child')
        var parent = child.parentElement
        var text = document.createElement("h1")
        text.textContent = 'Hello Parent'
        var root = parent.parentElement

        root.insertBefore(text, parent.nextSibling)
    }
    // 在当前元素前插入元素
    function appendPre(){
        var child = document.getElementById('child')
        var text = document.createElement("h2")
        text.textContent = 'Hello Pre Child'
        child.parentElement.insertBefore(text, child)
    }
    // 在当前元素后插入元素
    function appendNext(){
        var child = document.getElementById('child')
        var text = document.createElement("h2")
        text.textContent = 'Hello Next Child'
        child.parentElement.insertBefore(text, child.nextSibling)
    }
    // 移除父元素中最后一个子元素
    function removeEle(){
        var container = document.getElementById('container')
        if (container.lastChild) {
            container.removeChild(container.lastChild)
        }
    }
    // 替换父元素中的子元素
    function replaceEle(){
        var child = document.getElementById("child")
        var newNode = document.createElement('div')
        newNode.innerHTML = "<button>button</button>hello new node replaced"
        var parent = child.parentElement
        parent.replaceChild(newNode, child)
    }
    // 创建按钮组
    var ButtonGroup = document.getElementById("buttonGroup")
    var EventList = [ 
        "appendChild", 
        "appendParent", 
        "appendPre", 
        "appendNext", 
        "removeEle", 
        "replaceEle" 
    ]

    var ButtonArr = []
    for (var key of EventList) {
        var btn = document.createElement('button')
        btn.textContent = key
        btn.onclick = eval(key)
        ButtonArr.push(btn)
    }
    for (var b of ButtonArr) {
        ButtonGroup.appendChild(b)
    }
</script>

以上代码实现了在各个位置插入元素元素的删除替换,点击此处查看运行结果。

简单实现 v-for、v-text、v-html、v-on 和 v-model 这些功能。

好吧,作为一个 Vue.js 爱好者,绕不开的想到了 Vue.js 操作 DOM 的一些功能。这里就试着简单实现下(不涉及 Virtual DOM,只是单纯的 DOM 修改)。
如果对 Vue 命令不了解可以去官网看看这些指令的用法。

<html>
    <head>
        <meta charset="UTF-8">
        <title>Hello Ele</title>
    </head>
    <body>
        <div id="container">
            <h1>v-text</h1>
            <span>{{ message }}</span>
            <h1>v-html</h1>  
            <div v-html="messagespan"></div>          
            <h1>v-model</h1>
            <input v-model="message"/>
            <h1>v-on</h1>
            <input id="myInput" v-on:blur="blur" v-on:focus="focus"/>
            <h1>v-for</h1>
            <ul></ul>
        </div>
        
        <script>
            // v-text
            var message = "Hello World"
            var messagespan = "<span>Hello World</span>"
          
            var spans = document.getElementsByTagName("span")
            for (var span of spans) {
                if (span.textContent == "{{ message }}") {
                    span.textContent = message
                }
            }
            // v-html
            var container = document.getElementById("container")
            var divs = container.getElementsByTagName("div")
            for(var div of divs){
                if (div.getAttribute("v-html") == "messagespan") {
                    div.innerHTML = messagespan
                }
            }
            // v-model
            var inputs = container.getElementsByTagName("input")
            for (var input of inputs) {
                if (input.getAttribute("v-model") == "message") {
                    input.setAttribute("value", message)
                }
            }
            // v-on
            var myInput = document.getElementById("myInput")
            myInput.onfocus = eval(myInput.getAttribute("v-on:focus"))
            myInput.onblur = eval(myInput.getAttribute("v-on:blur"))

            function focus(){
                myInput.setAttribute("value", "focus")
            }

            function blur() {
                myInput.setAttribute("value", "blur")
            }
            // v-for
            var liContents = [
                "jack",
                "rose",
                "james",
                "wade",
                "jordan"
            ]

            var liElementList = []
            for(var content of liContents) {
                var li = document.createElement("li")
                li.innerHTML = `<label><input type="checkbox"/><span>${content}</span></label>`
                liElementList.push(li)
            }
            var ul = container.getElementsByTagName("ul")[0]
            for (var liEle of liElementList) {
                ul.appendChild(liEle)
            }
        </script>
    </body>
</html>

点击此处看效果。最后结果如图:

显示结果

简单实现了 Vue.js 指令的这些功能,其实在 Vue.js 源码中也是用了这些 dom 操作的 api 来做的。
更多 Vue 源码中的 DOM 操作可以看下我的 《Vue.js 源码学习六 —— VNode虚拟DOM学习》这篇文章中。

最后

无论什么框架,其实都是万变不离其宗。最终都是用最基础的 API 来实现的各种功能。所以学好基础知识是非常重要的~
PS:还是 MDN 靠谱,w3school 的资料虽然也挺多,但是感觉不是很靠谱……以后查资料尽量去 MDN 英文网站去查(中文网站翻译有些问题)。

打个广告

链家上海研发中心招聘前端、后端、测试。
机会不多,需要内推机会的请将简历发送至 dingxiaojie001@ke.com

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342

推荐阅读更多精彩内容

  •   DOM(文档对象模型)是针对 HTML 和 XML 文档的一个 API(应用程序编程接口)。   DOM 描绘...
    霜天晓阅读 3,613评论 0 7
  • 原生DOM接口挺多的,需要花点时间研究下,不过先把基础整好,后面框架估计好学点。 1. DOM是啥 1.1 知识回...
    吴少在coding阅读 1,801评论 0 7
  •   DOM 1 级主要定义的是 HTML 和 XML 文档的底层结构。   DOM2 和 DOM3 级则在这个结构...
    霜天晓阅读 1,416评论 1 3
  • 当下很流行一句话:“人丑就要多读书”,就为了这句话,朵拉儿跟妈妈整整冷战了一个月。朵拉儿正是进入了高考倒计时...
    胡岱阅读 305评论 0 0
  • 同意啊你说话费
    畅华南阅读 63评论 0 0