拋開 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/