寫給新人的React快速入門手冊

大表妹吖發表於2017-12-08

基礎

元件

React元件大致可分為三種寫法 一種es6的class語法,繼承React.Component類實現帶有完整生命週期的元件

import React, { Component } from 'react';

export default class SingleComponent extends Component {
  /*
    包含一些生命週期函式、內部函式以及變數等
  */
  render() {
    return (<div>{/**/}</div>)
  }
}
複製程式碼

第二種是無狀態元件,也叫函式式元件

const SingleComponent = (props) => (
  <div>{props.value}</div>
);
export default SingleComponent;
複製程式碼

還有一種較為特殊,叫高階元件,嚴格來說高階元件只是用來包裝以上兩種元件的一個高階函式

const HighOrderComponent = (WrappedComponent) => {
  class Hoc extends Component {
    /*包含一些生命週期函式*/
    render() {
      return (<WrappedComponent {...this.props} />);
    }
  }
  return Hoc;
}
複製程式碼

高階元件的原理是接受一個元件並返回一個包裝後的元件,可以在返回的元件裡插入一些生命週期函式做相應的操作,高階元件可以使被包裝的元件邏輯不受干擾從外部進行一些擴充套件

props和state

react中元件自身的狀態叫做state,在es6+的類元件中可以使用很簡單的語法進行初始化

export default class Xxx extends Component {
  state = {
    name: 'sakura',
  }
  render() {
    const { name } = this.state;
    return (<div>{name}</div>);
  }
}
複製程式碼

state可以賦值給某個標籤,如果需要更新state可以呼叫this.setState()傳入一個物件,通過這個方法修改state之後繫結了相應值的元素也會觸發渲染,這就是簡單的資料繫結

不能通過this.state.name = 'xxx'的方式修改state,這樣就會失去更新state同時相應元素改變的效果

setState函式是react中較為重要也是使用頻率較高的一個api,它接受最多兩個引數,第一個引數是要修改的state物件,第二個引數為一個回撥函式,會在state更新操作完成後自動呼叫,所以setState函式是非同步的。 呼叫this.setState之後react並沒有立刻更新state,而是將幾次連續呼叫setState返回的物件合併到一起,以提高效能,以下程式碼可能不會產生期望的效果

class SomeButton extends Component {
  state = {
    value: 1,
  }
  handleClick = () => {
    const { value } = this.state;
    this.setState({ value: value + 1 });
    this.setState({ value: value + 1 });
    this.setState({ value: value + 1 });
    this.setState({ value: value + 1 });
  }
  render() {
    const { value } = this.state;
    return (<div>
      <span>{vlaue}</span>
      <button onClick={this.handleClick}>click!</button>
    </div>);
  }
}
複製程式碼

實際上這裡並沒有對value進行4次+1的操作,react會對這四次更新做一次合併,最終只保留一個結果,類似於

Object.assign({},
  { value: value + 1 },
  { value: value + 1 },
  { value: value + 1 },
);
複製程式碼

並且因為setState是非同步的,所以不能在呼叫之後立馬獲取新的state,如果要用只能給setState傳入第二個引數回撥函式來獲取

/*省略部分程式碼*/
this.setState({
  value: 11,
}, () => {
  const { value } = this.state;
  console.log(value);
})
複製程式碼

props是由父元素所傳遞給給子元素的一個屬性物件,用法通常像這樣

class Parent extends Component {
  /*父元件的state中儲存了一個value*/
  state = {
    value: 0,
  };

  handleIncrease = () => {
    const { value } = this.state;
    this.setState({ value: value + 1 });
  }

  render() {
    const { value } = this.state;
    // 通過props傳遞給子元件Child,並傳遞了一個函式,用於子元件點選後修改value
    return (<div>
      <Child value={value} increase={this.handleIncrease} />
    </div>)
  }
}

// 子元件通過props獲取value和increase函式
const Child = (props) => (
  <div>
    <p>{props.value}</p>
    <button onClick={props.increase}>click!</button>
  </div>
);
複製程式碼

props像一個管道,父元件的狀態通過props這個管道流向子元件,這個過程叫做單向資料流

react中修改state和props都會引起元件的重新渲染

元件的生命週期

生命週期是一組用來表示元件從渲染到解除安裝以及接收新的props以及state宣告的特殊函式

react生命週期函式執行過程
這張圖展示了react幾個生命週期函式執行的過程,可以簡單把元件的生命週期分為三個階段,共包含9個生命週期函式,在不同階段元件會自動呼叫

  • 掛載
    • componentWillMount
    • render
    • componentDidMount
  • 更新
    • componentWillReceiveProps
    • shouldComponentUpdate
    • componentWillUpdate
    • render
    • componentDidUpdate
  • 解除安裝
    • componentWillUnmount

掛載--componentWillMount

這個階段元件準備開始渲染DOM節點,可以在這個方法裡做一些請求之類的操作,但是因為元件還沒有首次渲染完成,所以並不能拿到任何dom節點

掛載--render

正式渲染,這個方法返回需要渲染的dom節點,並且做資料繫結,這個方法裡不能呼叫this.setState方法修改state,因為setState會觸發重新渲染,導致再次呼叫render函式觸發死迴圈

掛載--componentDidMount

這個階段元件首次渲染已經完成,可以拿到真實的DOM節點,也可以在這個方法裡做一些請求操作,或者繫結事件等等

更新--componentWillReceiveProps

當元件收到新的props和state且還沒有執行render時會自動觸發這個方法,這個階段可以拿到新的props和state,某些情況下可能需要根據舊的props和新的props對比結果做一些相關操作,可以寫在這個方法裡,比如一個彈窗元件的彈出狀態儲存在父元件的state裡通過props傳給自身,判斷這個彈窗彈出可以這樣寫

class Dialog extends Component {
  componentWillReveiceProps(nextProps) {
    const { dialogOpen } = this.props;
    if (nextProps.dialogOpen && nextProps.dialogOpen !== dialogOpen) {
      /*彈窗彈出*/
    }
  }
}
複製程式碼

更新--shouldComponentUpdate

shouldComponentUpdate是一個非常重要的api。react的元件更新過程經過以上幾個階段,到達這個階段需要確認一次元件是否真的需要根據新的狀態再次渲染,確認的依據就是對比新舊狀態是否有所改變,如果沒有改變則返回false,後面的生命週期函式不會執行,如果發生改變則返回true,繼續執行後續生命週期,而react預設就返回true

所以可以得出shouldComponentUpdate可以用來優化效能,可以手動實現shouldComponentUpdate函式來對比前後狀態的差異,從而阻止元件不必要的重複渲染

class Demo extends Component {
  shouldComponentUpdate(nextProps, nextState) {
    return this.props.value !== nextProps.value;
  }
}
複製程式碼

這段程式碼是一個最簡單的實現,通過判斷this.props.valuenextProps.value是否相同來決定元件要不要重新渲染,但是實際專案中資料複雜多樣,並不僅僅是簡單的基本型別,可能有物件、陣列甚至是更深巢狀的物件,而資料巢狀越深就意味著這個方法裡需要做更深層次的對比,這對react效能開銷是極大的,所以官方更推薦使用Immutable.js來代替原生的JavaScript物件和陣列

由於immutablejs本身是不可變的,如果需要修改狀態則返回新的物件,也正因為修改後返回了新物件,所以在shouldComponentUpdate方法裡只需要對比物件的引用就很容易得出結果,並不需要做深層次的對比。但是使用immutablejs則意味著增加學習成本,所以還需要做一些取捨

更新--componentWillUpdate

這個階段是在收到新的狀態並且shouldComponentUpdate確定元件需要重新渲染而還未渲染之前自動呼叫的,在這個階段依然能獲取到新的props和state,是元件重新渲染前最後一次更新狀態的機會

更新--render

根據新的狀態重新渲染

更新--componentDidMount

重新渲染完畢

解除安裝--componentWillmount

元件被解除安裝之前,在這裡可以清除定時器以及解除某些事件

元件通訊

很多業務場景中經常會涉及到父=>子元件或者是子=>父元件甚至同級元件間的通訊,父=>子元件通訊非常簡單,通過props傳給子元件就可以。而子=>父元件通訊則是大多數初學者經常碰到的問題 假設有個需求,子元件是一個下拉選擇選單,父元件是一個表單,在選單選擇一項之後需要將值傳給父級表單元件,這是典型的子=>父元件傳值的需求

const list = [
  { name: 'sakura', id: 'x0001' },
  { name: 'misaka', id: 'x0003' },
  { name: 'mikoto', id: 'x0005' },
  { name: 'react', id: 'x0002' },
];

class DropMenu extends Component {
  handleClick = (id) => {
    this.props.handleSelect(id);
  }

  render() {
    <MenuWrap>
      {list.map((v) => (
        <Menu key={v.name} onClick={() => this.handleClick(v.id)}>{v.name}</Menu>
      ))}
    </MenuWrap>
  }
}

class FormLayout extends Component {
  state = {
    selected: '',
  }
  handleMenuSelected = (id) => {
    this.setState({ selected: id });
  }
  render() {
    <div>
      <MenuWrap handleSelect={this.handleMenuSelected} />
    </div>
  }
}
複製程式碼

這個例子中,父元件FormLayout將一個函式傳給子元件,子元件的Menu點選後呼叫這個函式並把值傳進去,而父元件則收到了這個值,這就是簡單的子=>父元件通訊

而對於更為複雜的同級甚至類似於叔侄關係的元件可以通過狀態提升的方式互相通訊,簡單來說就是如果兩個元件互不巢狀,沒有父子關係,這種情況下,可以找到他們上層公用的父元件,將state存在這個父元件中,再通過props給兩個元件傳入相應的state以及對應的回撥函式即可

路由

React中最常用的路由解決方案就是React-router,react-router迄今為止已經經歷了四個大版本的迭代,每一版api變化較大,本文將按照最新版react-router-v4進行講解

基本用法

使用路由,要先用Router元件將App包起來,並把history物件通過props傳遞進去,最新版本中history被單獨分出一個包,使用的時候需要先引入。對於同級元件路由的切換,需要使用Switch元件將多個Route包起來,每當路由變更,只會渲染匹配到的一個元件

import ReactDOM from 'react-dom';
import createHistory from 'history/createBrowserHistory';
import { Router } from 'react-router';

import App from './App';

const history = createHistory();

ReactDOM.render(
  <Router history={history}>
    <App />
  </Router>,
  element,
);

// App.js
//... 省略部分程式碼

import {
  Switch, Route,
} from 'react-router';

class App extends Component {
  render() {
    return (
      <div>
        <Switch>
          <Route exact path="/" component={Dashboard} />
          <Route path="/about" component={About} />
        </Switch>
      </div>
    );
  }
}
複製程式碼

CodesanBox線上示例

狀態管理

關於單頁面應用狀態管理可以先閱讀民工叔這篇文章單頁應用的資料流方案探索

React生態圈的狀態管理方案由facebook提出的flux架構為基礎,並有多種不同實現,而最為流行的兩種是

flux架構

Flux

Flux is the application architecture that Facebook uses for building client-side web applications. It complements React's composable view components by utilizing a unidirectional data flow. It's more of a pattern rather than a formal framework, and you can start using Flux immediately without a lot of new code.

Flux是facebook用於構建web應用的一種架構,它通過使用單向資料流補充來補充React的元件,它只是一種模式,而不是一個正式的框架

首先,Flux將一個應用分為三個部分:

  • dispatcher
  • stores
  • views

dispatcher

dispatcher是管理Flux應用中所有資料流的中心樞紐,它的作用僅僅是將actions分發到stores,每一個store都監聽自己並且提供一個回撥函式,當使用者觸發某個操作時,應用中的所有store都將通過監聽的回撥函式來接收這個操作

facebook官方實現的Dispatcher.js

stores

stores包含應用程式的狀態和邏輯,類似於傳統MVC中的model,stores用於儲存應用程式中特定區域範圍的狀態

一個store向dispatcher註冊一個事件並提供一個回撥函式,這個回撥函式可以接受action作為引數,並且基於actionType來區分並解釋操作。在store中提供相應的資料更新函式,在確認更新完畢後廣播一個事件用於應用程式根據新的狀態來更新檢視

// Facebook官方實現FluxReduceStore的用法
import { ReduceStore, Dispatcher } from 'flux';
import Immutable from 'immutable';
const dispatch = new Dispatcher();

class TodoStore extends ReduceStore {
  constructor() {
    super(dispatch);
  }
  getInitialState() {
    return Immutable.OrderedMap();
  }
  reduce(state, action) {
    switch(action.type) {
      case 'ADD_TODO':
        return state.set({
          id: 1000,
          text: action.text,
          complete: false,
        });
      default:
        return state;
    }
  }
}

export default new TodoStore();

複製程式碼

views

React提供了views所需的可組合以及可以自由的重新渲染的檢視,在React最頂層元件裡,通過某種粘合程式碼從stores中獲取所需資料,並將資料通過props傳遞到它的子元件中,我們就可以通過控制這個頂層元件的狀態來管理頁面任何部分的狀態

Facebook官方實現中有一個FluxContainer.js用於連線store與react元件,並在store更新資料後重新整理元件狀態更新檢視。基本原理是用一個高階元件傳入Stores和元件需要的state與方法以及元件本身,返回注入了state和action方法的元件,基本用法像這樣

import TodoStore from './TodoStore';
import Container from 'flux';
import TodoActions from './TodoActions';

// 可以有多個store
const getStores = () => [TodoStore];

const getState = () => ({
  // 狀態
  todos: TodoStore.getState(),

  // action
  onAdd: TodoActions.addTodo,
});

export default Container.createFunctional(App, getStore, getState);
複製程式碼

CodeSanbox線上示例 後續會補充flux官方實現的原始碼解析

Redux

Redux是由Dan Abramov對Flux架構的另一種實現,它延續了flux架構中viewsstoredispatch的思想,並在這個基礎上對其進行完善,將原本store中的reduce函式拆分為reducer,並將多個stores合併為一個store,使其更利於測試

redux
The Evolution of Flux Frameworks這篇文章,是他對原Flux架構的看法以及他的改進

The first change is to have the action creators return the dispatched action.What looked like this:


export function addTodo(text) {
  AppDispatcher.dispatch({
    type: ActionTypes.ADD_TODO,
    text: text
  });
}
複製程式碼

can look like this instead:

export function addTodo(text) {
  return {
    type: ActionTypes.ADD_TODO,
    text: text
  };
}
複製程式碼

stores拆分為單一store和多個reducer

const initialState = { todos: [] };
export default function TodoStore(state = initialState, action) {
  switch (action.type) {
  case ActionTypes.ADD_TODO:
    return { todos: state.todos.concat([action.text]) };
  default:
    return state;
}
複製程式碼

Redux把應用分為四個部分

  • views
  • action
  • reducer
  • store

views可以觸發一個action,reducer函式內部根據action.type的不同來對資料做相應的操作,最後返回一個新的state,store會將所有reducer返回的state組成一個state樹,再通過訂閱的事件函式更新給views

views

react元件作為應用中的檢視層

action

action是一個簡單的JavaScript物件,包含一個type屬性以及action操作需要用到的引數,推薦使用actionCreator函式來返回一個action,actionCreator函式可以作為state傳遞給元件

function singleActionCreator(payload) {
  return {
    type: 'SINGLE_ACTION',
    paylaod,
  };
}
複製程式碼

reducer

reducer是一個純函式,簡單的根據指定輸入返回相應的輸出,reducer函式不應該有副作用,並且最終需要返回一個state物件,對於多個reducer,可以使用combineReducer函式組合起來

function singleReducer(state = initialState, action) {
  switch(action.type) {
    case 'SINGLE_ACTION':
      return { ...state, value: action.paylaod };
    default:
      return state;
  }
}

function otherReducer(state = initialState, action) {
  switch(action.type) {
    case 'OTHER_ACTION':
      return { ...state, data: action.data };
    default:
      return state;
  }
}

const rootReducer = combineReducer([
  singleReducer,
  otherReducer,
]);

複製程式碼

store

redux中store只有一個,通過呼叫createStore傳入reducer就可以建立一個store,並且這個store包含幾個方法,分別是subscribe, dispatch,getState,以及replaceReducer,subscribe用於給state的更新註冊一個回撥函式,而dispatch用於手動觸發一個action,getState可以獲取當前的state樹,replaceReducer用於替換reducer,要在react專案中使用redux,必須再結合react-redux

import { connect } from 'react-redux';
const store = createStore(rootReducer);

// App.js
class App extends Component {
  render() {
    return (
      <div>
        test
      </div>
    );
  }
}

const mapStateToProps = (state) => ({
  vlaue: state.value,
  data: state.data,
});

const mapDispatchToProps = (dispatch) => ({
  singleAction: () => dispatch(singleActionCreator());
});

export default connect(mapStateToProps, mapDispatchToProps)(App);

// index.js
import { Provider } from 'react-redux';

ReactDOM.render(
  <Provider store={store}>
    <APP />
  </Provider>,
  element,
);
複製程式碼

CodeSanbox線上示例

因本人技術水平有限,文中若有錯誤或紕漏歡迎大佬們指出

部落格地址,不定時更新

相關文章