React 應用實踐(基礎篇)

大隻魚00發表於2018-01-24

React基礎

A Simple Component

class HelloWorld extends Component {
    render(){
        return <div>hello world</div>
    }
}
//react-dom render
render(<HelloWorld />, document.getElementById('app'))
複製程式碼

JSX

約定使用首字母大小寫來區分本地元件和HTML標籤

裝換createElement

//JSX
var root = <ul className="my-list">
             <li>Text Content</li>
           </ul>;
React.render(root, document.body);

//Javascript
var child = React.createElement('li', null, 'Text Content');
var root = React.createElement('ul', { className: 'my-list' }, child);
React.render(root, document.body);
複製程式碼

JS表示式

//屬性比阿達式
<div className={isDisplay ? "show" : "hide"}></div>

//子節點表示式
<div>
    {someFlag ? <span>something</span> : null}
</div>
複製程式碼

如果三元操作表示式不夠用,可以通過if語句來決定渲染哪個元件

class Sample extends React.Component {
    _decideWitchToRender(){
        if (this.props.index > 0 && this.props.someCondition){
            return <div>condition1</div>
        } else if (this.props.otherCondition) {
            return <div>condition2</div>
        }
        return <div>condition3</div>
    }
    render() {
        return <div>{ this.() }</div>
    }
}
複製程式碼

屬性擴散

spread operator(...)

var props = {}
props.a = x
props.b = y
var component = <Component {...props} />
複製程式碼

HTML實體處理

//Unicode字元
<div>{'First · Second'}</div>
//Unicode編號
<div>{'First \u00b7 Second'}</div>
<div>{'First ' + String.fromCharCode(183) + ' Second'}</div>
//陣列裡混合使用字串
<div>{['First ', <span>&middot;</span>, ' Second']}</div>
//dangerouslySetInnerHTML
<div dangerouslySetInnerHTML={{__html: 'First &middot; Second'}} />
複製程式碼

自定義HTML屬性

如果往原生 HTML 元素裡傳入 HTML 規範裡不存在的屬性,React 不會顯示它們,data、aria(視聽無障礙)字首除外 特殊情況:自定義元素支援任意屬性

<x-my-component custom-attribute="foo" />
複製程式碼

JSX與HTML差異

class -> className, for -> htmlFor, style由css屬性構成的JS物件

...
render(){
    const imgStyle = {
        transform: "rotate(0)",
        WebkitTransform: "rotate(0)"
    }
    return <div className="rotate" style={imgStyle} ></div>
}
...
複製程式碼

React Component

事件系統(合成事件和原生事件) 合成事件會以事件委託(event delegation)的方式繫結到元件最上層,並且在元件解除安裝(unmount)的時候自動銷燬繫結的事件 React合成事件最終是通過委託到document這個DOM節點進行實現,其他節點沒有繫結事件* React合成事件有自己的佇列方式,可以從觸發事件的組建向父元件回溯,可以通過e.stopPropagation來停止合成事件的傳播,但無法阻止原生事件,原生事件可以阻止合成事件 React會管理合成事件的物件建立和銷燬

DOM操作

findDOMNode()

import { findDOMNode } from 'react-dom'
...
componentDidMound() {
  const el = findDOMNode(this)
}
...
複製程式碼

Refs HTML元素,獲取到DOM元素 自定義元件,獲取到元件例項 無狀態元件無法新增ref

...
componentDidMound() {
    const el = this.refs.el
}
render() {
    return <div ref="el"></div>
}
...
複製程式碼

組合元件

const ProfilePic = (props) => {
    return (
        <img src={'http://graph.facebook.com/' + props.username + '/picture'} />
    )
}
const ProfileLink = (props) => {
    return (
        <a href={'http://www.facebook.com/' + props.username}>
            {props.username}
        </a>
    )
}
const Avatar = (props) => {
    return (
        <div>
            <ProfilePic username={props.username} />
            <ProfileLink username={props.username} />
        </div>
    )
}
複製程式碼

父級能通過專門的this.props.children來讀取子級

const Parent = (props) => {
    return <div>
        <p>something</p>
        {props.children}
    </div>
}
React.render(<Parent><Avatar username="clark" /></Parent>, document.body)
複製程式碼

props.children 通常是一個元件物件的陣列,當只有一個子元素的時候prop.children將是這個唯一子元素

元件生命週期

例項化

  • getDefaultProps 只呼叫一次,所有元件共享
  • getInitialState 每個例項呼叫有且只有一次
  • componentWillMount
  • render 通過this.props/this.state訪問資料、可以返回null/false/React元件、只能出現一個頂級元件、不能改變元件的狀態、 不要去修改DOM的輸出
  • componentDidMount 真實DOM已被渲染,這時候可以通過ref屬性(this.refs.[refName])獲取真實DOM節點進行操作,如一些事件繫結

存在期

  • componentWillReceiveProps 元件的props通過父元件來更改,可以在這裡更新state以觸發render來重新渲染元件
  • shouldComponentUpdate 判斷在props/state發生改變後需不需要重新渲染,優化效能的重要途徑
  • componentWillUpdate 注意不要在此方法中更新props/state
  • render
  • componentDidUpdate 類似於componentDidMount

銷燬期

  • componentWillUnmount 如在componentDidMount中新增相應的方法與監聽,需在此時銷燬

Mixins

ES6寫法中已經不支援,因為使用mixin場景可以用組合元件方式實現

高階元件

export const ShouldUpdate = (ComposedComponent) => class extends React.Component{
    constructor(props) {
        super(props)
    }
    shouldComponentUpdate(nextProps) {
        ...
    }
    render() {
        return <ComposedComponent {...this.props}/>
    }
}

//usage
class MyComponent = class extends Component {
    ...
}

export default ShouldUpdate(MyComponent)
複製程式碼

高階元件的思路是函式式的 每一個擴充套件就是一個函式

const newComponent = A(B(C(MyComponent)))
複製程式碼

實現方式包括: 屬性代理(Props Proxy)和反向繼承(Inheritance Inversion)

//屬性代理
const ppHOC = ComponsedComponent => class extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            newState: ''
        };
    }

    handleSomething = (e) => {

    };

    render() {
        const newProps = Object.assign({}, this.props, this.state);
        return <ComponsedComponent ref="compnent" {...this.props} onSomething={this.handleSomething}/>;
    }
}

//反向繼承
function iiHOC(WrappedComponent) {
    return class Enhancer extends WrappedComponent {
        render() {
            return supper.render();
        }
    }
}

複製程式碼

注:反向繼承可以做到渲染劫持,通常不建議操作state特別是新增

虛擬DOM Diff

這裡有兩個假設用來降低Diff演算法的複雜度O(n^3) -> O(n) 1.兩個相同的元件產生類似的DOM結構,不同元件產生不同的DOM結構 2.對於同一層次的元件子節點,它們可以通過唯一的id進行區分 React的比較僅僅是按照樹的層級分解 React僅僅對同一層的節點嘗試匹配,因為實際上,Web中不太可能把一個Component在不同層中移動 不同節點型別比較

//節點型別不同
renderA:<div />
renderB:<span />
=>[removeNode <div />], [insertNode <span />]
//對於React元件的比較同樣適用,React認為不需要話太多時間去匹配兩個不大可能有相似之處的component
renderA:<Header />
renderB:<Content />
=>[removeNode <Header />], [insertNode <Content />]
複製程式碼

相同型別節點比較

renderA: <div id="before" />
renderB: <div id="after" />
=> [replaceAttribute id "after"]
renderA: <div style={{color: 'red'}} />
renderB: <div style={{fontWeight: 'bold'}} />
=> [removeStyle color], [addStyle font-weight 'bold']
複製程式碼

tips:實現自己的元件的時候,保持穩定的DOM結構會有助於效能的提升,例如我們可以通過css隱藏來控制節點顯示,而不是新增/刪除

列表節點的比較 當React校正帶有key的子級時,它會被確保它們被重新排序

import React from 'react'
import { render } from 'react-dom'
function createComponent(name) {
    class TestNode extends React.Component{
        constructor(props) {
            super(props)
            console.log(name + ' is init')
        }
        componentDidMount() {
            console.log(name + ' did mount')
        }

        componentWillUnmount() {
            console.log(name + ' will unmount')
        }

        componentDidUpdate() {
            console.log(name + ' is updated')
        }

        render() {
          return (
                <div className={'node ' + name} data-name={name}>
                    {this.props.children}
                </div>
            )
        }
    }

    return TestNode
}

const Root = createComponent('R')
const A = createComponent('A')
const B = createComponent('B')
const C = createComponent('C')
const D = createComponent('D')

class Wrapper extends React.Component{
    test1() {
        return (<Root>
                    <A>
                        <B />
                        <C />
                    </A>
                </Root>)
    }
    test2() {
        return (<Root>
                    <A>
                        <C />
                        <B />
                    </A>
                </Root>)
    }
    test3() {
        return (<Root>
                    <A>
                        <B key="B"/>
                        <C key="C"/>
                    </A>
                </Root>)
    }
    test4() {
        return (<Root>
                    <A>
                        <C key="C"/>
                        <B key="B"/>
                    </A>
                </Root>)
    }

    render() {
        if (this[this.props.testType]) {
            return this[this.props.testType]()
        } else {
            return <Root />
        }
    }
}
window.renderTest = function(testType){
    render(<Wrapper testType={testType} />, document.getElementById('app'))
}

複製程式碼

避免使用state

  • componentDidMount、componentDidUpdate、render
  • computed data, react components, duplicated data from props

React與AJAX

componentDidMount中發起ajax請求,拿到資料後通過setState方法更新UI 如果非同步請求請注意在componentWillUnmount中的abort請求

元件之間相互呼叫

  • 父子元件 React資料流是單向的,父元件資料可以通過設定子元件props進行傳遞,如果想讓子元件改變父元件資料,父元件傳遞一個回撥函式給子元件即可(函式傳遞注意事項this.fn.bind(this)和箭頭函式均會返回新的函式)
  • 兄弟元件 將資料掛在在父元件中,由多個子元件共享資料,元件層次太深的問題 -> 全域性事件/Context(getChildContext&childContextTypes)
class Parent extends React.Component {
    getChildContext() {
        return { value: 'parent' };
    }

    render() {
        return <div>
                {this.props.children}
            </div>
    }
}
Parent.childContextTypes = {
    value: React.PropTypes.string
}

class Children extends React.Component {
  // 如果不需要在建構函式中使用可以不寫,沒有影響
    constructor(props, context) {
        super(props, context)
        console.log(context)
    }
    render() {
        return <div>{'context is: ' + this.context.value}</div>
    }
}
//如果要context的內容必須校驗contextTypes
Children.contextTypes = {
    value: React.PropTypes.string
}
複製程式碼

元件化開發的思考

  • 元件儘可能無狀態化(stateless)
  • 細粒度的把握,提高複用性
  • 配合高階元件實現複雜邏輯

React API unstable_renderSubtreeIntoContainer

Redux

  • 整個應用只有一個store(單一資料來源)
  • State只能通過觸發Action來更改
  • State每次更改總是返回一個新的State,即Reducer

Actions

一個包含{type, payload}的物件, type是一個常量標示的動作型別, payload是動作攜帶的資料, 一般我們通過建立函式的方式來生產action,即Action Creator

{
    type: 'ADD_ITEM',
    name: 'item1'
}
function addItem(id, name) {
    return {
        type: 'ADD_ITEM',
        name,
        id
    }
}
複製程式碼

Reducer

Reducer用來處理Action觸發的對狀態樹的更改 (oldState, action) => newState Redux中只有一個Store,對應一個State狀態,所以如果把處理都放到一個reducer中,顯示會讓這個函式顯得臃腫和難以維護 將reducer拆分很小的reducer,每個reducer中關注state樹上的特定欄位,最後將reducer合併(combineReducers)成root reducer

function items(state = [], action) {
    switch (action.type) {
        case 'ADD_ITEM'
            return [...state, {
                action.name,
                action.id
            }]
        default:
            return state
    }
}
function selectItem(state = '', action) {
    switch (action.type) {
        case 'SELECT_ITEM'
            return action.id
        default:
            return state
    }
}
var rootReducer = combineReducers({
    items,
    selectItem
})
複製程式碼

Store

  • 提供State狀態樹
  • getState()方法獲取State
  • dispatch()方法傳送action更改State
  • subscribe()方法註冊回撥函式監聽State的更改

根據已有的reducer建立store非常容易

import { createStore } from 'redux'
let store = createStore(rootReducer)
複製程式碼

資料流

store.dispatch(action) -> reducer(state, action) -> store.getState()

  • 1.呼叫store.dispatch(action)
  • 2.redux store呼叫傳入的reducer函式,根reducer應該把多個reducer輸出合併成一個單一的state樹
  • 3.redux store儲存了根reducer返回的完整的state樹

react-redux

  • Provider: 容器元件,用來接收Store,讓Store對子元件可用
  • connect: 提供引數如下:
    • mapStateToProps 返回state中挑出部分值合併到props
    • mapDispatchToProps 返回actionCreators合併到props
    • mergeProps 自定義需要合併到props的值
    • options pure、withRef

Connect本身是一個元件,通過監聽Provider提供的store變化來呼叫this.setState操作,這裡特別需要注意傳入的mapStateToProps必需只是你需要的資料,不然作為全域性公用的store每次都進行對比顯然不高效也不合理 通過connect()方法包裝好的元件可以得到dispath方法作為元件的props,以及全域性state中所需的內容

四個要點

  • Redux提供唯一store
  • Provider元件包含住最頂層元件,將store作為props傳入
  • connect方法將store中資料以及actions通過props傳遞到業務子元件
  • 子元件呼叫action,dispatch到reducer返回新的state,store同步更新,並通知元件進行更新

展示元件

  • 關注UI
  • 不依賴action來更新元件
  • 通過props接收資料和回撥函式改變資料
  • 無狀態元件,一般情況下沒有state

容器元件

  • 關注運作方式
  • 呼叫action
  • 為展示元件提供資料和方法
  • 作為資料來源,展示元件UI變化控制器

UI與邏輯的分離,利於複用,易於重構

redux中介軟體

//redux中介軟體格式
({dispatch, store}) => (next) => (action) => {}
import {createStore, applyMiddleware, combineReducers} from "redux"

let reducer = (store={},action)=>{
    return store
}

let logger1 = ({dispatch, getState}) => (next) => (action) => {
    console.log("第一個logger開始");
    next(action);
}

let logger2 = ({dispatch, getState}) => (next) => (action) => {
    console.log("第二個logger開始");
    next(action);
}

let store = createStore(reducer,applyMiddleware(logger1,logger2));

store.dispatch({
  type: "type1",
})
//輸出
第一個logger開始
第二個logger開始

//通過applyMiddleware包裝後的dispatch
let new_dispatch = (...args) = >  logger1(logger2(dispatch(...args)))
複製程式碼

immutableJS

替代方案 seamless-immutable

immutable物件的任何修改或者新增刪除都會返回一個新的immutable物件 其實現原理是持久化資料結構(Persistent Data Structure),即舊資料建立新資料時,保證舊資料可用且不變,避免deepcopy把所有節點都複製一遍

兩個 immutable 物件可以使用 === 來比較,這樣是直接比較記憶體地址,效能最好。但即使兩個物件的值是一樣的,也會返回 false,為了直接比較物件的值,immutable.js 提供了 Immutable.is 來做『值比較』

let map1 = Immutable.Map({a:1, b:1, c:1});
let map2 = Immutable.Map({a:1, b:1, c:1});
map1 === map2;             // false
Immutable.is(map1, map2);  // true
複製程式碼

Immutable.is通過hashCode/valueOf提升比較效能,在react中使用shouldComponentUpdate來進行效能優化的時候能避免deepCopy和deepCompare造成的效能損耗

normalizr

儘可能把state正規化化,不存在巢狀

[{
    id: 1,
    title: 'Some Article',
    author: {
        id: 1,
        name: 'Dan'
    }
}, {
    id: 2,
    title: 'Other Article',
    author: {
        id: 1,
        name: 'Dan'
    }
}]
//希望的結果
{
    result: [1, 2],
    entities: {
        articles: {
            1: {
                id: 1,
                title: 'Some Article',
                author: 1
            },
            2: {
                id: 2,
                title: 'Other Article',
                author: 1
            }
        },
        users: {
            1: {
                id: 1,
                name: 'Dan'
            }
        }
    }
}
複製程式碼

相關文章