DOM编程

251 阅读7分钟

网页其实是一棵树

JS如何操作这颗树?浏览器往window上加一个 document即可

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

获取元素,也叫标签

有很多API

  • document.getElementByid('idxxx')
  • document.getElementsByTagName('div')[0] (找到全部为div的元素)
  • document.getElementsByClassName('red')[0] (根据css class获取)
  • document.querySelector('#idxxx')(css选择器怎么写这里怎么写)document.querySelector('div>span:nth-child(2)')
  • document.querySelectorAll('red')[0]

用哪一个

  • 工作中用querySelector 和 querySelectorAll
  • 要兼容IE才用getElement(s)Byxxx (前3个)

获取特定元素

  • 获取html 元素 document.documentElement (只获取了html标签,不含里面的元素)

  • 获取head 元素 document.head

  • 获取body 元素 document.body

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

  • 获取所有元素 document.all (可以获取到页面上所有标签) 这个document.all为第6个falsy值

    //它是个假的,它开始是IE发明的,早期的时候,用它检测是否是ie浏览器
    if(documents.all){console.log('ie 浏览器');代码只能在IE里运行}else{
      console.log('其他浏览器');只能在非IE 运行
    }
    //但是你不做判断,直接用是可以直接用的
    document.all[3]
    

获取到元素是什么

它是一个对象,我们需要搞清楚它的原型

抓一只div 对象来看看(console.dir()

let div = document.getElementsByTagName('div')[9];
console.dir(div)
  • 自身属性:className、id、style等等(都是由构造函数添加上去的this.xx =xx)
  • 第一层原型 HTMLDivElement.prototype,这里面是所有div共有属性
  • 第二层原型HTMLElement.prototype,这里面是所有HTML标签共有属性
  • 第三层原型 Element.prototype,这里面是所有XML、HTML标签的共有属性,你不会以为浏览器只能展示HTML吧
  • 第四层原型Node.prototype,这里面是所有节点共有属性,节点包括XML标签文本注释、HTML标签文本注释等等
  • 第五层原型 EventTarget.prototype 里面最重要的函数属性是 addEventListener
  • 最后一层原型 就是Object.prototype

节点?元素?分不清

节点Node包括以下几种

  • MDN有完整描述,x.nodeType得到一个数字 div.nodeType//1
  • 1表示元素Element,也叫标签Tag
  • 3表示文本Text
  • 8表示注释 Comment
  • 9表示文档Document
  • 11表示文档片段DocumentFragment

节点的增删改查

创建一个标签节点

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

创建一个文本节点

  • text1 = document.createTextNode('你好')

标签里面插入文本

  • div1.appendChild(text1) (node函数提供的)

    let div = document.createElement('div')
    let text1 = document.createTextNode('你好')
    div.appendChild(text1)
    div
    <div>你好 </div>
    
  • div1.innerText='你好'或者div1.textContent='你好' (element 提供的)

    let div = document.createElement('div')
    div.innerHTML='hello'
    div
    <div>hello</div>
    

插入页面中

  • 你创建的标签默认处于JS线程中
  • 你必须把它插到head 或者 body里面,它才会生效
  • document.body.appendChild(div)
  • 或者已在页面中的元素.appendChild(div)

appendChild

  • 页面中有div#test1 和 div#test2

    let div = document.createElement('div')
    test1.appendChild(div)
    test2.appendChild(div)
    //最终div在test2里面,一个元素不能出现在两个地方,除非复制一份
    //复制
    div2 = div.cloneNode(true)
    document.head.appendChild(div)
    document.body.appendChild(div2)
    

两种方法

  • 旧方法:parentNode.childChild(childNode)

  • 新方法:childNode.remove()(IE不支持)

    div.remove()//直接删除
    

思考

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

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

    //可以
    div.parentNode.removeChild(div);//删除了
    document.body.appendChild(div)//又回来了,只是把它从树删到内存里了,它还在内存里面,如果要加回来就还是可以加
    
    //如何在内存中移除
    div2.remove()
    div2 = null //当等于空时,div2就和内存断开了联系,然后内存就被垃圾回收了
    

写标准属性

  • 改class名:div.className = 'red'(全覆盖)

    div.className = 'red' //一开始改成red
    //如果在后面想加必须加上前面写的,如果只写一个会被覆盖
    div.className += ' blue'
    
  • 改class名:div.classList.add('red') (新语法)

    div.className = 'red'//一开始的
    //加新的
    div.classList.add('blue')
    
  • 改Id : div.id = 'div1'

  • 改style:div.style = 'width:100px;color:blue'(把之前的样式全部覆盖)

  • 改style的一部分:div.style.width = '200px'(只改style中width)

  • 大小写:div.style.backgroundColor ='white'

  • 改date-*属性:div.dataset.x = 'frank'

读标准属性

  • div.classList/ div.className/div.id .....

  • 但是a 标签不一样

    <a href="/xxxx" id=test>/xxxx</a> //在一个相对路径里
    console.log(test.href)
    "http://xx.xxxxx.com/xxxx" //打印出这个,js会把前面的域名补齐
    //如果就只想获取原本的href
    console.log(test.getAttribute('href'))//"/xxxx"
    
  • div.getAttribute('class')/a.getAttribute('href')

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

改事件处理函数

  • div.onclick 默认为null

    • 默认点击div不会有任何事情发生
    • 但是如果你把div.onclick 改为一个函数fn
    • 那么点击div的时候,浏览器就会调用这个函数
    • 并且是这样调用的 fn.call(div,event)
    • div会被当做this
    • event则包含了点击事件的所有信息,如坐标
    <button id=test>按钮</button>
    <script>
    console.log(test.onclick) //开始onclick为空
    test.onclick= function(e){
      console.log(this)
      console.log(e)//console.log(test.onclick)
    
    test.onclick= function(e){
      console.log(this)//<button id="test">按钮</button>
      console.log(e)//MouseEvent {isTrusted: true, screenX: 646, screenY: 203, clientX: 32, clientY: 21, …}
    }
      //test.onclick.call(test,event) this和e是怎么来的,是浏览器用call 传进来的
      
    }
      </script>
    
  • div.addEventListener

    • 是div.onclick 的升级版

改文本内容

  • div.innerText = 'xxx'
  • div.textContent = 'xxx'
  • 两者没有区别,你想用哪个就用哪个

改HTML内容

  • div.innerHTML ='<strong>内容</strong>'
  • 如果字符串中超过2万个字符,网页会卡

改标签

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

改爸爸

  • newParent.appendChild(div)
  • 直接这样就可以了,直接从原来的地方消失了

查爸爸

  • node.parentNode 或者 node.paentElement

查爷爷

  • node.parentNode.parentNode

查子代

  • node.childNodes或者 node.children(优先使用)

    <ul id=test>
        <li>1</li>
        <li>2</li>
        <li>3</li>
    </ul>
    <script>
    console.log(test.childNodes.length)//打印出了7 会包含空格
    console.log(test.children.length) // 打印3 不包含空格
    </script>
    
  • 思考:当子代变化时,两者也会实时变化吗?会

    console.log(test.childNodes.length) //7
    console.log(test.children.length)//3
    test.querySelector('li').remove()
    console.log(test.childNodes.length)//6
    console.log(test.children.length)//2
    //test.querySelectorAll('li')不会实时改变
    

查兄弟姐妹

  • node.parentNode.childNodes 要排除自己,和所有文本节点,所以更麻烦

  • node.parentNode.children 还有排除自己

    div.parentElement.children
    let siblings= []
    let c = div.parentElement.children
    for(let i = 0; i < c.length; i++){
      if(c[i] !== div){
        siblings.push(c[i])
      }
    }
    

查看老大

  • node.firstChild

查看最小孩子

  • node.lastChild

查看上一个哥哥/姐姐

  • node.previousSibling包含文本节点node.previousElementSibling只有元素

查看下一个弟弟/妹妹

  • node.nextSibling

查看div所有元素,遍历一个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在body里面加了一个div1对象

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

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

插入新标签的完整过程

  • 在div1 放入页面之前,你对div1 所有操作都属于JS线程内的操作

  • 把div1 放入页面之时, 浏览器会发现JS的意图,就会通知渲染线程在页面中渲染div1对应的元素

  • 把div1放入页面后

    div1 的操作都 有可能 会触发渲染

    div1.id = 'newid' 可能会重新渲染(当id有css样式),也可能不会

    div1.title = 'new' 可能会重新渲染 ,也可能不会

    如果你 连续对 div1 多次操作,浏览器可能会 合并成一次操作, 也可能不会

    <div id="test"></div>
    .start{
      border: 1px solid red;
      width: 100px;
      height: 100px;
      transition: width 1s;
    }
    .end{
      width: 200px;
    }
    test.classList.add('start')
    // test.clientWidth // 这句话看似无用,实际会触发重新渲染,如果没有这句话,浏览器会合并成一次操作,就没有100px到200px这个动画
    test.classList.add('end')
    

属性同步

  • 标准属性:

    • 对div1的标准属性的修改,会被浏览器同步到页面中
    • 例:id、className、title等
  • date-* 属性:

    • 同上
  • 非标准属性

    • 对非标准属性的修改,则只会停留在JS线程中
    • 不会同步 到页面里
  • 启示

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

property vs Attribute

property属性

  • JS线程中div1的所有属性,也叫做 div1 的property

attribute 也是属性

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

区别

  • 大部分时候,同名property 和 attribute值相等
  • 但如果不是标准属性,那么它俩只会在一开始相等
  • 但是注意 attribute 只支持字符串
  • 而property 支持字符串、布尔等类型