owned this note
owned this note
Published
Linked with GitHub
---
tags: JS
---
# 忍者 Ch12 - 文件物件模型
## 屬性項(attribute)與屬性(property)
👍👍 推薦文章:[dom-attributes-and-properties](https://javascript.info/dom-attributes-and-properties#property-attribute-synchronization)
**attribute 和 property 都被翻譯成屬性,兩者有何差異?**
<br>
### 屬性項(attribute)
> attribute 在 HTML 標籤內被定義,例如 id、class。
可以透過以下方式存取 attribute:
- hasAttribute(name) – 檢查 attribute
- getAttribute(name) – 獲取 attribute
- setAttribute(name, value) – 設定 attribute
- removeAttribute(name) – 移除 attribute
#### attribute 特性:
- 不區分大小寫,id 等同 ID
- 值永遠是字串
- **包含內建屬性項(standard attribute)與非內建屬性項(non-standard attribute)**
<br>
### 屬性(property)
> property 為 DOM 物件(HTML 被解析成 DOM 後)上的屬性。
- 值不一定是字串,例如: element.checked 是 布林值
<br>
通常 attribute 會和 property 連動,也就是更新了 attribute,對應的 property 會同步更新,**除了非內建的 attribute 和 input 的 value**
- 通常 attribute 會和 property 連動:
```htmlmixed=
<div id="foo"></div>
```
所以為了取得 div 元素的 id,可以使用:
```javascript=
const div = document.getElementById('foo');
// 法一: 透過 attribute
div.getAttribute('id'); // foo
// 法二: 透過 property
div.id; // foo
```
- 例外 1:**input 的 value 僅會單向連動**
```htmlmixed=
<input value="123">
```
```javascript=
let input = document.querySelector('input');
// attribute => property
input.setAttribute('value', '456');
alert(input.value); // 456
// NOT property => attribute
input.value = '789';
alert(input.getAttribute('value')); // 456
```
- 例外 2:**自訂的 attribute 與 property 不會彼此連接**
例如,在 div 上自訂一個 `name` 的 attribute,為了取得自訂的 attribute 值,則需要使用 `getAttribute`:
```javascript=
<div id="foo" name="abc"></div>
```
```javascript=
const div = document.getElementById('foo');
div.getAttribute('name'); // abc
div.name; // undefined
```
<br>
**忍者書上範例**:內建的屬性項(attribute)與屬性(property)**通常**會彼此連接,所以更改 attribute 值時,會連帶更改 property 的值,反之亦然。
![](https://i.imgur.com/qKTfYk3.png)
![](https://i.imgur.com/yi3W8Q1.png)
:::success
**好習慣**:
在 HTML5 裡,在自訂的屬性項(attribute) 加上 「data-」前綴,明確地將自訂的 attribute 與內建的 attribute 區隔開來。
:::
<br>
<hr>
## 樣式屬性項(styling attribute)
如同前一節提到的屬性項(attribute)與屬性(property),style 也有attribute 和 property
**style 屬性(property)是一個帶有樣式資訊的物件,這些樣式資訊是在元素標記文字中所設定的(inline style)**,例如 `style="color:red;"`,會把樣式資料存在 style 物件裡。
❗️需注意的是,**style 物件上並沒有存放來自 \<style> 元素或外部樣式表的值**,要取得 \<style> 元素或外部樣式表的值,需透過另一種方式。
<br>
### 檢驗 style 屬性
下方程式碼**證明 style 物件僅存放 inline style 的樣式資訊**:
```htmlmixed=
<div style="color:#000;"></div>
<style>
/* 這些樣式資訊不會被記錄在 style 物件裡*/
div {
font-size: 1.8em;
border: 0 solid gold;
}
</style>
```
```javascript=
document.addEventListener("DOMContentLoaded", () => {
const div = document.querySelector("div");
// 多數瀏覽器會將色彩標準化為 RGB 表示法
console.log(div.style.color === 'rgb(0, 0, 0)' ||
div.style.color === '#000'); // true ⭕️
console.log(div.style.fontSize === '1.8em'); // false ❌
console.log(div.style.borderWidth === '0'); // false ❌
// 透過 style property 修改邊框寬度
div.style.borderWidth = "4px";
console.log(div.style.borderWidth === '4px'); // true ⭕️
});
```
註:元素的 style 屬性裡的任何一個值,都優先於繼承自樣式表的值,也就是 **inline style 的權重永遠大於 CSS 樣式表**。
<br>
### style 屬性的命名方式
JavaScript 解析器會把橫線(-)看成減法運算子,導致無法存取到屬性,所以在 JavaScript 裡,應改為駝峰式寫法,例如:
- font-size 應寫成 fontSize
```javascript=
element.style.fontSize
```
- background-color 應寫成 backgroundColor
```javascript=
element.style.backgroundColor
```
<br>
<br>
<hr>
### 取得計算樣式(取得實際渲染在畫面上的樣式資訊)
元素的計算樣式是多個樣式來源的組合,包括瀏覽器所有內建的樣式(例如預設 font-size: 16px),以及透過樣式表、元素的 style 屬性項(attribute)、style 屬性(property) 所套用的所有樣式。
![](https://i.imgur.com/iZhQ8zy.png)
所有新款瀏覽器都有一個 `getComputedStyle` 方法,可以透過該方法**取得元素最終渲染在畫面上的樣式**。
![](https://i.imgur.com/Nw3i6ay.png)
![](https://i.imgur.com/JdvJG1w.png)
getComputedStyle 方法會回傳一個 [CSSStyleDeclaration 物件](https://developer.mozilla.org/zh-TW/docs/Web/API/CSSStyleDeclaration)的介面,可以透過該介面上的 `getPropertyValue` 方法來取得指定的 style 屬性的計算樣式。
不同於 style 物件屬性,`getPropertyValue` 方法接受的並**不是駝峰式的屬性名稱**,例如:font-size 不必寫成 fontSize。
<br>
**範例:取得樣式計算值**
獲取最終渲染在畫面上的樣式值,不論樣式是來自樣式表或是 inline style。
```htmlmixed=
<div style="color:black;"></div>
```
```css=
div {
background-color: #fff;
display: inline;
font-size: 1.8em;
border: 1px solid black;
color: green;
}
```
```javascript=
function fetchComputedStyle(element,property) {
// 取得計算樣式介面(computed style interface)
const computedStyles = getComputedStyle(element);
return computedStyles.getPropertyValue(property);
}
document.addEventListener("DOMContentLoaded", () => {
const div = document.querySelector("div");
fetchComputedStyle(div,'background-color'); // rgb(255, 255, 255)
fetchComputedStyle(div,'display'); // inline
fetchComputedStyle(div,'font-size'); // 28.8px,因 1.8*16
fetchComputedStyle(div,'color'); // rgb(0, 0, 0)
// 要取得屬性值,需要個別指定四邊(視瀏覽器而定)
fetchComputedStyle(div,'border-top-color'); // rgb(0, 0, 0)
// 要取得屬性值,需要個別指定四邊(視瀏覽器而定)
fetchComputedStyle(div,'border-top-width'); // 1px
});
```
<br>
### 測量高度與寬度
若未指定,則元素預設下的寬高值為 `auto`,無法透過 width、height 取得明確的值。
**[offsetWidth](https://developer.mozilla.org/zh-CN/docs/Web/API/HTMLElement/offsetWidth)**、**[offsetHeight](offsetHeight)** 兩個屬性提供了這功能。
> HTMLElement.offsetWidth 是一個只讀屬性,返回一個元素的佈局寬度,包含盒模型的整個 border-box(也就是包含 padding 和 border)。
但須注意的是,**對 `display: none` 的元素取 `offsetWidth` 與 `offsetHeight` 屬性值,只會得到 `0`**。
若想要取得 `display: none` 隱藏元素在顯示時的大小,可以使用這樣的技巧:
**先短暫讓隱藏元素顯示出來,然後再隱藏它們**
方法如下:
1. 把 `display` 屬性改成 `block` (不要是 `none`)
2. 把 `visibility` 設為 `hidden` (不要讓使用者看到)
3. 把 `position` 設為 `adsolute` (不要影響佈局)
4. 取得元素的寬、高值
5. 將步驟 1~3 修改的值恢復原狀
<br>
**實作:取得隱藏元素的大小**
```htmlmixed=
<div>
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Suspendisse congue facilisis dignissim. Fusce sodales,
odio commodo accumsan commodo, lacus odio aliquet purus,
<img src="../images/ninja-with-pole.png" id="withPole" alt="ninja pole"/>
<!-- display: none -->
<img src="../images/ninja-with-shuriken.png" id="withShuriken" style="display:none" alt="ninja shuriken" />
vel rhoncus elit sem quis libero. Cum sociis natoque
penatibus et magnis dis parturient montes, nascetur
ridiculus mus. In hac habitasse platea dictumst. Donec
adipiscing urna ut nibh vestibulum vitae mattis leo
rutrum. Etiam a lectus ut nunc mattis laoreet at
placerat nulla. Aenean tincidunt lorem eu dolor commodo
ornare.
</div>
```
```javascript=
(function(){
const PROPERTIES = {
display: "block", // 顯示
visibility: "hidden", // 肉眼看不到
position: "absolute", // 不要影響佈局
};
window.getDimensions = element => {
const previous = {};
for (let key in PROPERTIES) {
// 儲存修改前的樣式資訊,以便之後復原
previous[key] = element.style[key];
// 修改樣式
element.style[key] = PROPERTIES[key];
}
// 取得元素大小
const result = {
width: element.offsetWidth,
height: element.offsetHeight
};
// 回復原本的值
for (let in PROPERTIES) {
element.style[key] = previous[key];
}
return result;
};
})();
document.addEventListener("DOMContentLoaded", () => {
setTimeout(() => {
const withPole = document.getElementById('withPole');
const withShuriken = document.getElementById('withShuriken');
// ======= 測試可見元素 =======
assert(withPole.offsetWidth === 41); // true
console.log(withPole.offsetHeight === 48); // true
// ======= 測試不可見元素 =======
console.log(withShuriken.offsetWidth === 36); // false
console.log(withShuriken.offsetHeight === 48); // false
const dimensions = getDimensions(withShuriken);
console.log(dimensions.width === 36); // true
console.log(dimensions.height === 48); // true
},3000);
});
```
<hr>
<br>
## 減少佈局震盪
**減少佈局震盪 = 減少 reflow(重排)、repaint(重繪)**,與性能優化有關,但在忍者書中並沒提到這些名詞。
<br>
補充:
推薦文章 - [輕鬆掌握瀏覽器重繪重排原理](http://obkoro1.com/web_accumulate/accumulate/tool/%E6%B5%8F%E8%A7%88%E5%99%A8%E9%87%8D%E7%BB%98%E9%87%8D%E6%8E%92.html#%E8%BD%BB%E6%9D%BE%E6%8E%8C%E6%8F%A1%E6%B5%8F%E8%A7%88%E5%99%A8%E9%87%8D%E7%BB%98%E9%87%8D%E6%8E%92%E5%8E%9F%E7%90%86)
:::info
### 網頁生成過程:
1. HTML 被 HTML 解析器解析成 DOM 樹
2. CSS 則被 CSS 解析器解析成 CSSOM 樹
3. 結合 DOM 樹和 CSSOM 樹,生成一棵渲染樹(Render Tree)
4. **生成佈局(flow)**,即將所有渲染樹的所有節點進行平面合成
5. **將佈局繪製(paint)在屏幕上**
第四步和第五步是最耗時的部分,這兩步合起來,就是我們通常所說的**渲染**,所以**重新渲染指的就是再次重複 4~5 步驟或僅重複第 5 步驟**。
重繪(repaint)不一定會重排(reflow),但重排一定會重繪。
<br>
![](https://i.imgur.com/LNTNea6.png)
<br>
### 渲染佇列:
瀏覽器會盡可能的把多筆==寫入==的動作放到**渲染佇列**中,當要==讀取==**特定佈局資訊**時,一次性的處理渲染佇列裡的寫入動作。
<br>
在讀取這些樣式資訊時,會觸發重新渲染:
(截自忍者,不保證是最新的~~~~~)
![](https://i.imgur.com/KfD0uf8.png)
<br>
範例 1:
```javascript=
div.style.left = '10px'; // 寫入 (放進渲染佇列)
console.log(div.offsetLeft); // 讀取 (清空渲染佇列)
div.style.top = '10px'; // 寫入 (放進渲染佇列)
console.log(div.offsetTop); // 讀取 (清空渲染佇列)
div.style.width = '20px'; // 寫入 (放進渲染佇列)
console.log(div.offsetWidth); // 讀取 (清空渲染佇列)
div.style.height = '20px'; // 寫入 (放進渲染佇列)
console.log(div.offsetHeight); // 讀取 (清空渲染佇列)
```
上方程式碼會觸發 4 次重排 + 重繪,因為讀取佈局資訊時,無論何時瀏覽器都會立即執行渲染隊列裡的任務。
#### 改善範例 1:**分離讀寫操作**
```javascript=
div.style.left = '10px';
div.style.top = '10px';
div.style.width = '20px';
div.style.height = '20px';
console.log(div.offsetLeft); // 清空渲染佇列
console.log(div.offsetTop);
console.log(div.offsetWidth);
console.log(div.offsetHeight);
```
<br>
範例 2
```htmlmixed=
<div id="ninja">I’m a ninja</div>
<div id="samurai">I’m a samurai</div>
<div id="ronin">I’m a ronin</div>
```
```javascript=
const ninja = document.getElementById("ninja");
const samurai = document.getElementById("samurai");
const ronin = document.getElementById("ronin");
const ninjaWidth = ninja.clientWidth; // 讀取 (清空渲染佇列)
ninja.style.width = ninjaWidth/2 + "px"; // 寫入
const samuraiWidth = samurai.clientWidth;
samurai.style.width = samuraiWidth/2 + "px";
const roninWidth = ronin.clientWidth;
ronin.style.width = roninWidth/2 + "px";
```
**改善範例 2:讀寫分離**
```javascript=
const ninjaWidth = ninja.clientWidth;
const samuraiWidth = samurai.clientWidth;
const roninWidth = ronin.clientWidth;
ninja.style.width = ninjaWidth/2 + "px";
samurai.style.width = samuraiWidth/2 + "px";
ronin.style.width = roninWidth/2 + "px";
```
:::
<br>
<br>
<br>
**參考資料**
1. [忍者 Ch12](https://drive.google.com/file/d/14ROBKaV1PcceyaPw8qpRO-o96kh6Iz6W/view)
2. [dom-attributes-and-properties](https://javascript.info/dom-attributes-and-properties#property-attribute-synchronization)
3. [MDN - getComputedStyle()](https://developer.mozilla.org/zh-TW/docs/Web/API/Window/getComputedStyle)
4. [Jake Archibald: In The Loop - JSConf.Asia](https://www.youtube.com/watch?v=cCOL7MC4Pl0&t=1450s)
5. [阮一峰 - 網頁性能管理詳解](http://www.ruanyifeng.com/blog/2015/09/web-page-performance-in-depth.html)
6. [輕鬆掌握瀏覽器重繪重排原理](http://obkoro1.com/web_accumulate/accumulate/tool/%E6%B5%8F%E8%A7%88%E5%99%A8%E9%87%8D%E7%BB%98%E9%87%8D%E6%8E%92.html#%E5%BC%BA%E5%88%B6%E5%88%B7%E6%96%B0%E9%98%9F%E5%88%97)
![](https://i.imgur.com/Wc552sC.png)