Eloquent JavaScript 笔记 十三:DOM

1. Document Structure

再看上一章的html例子:

<!doctype html>
<html>
  <head>
    <title>My home page</title>
  </head>
  <body>
    <h1>My home page</h1>
    <p>Hello, I am Marijn and this is my home page.</p>
    <p>I also wrote a book! Read it
      <a href="http://eloquentjavascript.net">here</a>.</p>
  </body>
</html>
这个HTML文档的结构 如下

document

    在js代码中,document是一个全局变量,它代表了整个HTML文档。 document.documentElement 就是 <html> 这个tag对应的对象。 document.head, document.body 分别对应<head> 和 <body> 对象。


2. Trees

还记得第11章的语法树吗?那么多递归访问,把我转的晕头转向。DOM也是这样的语法树。树根是 document.documentElement。


上图中每一个方框都是node。node有多种类型(nodeType),每种类型在js中都有一个对应的常量值,常见的三种类型是:

1. regular elements: 

    document.ELEMENT_NODE: 1

    上图中的灰色方框

2. text nodes:

   document.TEXT_NODE: 3

    上图中的淡蓝色方框

3. comments

    document.COMMENT_NODE: 8
    <!-- 这是注释 -->  


3. The Standard

DOM 不是专门为js设计的,甚至都不是专门为HTML设计的,它是一种语言中立的接口,或者说它是为XML设计的接口。而XML是一种更宽泛领域的超语言,HTML只是它的一个小应用。所以,后面会看到,我们在使用DOM操纵HTML时,经常会感觉有些别扭。例如,每一个element 都有childNodes属性,通过该属性可以访问它的所有子节点,而childNodes不是Array类型,它是NodeList类型,它没有slice和forEach方法。

如果操作DOM的行为比较多,代码会非常的冗长而难看。还好,已经有写第三方库提供了简单的方法,例如 jQuery。


4. Moving through the tree


上图描述了一个DOM结构,以及访问各个node的方法:

childNodes, parentNode, firstChild, previousSibling, nextSibling, lastChild

举个例子,在HTML中查找是否存在 "book" 这个单词:

function talksAbout(node, string) {
  if (node.nodeType == document.ELEMENT_NODE) {
    for (var i = 0; i < node.childNodes.length; i++) {
      if (talksAbout(node.childNodes[i], string))
        return true;
    }
    return false;
  } else if (node.nodeType == document.TEXT_NODE) {
    return node.nodeValue.indexOf(string) > -1;
  }
}

console.log(talksAbout(document.body, "book"));
// → true

5. Finding Elements

三个函数:

1. getElementsByTagName()

var link = document.body.getElementsByTagName("a")[0];
console.log(link.href);

2. getElementById()

<p>My ostrich Gertrude:</p>
<p><img id="gertrude" src="img/ostrich.png"></p>

<script>
  var ostrich = document.getElementById("gertrude");
  console.log(ostrich.src);
</script>
3. getElementsByClassName()

   这个函数和getElementsByTagName()类似。 <p class="big"> ... </p>


6. Changing the Document

functions:

  removeChild()

  appendChild()

  insertBefore()

  replaceChild()

例子:

<p>One</p>
<p>Two</p>
<p>Three</p>

<script>
  var paragraphs = document.body.getElementsByTagName("p");
  document.body.insertBefore(paragraphs[2], paragraphs[0]);
</script>

一个node只能存在于一个位置,所以,把第2个p插入第0个p时,会先把它从原来的位置删除。

注意,replaceChild() 和 insertBefore() 的第一个参数是新插入的节点,第二个节点是参考位置的节点。

7. Creating Nodes

先看个例子:

把<img> 节点替换成Text节点,Text的内容是 <img> 的alt属性的值。

<p>The <img src="img/cat.png" alt="Cat"> in the
  <img src="img/hat.png" alt="Hat">.</p>

<p><button οnclick="replaceImages()">Replace</button></p>

<script>
  function replaceImages() {
    var images = document.body.getElementsByTagName("img");
    for (var i = images.length - 1; i >= 0; i--) {
      var image = images[i];
      if (image.alt) {
        var text = document.createTextNode(image.alt);
        image.parentNode.replaceChild(text, image);
      }
    }
  }
</script>
用 document.createTextNode() 创建Text节点。

注意,getElementsByTagName()、getElementsByClassName() 和 childNodes 等得到的节点列表是 live 的,也就是说,当DOM结构发生变化之后,通过这些方法的node列表(上面的var images)也会随之变化。
所以,上面代码遍历images时,要从后往前遍历。

可以把live的节点列表变成solid(固定的):

var arrayish = {0: "one", 1: "two", length: 2};
var real = Array.prototype.slice.call(arrayish, 0);
real.forEach(function(elt) { console.log(elt); });
// → one
//   two
第十一章也用过类似的方法:把一个 “像” 数组的对象转换成数组,因为它们的数据存储方式相同。

再看一个例子:

<blockquote id="quote">
  No book can ever be finished. While working on it we learn
  just enough to find it immature the moment we turn away
  from it.
</blockquote>

<script>
  function elt(type) {
    var node = document.createElement(type);
    for (var i = 1; i < arguments.length; i++) {
      var child = arguments[i];
      if (typeof child == "string")
        child = document.createTextNode(child);
      node.appendChild(child);
    }
    return node;
  }

  document.getElementById("quote").appendChild(
    elt("footer", "—",
        elt("strong", "Karl Popper"),
        ", preface to the second editon of ",
        elt("em", "The Open Society and Its Enemies"),
        ", 1950"));
</script>
使用 document.createElement(tag) 创建普通的节点。

8. Attributes

8.1. 有些节点的属性,在DOM中有相同名字的属性。

例如: <a id="mylink" href="..."> 中的href。

var mylink = document.getElementById("mylink");
mylink.href="http://blog.csdn.net";
8.2. 有些节点属性在DOM中没有对应的属性

我们自己定义的属性也没有,所以,需要用 getAttribute 和 setAttribute 来访问:

<p data-classified="secret">The launch code is 00000000.</p>
<p data-classified="unclassified">I have two feet.</p>

<script>
  var paras = document.body.getElementsByTagName("p");
  Array.prototype.forEach.call(paras, function(para) {
    if (para.getAttribute("data-classified") == "secret")
      para.parentNode.removeChild(para);
  });
</script>
我们自己定义的属性最好加上data-前缀,以避免命名冲突。

8.3. 再看一个比较复杂的例子

查找 <pre> 中代码的关键字,把它们用粗体显示 (<strong>)

<p>Here it is, the identity function:</p>
<pre data-language="javascript">
function id(x) { return x; }
</pre>
<script>
    function highlightCode(node, keywords) {
        var text = node.textContent;
        node.textContent = ""; // Clear the node

        var match, pos = 0;
        while (match = keywords.exec(text)) { // regex 中有 /g,全局匹配,那么,有多少次匹配,while循环就会执行多少次。
            var before = text.slice(pos, match.index);
            node.appendChild(document.createTextNode(before));
            var strong = document.createElement("strong");
            strong.appendChild(document.createTextNode(match[0]));
            node.appendChild(strong);
            pos = keywords.lastIndex; // 本次匹配的下一个字符的位置
        }
        var after = text.slice(pos);
        node.appendChild(document.createTextNode(after));
    }

    var languages = {
        javascript: /\b(function|return|var)\b/g 
    };

    function highlightAllCode() {
        var pres = document.body.getElementsByTagName("pre");
        for (var i = 0; i < pres.length; i++) {
            var pre = pres[i];
            var lang = pre.getAttribute("data-language");
            if (languages.hasOwnProperty(lang))
                highlightCode(pre, languages[lang]);
        }
    }
</script>
上面代码中,attribute的访问很容易理解,反倒是RegExp的使用方法有些费解。仔细看第11行和第17行的注释。

8.4. node的class属性

例如: <p class="big"> ... </p>

在DOM中class对应的属性是className,我们也可以用 getAttribute("class") 、setAttribute("class") 来访问它。


9. Layout

9.1. 类型

  block:另起一行

<p>, <h1>, <div> 等

  inline:不会另起一行

<a>, <strong>, <span> 等

9.2. size

  offsetWith, offsetHeight:带border的宽和高

  clientWith, clientHeight: 不带border的宽和高

9.3. 例子

<p style="border: 3px solid red">
    I'm boxed in
</p>

<script>
    var para = document.body.getElementsByTagName("p")[0];
    console.log("clientHeight:", para.clientHeight);
    console.log("offsetHeight:", para.offsetHeight);
</script>
9.4.  获取一个element的精确位置

getBoundingClientRect( )

9.5. 浏览器的滚动条滚动之后的坐标

pageXOffset, pageYOffset

9.6. 重新计算layout

改动DOM内容,读取position和size属性,或则调用getBoundingClientRect( ) 都需要重新计算layout,如果操作过于频繁(例如:在循环中),会导致性能很差,网页响应慢。

看个例子:

<p><span id="one"></span></p>
<p><span id="two"></span></p>

<script>
  function time(name, action) {
    var start = Date.now(); // Current time in milliseconds
    action();
    console.log(name, "took", Date.now() - start, "ms");
  }

  time("naive", function() {
    var target = document.getElementById("one");
    while (target.offsetWidth < 2000)
      target.appendChild(document.createTextNode("X"));
  });
  // → naive took 32 ms

  time("clever", function() {
    var target = document.getElementById("two");
    target.appendChild(document.createTextNode("XXXXX"));
    var total = Math.ceil(2000 / (target.offsetWidth / 5));
    for (var i = 5; i < total; i++)
      target.appendChild(document.createTextNode("X"));
  });
  // → clever took 1 ms
</script>

10. Styling

10.1. style 属性:

<p><a href=".">Normal link</a></p>
<p><a href="." style="color: green">Green link</a></p>
10.2. display 类型:

  display:block;   自己占一行

  display:inline;   自己不单独占一行

  display:none;    隐藏 (不改变DOM结构)

例如:

This text is displayed <strong>inline</strong>,
<strong style="display: block">as a block</strong>, and
<strong style="display: none">not at all</strong>.
10.3.  修改style
<p id="para" style="color: purple">
  Pretty text
</p>

<script>
  var para = document.getElementById("para");
  console.log(para.style.color);
  para.style.color = "magenta";
</script>

中间带连接线的style,有两种访问方式:

style["font-family"]

style.fontFamily

11. Cascading Styles

11.1. cascading

  多处设置的style合并到一起(三种地方:.css 文件, <head> 中的style段,elements的style属性)

   element的style属性中的设置,优先级最高

11.2. selector

tag 

  p {

      font-size: 16px;

  }

class

  .subtle {

      color: gray;

  }

id

  #header {

      background: blue;

  }

匹配的越精确,优先级越高。下面的p.a.b#main 就比上面的 p 优先级高。

/* p elements, with classes a and b, and id main */
p.a.b#main {
  font-size: 20px;
}

p > a { ... }

  <p> 的第一级 <a> 类子节点

p a { ... }

  <p> 下面所有级别的 <a> 类节点


这些是最常用的selector,还有一些更复杂的,不再一一列出。

12. Query Selectors

上面讲了用 tag、class 和 id 获取elements,也可以通过selector获取elements。

querySelectorAll(sel), 

  获取所有符合selector的elements,document.querySelectorAll(), element.querySelectorAll()

querySelector(sel)

  返回第一个符合条件的element

这两个方法返回的elements不是live的,不会随着DOM的变化而变化

例:

<p>And if you go chasing
  <span class="animal">rabbits</span></p>
<p>And you know you're going to fall</p>
<p>Tell 'em a <span class="character">hookah smoking
  <span class="animal">caterpillar</span></span></p>
<p>Has given you the call</p>

<script>
  function count(selector) {
    return document.querySelectorAll(selector).length;
  }
  console.log(count("p"));           // All <p> elements
  // → 4
  console.log(count(".animal"));     // Class animal
  // → 2
  console.log(count("p .animal"));   // Animal inside of <p>
  // → 2
  console.log(count("p > .animal")); // Direct child of <p>
  // → 1
</script>


13. Positioning and Animating

style的position属性有三种类型:

static 

  默认。从上到下,从左到右,在浏览器中流式布局。

relative

  top和left 相对于它的默认 (position:static时) 的位置

absolute

  top和left 相对于父节点的坐标。父节点的position不能是static。如果父节点是static,则向上层找父节点。如果找不到符合条件的父节点,则相对于document。

例子: 动态修改positin: relative 的top和left

<p style="text-align: center">
    <img src="img/cat.png" style="position: relative">
</p>
<script>
    var cat = document.querySelector("img");
    var angle = 0, lastTime = null;
    function animate(time) {
        if (lastTime != null)
            angle += (time - lastTime) * 0.001;
        lastTime = time;
        cat.style.top = (Math.sin(angle) * 20) + "px";
        cat.style.left = (Math.cos(angle) * 200) + "px";
        requestAnimationFrame(animate);
    }
    requestAnimationFrame(animate);
</script>
一只猫沿着椭圆轨迹平移。

requestAnimationFrame(func) 这个函数告诉browser,下次刷新屏幕时,执行参数中的函数。浏览器大约每秒刷新60次屏幕。
当然,我们也可以使用 setTimeout() 或 setInterval() 来定时移动,但,requestAnimationFrame() 可以获得更平滑的效果。

注意,坐标后面一定要加上单位(px, em等),否则,设置无效。


14. Exercise: Build a Table

<script>
    function buildTable(data) {
        var table = document.createElement("table");
        var header = document.createElement("tr");
        table.appendChild(header);

        var keys = Object.keys(data[0]);
        keys.forEach(function (name) {
            var th = document.createElement("th");
            th.textContent = name;
            header.appendChild(th);
        });

        data.forEach(function (row) {
            var tr = document.createElement("tr");
            table.appendChild(tr);
            keys.forEach(function (name) {
                var value = row[name];
                var td = document.createElement("td");
                td.textContent = value;
                if (name == "height") {
                    td.className = "number";
                }
                tr.appendChild(td);
            });
        });

        return table;
    }

    document.body.appendChild(buildTable(MOUNTAINS));
</script>

比在控制台打印table简单多了,因为,不再需要考虑布局问题。

15. Exercise: Elements by Tag Name

function byTagName(node, tagName) {
    var nodes = [];

    if (node.nodeType == document.ELEMENT_NODE) {
        if (node.tagName.toLowerCase() == tagName.toLowerCase()) {
            nodes.push(node);
        }

        for (var i = 0; i < node.childNodes.length; i++) {
            var eles = byTagName(node.childNodes[i], tagName);
            nodes = nodes.concat(eles);
        }
    }
    return nodes;
}


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值