# 前端組件化(二):優化 DOM 操作
> 作者:[胡子大哈](http://huziketang.com/books/react) </br>
> 原文鏈接: [http://huziketang.com/books/react/lesson3](http://huziketang.com/books/react/lesson3)
**轉載請注明出處,保留原文鏈接以及作者信息**
看看上一節我們的代碼,仔細留意一下?`changeLikeText`?函數,這個函數包含了 DOM 操作,現在看起來比較簡單,那是因為現在只有?`isLiked`?一個狀態。由于數據狀態改變會導致需要我們去更新頁面的內容,所以假想一下,如果你的組件依賴了很多狀態,那么你的組件基本全部都是 DOM 操作。
一個組件的顯示形態由多個狀態決定的情況非常常見。代碼中混雜著對 DOM 的操作其實是一種不好的實踐,手動管理數據和 DOM 之間的關系會導致代碼可維護性變差、容易出錯。所以我們的例子這里還有優化的空間:如何盡量減少這種手動 DOM 操作?
## 狀態改變 -> 構建新的 DOM 元素更新頁面
這里要提出的一種解決方案:一旦狀態發生改變,就重新調用?`render`?方法,構建一個新的 DOM 元素。這樣做的好處是什么呢?好處就是你可以在?`render`?方法里面使用最新的?`this.state`?來構造不同 HTML 結構的字符串,并且通過這個字符串構造不同的 DOM 元素。頁面就更新了!聽起來有點繞,看看代碼怎么寫,修改原來的代碼為:
~~~
class LikeButton {
constructor () {
this.state = { isLiked: false }
}
setState (state) {
this.state = state
this.el = this.render()
}
changeLikeText () {
this.setState({
isLiked: !this.state.isLiked
})
}
render () {
this.el = createDOMFromString(`
<button class='like-btn'>
<span class='like-text'>${this.state.isLiked ? '取消' : '點贊'}</span>
<span></span>
</button>
`)
this.el.addEventListener('click', this.changeLikeText.bind(this), false)
return this.el
}
}
~~~
其實只是改了幾個小地方:
1. `render`?函數里面的 HTML 字符串會根據?`this.state`?不同而不同(這里是用了 ES6 的模版字符串,做這種事情很方便)。
2. 新增一個?`setState`?函數,這個函數接受一個對象作為參數;它會設置實例的?`state`,然后重新調用一下?`render`?方法。
3. 當用戶點擊按鈕的時候,?`changeLikeText`?會構建新的?`state`?對象,這個新的?`state`?,傳入?`setState`?函數當中。
這樣的結果就是,用戶每次點擊,`changeLikeText`?都會調用改變組件狀態然后調用?`setState`?;`setState`?會調用?`render`?,`render`?方法會根據?`state`?的不同重新構建不同的 DOM 元素。
也就是說,你只要調用?`setState`,組件就會重新渲染。我們順利地消除了手動的 DOM 操作。
## 重新插入新的 DOM 元素
上面的改進不會有什么效果,因為你仔細看一下就會發現,其實重新渲染的 DOM 元素并沒有插入到頁面當中。所以在這個組件外面,你需要知道這個組件發生了改變,并且把新的 DOM 元素更新到頁面當中。
重新修改一下?`setState`?方法:
~~~
...
setState (state) {
const oldEl = this.el
this.state = state
this.el = this.render()
if (this.onStateChange) this.onStateChange(oldEl, this.el)
}
...
~~~
使用這個組件的時候:
~~~
const likeButton = new LikeButton()
wrapper.appendChild(likeButton.render()) // 第一次插入 DOM 元素
component.onStateChange = (oldEl, newEl) => {
wrapper.insertBefore(newEl, oldEl) // 插入新的元素
wrapper.removeChild(oldEl) // 刪除舊的元素
}
~~~
這里每次?`setState`?都會調用?`onStateChange`?方法,而這個方法是實例化以后時候被設置的,所以你可以自定義?`onStateChange`?的行為。這里做的事是,每當?`setState`中構造完新的 DOM 元素以后,就會通過?`onStateChange`?告知外部插入新的 DOM 元素,然后刪除舊的元素,頁面就更新了。這里已經做到了進一步的優化了:現在不需要再手動更新頁面了。
非一般的暴力,因為每次?`setState`?都重新構造、新增、刪除 DOM 元素,會導致瀏覽器進行大量的重排,嚴重影響性能。不過沒有關系,這種暴力行為可以被一種叫 Virtual-DOM 的策略規避掉,但這不是本文所討論的范圍。
這個版本的點贊功能很不錯,我可以繼續往上面加功能,而且還不需要手動操作DOM。但是有一個不好的地方,如果我要重新另外做一個新組件,譬如說評論組件,那么里面的這些?`setState`?方法要重新寫一遍,其實這些東西都可以抽出來,變成一個通用的模式。下一節我們把這個通用模式抽離到一個類當中。
- 前言
- 第一階段
- Lesson 1 - React.js 簡介
- Lesson 2 - 前端組件化(一):從一個簡單的例子講起
- Lesson 3 - 前端組件化(二):優化 DOM 操作
- Lesson 4 - 前端組件化(三):抽象出公共組件類
- Lesson 5 - React.js 基本環境安裝
- Lesson 6 - 使用 JSX 描述 UI 信息
- Lesson 7 - 組件的 render 方法
- Lesson 8 - 組件的組合、嵌套和組件樹
- Lesson 9 - 事件監聽
- Lesson 10 - 組件的 state 和 setState
- Lesson 11 - 配置組件的 props
- Lesson 12 - state vs props
- Lesson 13 - 渲染列表數據
- Lesson 14 - 實戰分析:評論功能(一)
- Lesson 15 - 實戰分析:評論功能(二)
- Lesson 16 - 實戰分析:評論功能(三)
- 第二階段
- Lesson 17 - 前端應用狀態管理 —— 狀態提升
- Lesson 18 - 掛載階段的組件生命周期(一)
- Lesson 19 - 掛載階段的組件生命周期(二)
- Lesson 20 - 更新階段的組件生命周期
- Lesson 21 - ref 和 React.js 中的 DOM 操作
- Lesson 22 - props.children 和容器類組件
- Lesson 23 - dangerouslySetHTML 和 style 屬性
- Lesson 24 - PropTypes 和組件參數驗證
- Lesson 25 - 實戰分析:評論功能(四)
- Lesson 26 - 實戰分析:評論功能(五)
- Lesson 27 - 實戰分析:評論功能(六)