DOM 编程

目录

  • 前置知识
  • DOM是一棵树
  • 手写 DOM 库

前置知识

  • 简单JS语法(变量、if else、循环)
  • JS的七种数据类型
  • JS的五个Falsy
  • 函数、数组是对象
  • divspan标签
  • 简单CSS布局

网页DOM是一棵树

JS如何操作这棵树

浏览器往window上加一个document

在控制台中打

1
2
window.document
// #document 操作整个网页
  • JS用document操作网页

即文档对象模型Document Object Model

  • DOM很难用

具体操作

  • 获取元素(标签)
  • 节点的增删改查

获取元素(标签)

有很多API

  • window.id_xxx或者id_xxx,有可能取不到
  • document.getElementById('id_xxx'),无#
  • document.getElementsByTagName('div')[0],伪数组
  • document.getElementsByClassName('red')[0],伪数组
  • document.querySelector('#id_xxx')
  • document.querySelectorAll('.red')[0],伪数组

直接用 ID (省略 id)获取

1
window.kw // 页面地址为 baidu.com 时 <input id="kw">

如果 id 值为 window下已存在的属性,比如parent,只可用document.getElementById('parent')才能获取到,但工作中不使用这样与全局属性冲突的命名

判断用哪一个

  • 工作中用querySelectorquerySelectorAll
  • demo直接用id_xxx,千万别让人发现
  • 兼容IE用getElement(s)ByXXX,IE已死有事烧纸
1
2
3
// 借鉴了 CSS 的语法
document.querySelector('div>span:nth-child(2)')
// 可以在 chrome 中右键点击 reveal in Elements panel 中查看

获取特定的元素

获取html元素

  • document.documentElement
  • 反人类document.documentElement.tagName,返回的值HTML

获取head元素

  • document.head

获取body元素

  • document.body

获取窗口(不是元素)

  • window
  • 可以监听事件window.onclick = ()=>{console.log('Hi')}

获取所有元素

  • document.all
  • 这个document.all是个奇葩,第6个falsy
  • IE 专属,被用来判断是否 IE
  • 隔绝 ie 代码
1
2
3
4
if(document.all){console.log('true')}else{console.clog('falsy')}
if(document.all){console.log('ie')}else{console.clog('other browser')}
// 但功能仍可用
document.all[3]

获取的元素是什么

显然是一个对象,需要搞清它的原型

div对象举例

console.dir(div1)原型链

1
2
3
4
5
6
// baidu.com
let div = document.getElementBiTagName('div')[0]
console.dir(div)
// 查看返回的对象,隐藏属性`__proto__`指向这个对象的原型
div.__proto__ === HTMLDivElement // false
div.__proto__ === HTMLDivElement.prototype // true
  • chrome显示错了,应该加xxx.prototype
  • 自身属性:classNameidstyle等等

元素的六层原型链图解

  • 第一层:HTMLDivElement.prototype,这里面是所有div共有的属性,不用细看
  • 第二层:HTMLElement.prototype,这里面是所有HTML标签的共有属性,不用细看
  • 第三层:Element.prototype,这里面是所有XML、HTML标签的共有属性
  • 第四层:Node.prototype,这里是所有节点的共有属性,节点包括XML标签、文本、注释和HTML标签。文本,注释等等
  • 第五层:EventTarget.prototype,这里最重要的函数属性是addEventListener
  • 第六层,最后一层原型就是Object.prototype

div完整原型链

包括自身属性和共有属性

节点和元素

MDN有完整描述:Node.nodeTypexxx.nodeType得到一个数字

节点Node包括以下几种

  • 1表示元素Element,也叫标签Tag
  • 3表示文本Text
  • 8表示注释Comment
  • 9表示文档Document
  • 11表示文档片段DocumentFragment

记住1和2即可


节点的增删改查

增:创建元素的API

创建一个标签节点

  • let div1 = document.createElement('div')
  • document.createElement('style')
  • document.createElement('script')
  • document.createElement('li')

创建一个文本节点

  • let text1 = document.createTextNode('你好'),将字符串变为文本节点(对象)
  • text1-> Text-> CharacterData-> Node
1
console.dir(text1) // 查看原型链 text1-> Text-> CharacterData-> Node-> EventTarget-> Object

标签里面插入文本

  • Node 层提供的接口div1.appendChild(text1),连接标签和文本节点,appendChild()的参数只接受节点类型type 'Node'
  • div1.innerText = '你好'
  • Element层提供的接口div1.textContent = '不送'
  • 但是注意不能混用:div1.appendChild('走好')
  • 分别是6层不同原型上的不同接口
1
2
3
4
// 在页面中显示一段文字
let div1 = document.createElement('div')
let text1 = document.createTextNode('你好')
div1.appendChild(text1)

增(续)

插入并显示到页面中

  • 创建的标签默认处于JS线程中
  • 必须把它插入到body或者head里面,才会生效
  • document.body.appendChild(div1)
  • 或者找到 已在页面中的元素(标签).appendChild(div)
  • 注意分清括号里的是否有引号
1
2
3
4
5
document.body.appendChild(div1) // <div>你好</div>
div1.style.position = 'fixed'
div1.style.top = 0
div1.style.left = 0
div1.style.color = 'red'

问题appendChild()的独占性

  • 代码
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
let test1 = document.createElement('div'),
test2 = document.createElement('div')
test1.id = "test1"
test2.id = "test2"
document.body.appendChild(test1)
document.body.appendChild(test2)

let div = document.createElement('div')
test1.appendChild(div)
test2.appendChild(div) // 移走

问:最终div出现在哪里?

  • 答:出现在test2里,同一个元素(节点)不能出现在 DOM 的两个地方,除非复制一份(let div2 = div1.cloneNode()),否则会被移走
1
2
3
4
5
// 复制节点
let div1 = document.createElement('div')
let div2 = div1.cloneNode(true) // 深拷贝
document.head.appendChild(div1)
document.body.appendChild(div2)

两种方法

  • 旧方法:parentNode.removeChild(childNode),由Node提供(或者parentElement.removeChild(childNode)
  • 新方法:childNode.remove(),由Element提供,不支持 IE
1
2
3
4
5
6
7
div1.parentNode.removeChild(div1)
div2.parentElement.removeChild(div2)
// 再加回来,因为还在内存里
document.body.appendChild(div2)
// 除非释放(删掉) div2
div2.remove() // 还在内存里
div2 = null // 垃圾回收

思考

  • 如果一个node被移除页面(DOM树)
  • 那么它还可以再次回到页面中吗?
  • 可以,只是移出页面,还存在与 JS 线程中

改属性

写标准属性

  • iddiv2.id = 'div2'
  • classdiv.className = 'red blue' // 全覆盖,不于保留字冲突
  • div2 += ' red',注意有空格,加一个,不覆盖属性
  • classdiv.classList.add('red'),新 API
  • style覆盖之前全部:div.style = 'width:100px;color:blue;',一般不用
  • style的一部分:div.style.width = '200px'
  • div2.style.color = 'blue',改什么就写什么
  • 大小写法:div.style.backgroundColor = 'white',JS 不支持含有-的 key,全部改为大小写组成的字符串
  • 麻烦的写法:div2.style['background-color'] = 'gray'
  • data-*属性:div.dataset.xxx = 'fuck'
  • div2.setAttribute('data-xxx','text内容')

不用div2.class = 'red'

  • class为保留字,JS 对象不能用保留字作为key
  • 比如idv.if非法只能用div2.className = 'blue'

加一个,不覆盖

1
2
div2.className += ' red' // 难用
div2.classList.add('green')

读标准属性,获取

  • div.classLista.href,或div2.dataset.xxx
  • div.getAttribute('class')a.getAttribute('href'),更保险
  • 两者皆可,但值可能不同
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// 读
div2.id
div2.style
div2.className
// 获取值
div2.getAttribute('data-xxx')
div2.dataset.xxx
// 特殊 坑
// <a id= "test" href="/xxx"> /xxx 相对路径</a>
console.log(test.href) // 返回http://js.jirengu.com/
// 直接读取 JS 属性  浏览器加了一些工,自动域名补全
console.log(test.getAttribute('href')) // 返回原本的值'/xxx',符合预期,更保险

小结


改事件处理函数

div.onclick默认为null

运行原理

  • 默认点击div不发生任何事,应为并未赋值
  • 如果把div.onclick改为一个函数fn,点击会使浏览器调用它
  • 这样调用的:fn.call(div, event)的 div 会被当做this
  • event则包含了点击事件的所有信息,比如坐标
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// <div id="target">target</div>
console.log(target.onclick) // null
// 简单赋值 改写 (箭头函数不支持内部 this)
target.onclick = function(e){
    console.log('hi')
    console.log(this)
    console.log(e) // 包含了点击事件的所有信息
}

// target.onclick.call(target, event)
// this <- target
// event <- e
// this 是浏览器在用户点击时,用 .call() 按顺序传进来的

div.addEventListener

  • div.onclick的升级版,就像推荐用.call(),只使用它
1

改内容

改文本内容

  • div.innerText = 'xxx'
  • div.textContent = 'xxx',标准浏览器
  • 两者几乎没有区别

改HTML内容(包括文本和标签节点)

  • div.innerHTML = '<strong>重要内容</strong>',显示粗体的文本
  • 赋值给innerHTML的字符过长会影响页面加载
1
2
3
4
5
test.innerHTML = `
<p>
    <strong>Hi</strong>
</p>
`

改子标签

1
2
div.innerHTML = '' // 先清空
div.appendChild(div2) // 再加内容

改父标签

1
anyNewParent.appendChild(div) // 新父节点的引用,直接从原来的地方消失,即改变节点的位置

查看元素的API

查自己:直接打出 id

查父标签:parentNodeparentElement

  • node.parentNode或者node.parentElement

查祖标签:调两次parentNodeparentElement

  • node.parentNode.parentElement

查子代标签

  • div.childNodes,返回伪数组NodeList[]
  • node.childNodes或者node.children
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/* <ul id="test">
*       <li>1</li>
*       <li>2</li>
*       <li>3</li>
*   </ul>
*
**/
console.log(test.childNodes.length) // 7
// 第一处的回车也会被认为是一个节点 缩成一个空格

/* <ul id="test"> // 第一个回车缩成的 空文本节点
*       <li>1</li> // 第二个li 加 第三个 回车缩成的 空文本节点
*       <li>2</li> // 第四个li 加 第五个 回车缩成的 空文本节点
*       <li>3</li> // 第六个li 加 第七个 回车缩成的 空文本节点
*   </ul>
*
**/
console.log(test.childNode) // 看具体的节点
/* 在一行时 无空格 */
/* <ul id="test"><li>1</li><li>2</li><li>3</li></ul>
*
**/
console.log(test.childNodes.length) // 3
  • 为了解决不确定性,使用Element提供的 API:.children,来代替Node提供的 API,childNodes
  • 即使有回车,也只返回元素(标签)
  • 优先使用不包括文本节点的.children

思考:当子代变化时,两者也会实时变化吗?

 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
/* <ul id="test">
*       <li>1</li>
*       <li>2</li>
*       <li>3</li>
*   </ul>
*
**/
/* 判断test.childNodes的长度是否会变化  */
let c = test.childNodes // 不再次获取对象长度 将对象缓存起来
console.log(c.length) // 7
test.querySelector('li').remove()
console.log(c.length) // 6

/* <ul id="test">
*       <li>1</li>
*       <li>2</li>
*       <li>3</li>
*   </ul>
*
**/
/* 判断test.children的长度是否会变化  */
let c = test.children // 不再次获取对象长度 将对象缓存起来
console.log(c.length) // 3
test.querySelector('li').remove()
console.log(c.length) // 2

/* <ul id="test">
*       <li>1</li>
*       <li>2</li>
*       <li>3</li>
*   </ul>
*
**/
/* 判断document.querySelectorAll('li')的长度是否会变化  */
let c = document.querySelectorAll('li') // 不再次获取对象长度 将对象缓存起来
console.log(c.length) // 3
test.querySelector('li').remove()
console.log(c.length) // 3
  • querySelectorAll不会实时响应页面的改变
  • 即获取一次后就不变
  • 实时更新
  • 区别于computedStylegetComputedStyle()

查兄弟标签

  • node.parentNode.childNodes并且排除自己,还得排除所有文本节点
  • node.parentElement.children并且排除自己

遍历并排除自己

1
2
3
4
5
6
7
8
// div2.parentElement.children
let siblings = []
let c = div2.parentElement.children
for (let i = 0; i < c.length; i++){
    if(c[i] != div2  ){ // 不相同的放入数组 siblings
        siblings.push(c.[i])
    }
}

查(续)

查看老大

  • node.firstChild

查看老幺

  • node.lastChild

查看上一个兄弟

  • node.previousSibling
  • node.previousElementSibling

查看下一个兄弟

  • node.nextSibling
  • node.nextElementSibling
1
2
3
4
5
6
7
8
document.body.children[0]
document.body.firstChild
document.body.lastChild
/* 注意没有 ‘s’ */
div2.previousSibling // 有可能查到文本节点
div2.nextSibling // 有可能查到文本节点
div2.previousElementSibling
div2.nextElementSibling

查所有

  • 遍历一个元素里的所有标签
  • 即数据结构中的遍历一棵树
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// 只遍历元素 暂不遍历文本节点
travel = (node, fn) => {
    fn(node) // 先调用一下 处理当前节点
    if(node.children){ // 如果当前节点存在子节点
        for(let i = 0; i < node.children.length; i++){
            travel(node.children[i], fn) // 回调节点
        }
    }
}

travel(div1, (node)=>console.log(node))
  • DOM 就是数据结构中的树

DOM跨线程操作

为什么 DOM 操作慢

  • 浏览器分为渲染引擎和JS引擎

跨线程操作

各线程各司其职,互不相干

  • JS引擎不能操作页面,只能操作JS window、DOM、BOM 对象
  • 渲染引擎不能操作JS,只能操作页面
  • document.body.appendChild(div1)是通过跨线程通信,改变页面

跨线程通信

  • 当浏览器发现JS在body里面加了个div1对象
  • 浏览器就会通知其渲染引擎在页面里也新增一个div元素
  • 新增的div元素所有属性都照抄div1对象
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
let div = document.createElement("div")
div.textContent  = "hi"
document.body.appendChild(div) // 浏览器发现body 有新的子节点加入,通知渲染引擎进行渲染
// ... 省略 100 行
div.textContent += 'Jack' // 浏览器发现 div 内的文本改变了,通知渲染引擎进行渲染

/* 时间就消耗在浏览器通知渲染引擎的时候
*  使渲染显得比创建节点、创建文本内容慢
*  优点是可以分开分别优化各自的不同引擎 解耦
*  创建过程(JS)-> 通信过程(浏览器)-> 渲染过程(渲染引擎)
*/

插入新标签的完整过程

div1放入页面之前

  • 对于div1所有的操作都属于JS线程内的操作

div1放入页面之时

  • 浏览器发现JS的操作
  • 然后通知渲染进程在页面中渲染div1对应的元素

div1放入页面之后

  • 对于div1的操作都有可能会触发重新渲染
  • div1.id = 'newId'可能会重新渲染,也可能不会(新的 id 有不同的 CSS)
  • div.title = 'new'根据浏览器内核的不同,有可能重新渲染
  • 如果连续多次对div1操作,浏览器可能会合并成一次操作,也可能不会(动画的例子
  • 创建过程(JS)-> 通信过程(浏览器)-> 渲染过程(渲染引擎)
1
2
3
4
5
// <div title="titlHi"></div>
div::after{
    content: attr(titlHi)
}
// 之后修改伪元素的内容就有可能重新渲染
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>JS Bin</title>
<style>
.start{
  border: 1px solid red;
  width: 100px;
  height: 100px;
  transition: width 1s;
}
.end{
  width: 200px;
}
</style>
</head>
<body>
<div id="test"></div>
</body>
</html>

此时

1
2
3
4
test.classList.add('start')
// test.clientWidth // 获取test客户端宽度 这句话看似无用,实际会立即触发start的渲染
test.classList.add('end') // 读取test.clientWidth 之后重新渲染 end 的样式
// 注释掉test.clientWidth 浏览器会合并两次操作 集中为 一次渲染 会使动画失效

属性同步

  • 看属性,下菜碟

标准属性

  • div1的标准属性的修改,会被浏览器同步到页面中
  • 比如idclassNametitle

data-*属性

  • 同上

非标准属性

  • 对于非标准属性的修改,只会停留在JS线程中
  • 不会同步到页面里
  • 比如x123属性:示例代码
1
2
3
4
5
6
7
8
<div id="test" x="test" data-x="test">
  <p>id 一开始为 test,后来改为 frank
  <p>你会发现页面中 id 变为 frank
  <p>data-x 属性一开始为 test,后来改为 frank
  <p>你会发现页面中的 data-x 变成了 frank
  <p>x 属性一开始为 test,后来改为 frank
  <p>你会发现页面中的 x 还是 test
</div>

操作JS

1
2
3
4
5
let div1 = document.querySelector('#test')
div1.id = 'frank' // 同步过去了
div1.dataset.x = 'frank' // 同步过去了
div1.x = 'frank' // 没有同步过去 不影响页面
div1.style.border = '1px solid red'

启示

  • 如果有自定义属性,又想被同步到页面中,请使用data-作为前缀

Property(JS执行线程) V.S. Attribute(渲染线程 HTML页面属性)

property属性

  • JS线程中div1的所有属性,叫做div1property
  • styleidclassName

attribute也是属性

  • 渲染引擎中div1对应标签的属性,叫做attribute

区别

  • 大部分时候吗,同名的propertyattribute相等
  • 当不是标准属性,那么两者只会在一开始相等
  • attribute只支持字符串
  • property支持字符串、布尔等类型()
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// `property`(JS执行线程)--> `attribute`(渲染线程 HTML页面属性)
div = {
    id: 'test', // 自动同步
    data: { // 自动同步
        x: 'test'
    }
    x: 'test' // 不同步
}
div.id = 1
console.log(typeof div.id)

网上都说 DOM 操作慢,实际上只是比 JS 操作慢,DOM 操作比网络请求还是快很多的。 关于这一部分内容,大家可以延伸阅读一些文章:

DOM 编程.pdf


封装 DOM 库

BOM/DOM/Canvas/jQuery/Date/LocalStorage/RegExp/Lodash/Underscore/URL 处理


封装 DOM 实现下面三个接口:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
window.dom = {
    find(){
        你来补全
    },
    style(){
        你来补全
    },
    each(){
        你来补全
    }
}

const div = dom.find('#test>.red')[0] // 获取对应的元素
dom.style(div, 'color', 'red') // 设置 div.style.color

const divList = dom.find('.red') // 获取多个 div.red 元素
dom.each(divList, (n)=> console.log(n)) // 遍历 divList 里的所有元素

GitHub Repo源代码链接

手写 DOM 库 代码:https://github.com/FrankFang/dom-demo-1/tree/master



参考文章

相关文章


  • 作者: Joel
  • 文章链接:
  • 版权声明
  • 非自由转载-非商用-非衍生-保持署名