DOM编程

127 阅读10分钟

DOM编程

   JS用document操作网页,这就是Document Object Model文档对象模型。

   什么是DOM呢,就是把网页抽象成一个document对象,并对它进行操作的一种方式就是DOM

  • 网页其实是一棵树

   看一下代码结构

看代码

   看树每个节点

代码树

   JS 如何操作这棵树

浏览器往window上加一个document即可

操作

  • JS 用document 操作网页

   这就是Document Object Mod,DOM

  • DOM很难用

   API很长,不好用,不得不来封装


获取元素,也叫做标签

获取任意元素的API

   1. window.idxxx或者直接idxxx

   2. document.getElementByld('idxxx')如果id和全局变量冲突了可以用这个

   3. document.getElementsByTagName('div')[0]找到所有标签名为div的元素,只有加下标才可以来操作对应div。 比如

示例

   4. document.getElementsByClassName('red')[0]根据class类名来获取,比如把所以类名class为red的标签用下标获取到,取第0个下标即第1个元素。

   5. document.querySelector('#idxxx')可以像css选择器一样写的很复杂,比如某div里的span并且还是div的第二个儿子

示例图

   6. document.querySelectorAll('.red')[0]找到所有满足这个条件的元素

  • 用哪一个

   工作中用querySelectorquerySelectorAll要兼容IE的才用getElement(s)ByXXX

获取特定元素

  • 获取html根元素

   document.documentElement    好长啊

如果你打出它的标签名的话:document.documentElement.tagName获取html元素的标签名

打出来的标签是大写

你会发现本来是小写的标签html,用DOM打印出来就变成了大写HTML。。

  • 获取head元素

   document.head

  • 获取body元素

   document.body

  • 获取窗口(窗口不是元素)

   window

window虽然不是一个标签,但我们有时确实会获取到这个window,然后可以添加一些事件监听

  • 获取所有元素

   document.all

  可以看到当前页面一共有多少个标签。

   IE写的,可以用来判断浏览器是否是IE。if(document.all){console.log( 'ie 浏览器');只能在ie里运行}else{console.log('其他浏览器');}

   这个document.all是个奇葩,可以认为是第6个 falsy值.其他浏览器统一约定,为了反对IE,只要看到oucument.all就认为它为假值,这样就可以执行其他浏览器该执行的代码了。

获取到的元素是个啥

   获取到的所有元素显然是一个对象,我们需要搞清它自身有哪些属性以及它的原型。

   抓一只div对象来看看let div = document.getElementsByTagName('div')[9]。然后用console.dir(div1)看原型链。可以看到六层原型链

元素的六层原型链

先上图

6层原型链

  • 第一层原型HTMLDivElement.prototype

   这里面是所有div共有的属性,不用细看。

  • 第二层原型HTMLElement.prototype

   这里面是所有HTML标签共有的属性,不用细看

  • 第三层原型Element.prototype

   这里面是所有XML、HTML标签的共有属性,你不会以为浏览器只能展示HTML吧

  • 第四层原型Node.prototype

   这里面是所有节点共有的属性,节点包括XML标签文本注释、HTML标签文本注释等等

  • 第五层原型EventTarget.prototype

   里面最重要的函数属性是addEventListener

  • 最后一层原型就是Object.prototype 了

节点?元素?傻傻分不清

在任意一个节点上输入.nodeType可以获取到它的node类型    节点包括元素和文本等,元素就是标签,叫法不同而已。

  • 节点Node包括以下几种

MDN有完整描述,x.nodeType得到一个数字

   1表示元素Element,也叫标签Tag

   3表示文本Text

   8表示注释Comment

   9表示文档Document

   11表示文档片段DocumentFragment

   记住1和3即可

图示例

示例


节点的增删改查

程序员的宿命就是增删改查

  • 创建一个标签节点
let div1 = document.createElement('div')
document.createElement('style')
document.createElement('script')
document.createElement('li')

好长啊,所以要去使用jQuery,vue,react

  • 创建一个文本节点
text1 = document.createTextNode('你好')
  • 标签里面插入文本
div1.appendChild(text1)   --Node提供的接口

div1.innerText= '你好'或者div1.textContent= '你好'   --Element提供的接口

但是不能用div1.appendChild('你好')
  • 插入页面中

   你创建的标签默认处于JS线程中,你必须把它插到head或者body里面,它才会生效

用document.body.appendChild(div),或者,已在页面中的元素.appendChild(div),才可以把div放入对应位置

appendChild
  • 代码

   假如页面中有 div#test1 和 div#test2

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

请问最终div 出现在哪里?

  [ ] test1里面
  [x] test2里面
  [ ] test1里面和test2里面

答案:test2里面。

一个元素不能出现在两个地方,除非复制一份。

用div2 = div1.cloneNode(true)把div1全部拷贝到div2。( 是否采用深度克隆,如果为true,则该节点的所有后代节点也都会被克隆,如果为false,则只克隆该节点本身.)

  • 两种方法

   旧方法:parentNode.childChild(childNode) 找到粑粑搧掉儿子

   比如div1.parentNode找到div1的爸爸,然后div1.parentNode.removeChild(div1),用爸爸删掉儿子

   还可以再加回来document.body.appendChild(div1)。。。

   这只是从树上删到了内存里,还没死呢。

   新方法: childNode.remove() 不兼容IE。一样的效果

  • 思考

   如果一个node被移出页面(DOM树)

   那么它还可以再次回到页面中吗?

   还可以,只是从树移到了内存里

   彻底删掉要让它为空,内存就会把它当垃圾回收掉了。

改属性

  • 写标准属性

改id:div1.id = "div1"

   改全class: div.className = 'red blue'(全覆盖,每次改会把之前的覆盖了)

class是保留字不能用div.class直接添加于是用className来添加class

   改一部分className:div.className += ' red'(在原来的基础上加上空格red。)

   改class: div.classList.add('red') 新的API, classList可以告诉你class有哪些,长度多少。

   改style: div.style = 'width: 100px; color: blue;'(原先的无了,被覆盖了)

   改style的一部分:div.style.width = '200px'

   大小写:div.style.backgroundColor = 'white'就是把-后面接的字母变成大写。或者div.style['background-color']。

   改data-*属性: div.dataset.x = 'dw'(div.dataset。在属性名前加上data-,通过dataset.属性名就可以得到内容。改的话直接 = '要改的') 自定义属性。

div.setAttribute('data-x', 'test') 在div属性里加个data-x属性,内容是test。可以通过div.getAttribute('data-x')得到内容

  • 读标准属性

基本上名字上一一对应的

   div.classList / a.href 这种可能会有问题,例子

   div.getAttribute('class') / a.getAttribute('href')获取原原本本的值

   两种方法都可以,但值可能稍微有些不同

改事件处理函数

还可以改on开头的属性

  • div.onclick默认为null

代码示例

   默认点击div不会有任何事情发生,但是如果你把div.onclick改为一个函数fn,那么点击div的时候,浏览器发现这个div的onclick是有的,于是就会调用这个onclick函数,并且浏览器是用call调用的fn.call(div, event)(触发元素的引用,事件的详细信息)。fn.call(div, event)里div会被当做this,第一个参数就是event则包含了点击事件的所有信息,如坐标。函数是被调的,这样就可以传个参数。

  • div.addEventListener

   是div.onclick的升级版.onclick只能写一个函数,写第二个会覆盖前面的。addEventListener可以写无数个函数。

改内容

   改儿子

  • 改文本内容

   div.innerText = 'xxx' IE写的

   div.textContent = 'xxx' 标准浏览器

   两者几乎没有区别,浏览器同时支持这两个

  • 改HTML内容

   div.innerHTML = '重要内容'内容太多太多字符(2W字符左右)可能会卡浏览器

  • 改标签

   div.innerHTML= "//先清空

   div.appendChild(div2)/再加内容

改爸爸

  • 想要找一个新爸爸?

   newParent.appendChild(div)在这个新爸爸节点调用appendChild把自己放进去

   直接这样就可以了,直接从原来的地方消失,到新爸爸家里去了

  • 查爸爸

   node.parentNode或者node.parentElement

  • 查爷爷

   node.parentNode.parentNode 爸爸的爸爸叫爷爷以此类推

  • 查子代

   node.childNodes(包括文本节点)或者node.children(不包括文本节点)

   console.log(node.childNodes.length)打印出子节点的长度。可能会获取到你不想要的,空格也算一个长度(包括文本节点)。 代码示例

   一般用node.children(不包括文本节点)

  • 思考:

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

   都会实时更新,但querySelectorAll不会实时更新

  • 查兄弟姐妹

   node.parentNode.childNodes不仅要排除所有文本节点还要排除自己

   node.parentNode.children还要排除自己

获取到自己的兄弟姐妹,需要先获取爸爸所有的children,然后遍历这个children排除掉自己

  • 查看老大

   看第一个儿子

   node.firstChild

  • 查看老幺

   node.lastChild

  • 查看上一个哥哥/姐姐

   node.previousSibling有可能查看到文本节点

   node.previousElementSibling查找元素的兄弟

  • 查看下一个弟弟/妹妹

   node.nextSibling

  • 遍历一个div里面的所有元素

先遍历当前节点在遍历子节点

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是跨线程的

   浏览器分为渲染引擎和JS引擎,这两个在不同线程,互不相干

   跨线程操作

  • 各线程各司其职

   JS引擎不能操作页面,只能操作JS

   渲染引擎不能操作JS,只能操作页面

   document.body.appendChild(div1)

   这是JS调用,理论只能存在内存里,为什么可以出现在屏幕上呢,这句JS是如何改变页面的?

  • 跨线程通信

   当浏览器发现JS在body里面加了个div1对象

   浏览器就会通知渲染引擎在页面里也新增一个div元素

   新增的div元素所有属性都照抄div1对象

图示跨线程操作

跨线程操作

   时间花在浏览器发现并通知上,显得更新div的操作会比其他的所有操作都要慢

插入新标签的完整过程

  • 在div1放入页面之前

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

  • 把div1放入页面之时

   浏览器会发现JS的意图

   就会通知渲染线程在页面中渲染div1对应的元素

  • 把div1放入页面之后

   你对div1的操作都有可能会触发重新渲染

   div1.id = 'newld'可能会重新渲染,也可能不会

   div1.title = 'new'可能会重新渲染,也可能不会。可以看CSS tricks

   如果你连续对div1多次操作,浏览器可能会合并成一次操作,也可能不会(以前动画里提到过,代码示例)

如test.classList.add ( ' start' )

//test.clientwidth //这句话看似无用,实际会触发重新渲染,动画会有效果

test.classList.add ( 'end ' )改变宽度

属性同步

  • 标准属性

   对div1的标准属性的修改,会被浏览器同步到页面中。比如id、className、title等

  • data-*属性

同上

  • 非标准属性

   对非标准属性而且也不是data的修改,则只会停留在JS线程中 ,不会同步到页面里

   比如x属性,示例代码

  • 启示

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

图示

图示

Property v.s.Attribute

  • property属性

   指的是JS线程中div1的所有属性(也就是div的在内存里的那个对象),叫做div1的property(比如id是它的property,className也是它的property)

  • attribute也是属性

   指的是渲染引擎中div1对应 标签的属性(比如id='',那id=''就是它的属性),叫做attribute

  • 区别

   大部分时候,同名的property和attribute值相等

   但如果不是标准属性,那么它俩只会在一开始时相等

   但注意attribute只支持字符串

   而property支持字符串、布尔等类型


DOM操作慢么?

网上都说 DOM 操作慢,实际上只是比 JS 操作慢,DOM 操作比网络请求还是快很多的。

   关于这一部分内容,大家可以延伸阅读参考一些文章: