React組件化

react基本全是組件,須要使用什麼組件就安裝什麼組件。css

ant-design組件庫


npm install antd -D
// 全局導入使用

import React, {Component} from 'react'
import Button from 'antd/lib/button'
import 'antd/dist/antd.css'

export default class AntdTest extends Component {
    render(){
        return (
            <div>
                <Button type="primary">按鈕</Button>
            </div>
        )
    }
}

全局導入代碼會比較大,不利於項目開發,建議使用按需加載的方式。vue

按需加載

一、安裝react-app-rewired取代react-script,能夠擴展webpack配置
npm install react-app-rewired@2.0.2-next.0 babel-plugin-import -D
二、根目錄建立 config-overrides.js 作以下配置
const { injectBabelPlugin } = require('react-app-rewired');
module.exports = function override(config, env) {
    config = injectBabelPlugin( // 在默認配置基礎上注入
        // 插件名,插件配置
        ['import', { libraryName: 'antd', libraryDirectory: 'es', style: 'css' }],
        config
    );
    return config
}

圖文解釋

image

三、package.json 作以下修改
"scripts": {
    "start": "react-app-rewired start",
    "build": "react-app-rewired build",
    "test": "react-app-rewired test",
    "eject": "react-app-rewired eject"
}
四、修改引用方式
import React, {Component} from 'react'
import { Button } from 'antd'

export default class AntdTest extends Component {
    render(){
        return (
            <div>
                <Button type="primary">按鈕</Button>
            </div>
        )
    }
}
五、由於修改了package.json,須要重啓服務

容器組件 && 展現組件


基本原則:容器組件負責數據獲取,展現組件負責根據 props展現信息
// CommentList.js

import React,{Component} from 'react';

export default class CommentList extends Component {
    constructor(props){
        super(props);
        this.state = {
            comments: [] 
        };
    }

    componentDidMount(){
        setTimeout(() => {
            this.setState({
                comments: [
                    {
                        body: 'react is very good',
                        author: 'facebook'
                    },
                    {
                        body: 'vue is very good',
                        author: 'youyuxi'
                    }
                ]
            })
        },1000)
    }

    render(){
        return (
            <div>
                {this.state.comments.map((c,i) => <Comment key={i} data={c}></Comment>)}
            </div>
        )
    }
}

// 展現組件 (傻瓜組件)
class Comment extends Component {
    console.log('render comment')
    return (
        <div>
            <p>{this.props.data.body}</p>
            <p>---{this.props.data.author}</p>
        </div>
    )
}

思考:

若是在更改數據的時候須要輪訓,一直修改數據,如何進行性能優化?
componentDidMount(){
    setInterval(() => { 
        this.setState({
            comments: [
                {
                    body: 'react is very good',
                    author: 'facebook'
                },
                {
                    body: 'vue is very good',
                    author: 'youyuxi'
                }
            ]
        })
    },1000)
}

class Comment extends PureComponent {
    render(){
        // 由於數據的一直更新,其實數據是沒有變化的,render函數卻會一直執行,消耗性能
        console.log('render comment')
        return (
            <div>
                <p>{this.props.data.body}</p>
                <p>---{this.props.data.author}</p> 
            </div>
        )
    }
}

PureComponent (15.3出現)


定製了 shouldComponentUpdate後的 Component(淺比較)

由於是淺比較,只會比較引用類型地址,只會比較一層。因此要麼傳值類型,要麼引用類型地址不能發生變化而且地址只能有一層。大體源碼解析以下:react

export default function PureComponent(props, context){
    Component.call(this,props,context)
}

PureComponent.prototype = Object.create(Component.prototype)
PureComponent.prototype.constructor = PureComponent
PureComponent.prototype.isPureReactComponent = true
// 着重擴展了shouldComponentUpdate的實現
PureComponent.prototype.shouldComponentUpdate = shallowCompare

// 淺比較
function shallowCompare(nextProps, nextState){
    return !shallowEqual(this.props,nextProps) || !shallowEqual(this.state, nextState)
}

export default function shallowEqual(objA, objB){
    // 比較的是對象的引用,只會看引用地址是否發生變化
    if(objA === objB){
        return true
    }

    if(typeof objA !== 'object' || objA === null || typeof objB !== 'object' || objB === null){
        return false
    }

    var keysA = Object.keys(objA)
    var keysB = Object.keys(objB)

    if(keysA.length !== keysB.length){
        return false
    }

    // test for A's keys different from B.
    // 只作了一層循環
    for(var i = 0; i < keysA.length; i++){
        if(!objB.hasOwnProperty(keysA[i]) || objA[keysA[i]] !== objB[keysA[i]]){
            return false
        }
    }

    return true
}
因此以前的代碼要作以下修改
import React,{Component, PureComponent} from 'react';

export default class CommentList extends Component {
    render(){
        return (
            <div>
                {this.state.comments.map((c,i) => (
                    // <Comment key={i} data={c}></Comment>
                    // <Comment key={i} body={c.data.body} author={c.data.author}></Comment>
                    <Comment key={i} {...c}></Comment>
                ))}
            </div>
        )
    }
}

class Comment extends PureComponent {
// class Comment extends Component{
    render(){
        console.log('render comment')
        return (
            <div>
                {/* <p>{this.props.data.body}</p>
                <p>---{this.props.data.author}</p> */}

                <p>{this.props.body}</p>
                <p>---{this.props.author}</p>
            </div>
        )
    }
}

// react 16.6.0版本以上擴展出一個高階組件 React.memo 實現了PureComponent
// 可用該組件改寫上述代碼

const Comment = React.memo(props => {
    return (
        <div>
            <p>{props.body}</p>
            <p>---{props.author}</p>
        </div>
    )
})
總結:儘量抽取出展現組件,並使用PureComponent

高階組件


提升複用率,首先想到的就是抽離相同邏輯,在React裏就有了 HOC(higher-Order Component)的概念。
高階組件簡單講就是一個函數,接收一個組件返回一個新組件,產生的新組件能夠對屬性進行包裝,也能夠重寫部分生命週期。
// Hoc.js

import React, {Component} from 'react'

function Study(props){
    // stage 是從父組件傳入的,name是動態獲取的
    return <div>{props.stage}-{props.name}</div>
}

// 高階組件
const WithStudy = Comp => {
    // 獲取name,name可能來自接口或其餘手段
    const name = '高階組件';
    return props => <Comp {...props} name={name}></Comp>
}

const NewStudy = WithStudy(Study);

export default class Hoc extends Component {
    render(){
        return (
            <div>
                <NewStudy stage="React"></NewStudy>
            </div>
        )
    }
}
重寫組件生命週期
// 高階組件
const WithStudy = Comp => {
    // 獲取name,name可能來自接口或其餘手段
    const name = '高階組件';

    return class NewComp extends Component {
        componentDidMount(){
            console.log('do something')
        }

        render(){
            return <Comp {...this.props} name={name}></Comp>
        }
    }
    // return props => <Comp {...props} name={name}></Comp>
}

高階鏈式調用


// Hoc.js

function Study(props){
    return <div>{props.stage}-{props.name}</div>
}

const WithStudy = Comp => {
    // 獲取name,name可能來自接口或其餘手段
    const name = '高階組件';

    // 可省略類名
    return class NewComp extends Component {
        componentDidMount(){
            console.log('do something')
        }

        render(){
            return <Comp {...this.props} name={name}></Comp>
        }
    }
}

const widthLog = Comp => {
    console.log(Comp.name + '渲染了')
    return props => <Comp {...props}></Comp>
}

// 鏈式調用
const NewStudy = WithStudy(widthLog(Study));

export default class Hoc extends Component {
    render(){
        return (
            <div>
                <NewStudy stage="React"></NewStudy>
            </div>
        )
    }
}

高階組件裝飾器寫法


由於鏈式語法太過累贅,es7有一個優秀的語法-裝飾器,專門處理這種問題,只能用於class組件webpack

npm install  babel-plugin-transform-decorators-legacy -D
// config-overrides.js 增長

config = injectBabelPlugin(
    ['@babel/plugin-proposal-decorators', { 'legacy': true }],
    config
)

修改了配置文件記得重啓服務
import React, {Component} from 'react'
import { render } from 'react-dom';

// 高階組件
const WithStudy = Comp => {
    // 獲取name,name可能來自接口或其餘手段
    const name = '高階組件a';

    // 可省略類名
    return class NewComp extends Component {
        componentDidMount(){
            console.log('do something')
        }

        render(){
            return <Comp {...this.props} name={name}></Comp>
        }
    }
}

const widthLog = Comp => {
    console.log(Comp.name + '渲染了')
    return props => <Comp {...props}></Comp>
}

// 調用執行順序從上往下,裝飾的是下面緊挨着的class
@widthLog  // 裝飾的是Study
@WithStudy  // 裝飾的是上一層包裝返回的新組件
@widthLog // 裝飾的是前兩層包裝返回的新組件

// 這裏的study就是進行修飾過的最新的study,這就是爲何高階組件代碼要放在上面
class Study extends Component { 
    render(){
        return <div>{this.props.stage}-{this.props.name}</div>
    }
}

export default class Hoc extends Component {
    render(){
        return (
            <div>
                <Study stage="React"></Study>
            </div>
        )
    }
}

組件複合-Composition


組件複合給與你足夠的敏捷去定義自定義組件的外觀和行爲,並且是以一種明確而安全的方式進行。若是組件有公用的非 ui邏輯,將它們抽取爲 js模塊導入使用而不是繼承它。
// Composition.js

import React from 'react';
// Dialog做爲容器,不關心內容和邏輯
// 等於vue中的slot
function Dialog(props){
    // children 預留屬性固定的
    return <div style={{border: "4px solid blue"}}>{props.children}</div>
}

// WelcomeDialog經過複合提供內容
function WelcomeDialog(){
    return (
        <Dialog>
            <h1>歡迎光臨</h1>
            <p>感謝使用react</p>
        </Dialog>
    )
}

export default function(){
    return <WelcomeDialog></WelcomeDialog>
}
如何擴展傳值?
import React from 'react';
// Dialog做爲容器,不關心內容和邏輯
// 等於vue中的slot
function Dialog(props){
    // children 預留屬性固定的
    return <div style={{border: `4px solid ${props.color || 'blue'}`}}>{props.children}</div>
}

// WelcomeDialog經過複合提供內容
function WelcomeDialog(props){
    return (
        <Dialog {...props}>
            <h1>歡迎光臨</h1>
            <p>感謝使用react</p>
        </Dialog>
    )
}

export default function(){
    return <WelcomeDialog color="green"></WelcomeDialog>
}
相似於vue,具名插槽怎麼處理?
import React from 'react';
// Dialog做爲容器,不關心內容和邏輯
// 等於vue中的slot
function Dialog(props){
    // children 預留屬性固定的
    return (
        <div style={{border: `4px solid ${props.color || 'blue'}`}}>
            {/* 可理解爲匿名插槽 */}
            {props.children}
            <div className="footer">
                {/* footer是jsx */}
                {props.footer}
            </div>
        </div>
    )
}

// WelcomeDialog經過複合提供內容
function WelcomeDialog(props){
    return (
        <Dialog {...props}>
            <h1>歡迎光臨</h1>
            <p>感謝使用react</p>
        </Dialog>
    )
}

export default function(){
    const footer = <button onClick={() => alert('肯定!')}>肯定</button>
    return <WelcomeDialog color="green" footer={footer}></WelcomeDialog>
}
children的深層次解讀
children多是jsx,也多是函數,也多是數組

一、對children做爲函數解讀web

import React from 'react';

const Api = {
    getUser(){
        return {
            name: 'pj',
            age: 20
        }
    }
}

function Fetcher(props) {
    const user = Api[props.name]();
    return props.children(user)
}

export default function(){
    return (
        <div>
            <Fetcher name="getUser">
                {({name,age}) => (
                    <p>
                        {name} - {age}
                    </p>
                )}
            </Fetcher>
        </div>
    )
}

二、利用children是數組作濾器npm

import React from 'react';

function Filter(props){
    return (
        <div>
            {/* React.Children 提供了不少方法,而且會有異常捕獲判斷 */}
            {React.Children.map(props.children,child => {
                if(child.type !== props.type){
                    return
                }
                return child
            })}
        </div>
    )
}

export default function(){
    return (
        <div>
            {/* 過濾器能夠過濾指定標籤類型 */}
            <Filter type="p">
                <h1>react</h1>
                <p>react很不錯</p>
                <h1>vue</h1>
                <p>vue很不錯</p>
            </Filter>
        </div>
    )
}

三、修改childrenjson

import React from 'react';

// 修改children
function RadioGroup(props){
    return (
        <div>
            {React.Children.map(props.children, child => {
                // vdom不可更改,克隆一個新的去修改
                return React.cloneElement(child, {name: props.name})
            })}
        </div>
    )
}

function Radio({children, ...rest}){
    return (
        <label>
            <input type="radio" {...rest} />
            {children}
        </label>
    )
}

export default function(){
    return (
        <div>
            <RadioGroup name="mvvm">
                <Radio value="vue">vue</Radio>
                <Radio value="react">react</Radio>
                <Radio value="angular">angular</Radio>
            </RadioGroup>
        </div>
    )
}

Hook


HookReact16.8一個新增項,它可讓你在不編寫class的狀況下使用state以及其餘的React特性。api

Hook的特色

  • 使你在無需修改組件結構的狀況下複用狀態邏輯
  • 可將組件間相互關聯的部分拆分紅更小的函數,複雜組件將變得更容易理解
  • 更簡潔、更易理解的代碼
// HookTest.js
import React, {useState} from 'react'

export default function HookTest(){
    // useState(initState) 返回一個數組,索引0的位置存放的是定義的狀態,索引1的位置是改對應狀態的函數
    const [count, setCount] = useState(0);
    return (
        <div>
            <p>點擊了{count}次</p>
            <button onClick={() => setCount(count+1)}>點擊</button>
        </div>
    )
}
多個狀態如何處理
import React, {useState} from 'react'

export default function HookTest(){
    // 多個狀態
    const [age] = useState(20);
    const [fruit, setFruit] = useState('banana');
    const [input, setInput] = useState('');
    const [fruits, setFruits] = useState(['apple','banana']);

    return (
        <div>
            <p>年齡:{age}</p>
            <p>選擇的水果:{fruit}</p>
            <p>
                <input type="text" value={input} onChange={e => setInput(e.target.value)} />
                <button onClick={() => setFruits([...fruits,input])}>新增水果</button>
            </p>
            <ul>
                {fruits.map(f => <li key={f} onClick={() => setFruit(f)}>{f}</li>)}
            </ul>
        </div>
    )
}

反作用鉤子-Effect Hook


useEffect就是一個Effect Hook,給函數組件增長了操做反作用的能力。它跟class組件中的componentDidMount,componentDidUpdatecomponentcomponentWillMount具備相同的做用,只不過被合併成了一個API.數組

import React, {useState, useEffect} from 'react'

export default function HookTest(){
    const [count, setCount] = useState(0);

    // 反作用鉤子會在每次渲染時都執行
    useEffect(() => {
        document.title = `您點擊了${count}次`
    })

    return (
        <div>
            <p>點擊了{count}次</p>
            <button onClick={() => setCount(count+1)}>點擊</button>
        </div>
    )
}
若是不想每次更新的時候都調用,如何處理?
// 若是僅打算執行一次,傳遞第二個參數爲[],相似於componentDidMount
// useEffect能夠有多個,便於邏輯的拆分
useEffect(() => {
    // api 調用
    console.log('api 調用')
},[])
若是不想其餘狀態更新也執行該方法,只針對某個值或某些值的變化才執行該方法如何處理?
// 只有count發生變化,該方法纔會執行
useEffect(() => {
    document.title = `您點擊了${count}次`
},[count])

// 若是跟多個值有關
useEffect(() => {
    document.title = `您點擊了${count}次`
},[count,age])

自定義鉤子-Custom Hook


自定義 hook是一個函數,名稱用' use'開頭,函數內部能夠調用其餘鉤子
import React, {useState, useEffect} from 'react'

function useAge(){
    const [age, setAge] = useState(0);
    useEffect(() => {
        setTimeout(() => {
            setAge(20)
        },2000)
    })
    return age
}

export default function HookTest(){
    const age = useAge();

    return (
        <div>
            <p>年齡:{age ? age : 'loading...'}</p>
        </div>
    )
}

其餘Hook


useContextuseReduceruseCallbackuseMemo安全

組件跨層級通訊-Context


上下文提供一種不須要每層設置 props就能跨多級組件傳遞數據的方式

Context相關API

  • React.createContext
  • Context.Provider
  • Class.contextType
  • Context.Consumer
深層次的組件想拿到外層的數據,有3種方式

一、第一種 消費者Consumer方式

import React from 'react'

// 建立上下文
const MyContext = React.createContext();
const {Provider, Consumer} = MyContext;

function Child(prop){
    return (
        <div>
            Child: {prop.foo}
        </div>
    )
}

export default function ContextTest(){
    return (
        <div>
            {/* 套在Provider下,無論嵌套多少層,均可以拿到Provider上傳過來的值 */}
            <Provider value={{foo: 'bar'}}>
                {/* 第一種 消費者Consumer方式  想拿到值的組件必須被Consumer包圍 */}
                <Consumer>
                    {value => <Child {...value}></Child>}
                </Consumer>
            </Provider>
        </div>
    )
}

二、消費方法二、Hook

import React, {useContext} from 'react'

// 建立上下文
const MyContext = React.createContext();
const {Provider} = MyContext;

// 使用Hook消費
function Child2(){
    const context = useContext(MyContext);
    return <div>Child2: {context.foo}</div>
}

export default function ContextTest(){
    return (
        <div>
            {/* 套在Provider下,無論嵌套多少層,均可以拿到Provider上傳過來的值 */}
            <Provider value={{foo: 'bar'}}>
                {/* 消費方法二、Hook */}
                <Child2></Child2>
            </Provider>
        </div>
    )
}

三、消費方法三、contextType

import React, {Component} from 'react'

// 建立上下文
const MyContext = React.createContext();
const {Provider} = MyContext;

// 使用class指定靜態contextType
class Child3 extends Component{
    static contextType = MyContext;
    render(){
        return <div>Child3: {this.context.foo}</div>
    }
}

export default function ContextTest(){
    return (
        <div>
            {/* 套在Provider下,無論嵌套多少層,均可以拿到Provider上傳過來的值 */}
            <Provider value={{foo: 'bar'}}>
                {/* 消費方法三、contextType */}
                <Child3></Child3>
            </Provider>
        </div>
    )
}

組件設計與實現案例


// KForm.js

import React, { Component } from 'react'
import {Input, Button} from 'antd'

// 建立一個高階組件,擴展示有表單,事件處理,數據收集,校驗
function KFormCreate(Comp){
    return class extends Component {
        constructor(props){
            super(props);

            this.options = {};
            this.state = {};
        }

        handleChange = e => {
            const {name, value} = e.target;
            this.setState({
                [name]: value
            }, ()=>{
                // 確保值發生變化再校驗
                this.validateField(name);
            })
        }

        // 單項校驗
        validateField = field => {
            // 一、獲取校驗規則
            const rules = this.options[field].rules;
            // 任意一項失敗則返回false
            const ret = !rules.some(rule => {
                if(rule.required){
                    if(!this.state[field]){
                        // 校驗失敗
                        this.setState({
                            [field + 'Message']: rule.message
                        })
                        return true
                    }
                }
            })

            // 校驗成功
            if(ret){  
                this.setState({
                    [field + 'Message']: ''
                })
            }
            return ret
        }

        // 校驗全部字段
        validate = cb => {
            const rets = Object.keys(this.options).map(field => 
                this.validateField(field)
            );
            
            const ret = rets.every( v => v == true);
            cb(ret,this.state)
        }
        
        // 建立input包裝器
        getFieldDec = (field,option) => {
            // 保存當前輸入項配置
            this.options[field] = option;
            return InputComp => (
                <div>
                    {React.cloneElement(InputComp, {
                        name: field,
                        value: this.state[field] || '',
                        onChange: this.handleChange
                    })}
                    {/* 校驗錯誤信息 */}
                    {this.state[field + 'Message'] && (
                        <p style={{color: 'red'}}>{this.state[field + 'Message']}</p>
                    )}
                </div>
            )
        }

        render(){
            return <Comp getFieldDec={this.getFieldDec} validate={this.validate}></Comp>
        }
    }
}

@KFormCreate

class KForm extends Component {
    onSubmit = () => {
        // 校驗全部項
        this.props.validate((isValid, data) => {
            if(isValid){
                // 提交登陸
                console.log('登陸',data)
                // 後續登陸邏輯
            }else {
                alert('校驗失敗')
            }
        });
    }

    render(){
        const {getFieldDec} = this.props;
        return (
            <div>
                {getFieldDec('uname',{
                    rules: [
                        {
                            required: true,
                            message: '用戶名必填'
                        }
                    ]
                })(<Input></Input>)}

                {getFieldDec('pwd',{
                    rules: [
                        {
                            required: true,
                            message: '密碼必填'
                        }
                    ]
                })(<Input type="password"></Input>)}

                <Button onClick={this.onSubmit}>登陸</Button>
            </div>
        )
    }
}


export default KForm
相關文章
相關標籤/搜索