抛开 React 学习 React 第二部分
hubuke
9年前
<p>让我们继续第一部分没讲到的东西。 这次的文章主要是专注于如何重构我们的 todo list。现在,我们实现了可以渲染整个应用的函数(组合),还有管理我们状态(state)的 <em>store</em>。然而,我们还有很多方法去优化我们的应用。<a href="/misc/goto?guid=4959671216640253051">完整代码请查看这里</a>。</p> <p>首先,我们还没有正确地处理事件。现在,我们的组件根本就没有绑定任何事件。在 React 里面,数据流是从上往下,而事件流则是从下往上(In React data flows down while events move up)。也就是说,当事件触发的时候,我们应该沿着组件链,从下往上找其对应的回调函数。比如,我们的 <code>ItemRow</code> 函数应该调用一个从 <code>props</code> 传递下来的函数。</p> <p>那么,我们怎么实现呢?下面是一个小尝试:</p> <pre> <code class="language-javascript">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) } </code></pre> <p>在上面,我们给 <code>list</code> 元素绑定了一个事件。当点击他们的时候,<code>onUpdate</code> 函数就会被调用。可以看到, <code>onUpdate</code> 函数是从 <code>props</code> 传递下来的。</p> <p>现在,我们不妨定义一个函数,他可以在创建元素的同时为其绑定事件。</p> <pre> <code class="language-javascript">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) } </code></pre> <p>这样一来,我们的 <code>ItemRow</code> 函数可以写成这样:</p> <pre> <code class="language-javascript">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) } </code></pre> <p>需要注意的是,React 中的 <code>createElement</code> 函数是创建了一个 JavaScript 对象来表示 DOM 元素。还有一点,让我们来看看 React 中的 JSX 语法到底是怎样子的。</p> <p>下面就是一个 JSX 例子:</p> <pre> <code class="language-javascript">return ( <div id='el' className='entry'> Hello </div>) </code></pre> <p>接着会转换成:</p> <pre> <code class="language-javascript">var SomeElement = React.createElement('div', { id: 'el', className: 'entry' }, 'Hello') </code></pre> <p>然后调用 <code>SomeElement</code> 函数会返回一个像下面差不多的 JavaScript 对象:</p> <pre> <code class="language-javascript">{ // ... type: 'div', key: null, ref: null, props: { children: 'Hello', className: 'entry', id: 'el' } } </code></pre> <p>想要了解更多的话,请阅读 <a href="/misc/goto?guid=4959671217370078356">React Components, Elements, and Instances</a>。</p> <p>回到我们的例子中,<code>onUpdate</code> 函数是从哪里来的?</p> <p>首先来看看我们的 <code>render</code> 函数。他定义了一个 <code>updateState</code> 函数,然后通过 <code>props</code> 把这个函数传给 <code>ItemList</code> 组件。</p> <pre> <code class="language-javascript">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 })]) } </code></pre> <p>然后,<code>ItemList</code> 函数会把 <code>onUpdate</code> 传递到每个 <code>ItemRow</code>。</p> <pre> <code class="language-javascript">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)) } </code></pre> <p>通过以上我们实现了:数据流是沿着组件链从上往下流,而事件流是从下往上。这就意味着我们可以把定义在全局的监听器移除掉(用来监听点击 item 的时候改变其状态的监听器)。那么,我们把这个函数移到了 <code>render</code> 函数里面,也就是前面所讲的 <code>updateState</code>。</p> <h2>我们还可以重构</h2> <p>现在我们把 <code>input</code> 和 <code>button</code> 从 HTML 标签变成了函数。因此,我们整个 HTML 文件就只剩下一个 <code>div</code>。</p> <pre> <code class="language-javascript"><div id="app"></app> </code></pre> <p>因此,我们可以很简便地创建 <code>input</code> 元素,就这样:</p> <pre> <code class="language-javascript">var input = createElement('input', {id: 'input'}) </code></pre> <p>同样地,我们也可以把监听 <em>searchBar button</em> 点击事件的全局函数放在我们的 <code>SearchBar</code> 函数里面。<code>SearchBar</code> 函数会返回一个 <code>input</code> 和一个 <code>button</code> 元素,他会通过 <code>props</code> 传进来的回调函数来处理点击事件。</p> <pre> <code class="language-javascript">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]) } </code></pre> <p>在上面,我们的 <code>render</code> 函数在调用 <code>SearchBar</code> 的同时需要传递正确的 <code>props</code> 参数。</p> <p>在我们重构 <code>render</code> 函数之前,让我们想想 <em>re-render</em> 应该在哪里调用才是正确的。首先,忽略我们的 <code>store</code>,把注意力集中在如何在一个 high level component 中处理 <em>state</em>。</p> <p>目前为止,所有的函数都是 <em>stateless</em> 的。接下来我们会创建一个函数,他会处理 <em>state</em>,以及在适当的时候更新子组件(children)。</p> <h2>Container Component</h2> <p>让我们来创建一个 high level container 吧。与此同时,为了更好理解,你可以阅读 <a href="/misc/goto?guid=4959671217467865855">Presentational and Container Component</a>。</p> <p>首先,我们给这个 container component 取名为 <code>App</code>。他所做的事情就是调用 <code>SearchBar</code> 和 <code>ItemList</code> 函数。现在,我们继续重构 <code>render</code> 函数。其实就是把代码移到 <code>App</code> 里面去而已。</p> <p>我们不妨先来看看 <code>render</code> 现在是怎样子的:</p> <pre> <code class="language-javascript">function render (component, node) { node.empty().append(component) } render(App(state), $('#app')) </code></pre> <p>我们的 <code>render</code> 函数只是简单地把整个应用渲染到某个 HTML 节点。但是,React 的实现会比这个复杂一点,而我们仅仅把一棵 element tree 添加到指定的节点中而已。但是抽象起来理解的话,这个已经足够了。</p> <p>现在,我们的 <code>App</code> 函数其实就是我们旧的 <code>render</code> 函数,除了 DOM 操作被删掉。</p> <pre> <code class="language-javascript">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}) ] } </code></pre> <p>我们还需要改进一样东西:我们访问的 <code>store</code> 是全局的,并且重新渲染的话需要调用 <code>setState</code> 函数。</p> <p>我们现在来重构 <code>App</code> 函数,使得他的子组件重新渲染的是不需要调用 <code>store</code>。那么应该要怎么实现呢?</p> <p>首先我们暂时不考虑 <code>store</code>,而是想想怎么调用 <code>setState</code> 函数,使得组件和他的子组件重新渲染。</p> <p>我们需要跟踪这个 high level component 当前的状态,并且只要 <code>setState</code> 一调用,就立马重新渲染。下面是一个简单的实现:</p> <pre> <code class="language-javascript">function App (props) { function getInitialState (props) { return { items: [], id: 0 } } var _state = getInitialState(), _node = null function setState (state) { _state = state render() } // .. } </code></pre> <p>我们通过调用 <code>getInitialState</code> 来初始化我们的 <code>state</code>,然后每当使用 <code>setState</code> 来更新状态的时候,我们会调用 <code>render</code> 函数。</p> <p>而 <code>render</code> 函数要么创建一个 node,要么简单地更新 node,只要 <code>state</code> 发生改变。</p> <pre> <code class="language-javascript">// 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) } } </code></pre> <p>很显然,这对性能来说是不好的。需要知道的是,React 中的 <code>setState</code> 不会渲染整个应用,而是组件和他的子组件。</p> <p>下面是 <code>render</code> 函数的最新代码,我们调用 <code>App</code> 时不需要带任何参数,只是需要在 <code>App</code> 里面简单地调用 <code>getInitialState</code> 来初始化 <code>state</code>。</p> <pre> <code class="language-javascript">function render(component, node) { node.empty().append(component) } render(App(), $('#app')) </code></pre> <p><a href="/misc/goto?guid=4959671217550837351">查看的所有的代码请点击这里</a></p> <h2>继续改进</h2> <p>如果有一个函数,他会返回一个对象。这个对象包含了 <code>setState</code> 函数,还能够区分传进来 <code>props</code> 和 组件本身自己的 <code>state</code>。</p> <p>差不多就像下面这样:</p> <pre> <code class="language-javascript">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) } }) </code></pre> <p>很幸运的是,在 React 中,你可以通过调用 <code>React.createClass</code> 来创建这样的组件。他还提供了很多选择,比如 ES6 Class ,stateless function 等,<a href="/misc/goto?guid=4959660100297256783">更多请查看文档</a>。</p> <p>综上,我们讲解了数据流如何从上往下,而事件流从下往上。我们也看到了如何处理一个组件的状态。关于 React 的东西,还有很多要学习。下面的链接也许可以帮助到你。</p> <h2>扩展阅读</h2> <ul> <li><a href="/misc/goto?guid=4959641658273231827">Thnking in React</a></li> <li><a href="/misc/goto?guid=4958874692283405843">Getting Start React</a></li> <li><a href="/misc/goto?guid=4959671217720470920">JSX</a></li> <li><a href="/misc/goto?guid=4959666808265244082">React How to</a></li> <li><a href="/misc/goto?guid=4959617546847892632">Removing User Interface Complexity, or Why React is Awesome</a></li> <li><a href="/misc/goto?guid=4959671217860317991">Presentational and Container Component</a></li> <li><a href="/misc/goto?guid=4959671217948602209">React Component, Elements, and Instances</a></li> </ul> <h2>结尾语</h2> <p>本来打算在这篇文章讲解如何创建一个 <em>advanced state container</em>,实现 <em>undo/redo</em> 以及更多 feature,但是我认为已经超出了这篇文章的范围。</p> <p>如果大家有兴趣的话,我也许会写 Part 2.1。</p> <p><em>原文链接:<a href="/misc/goto?guid=4959671218023672842">Learning React Without Using React Part 2</a></em></p> <h2>译者注</h2> <p>TL;DR:</p> <ul> <li>把事件处理放在组件(<code>createElement</code>)里面,事件处理程序可通过 <code>props</code> 委托到父组件中。</li> <li>创建一个 container component,他包含了整个应用的状态,并且可以传递给其他组件。</li> </ul> <p>看完这两篇文章后,我根据这种思路,实现了一个二叉树的遍历。(<a href="/misc/goto?guid=4959671218120836980">CODE</a>,<a href="/misc/goto?guid=4959671218207530727">DEMO</a>)</p> <p><em>原文链接:<a href="/misc/goto?guid=4959671218023672842">Learning React Without Using React Part 2</a></em></p> <p><em>来自:</em>http://qianduan.guru/2016/03/31/Learning-React-Without-Using-React-Part2/</p>