拋開 React 學習 React 第二部分

hubuke 8年前發布 | 8K 次閱讀 React JavaScript開發

讓我們繼續第一部分沒講到的東西。 這次的文章主要是專注于如何重構我們的 todo list。現在,我們實現了可以渲染整個應用的函數(組合),還有管理我們狀態(state)的 store。然而,我們還有很多方法去優化我們的應用。完整代碼請查看這里

首先,我們還沒有正確地處理事件。現在,我們的組件根本就沒有綁定任何事件。在 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]

    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 函數是創建了一個 JavaScript 對象來表示 DOM 元素。還有一點,讓我們來看看 React 中的 JSX 語法到底是怎樣子的。

下面就是一個 JSX 例子:

return ( <div id='el' className='entry'> Hello </div>)

接著會轉換成:

var SomeElement = React.createElement('div', {
  id: 'el',
  className: 'entry'
}, 'Hello')

然后調用 SomeElement 函數會返回一個像下面差不多的 JavaScript 對象:

{
  // ...
  type: 'div',
  key: null,
  ref: null,
  props: {
    children: 'Hello',
    className: 'entry',
    id: 'el'
  }
}

想要了解更多的話,請閱讀 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

我們還可以重構

現在我們把 inputbutton 從 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。他所做的事情就是調用 SearchBarItemList 函數。現在,我們繼續重構 render 函數。其實就是把代碼移到 App 里面去而已。

我們不妨先來看看 render 現在是怎樣子的:

function render (component, node) {
  node.empty().append(component)
}

render(App(state), $('#app'))

我們的 render 函數只是簡單地把整個應用渲染到某個 HTML 節點。但是,React 的實現會比這個復雜一點,而我們僅僅把一棵 element tree 添加到指定的節點中而已。但是抽象起來理解的話,這個已經足夠了。

現在,我們的 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)
  }
})

很幸運的是,在 React 中,你可以通過調用 React.createClass 來創建這樣的組件。他還提供了很多選擇,比如 ES6 Class ,stateless function 等,更多請查看文檔

綜上,我們講解了數據流如何從上往下,而事件流從下往上。我們也看到了如何處理一個組件的狀態。關于 React 的東西,還有很多要學習。下面的鏈接也許可以幫助到你。

擴展閱讀

結尾語

本來打算在這篇文章講解如何創建一個 advanced state container,實現 undo/redo 以及更多 feature,但是我認為已經超出了這篇文章的范圍。

如果大家有興趣的話,我也許會寫 Part 2.1。

原文鏈接:Learning React Without Using React Part 2

譯者注

TL;DR:

  • 把事件處理放在組件(createElement)里面,事件處理程序可通過 props 委托到父組件中。
  • 創建一個 container component,他包含了整個應用的狀態,并且可以傳遞給其他組件。

看完這兩篇文章后,我根據這種思路,實現了一個二叉樹的遍歷。(CODEDEMO

原文鏈接:Learning React Without Using React Part 2

來自:http://qianduan.guru/2016/03/31/Learning-React-Without-Using-React-Part2/

 本文由用戶 hubuke 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!