抛开 React 学习 React 第二部分

hubuke 8年前
   <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>