抛开 React 学习 React(2)

让我们继续第一部分没讲到的东西。 这次的文章主要是专注于如何重构我们的 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
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值