目录
- 前置知识
- DOM是一棵树
- JS具体操作
- 获取元素
- div完整原型链
- 增删改查
- 改事件处理函数
- 获取元素
- DOM跨线程操作
- Property(JS执行线程) V.S. Attribute(渲染线程 HTML页面属性)
- Property(JS执行线程) V.S. Attribute(渲染线程 HTML页面属性)
前置知识
- 简单JS语法(变量、if else、循环)
- JS的七种数据类型
- JS的五个
Falsy
值
- 函数、数组是对象
- 用
div
和span
标签
- 简单CSS布局
在使用jQuery操作DOM也好,Vue操作DOM也好,React的虚拟DOM也好,先了解下原生JS的DOM多么难用
网页DOM是一棵树
JS如何操作这棵树
浏览器往window上加一个
document
在控制台中打
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)获取
window.kw // 页面地址为 baidu.com 时 <input id="kw">
如果 id 值为 window
下已存在的属性,比如parent
,只可用document.getElementById('parent')
才能获取到,但工作中不使用这样与全局属性冲突的命名
判断用哪一个
- 工作中用
querySelector
和querySelectorAll
- 做
demo
直接用id_xxx
,千万别让人发现
- 兼容IE用
getElement(s)ByXXX
,IE已死有事烧纸
// 借鉴了 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 代码
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)
原型链
// baidu.com
let div = document.getElementBiTagName('div')[0]
console.dir(div)
// 查看返回的对象,隐藏属性`__proto__`指向这个对象的原型
div.__proto__ === HTMLDivElement // false
div.__proto__ === HTMLDivElement.prototype // true
-
chrome
显示错了,应该加xxx.prototype
- 自身属性:
className
、id
、style
等等
元素的六层原型链图解
- 第一层:
HTMLDivElement.prototype
,这里面是所有div共有的属性,不用细看
- 第二层:
HTMLElement.prototype
,这里面是所有HTML标签的共有属性,不用细看
- 第三层:
Element.prototype
,这里面是所有XML、HTML标签的共有属性
- 第四层:
Node.prototype
,这里是所有节点的共有属性,节点包括XML标签、文本、注释和HTML标签。文本,注释等等
- 第五层:
EventTarget.prototype
,这里最重要的函数属性是addEventListener
- 第六层,最后一层原型就是
Object.prototype
了
div完整原型链
包括自身属性和共有属性
节点和元素
MDN
有完整描述:Node.nodeType
,xxx.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
console.dir(text1) // 查看原型链 text1-> Text-> CharacterData-> Node-> EventTarget-> Object
标签里面插入文本
- Node 层提供的接口
div1.appendChild(text1)
,连接标签和文本节点,appendChild()
的参数只接受节点类型type 'Node'
-
div1.innerText = '你好'
- Element层提供的接口
div1.textContent = '不送'
- 但是注意不能混用:
div1.appendChild('走好')
- 分别是6层不同原型上的不同接口
// 在页面中显示一段文字
let div1 = document.createElement('div')
let text1 = document.createTextNode('你好')
div1.appendChild(text1)
增(续)
插入并显示到页面中
- 创建的标签默认处于JS线程中
- 必须把它插入到
body
或者head
里面,才会生效
-
document.body.appendChild(div1)
- 或者找到
已在页面中的元素(标签).appendChild(div)
- 注意分清括号里的是否有引号
document.body.appendChild(div1) // <div>你好</div>
div1.style.position = 'fixed'
div1.style.top = 0
div1.style.left = 0
div1.style.color = 'red'
问题
appendChild()
的独占性
- 代码
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()
),否则会被移走
// 复制节点
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
div1.parentNode.removeChild(div1)
div2.parentElement.removeChild(div2)
// 再加回来,因为还在内存里
document.body.appendChild(div2)
// 除非释放(删掉) div2
div2.remove() // 还在内存里
div2 = null // 垃圾回收
思考
- 如果一个
node
被移除页面(DOM树)
- 那么它还可以再次回到页面中吗?
- 可以,只是移出页面,还存在与 JS 线程中
改属性
写标准属性
- 改
id
:div2.id = 'div2'
- 改
class
:div.className = 'red blue' // 全覆盖
,不于保留字冲突
-
div2 += ' red'
,注意有空格,加一个,不覆盖属性
- 改
class
:div.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'
加一个,不覆盖
div2.className += ' red' // 难用
div2.classList.add('green')
读标准属性,获取
-
div.classList
或a.href
,或div2.dataset.xxx
-
div.getAttribute('class')
或a.getAttribute('href')
,更保险
- 两者皆可,但值可能不同
// 读
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
则包含了点击事件的所有信息,比如坐标
// <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()
,只使用它
改内容
改文本内容
-
div.innerText = 'xxx'
-
div.textContent = 'xxx'
,标准浏览器
- 两者几乎没有区别
改HTML内容(包括文本和标签节点)
-
div.innerHTML = '<strong>重要内容</strong>'
,显示粗体的文本
- 赋值给
innerHTML
的字符过长会影响页面加载
test.innerHTML = `
<p>
<strong>Hi</strong>
</p>
`
改子标签
div.innerHTML = '' // 先清空
div.appendChild(div2) // 再加内容
改父标签
anyNewParent.appendChild(div) // 新父节点的引用,直接从原来的地方消失,即改变节点的位置
查看元素的API
查自己:直接打出 id
查父标签:
parentNode
或parentElement
-
node.parentNode
或者node.parentElement
查祖标签:调两次
parentNode
或parentElement
-
node.parentNode.parentElement
查子代标签
-
div.childNodes
,返回伪数组NodeList[]
-
node.childNodes
或者node.children
/* <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
思考:当子代变化时,两者也会实时变化吗?
/* <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
不会实时响应页面的改变
- 即获取一次后就不变
- 实时更新
- 区别于
computedStyle
和getComputedStyle()
查兄弟标签
-
node.parentNode.childNodes
并且排除自己,还得排除所有文本节点
-
node.parentElement.children
并且排除自己
遍历并排除自己
// 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
document.body.children[0]
document.body.firstChild
document.body.lastChild
/* 注意没有 ‘s’ */
div2.previousSibling // 有可能查到文本节点
div2.nextSibling // 有可能查到文本节点
div2.previousElementSibling
div2.nextElementSibling
查所有
- 遍历一个元素里的所有标签
- 即数据结构中的遍历一棵树
// 只遍历元素 暂不遍历文本节点
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
对象
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)-> 通信过程(浏览器)-> 渲染过程(渲染引擎)
// <div title="titlHi"></div>
div::after{
content: attr(titlHi)
}
// 之后修改伪元素的内容就有可能重新渲染
<!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>
此时
test.classList.add('start')
// test.clientWidth // 获取test客户端宽度 这句话看似无用,实际会立即触发start的渲染
test.classList.add('end') // 读取test.clientWidth 之后重新渲染 end 的样式
// 注释掉test.clientWidth 浏览器会合并两次操作 集中为 一次渲染 会使动画失效
属性同步
- 看属性,下菜碟
标准属性
- 对
div1
的标准属性的修改,会被浏览器同步到页面中
- 比如
id
、className
、title
等
data-*
属性
- 同上
非标准属性
- 对于非标准属性的修改,只会停留在JS线程中
-
不会同步到页面里
- 比如
x123
属性:示例代码
<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
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
的所有属性,叫做div1
的property
-
style
、id
、className
attribute
也是属性
- 渲染引擎中
div1
对应标签的属性,叫做attribute
区别
- 大部分时候吗,同名的
property
和attribute
的值相等
- 当不是标准属性,那么两者只会在一开始相等
-
attribute
只支持字符串
-
property
支持字符串、布尔等类型()
// `property`(JS执行线程)--> `attribute`(渲染线程 HTML页面属性)
div = {
id: 'test', // 自动同步
data: { // 自动同步
x: 'test'
}
x: 'test' // 不同步
}
div.id = 1
console.log(typeof div.id)
·未完待续·
参考链接
div 原型链
相关文章
网上都说 DOM 操作慢,实际上只是比 JS 操作慢,DOM 操作比网络请求还是快很多的。
关于这一部分内容,大家可以延伸阅读一些文章:
- 作者: Joel
- 文章链接:
-
版权声明
- 非自由转载-非商用-非衍生-保持署名