非死book 的 React 框架解析
原文: http://blog.reverberate.org/2014/02/react-demystified.html
作者: Josh Haberman
這篇文章跟博客已有的其他文章有一些分離, 博客大部分是語言解析和底層編程的,
最近我對一些 JavaScript 框架有了興趣, 包括 非死book 的 React.
我最近閱讀的文章, 特別是 The Future of JavaScript MVC Frameworks,
讓我相信在 React 當中有一些深入的強大的想法在里邊,
然而我沒找到文章或者文檔能把它核心的抽象解釋到我滿意的.
就像我前一篇文章 LL and LR Parsing Demystified,
這篇文章嘗試解釋 React 里對我有意義的思想.
The 1000-Foot View
傳統的 Web app 當中, 你要花費高昂的代價和 DOM 進行交互, 通常是用 jQuery:
我把 DOM 標記成了紅色, 因為更新 DOM 開銷是很大的.
現在的很多 "App" 會有個 Model class 用來在內部表示狀態,
但在我們這里認為只是 app 內部的實現細節.
React 主要的目標是提供一套不同的, 高效的方案來更新 DOM.
不是通過直接把 DOM 變成可變的數據, 而是通過構建 "Virtual DOM", 虛擬的 DOM,
隨后 React 處理真實的 DOM 上的更新來進行模擬相應的更新:
引入額外的一個層怎么就更快了呢?
那不是意味著瀏覽器的 DOM 操作不是最優的, 如果在上邊加上一層能讓整體變快的話?
是有這個意思, 只不過 virtual DOM 在語義上和真實的 DOM 有所差別.
最主要的是, virtual DOM 的操作, 不保證馬上就會產生真實的效果.
這樣就使得 React 能夠等到事件循環的結尾, 而在之前完全不用操作真實的 DOM.
在這基礎上, React 計算出幾乎最小的 diff, 以最小的步驟將 diff 作用到真實的 DOM 上.
批量處理 DOM 操作和作用最少的 diff 是應用自身都能做到的.
任何應用做了這個, 都能變得跟 React 一樣地高效.
但人工處理出來非常繁瑣, 而且容易出錯. React 可以替你做到.
Components
我前面提到 virtual DOM 和真實的 DOM 有著不用的語義, 但同時也有明顯不同的 API.
DOM 樹上的節點被稱為元素, 而 virtual DOM 是完全不同的抽象, 叫做 components.
component 的使用在 React 里極為重要, 因為 components 的存在讓計算 DOM diff 更高效,
比起完整通用的 tree-diff 算法消耗的 O(n^3)
高效多了.
想知道為什么, 就要深入一點 components 的設計當中.
拿 React 首頁的 "Hello World" 做個例子:
/** @jsx React.DOM */ var HelloMessage = React.createClass({
render: function() {
return <div></span>Hello {this.props.name}</div></span>;</span> }
});
React.renderComponent(<HelloMessage name="John" /></span>, mountNode);</span></code></pre>
這里邊有多得可怕的運行細節沒有被解釋徹底.
這個例子盡管小, 卻展示了一些宏大的想法, 所以這里我花點時間慢慢講.
這個例子創建了 React component class HelloMessage
,
然后創建了一個 virtual DOM, 包含 component,
(<HelloMessage>
, 本質是是 HelloMessage
class 的一個實例)
并掛載到真實的 DOM 元素里的一個節點.
首先注意這個 virtual DOM 是由應用定義的 components 組成的(這里是 <HelloMessage>
).
這和瀏覽器真實的 DOM 有著顯著的不同, 那些都只是瀏覽器內建的比如 <p> <ul>
.
真實的 DOM 不含應用特定的邏輯, 而僅僅是可以托管事件回調的數據結構.
而 React 里的 virtual DOM, 則是含有應用特定內在邏輯的, 專為應用定制的 components.
這遠不止于一個 DOM 更新類庫. React 是一種新的抽象, 新的構建 View 的框架.
另外, 如果你一直關心 HTML 的消息, 你應該知道HTML 自定義標簽很快會有瀏覽器支持.
這將帶給真實的 DOM 相似的功能: 根據應用特定邏輯定制應用需要的 DOM 元素.
不過 React 不需要等待官方的自定義標簽支持, 因為 virtual DOM 不是真實的 DOM.
這使得 React 能提前應用, 嵌入類似自定義標簽和 Shadow DOM 的功能,
而不用等到瀏覽器加上了所有這些功能才能被使用.
回到例子里, 已經能確定, 其中創建了一個叫做 <HelloMessage>
的 component 掛載到了節點上.
我想用圖把最初的狀態表示為下面幾種形式. 先來展示 virtual DOM 和真實 DOM 之間的關系.
先假定掛載點是文檔的 <body>
標簽:

里邊的箭頭表示 virtual 標簽掛載到了真實的 DOM 元素上, 很快可以看到結果.
同時看一下現在應用的 view 的邏輯說明:

這里是說, 整張網頁內容是通過我們定制的 <HelloMessage>
component 展示的.
不過, 一個 <HelloMessage>
看起來是什么樣子呢?
component 的渲染通過 render()
函數定義.
React 沒有明確說明什么時候或者多頻繁他會去調用 render()
,
只是會盡量調用, 使得正確的界面更新能看清.
render()
方法返回的內容, 表示了瀏覽器里真實的 DOM 看起來應該怎樣.
這里例子當中, render()
返回了 <div>
, 里面還有一些內容.
React 調用了 render()
函數, 得到 <div>
, 并相應到真實的 DOM 做更新.
所以現在圖片更像是:

這里不僅更新了 DOM, 還保存了 component 過去被更新了怎么樣.
所以 React 才能進行在后面進行快速的 diff.
我掩蓋了一件事, render()
函數為什么能夠返回 DOM 節點.
這是通過 JSX 完成的, 不是通過單純 JavaScript. 看 JSX 的編譯結果更有好處:
/** @jsx React.DOM */ var HelloMessage = React.createClass({displayName: 'HelloMessage',
render: function() {
return React.DOM.div(null, "Hello ", this.props.name);
}
});
React.renderComponent(HelloMessage( {name:"John"} ), mountNode);</code></pre>
所以 return 的不是真實的 DOM 元素, 而是 React 類似 Shadow DOM 的實現,
(比如說是 React.DOM.div
) 對應到真實的 DOM 元素.
所以 React 的 shadow DOM 實際上沒有真實的 DOM 節點.
表示狀態和改變
到上面為止, 我跳過了很大一段故事, 就是 comonent 是怎樣被改變的.
如果 component 不允許改變, React 頂多只是個靜態渲染框架,
像是純粹的模板引擎, 比如 Mustache 或者 HandlebarsJS.
而 React 的要點是快速進行更新. 要更新, component 就需要能更改.
React 將其 state 作為 component 的 state 屬性建模存儲.
這在 React 頁面上的第二個例子里闡述了:
/** @jsx React.DOM */ var Timer = React.createClass({
getInitialState: function() {
return {secondsElapsed: 0};
},
tick: function() {
this.setState({secondsElapsed: this.state.secondsElapsed + 1});
},
componentDidMount: function() {
this.interval = setInterval(this.tick, 1000);
},
componentWillUnmount: function() {
clearInterval(this.interval);
},
render: function() {
return (
<div></span>Seconds Elapsed: {this.state.secondsElapsed}</div></span> );
}
});
React.renderComponent(<Timer /></span>, mountNode);</span></pre>
回調函數 getInitialState()
, componentDidMount()
, componentWillUnmount()
都會被 React 在對應的時機觸發, 他們的命名根據前面提到的應該寫的很清楚了.
而 component 和 state 改變背后的基本理解是這樣:
-
render()
僅僅是一個返回 component state 和 props 的函數
- state 只有在
setState()
調用時才改變
- props 不會改變, 除非父級 component 重新調用了渲染, 傳入新的 props
(props 屬性在前面沒有明確說, 他們是渲染時從父級元素傳進來的屬性.)
前面我是 React 會調用渲染函數"足夠頻繁",
意味著 React 不會再去調用 render()
, 直到 component 的 setState()
被調用,
或者被父級元素傳入不同的 props 屬性重新渲染.
把所有信息匯集到一起, 可以闡釋 app 初始化時 virtual 改變的數據流
(比如, 響應一個 Ajax 請求):

從 DOM 當中獲取數據
上面只討論了怎么把數據的更新傳播到 DOM.
實際的應用是, 也要從 DOM 獲取數據, 因為我們需要那樣從用戶獲取數據
要看是如何工作的, 可以看第三個 React 主頁上的例子:
/** @jsx React.DOM */ var TodoList = React.createClass({
render: function() {
var createItem = function(itemText) {
return <li>{itemText}</li>; };
return <ul>{this.props.items.map(createItem)}</ul>; }
});var TodoApp = React.createClass({
getInitialState: function() {
return {items: [], text: ''};
},
onChange: function(e) {
this.setState({text: e.target.value});
},
handleSubmit: function(e) {
e.preventDefault();
var nextItems = this.state.items.concat([this.state.text]);
var nextText = '';
this.setState({items: nextItems, text: nextText});
},
render: function() {
return (
<div> >h3<TODO</h3> <TodoList items={this.state.items} /> <form onSubmit={this.handleSubmit}> <input onChange={this.onChange} value={this.state.text} /> <button>{'Add #' + (this.state.items.length + 1)}</button> </form> </div> );
}
});
React.renderComponent(<TodoApp />, mountNode);
簡單說, 手動操作 DOM (像 onChange()
方法里寫的),
事件回調可以調用 setState()
來更新 UI.
如果你的應用里有 model 的 class, 那么你的事件回調是應該去相應更新 model,
還有就是調用 setState()
讓 React 知道數據有更新.
如果你已經習慣了一些自動進行雙向綁定的框架,
model 和 view 的數據兩個方向相互傳播, 這里可能有點落后了.
這個例子里有很多一眼能看見以外的東西. 雖然例子看起來是這樣的,
React 實際上沒有在真實的 <input>
元素上綁定 handler.
而是在整個文檔的級別綁定了 handler 等待事件冒泡, 再分發到 virtual DOM 對應的元素.
這帶來的好處有速度(在真實的 DOM 上綁定大量的 handler 會很慢),
還有是一致的跨瀏覽器兼容(即便瀏覽器行為遵循標準, 或者屬性不全).
所有這些放在一起, 終于能看到整個圖景里的數據流動,
從用戶事件(比如說鼠標點擊)開始, 最終完成 DOM 的更新:

結論
通過寫這篇文章我學到了不少關于 React 的東西. 下面是我主要的收獲.
React 是一個 View 的類庫
React 沒有影響到你使用任何 model.
React 的 component 是一個 view 級別的概念, 其中 state 對應這個 UI 部分的狀態.
你可以把任何 model 類庫結合到 React 來使用
(當然有些 model 的處理使得更新被優化得更深入, 比如 Om 的文章里寫的).
React 的 component 抽象很適合把更改作用到 DOM 上去.
component 的抽象是條理化的, 適合被復合, 這個設計帶來了 DOM 更新的高效.
React component 從 DOM 上獲取更新相對不那么方便
手寫 event handler 讓 React 看起來明顯比一些自動更新 view 更改到 model 的類庫低級.
React 的抽象是有漏洞的.
大多數時間你只是對 virtual DOM 進行編程, 但有時你需要能直接操作真的 DOM.
React 文檔里關于這個講了很多, 這在他們的Working With the Browser 章節是必需的.
根據我的理解, 我傾向認為在 The Future of JavaScript MVC Frameworks 里說的內容,
需要更深入去審視. 但這個不大一樣, 我要等到另一篇文章寫.