1. 程式人生 > >Eloquent JavaScript #11# The Document Object Model

Eloquent JavaScript #11# The Document Object Model

imm 節點 date case func ref upper 前綴 rpi

索引

  • Notes

    1. js與html
    2. DOM
    3. 在DOM樹中移動
    4. 在DOM中尋找元素
    5. 改變Document
    6. 創建節點
    7. html元素屬性
    8. 布局
    9. style
    10. CSS選擇器
    11. 動畫
  • Exercises

    1. Build a table
    2. Elements by tag name
    3. The cat

Notes

1、js與html

在html中運行js的3種方式:

<!--1-->
<h1>Testing alert</h1>
<script>alert("hello!");</script
> <!--2--> <h1>Testing alert</h1> <script src="code/hello.js"></script> <!--3--> <button onclick="alert(‘Boom!‘);">DO NOT PRESS</button>

js在瀏覽器中解釋、運行,瀏覽器對js具有諸多限制,例如不允許js訪問本地文件、不能修改任何和所訪問網頁無關的東西。因此js仿佛是在一個用鋼板圍起來的沙盒裏活動,而html會被瀏覽器獲取、解析為文檔模型(一種特殊的數據結構),瀏覽器依據這個文檔模型來渲染畫面,這個文檔模型是在js沙盒範圍內的東西,js可以自由地讀取和修改它。

2、DOM

html被解析為一種叫文檔對象模型(Document Object Model)的東西,瀏覽器就是依據它來進行頁面渲染的。全局綁定document是我們訪問這些文檔對象的入口,它的documentElement屬性指向html標簽所對應的對象,還有head和body屬性,分別指向各種對應的對象。DOM是一種樹形結構,document.documentElement即是根節點。再比如說document.body也是一個節點,它的孩子可以是元素節點,也可能是一段文本或者評論節點。

每個DOM節點對象都有一個nodeType屬性,該屬性包含標識節點類型的編碼(數字)。元素的編碼為Node.ELEMENT_NODE,該常量的真實值為數字1。 表示文檔中一段文本的文本節點編碼是3(Node.TEXT_NODE)。註釋的編碼為8(Node.COMMENT_NODE)。

DOM的可視化:

技術分享圖片

葉子是文本節點,箭頭則表明了父子關系。

DOM並非是專門為js設計的,它最開始是想成為xml一樣中立的東西。所以它本身和js集成得並不是特別好。一個典型的例子是childNodes屬性,它類似array,卻又不是真正的array,而是NodeList類型,所以它並不支持slice和map。再比如,DOM並沒有提供一次性創建節點並添加子節點、屬性的方法,而是必須要首先創建它,然後一個一個地添加子節點和屬性。

當然,這些缺點都不是致命的,我們顯然可以自定義函數來封裝一些操作,以便更好地和DOM交互。(已經有許多現成的庫做這件事情)

3、在DOM樹中移動

技術分享圖片

如果屬性對應的節點不存在,則為null。

有個與childNodes類似的children屬性,不過children屬性只包含元素子女(編碼為1的節點),當你對文本節點不感興趣的時候這通常是有用的。可以通過遞歸來遍歷DOM樹,因為childNodes不是真正的數組,因此只能通過常規的循環來遍歷它,不能用for/of循環。

<!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>
    
    <script type="text/javascript">
        // 遞歸,深度優先搜索DOM樹
        function talksAbout(node, string) {
          if (node.nodeType == Node.ELEMENT_NODE) {
            for (let i = 0; i < node.childNodes.length; i++) {
              if (talksAbout(node.childNodes[i], string)) {
                return true;
              }
            }
            return false;
          } else if (node.nodeType == Node.TEXT_NODE) {
              // text節點的nodeValue儲存著顯示文本
            return node.nodeValue.indexOf(string) > -1;
          }
        }
        
        console.log(talksAbout(document.body, "book"));
        // → true
    </script>
  </body>
</html>

4、在DOM中尋找元素

根據子元素的標簽類型:

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

根據id:

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

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

類似的還有getElementsByClassName

5、改變Document

remove,移除元素:

        <p>One</p>
        <p>Two</p>
        <p>Three</p>
        
        <script>
          let paragraphs = document.body.getElementsByTagName("p");
          paragraphs[0].remove();
          // two/three
        </script>

appendChild,用於插入元素到某個父元素的孩子元素的最後一位:

        <p>One</p>
        <p>Two</p>
        <p>Three</p>
        
        <script>
          let paragraphs = document.body.getElementsByTagName("p");
          document.body.appendChild(paragraphs[1]);
          // One/Three/Two
        </script>

insertBefore,一個節點只能存在於文檔中的一個地方,插入段落3會導致它在原來的位置移除,再被插入到相應位置:

        <p>One</p>
        <p>Two</p>
        <p>Three</p>
        
        <script>
          let paragraphs = document.body.getElementsByTagName("p");
          document.body.insertBefore(paragraphs[2], paragraphs[0]);
          // insertBefore插入到某某之前,這裏是把three插到one之前
          // Three/One/Two
        </script>

【All operations that insert a node somewhere will, as a side effect, cause it to be removed from its current position (if it has one).】

類似的還有replaceChild :

        <p>One</p>
        <p>Two</p>
        <p>Three</p>
        
        <script>
          let paragraphs = document.body.getElementsByTagName("p");
          document.body.replaceChild(paragraphs[0], paragraphs[1]);
          // 第一個作為新元素,第二個是被淘汰的舊元素
          // One/Three
        </script>

6、創建節點

點擊按鈕後將所有圖片替換成其alt屬性中的文字:

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

        <p><button onclick="replaceImages()">Replace</button></p>

        <script>
            function replaceImages() {
                let images = document.body.getElementsByTagName("img");
                // image的長度內容都是隨著document動態變化的
                for(let i = images.length - 1; i >= 0; i--) {
                    // 所以才要從最後一個元素開始替換,這樣移除元素
                    // 的時候才不會影響到訪問其余兄弟元素的下標值
                    let image = images[i];
                    if(image.alt) {
                        let text = document.createTextNode(image.alt);
                        image.parentNode.replaceChild(text, image);
                        // 必須獲得父元素的引用才能替換孩子元素
                    }
                }

                // 主題無關↓:從DOM獲取一個靜態的數組Array.from
                let arrayish = {
                    0: "one",
                    1: "two",
                    length: 2
                };
                let array = Array.from(arrayish);
                console.log(array.map(s => s.toUpperCase()));
                // → ["ONE", "TWO"]
            }
        </script>

document.createElement(tag name)返回一個相應類型的空元素,示例如下:

        <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>
            // 創建一個type類型的空元素
            // children是它的孩子節點,
            // 該函數可以接受n個參數
            function elt(type, ...children) {
                let node = document.createElement(type);
                for(let child of children) {
                    if(typeof child != "string") node.appendChild(child);
                    else node.appendChild(document.createTextNode(child));
                    // 如果是string類型就將其轉換為一個文本節點再插入
                }
                return node; // 返回這個創建好的節點
            }

            document.getElementById("quote").appendChild(
                elt("footer"/*type*/, "—"/*child-0*/,
                    elt("strong", "Karl Popper")/*child-1*/,
                    ", preface to the second editon of "/*child-2*/,
                    elt("em", "The Open Society and Its Enemies")/*child-3*/,
                    ", 1950"/*child-4*/));
        </script>

技術分享圖片

7、html元素屬性

技術分享圖片

不僅可以在js中設置、獲取元素的標準屬性,還可以用getAttribute和setAttribute方法設置自定義的屬性

(諸如alt、href等標準屬性,直接node.alt就可以訪問)

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

        <script>
            let paras = document.body.getElementsByTagName("p");
            for(let para of Array.from(paras)) { // 將動態的nodelist轉化為靜態的數組
                if(para.getAttribute("data-classified") == "secret") {
                    // 推薦用前綴修飾自定義屬性,這樣就不會與標準屬性沖突了
                    para.remove();
                }
            }
        </script>

getAttribute和setAttribute和屬於通用方法,也可以用來訪問標準屬性。

8、布局

訪問元素所占空間大小:

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

        <script>
            let para = document.body.getElementsByTagName("p")[0];
            console.log("clientHeight:", para.clientHeight); // 21 不包括邊框
            console.log("offsetHeight:", para.offsetHeight); // 27 加上邊框 2*3px        
        </script>

訪問元素位置最有效的方法:

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

        <script>
            let para = document.body.getElementsByTagName("p")[0];
            let boundingClientRect = para.getBoundingClientRect();
            // html元素左上角相對於瀏覽器顯示屏的準確位置
            console.log("top:", boundingClientRect.top); // 16
            console.log("buttom:", boundingClientRect.buttom); // undefined
            console.log("left:", boundingClientRect.left); // 8
            console.log("right:", boundingClientRect.right);    // 556
            
            // 相對於document的位置,需要加上滾動條位置
            console.log("pageXOffset:", window.pageXOffset);
            console.log("pageYOffset:", window.pageYOffset);
        </script>

進行文檔布局需要做很多工作。為了速度,瀏覽器引擎不會在每次更改文檔時立即重新布局文檔,而是盡可能長時間的等待。當更改文檔的JavaScript程序完成運行時,瀏覽器才必須計算新的布局以將更改的文檔繪制到屏幕上。當程序通過諸如offsetHeight或者getBoundingClientRect讀取某個東西的大小或者位置時,提供正確的信息也需要計算布局。在讀取DOM布局信息和更改DOM之間反復交替的程序會強制執行大量布局計算,因此運行速度非常慢↓

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

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

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

9、style

        <p id="para" style="color: purple">
            Nice text
        </p>

        <script>
            let para = document.getElementById("para");
            console.log(para.style.color);
            para.style.color = "magenta";
            para.style[color] = "red";
            para.style.fontFamily = "cursive";
        </script>

10、CSS選擇器

<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>

querySelectorAll與querySelector後者只返回一個元素(第一個)。

11、動畫

<p>_</p>
<p>_</p>
<p>_</p>
<p>_</p>
<p>_</p>
<p style="text-align: center">
  <img src="img/cat.png" style="position: relative">
</p>
<script>
  let cat = document.querySelector("img");
  let angle = Math.PI / 2;
  function animate(time, lastTime) {
    if (lastTime != null) {
      angle += (time - lastTime) * 0.001;
    }
    cat.style.top = (Math.sin(angle) * 20) + "px";
    cat.style.left = (Math.cos(angle) * 200) + "px";
    requestAnimationFrame(newTime => animate(newTime, time));
  }
  debugger;
  requestAnimationFrame(animate);
</script>

window.requestAnimationFrame

Exercises

① Build a table

<!DOCTYPE html>
<html>

    <head>
        <meta charset="UTF-8">
        <title></title>
    </head>

    <body>
        <h1>Mountains</h1>
        
        <div id="mountains"></div>
        
        <script>
             const MOUNTAINS = [
                {name: "Kilimanjaro", height: 5895, place: "Tanzania"},
                {name: "Everest", height: 8848, place: "Nepal"},
                {name: "Mount Fuji", height: 3776, place: "Japan"},
                {name: "Vaalserberg", height: 323, place: "Netherlands"},
                {name: "Denali", height: 6168, place: "United States"},
                {name: "Popocatepetl", height: 5465, place: "Mexico"},
                {name: "Mont Blanc", height: 4808, place: "Italy/France"}
              ];
        
              // Your code here
            function elt(type, ...children) {
                let node = document.createElement(type);
                for(let child of children) {            
                    if(typeof child != "string" && typeof child != "number") {
                        node.appendChild(child);
                    } else node.appendChild(document.createTextNode(child));
                    // 如果是string類型就將其轉換為一個文本節點再插入
                }
                return node; // 返回這個創建好的節點
            }
            
              let table = document.createElement("table");    
              let ths = document.createElement("tr");
              ths.appendChild(elt("th", "name"));
              ths.appendChild(elt("th", "height"));
              ths.appendChild(elt("th", "place"));
              table.appendChild(ths);
              for (let mountain of MOUNTAINS) {
                  let tds = document.createElement("tr");
                  tds.appendChild(elt("td", mountain[name]));
                  let tdHeight = elt("td", mountain[height]);
                  tdHeight.style.textAlign = "right";
                  tds.appendChild(tdHeight);
                  tds.appendChild(elt("td", mountain[place]));
                  table.appendChild(tds);
              }
        
              document.getElementById("mountains").appendChild(table);
        </script>
    </body>

</html>

————--- -- - ---- —— ——- -- -- - -- - -- - -- - - - -

② Elements by tag name

<!DOCTYPE html>
<html>

    <head>
        <meta charset="UTF-8">
        <title></title>
    </head>

    <body>
        <h1>Heading with a <span>span</span> element.</h1>
        <p>A paragraph with <span>one</span>, <span>two</span> spans.
        </p>

        <script>
            function byTagName(node, tagName) {
                // Your code here.
                let result = [];
                const byTagNameHelper = (node, tagName) => {
                    for (let child of Array.from(node.children)) {
                        if (child.nodeName == tagName.toUpperCase()) {
                            result.push(child);
                        }
                        byTagNameHelper(child, tagName);
                    }
                };
                byTagNameHelper(node, tagName);
                return result;
            }

            console.log(byTagName(document.body, "h1").length);
            // → 1
            console.log(byTagName(document.body, "span").length);
            // → 3
            let para = document.querySelector("p");
            console.log(byTagName(para, "span").length);
            // → 2
        </script>
    </body>
</html>

————--- -- - ---- —— ——- -- -- - -- - -- - -- - - - -

③ The cat’s hat

Eloquent JavaScript #11# The Document Object Model