抛开 React 学习 React(2)

来源:互联网 发布:java创建日志文件 编辑:程序博客网 时间:2024/06/06 19:25

让我们继续第一部分没讲到的东西。 这次的文章主要是专注于如何重构我们的 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)      }    })
0 0