让我们继续第一部分没讲到的东西。 这次的文章主要是专注于如何重构我们的 todo list。首先,我们还没有正确地处理事件。现在,我们的组件根本就没有绑定任何事件。在 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];
//如果传进来的attr属性数组中以on开头,那么为其绑定事件,否则当做普通属性处理
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 函数是创建了一个 js对象来表示 DOM 元素。这里让我们可以看看 React 中的 JSX 语法到底是怎样子的。
下面就是一个 JSX 例子:
return ( <div id='el' className='entry'> Hello </div>)
接着会转换成调用createElement:
var SomeElement = React.createElement('div', {
id: 'el',
className: 'entry'
}, 'Hello')
然后调用 SomeElement 函数会返回一个像下面差不多的 js对象:
{
// ...
type: 'div',
key: null,
ref: null,
props: {
children: 'Hello',
className: 'entry',
id: 'el'
}
}
想要了解JSX更多的话,请阅读 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 操作被删掉。
现在,我们的 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)
}
})