拋開 React 學習 React 第二部分
讓我們繼續第一部分沒講到的東西。 這次的文章主要是專注于如何重構我們的 todo list。現在,我們實現了可以渲染整個應用的函數(組合),還有管理我們狀態(state)的 store。然而,我們還有很多方法去優化我們的應用。完整代碼請查看這里。
首先,我們還沒有正確地處理事件。現在,我們的組件根本就沒有綁定任何事件。在 React 里面,數據流是從上往下,而事件流則是從下往上(In React data flows down while events move up)。也就是說,當事件觸發的時候,我們應該沿著組件鏈,從下往上找其對應的回調函數。比如,我們的 ItemRow
函數應該調用一個從 props
傳遞下來的函數。
那么,我們怎么實現呢?下面是一個小嘗試:
function ItemRow (props) {
var className = props.completed ? 'item completed' : 'item'
return $('<li>')
.on('click', props.onUpdate.bind(null, props.id))
.addClass(className)
.attr('id', props.id)
.html(props.text)
}
在上面,我們給 list
元素綁定了一個事件。當點擊他們的時候,onUpdate
函數就會被調用。可以看到, onUpdate
函數是從 props
傳遞下來的。
現在,我們不妨定義一個函數,他可以在創建元素的同時為其綁定事件。
function createElement (tag, attrs, children) {
var elem = $('<', + tag + '>')
for (var key in attrs) {
var val = attrs[key]
if (key.indexOf('on') === 0) {
var event = key.substr(2).toLowerCase()
elem.on(event, val)
} else {
elem.attr(key, val)
}
}
return elem.html(children)
}
這樣一來,我們的 ItemRow
函數可以寫成這樣:
function ItemRow (props) {
var className = props.completed ? 'item completed' : 'item'
return createElement('li', {
id: props.id,
class: props.className,
onClick: props.onUpdate.bind(null, props.id)
}, props.text)
}
需要注意的是,React 中的 createElement
函數是創建了一個 JavaScript 對象來表示 DOM 元素。還有一點,讓我們來看看 React 中的 JSX 語法到底是怎樣子的。
下面就是一個 JSX 例子:
return ( <div id='el' className='entry'> Hello </div>)
接著會轉換成:
var SomeElement = React.createElement('div', {
id: 'el',
className: 'entry'
}, 'Hello')
然后調用 SomeElement
函數會返回一個像下面差不多的 JavaScript 對象:
{
// ...
type: 'div',
key: null,
ref: null,
props: {
children: 'Hello',
className: 'entry',
id: 'el'
}
}
想要了解更多的話,請閱讀 React Components, Elements, and Instances。
回到我們的例子中,onUpdate
函數是從哪里來的?
首先來看看我們的 render
函數。他定義了一個 updateState
函數,然后通過 props
把這個函數傳給 ItemList
組件。
function render (props, node) {
function updateState (toggleId) {
state.items.forEach(function (el) {
if (el.id === toggleId) {
el.completed = !el.completed
}
})
store.setState(state)
}
node.empty().append([ItemList({
items: props.items,
onUpdate: updateState
})])
}
然后,ItemList
函數會把 onUpdate
傳遞到每個 ItemRow
。
function extending (base, item) {
return $.extend({}, item, base)
}
function ItemsList (props) {
return createElement('ul', {}, props.items
.map(extending.bind(null, {
onUpdate: props.onUpdate
}))
.map(ItemRow))
}
通過以上我們實現了:數據流是沿著組件鏈從上往下流,而事件流是從下往上。這就意味著我們可以把定義在全局的監聽器移除掉(用來監聽點擊 item 的時候改變其狀態的監聽器)。那么,我們把這個函數移到了 render
函數里面,也就是前面所講的 updateState
。
我們還可以重構
現在我們把 input
和 button
從 HTML 標簽變成了函數。因此,我們整個 HTML 文件就只剩下一個 div
。
<div id="app"></app>
因此,我們可以很簡便地創建 input
元素,就這樣:
var input = createElement('input', {id: 'input'})
同樣地,我們也可以把監聽 searchBar button 點擊事件的全局函數放在我們的 SearchBar
函數里面。SearchBar
函數會返回一個 input
和一個 button
元素,他會通過 props
傳進來的回調函數來處理點擊事件。
function SearchBar(props) {
function onButtonClick (e) {
var val = $('#input').val()
$('#input').val('')
props.update(val)
e.preventDefault()
}
var input = createElement('input', {id: 'input'})
// move listener to here
var button = createElement('button', {
id: 'add',
onClick: onButtonClick.bind(null)
}, 'Add')
return createElement('div', {}, [input, button])
}
在上面,我們的 render
函數在調用 SearchBar
的同時需要傳遞正確的 props
參數。
在我們重構 render
函數之前,讓我們想想 re-render 應該在哪里調用才是正確的。首先,忽略我們的 store
,把注意力集中在如何在一個 high level component 中處理 state。
目前為止,所有的函數都是 stateless 的。接下來我們會創建一個函數,他會處理 state,以及在適當的時候更新子組件(children)。
Container Component
讓我們來創建一個 high level container 吧。與此同時,為了更好理解,你可以閱讀 Presentational and Container Component。
首先,我們給這個 container component 取名為 App
。他所做的事情就是調用 SearchBar
和 ItemList
函數。現在,我們繼續重構 render
函數。其實就是把代碼移到 App
里面去而已。
我們不妨先來看看 render
現在是怎樣子的:
function render (component, node) {
node.empty().append(component)
}
render(App(state), $('#app'))
我們的 render
函數只是簡單地把整個應用渲染到某個 HTML 節點。但是,React 的實現會比這個復雜一點,而我們僅僅把一棵 element tree 添加到指定的節點中而已。但是抽象起來理解的話,這個已經足夠了。
現在,我們的 App
函數其實就是我們舊的 render
函數,除了 DOM 操作被刪掉。
function App (props) {
function updateSearchBar (value) {
state.items.push({
id: state.id++,
text: value,
completed: false
})
}
function updateState (toggleId) {
state.items.forEach(function (el) {
if (el.id === toggleId) {
el.completed = !el.completed
}
})
store.setState(state)
}
return [
SearchBar({update: updateSearchBar}),
ItemsList({items: props.items, onUpdate: updateState})
]
}
我們還需要改進一樣東西:我們訪問的 store
是全局的,并且重新渲染的話需要調用 setState
函數。
我們現在來重構 App
函數,使得他的子組件重新渲染的是不需要調用 store
。那么應該要怎么實現呢?
首先我們暫時不考慮 store
,而是想想怎么調用 setState
函數,使得組件和他的子組件重新渲染。
我們需要跟蹤這個 high level component 當前的狀態,并且只要 setState
一調用,就立馬重新渲染。下面是一個簡單的實現:
function App (props) {
function getInitialState (props) {
return {
items: [],
id: 0
}
}
var _state = getInitialState(),
_node = null
function setState (state) {
_state = state
render()
}
// ..
}
我們通過調用 getInitialState
來初始化我們的 state
,然后每當使用 setState
來更新狀態的時候,我們會調用 render
函數。
而 render
函數要么創建一個 node,要么簡單地更新 node,只要 state
發生改變。
// naive implement of render
function render () {
var children = [
SearchBar({update: updateSearchState}),
ItemList({
items: _state.items,
onUpdate: updateState
})
]
if (!_node) {
return _node = createElement('div', {class: 'top'}, children)
} else {
return _node.html(children)
}
}
很顯然,這對性能來說是不好的。需要知道的是,React 中的 setState
不會渲染整個應用,而是組件和他的子組件。
下面是 render
函數的最新代碼,我們調用 App
時不需要帶任何參數,只是需要在 App
里面簡單地調用 getInitialState
來初始化 state
。
function render(component, node) {
node.empty().append(component)
}
render(App(), $('#app'))
繼續改進
如果有一個函數,他會返回一個對象。這個對象包含了 setState
函數,還能夠區分傳進來 props
和 組件本身自己的 state
。
差不多就像下面這樣:
var App = createClass({
updateSearchState: function (string) { /*...*/ },
updateState: function (obj) { /*... */ },
render: function () {
var children = [
SearchBar({
updateSearchState: this.updateSearchState
}),
ItemsList({
items: this.state.items,
onUpdate: this.updateState
})
]
return createElement('div', {class: 'top'}, children)
}
})
很幸運的是,在 React 中,你可以通過調用 React.createClass
來創建這樣的組件。他還提供了很多選擇,比如 ES6 Class ,stateless function 等,更多請查看文檔。
綜上,我們講解了數據流如何從上往下,而事件流從下往上。我們也看到了如何處理一個組件的狀態。關于 React 的東西,還有很多要學習。下面的鏈接也許可以幫助到你。
擴展閱讀
- Thnking in React
- Getting Start React
- JSX
- React How to
- Removing User Interface Complexity, or Why React is Awesome
- Presentational and Container Component
- React Component, Elements, and Instances
結尾語
本來打算在這篇文章講解如何創建一個 advanced state container,實現 undo/redo 以及更多 feature,但是我認為已經超出了這篇文章的范圍。
如果大家有興趣的話,我也許會寫 Part 2.1。
原文鏈接:Learning React Without Using React Part 2
譯者注
TL;DR:
- 把事件處理放在組件(
createElement
)里面,事件處理程序可通過props
委托到父組件中。 - 創建一個 container component,他包含了整個應用的狀態,并且可以傳遞給其他組件。
看完這兩篇文章后,我根據這種思路,實現了一個二叉樹的遍歷。(CODE,DEMO)
原文鏈接:Learning React Without Using React Part 2
來自:http://qianduan.guru/2016/03/31/Learning-React-Without-Using-React-Part2/