DOM 编程
目录
- 前置知识
- DOM是一棵树
- 手写 DOM 库
前置知识
- 简单JS语法(变量、if else、循环)
- JS的七种数据类型
- JS的五个
Falsy
值 - 函数、数组是对象
- 用
div
和span
标签 - 简单CSS布局
网页DOM是一棵树
JS如何操作这棵树
浏览器往window上加一个
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)获取
|
|
如果 id 值为 window
下已存在的属性,比如parent
,只可用document.getElementById('parent')
才能获取到,但工作中不使用这样与全局属性冲突的命名
判断用哪一个
- 工作中用
querySelector
和querySelectorAll
- 做
demo
直接用id_xxx
,千万别让人发现 - 兼容IE用
getElement(s)ByXXX
,IE已死有事烧纸
|
|
获取特定的元素
获取
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 代码
|
|
获取的元素是什么
显然是一个对象,需要搞清它的原型
拿
div
对象举例
console.dir(div1)
原型链
|
|
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
|
|
标签里面插入文本
- Node 层提供的接口
div1.appendChild(text1)
,连接标签和文本节点,appendChild()
的参数只接受节点类型type 'Node'
div1.innerText = '你好'
- Element层提供的接口
div1.textContent = '不送'
- 但是注意不能混用:
div1.appendChild('走好')
- 分别是6层不同原型上的不同接口
|
|
增(续)
插入并显示到页面中
- 创建的标签默认处于JS线程中
- 必须把它插入到
body
或者head
里面,才会生效 document.body.appendChild(div1)
- 或者找到
已在页面中的元素(标签).appendChild(div)
- 注意分清括号里的是否有引号
|
|
问题
appendChild()
的独占性
- 代码
|
|
问:最终div出现在哪里?
- 答:出现在test2里,同一个元素(节点)不能出现在 DOM 的两个地方,除非复制一份(
let div2 = div1.cloneNode()
),否则会被移走
|
|
删
两种方法
- 旧方法:
parentNode.removeChild(childNode)
,由Node
提供(或者parentElement.removeChild(childNode)
) - 新方法:
childNode.remove()
,由Element
提供,不支持 IE
|
|
思考
- 如果一个
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'
加一个,不覆盖
|
|
读标准属性,获取
div.classList
或a.href
,或div2.dataset.xxx
div.getAttribute('class')
或a.getAttribute('href')
,更保险- 两者皆可,但值可能不同
|
|
小结
改事件处理函数
div.onclick
默认为null
运行原理
- 默认点击
div
不发生任何事,应为并未赋值 - 如果把
div.onclick
改为一个函数fn
,点击会使浏览器调用它 - 这样调用的:
fn.call(div, event)
的 div 会被当做this
event
则包含了点击事件的所有信息,比如坐标
|
|
div.addEventListener
- 是
div.onclick
的升级版,就像推荐用.call()
,只使用它
|
|
改内容
改文本内容
div.innerText = 'xxx'
div.textContent = 'xxx'
,标准浏览器- 两者几乎没有区别
改HTML内容(包括文本和标签节点)
div.innerHTML = '<strong>重要内容</strong>'
,显示粗体的文本- 赋值给
innerHTML
的字符过长会影响页面加载
|
|
改子标签
|
|
改父标签
|
|
查看元素的API
查自己:直接打出 id
查父标签:
parentNode
或parentElement
node.parentNode
或者node.parentElement
查祖标签:调两次
parentNode
或parentElement
node.parentNode.parentElement
查子代标签
div.childNodes
,返回伪数组NodeList[]
node.childNodes
或者node.children
|
|
- 为了解决不确定性,使用
Element
提供的 API:.children
,来代替Node
提供的 API,childNodes
- 即使有回车,也只返回元素(标签)
- 优先使用不包括文本节点的
.children
思考:当子代变化时,两者也会实时变化吗?
|
|
querySelectorAll
不会实时响应页面的改变- 即获取一次后就不变
- 实时更新
- 区别于
computedStyle
和getComputedStyle()
查兄弟标签
node.parentNode.childNodes
并且排除自己,还得排除所有文本节点node.parentElement.children
并且排除自己
遍历并排除自己
|
|
查(续)
查看老大
node.firstChild
查看老幺
node.lastChild
查看上一个兄弟
node.previousSibling
node.previousElementSibling
查看下一个兄弟
node.nextSibling
node.nextElementSibling
|
|
查所有
- 遍历一个元素里的所有标签
- 即数据结构中的遍历一棵树
|
|
- DOM 就是数据结构中的树
DOM跨线程操作
为什么 DOM 操作慢
- 浏览器分为渲染引擎和JS引擎
跨线程操作
各线程各司其职,互不相干
- JS引擎不能操作页面,只能操作JS window、DOM、BOM 对象
- 渲染引擎不能操作JS,只能操作页面
document.body.appendChild(div1)
是通过跨线程通信,改变页面
跨线程通信
- 当浏览器发现JS在body里面加了个
div1
对象 - 浏览器就会通知其渲染引擎在页面里也新增一个div元素
- 新增的div元素所有属性都照抄
div1
对象
|
|
插入新标签的完整过程
在
div1
放入页面之前
- 对于
div1
所有的操作都属于JS线程内的操作
把
div1
放入页面之时
- 浏览器发现JS的操作
- 然后通知渲染进程在页面中渲染
div1
对应的元素
把
div1
放入页面之后
- 对于
div1
的操作都有可能会触发重新渲染 div1.id = 'newId'
可能会重新渲染,也可能不会(新的 id 有不同的 CSS)div.title = 'new'
根据浏览器内核的不同,有可能重新渲染- 如果连续多次对
div1
操作,浏览器可能会合并成一次操作,也可能不会(动画的例子) - 创建过程(JS)-> 通信过程(浏览器)-> 渲染过程(渲染引擎)
|
|
|
|
此时
|
|
属性同步
- 看属性,下菜碟
标准属性
- 对
div1
的标准属性的修改,会被浏览器同步到页面中 - 比如
id
、className
、title
等
data-*
属性
- 同上
非标准属性
- 对于非标准属性的修改,只会停留在JS线程中
- 不会同步到页面里
- 比如
x123
属性:示例代码
|
|
操作JS
|
|
启示
- 如果有自定义属性,又想被同步到页面中,请使用
data-
作为前缀
Property
(JS执行线程) V.S. Attribute
(渲染线程 HTML页面属性)
property
属性
- JS线程中
div1
的所有属性,叫做div1
的property
style
、id
、className
attribute
也是属性
- 渲染引擎中
div1
对应标签的属性,叫做attribute
区别
- 大部分时候吗,同名的
property
和attribute
的值相等 - 当不是标准属性,那么两者只会在一开始相等
attribute
只支持字符串property
支持字符串、布尔等类型()
|
|
网上都说 DOM 操作慢,实际上只是比 JS 操作慢,DOM 操作比网络请求还是快很多的。 关于这一部分内容,大家可以延伸阅读一些文章:
封装 DOM 库
BOM/DOM/Canvas/jQuery/Date/LocalStorage/RegExp/Lodash/Underscore/URL 处理
封装 DOM 实现下面三个接口:
|
|
GitHub Repo源代码链接
手写 DOM 库 代码:https://github.com/FrankFang/dom-demo-1/tree/master
参考文章
相关文章
- 无
- 作者: Joel
- 文章链接:
- 版权声明
- 非自由转载-非商用-非衍生-保持署名
- 河 掘 思 知 简