image.png

网页其实是一棵树,JS 如何操作这棵树?

  • 浏览器往 window 上加一个 document 即可
  • JS 用 document 操作网页 ,就是 Document Object Model 文档对象模型


DOM 操作是跨线程的

  • 浏览器分为渲染引擎和 JS 引擎


跨线程操作

各线程各司其职

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

【思考】**document.body.appendChild(div1)** 这句话是如何改变页面的?

跨线程通信

  • 当浏览器发现 JS 在 body 里面加了个 div1 对象
  • 浏览器会通知渲染引擎在页面里也新增一个 div 元素
  • 新增的 div 元素所有属性都照抄 div1 对象

DOM2.png

插入新标签得到完整过程

  • 在 div1 放入页面之前
    • 对 div1 所有的操作都属于 JS 线程内的操作
  • 把 div1 放入页面之时
    • 浏览器会发现 JS 的意图,就会通知渲染线程在页面中渲染 div1 对应的元素
  • 把 div1 放入页面之后
    • 对 div1 的操作都有可能会触发重新渲染
    • div1.id = 'newId' 可能会重新渲染,也可能不会
    • div1.id = 'new' 可能会重新渲染,也可能不会
    • 如果你连续对 div1 多次操作,浏览器可能会合并成一次操作,也可能不会(动画代码示例

获取元素(也叫标签)

  1. /* 工作中常用 */
  2. document.querySelector('#idxxx')
  3. document.querySelectorAll('.red')[0] // 获取任意元素的方法
  4. /* 做 demo 时可直接用idxxx */
  5. window.idxxx 或直接 idxxx
  6. /* 需要兼容 ie 的才用 getElement(s)ByXXX */
  7. document.getElementById('idxxx')
  8. document.getElementsByTagName('div')[0]:获取 div 元素
  9. document.getElementsByClassName('red')[0]:根据类名获取元素
  10. // 有 s 的需要加下标才能获取


获取特定元素

获取 HTML 元素

  • document.documentElement

    获取 head 元素

  • document.head

  • head 里的元素默认看不见

    获取 body 元素

  • document.body

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

  • window

    获取所有元素

  • document.all 这算第 6 个 falsy 值,因为它过不了 if

    • 可用来检测代码是否运行在 ie 浏览器
      1. if (document.all) {
      2. console.log(' ie 浏览器')
      3. } else {
      4. console.log('其他浏览器')
      5. }

      获取到的元素是个啥?

  • 显然是对象,我们需要搞清楚它的原型

console.dir(div1) 看原型链

  • 自身属性和共有属性

DOM1.png

  • 自身属性:classNameidstyle 等等
  • 共有属性:
    • 第一层原型:HTMLDivElement.prototype——所有 div 共有的属性
    • 第二层原型:HTMLElement.prototype——所有 HTML 标签共有的属性
    • 第三层原型:Element.prototype——所有 XML、HTML 标签的共有属性
    • 第四层原型:node.prototype——所有节点的共有属性,节点包括 XML、HTML 标签文本注释
    • 第五层原型:EventTarget.prototype —— 里面最重要的函数属性是 addEventListener
    • 第六层原型:Obiect.prototype

节点 Node 包括以下几种:

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

节点的增删改查

  1. /* 创建一个标签节点 */
  2. let div1 = document.createElement('div')
  3. document.createElement('style')
  4. document.createElement('script')
  5. document.createElement('li')
  6. /* 创建一个文本节点 */
  7. text1 = document.createTextNode('你好')
  8. /* 标签里插入文本 */
  9. div1.appendChild(text1)
  10. div1.innerText = '你好'或者 div.textContent = '你好'
  11. // 但是不能用 div1.appendChild('你好')
  12. /* 插入页面中 */
  13. 建的 标签默认处于 JS 线程中
  14. 必须把它插到 head 或者 body 里面,它才会生效
  15. document.body.appendChild(div) 或者已在页面中的元素 .appendChild(div)

【思考】页面中有 div#test1 和 div#test2,请问最终 div 出现在哪里?

  1. // 代码如下
  2. let div = document.createElement('div')
  3. test1.appendChild(div)
  4. test2.appendChild(div)
  • test1 里面
  • test2 里面:因为一个元素不能出现在两个地方,除非复制一份
  • test1 里面和 test2 里面

两种方法:

  1. /* 旧方法 */
  2. parentNode.childChild(childNode) // node 提供的
  3. // 找到父节点删除子节点
  4. /* 新方法 */
  5. childNode.remove() // Element 提供的,不能兼容 ie 浏览器

【思考】如果一个 node 被移除页面(DOM 树),那么它还可以再次回到页面中吗?

  • 可以,因为这个删除只是把它从树立删到内存里面,并没有彻底删除
  • 如何彻底删除?
    1. // 代码示例
    2. div1.remove() --> // undefined
    3. div1 = null
    4. // 先把 div1 移除,再让 div1 等于空值,就使得 div1 和内存断开联系了,然后这个内存就会被垃圾回收掉了。

    改属性

早期的 JS 有个 bug :JS 的对象是不能拥有一个保留字作为它的 key 的

  • 比如 **if** 是 JS 保留字,就不能写 ~~div.if~~
    • 因为关键字的优先级最高, JS 会以为是 if 语句就会报错
  • 比如 **class** 也是 JS 保留字,不能写 ~~div.class~~
    • 要写成 **div.className**

**className** 有一个 bug:每次改 **className** 的时候会把之前的覆盖了

  • 如果不想删之前的,只加个红色怎么操作?
    • **div2.className += ' red'** (red 前面要留个空格,因为要保留之前的,然后再加 red)
  • 这样太麻烦了,就有了新的 API div.classList 打印出目前的值,要加的话就 div.classList.add('green') 图示:

image.png

  • 新的好用,旧的难用

写标准属性

  • (旧)改 class:div.**className** = 'red blue' (会导致全覆盖)
  • (新)改 class 的部分:div.**classList.add**('red')
  • (旧)改 style:div.**style** = 'width: 100px;color:blue;'(会导致全覆盖)
  • (新)改 style 的部分:div.**style.width** = '200px'
    • 注意大小写:所有用中划线隔开的都用大写代替:**background-Color** 可写成 div.style.**backgroundColor** = 'white'
  • 改 data- 属性:`div.*dataset.x = ‘tk’` 在每个元素上加特定的属性(现在没怎么用)

读标准属性

  1. div.classList / a.href // 直接用 JS 的属性读的话,有可能浏览器会加工补全域名
  2. div.getAttribute('class') / a.getAttribute('href') // 直接获取原本的 href(相对更保险一点)
  3. // 两种方法都可以,但值可能稍有不同

例子:

  1. <a id = test href = '/xxx'> /xxx </a>
  2. console.log(test.href) --> // "http://js.jirengu.com/xxx"
  3. console.log(test.getAttribute('href')) --> // "/xxx"

改事件处理函数

  • **div.onclik** 默认为 null

    • 默认点击 div 不会有任何事情发生
    • 但是如果把 div.onclik 改为一个函数 fn,那么点击 div 的时候,浏览器就会调用这个函数,并且是这样调用的 fn.call(div,event)
    • div 会被当作 this
      • (如果需要用 this ,就不能用箭头函数,因为箭头函数不支持 this,应改用 function())
    • event 则包含了点击事件的所有信息,如坐标
      • event 这个参数是浏览器在用户点击的时候用 call 传进来的
  • **div.addEventListener**

    • div.onclik 的升级版

改内容

  • 改文本内容

    1. div.innerText = 'xxx'
    2. div.textContent = 'xxx' // 二者几乎没有差别
  • 改 HTML 内容

    1. div.innerHTML = '<strong>重要内容</strong>' // innerHTML 虽然好用,但如果内容过长会卡
    2. // innerHTML 中的 HTML 必须大写
  • 改标签

    1. div.innerHTML = '' // 先清空
    2. div.appendChild(div1) // 再加内容

    改父节点

    1. newParent.appendChild(div)
    2. // 直接找到新父节点的引用,然后调用 appendChild 把自己放进去,就直接从原来的父节点消失去了新父节点

  1. /* 查爸爸 */
  2. node.parentNode node.parentElement
  3. /* 查爷爷 */
  4. node.parentNode.parentNode
  5. /* 查兄弟姐妹 */
  6. node.parentNode.childNodes // 还要排除自己
  7. node.parentNode.children // 还要排除自己
  8. /* 查子代 */
  9. node.childNodes // 可能会获取到不想要的元素
  10. node.children // 一般用这个

【思考】当子代变化时,两者也会实时变化吗?会自动变化

  1. /* 查看老大 */
  2. node.firstChild
  3. /* 查看老幺 */
  4. node.lastChild
  5. /* 查看上一个哥哥/姐姐 */
  6. node.previouSibling
  7. /* 查看下一个弟弟/妹妹 */
  8. node.nextSibling

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

  1. trave1 = (node, fn) => {
  2. fn(node)
  3. if(node.children) {
  4. for(let i = 0; i < node.children.length; i++) {
  5. trave1(node.children[i], fn)
  6. }
  7. }
  8. }
  9. trave1(div1, (node) => console.log(node))

属性同步

  • 标准属性
    • 对 div1 的标准属性的修改,会被浏览器同步到页面中
    • 比如 idclassNametitle
  • data-* 属性
    • 同上
  • 非标准属性
    • 对非标准属性的修改,则只会停留在 JS 线程中,不会同步到页面里
    • 比如 x 属性,示例代码
  • 启示
    • 如果有自定义属性,又不想被同步到页面中,就使用 data- 作为前缀

DOM3.png

Propert V.S. Attribute

  • property 属性
    • JS 线程中 div1 的所有属性,叫做 div1 的 **property**
  • attribute 也是属性
    • 渲染引擎中 div1 对应标签的属性(而不是 div1 对象的属性),叫做 **attribute**
      • 比如 div1 中的 id = 字符串,那么 id = 字符串就是它的属性
  • 区别:
    • 大部分时候,同名的 property 和 attribute 值相等
    • 但如果不是标准属性,那么它俩只会在一开始时相等
    • **attribute** 只支持字符串
      • 比如 id = 1,也只是省略了 "" 的字符串而已。
    • **property** 支持字符串、布尔等类型