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已死有事烧纸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 4if(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)原型链
|
|
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,也叫标签Tag3表示文本Text8表示注释Comment9表示文档Document11表示文档片段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
1console.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 5document.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 10let 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提供,不支持 IE1 2 3 4 5 6 7div1.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'
加一个,不覆盖
|
|
读标准属性,获取
div.classList或a.href,或div2.dataset.xxxdiv.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 5test.innerHTML = ` <p> <strong>Hi</strong> </p> `
改子标签
|
|
改父标签
|
|
查看元素的API
查自己:直接打出 id
查父标签:
parentNode或parentElement
node.parentNode或者node.parentElement
查祖标签:调两次
parentNode或parentElement
node.parentNode.parentElement
查子代标签
div.childNodes,返回伪数组NodeList[]node.childNodes或者node.children1 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
思考:当子代变化时,两者也会实时变化吗?
|
|
querySelectorAll不会实时响应页面的改变- 即获取一次后就不变
- 实时更新
- 区别于
computedStyle和getComputedStyle()
查兄弟标签
node.parentNode.childNodes并且排除自己,还得排除所有文本节点node.parentElement.children并且排除自己
遍历并排除自己
|
|
查(续)
查看老大
node.firstChild
查看老幺
node.lastChild
查看上一个兄弟
node.previousSiblingnode.previousElementSibling
查看下一个兄弟
node.nextSiblingnode.nextElementSibling1 2 3 4 5 6 7 8document.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 11let 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>
此时
|
|
属性同步
- 看属性,下菜碟
标准属性
- 对
div1的标准属性的修改,会被浏览器同步到页面中 - 比如
id、className、title等
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
|
|
启示
- 如果有自定义属性,又想被同步到页面中,请使用
data-作为前缀
Property(JS执行线程) V.S. Attribute(渲染线程 HTML页面属性)
property属性
- JS线程中
div1的所有属性,叫做div1的property style、id、className
attribute也是属性
- 渲染引擎中
div1对应标签的属性,叫做attribute
区别
- 大部分时候吗,同名的
property和attribute的值相等 - 当不是标准属性,那么两者只会在一开始相等
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 库
BOM/DOM/Canvas/jQuery/Date/LocalStorage/RegExp/Lodash/Underscore/URL 处理
封装 DOM 实现下面三个接口:
|
|
GitHub Repo源代码链接
手写 DOM 库 代码:https://github.com/FrankFang/dom-demo-1/tree/master
参考文章
相关文章
- 无