react基本全是组件,需要使用什么组件就安装什么组件。

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>
        )
    }
}

全局导入代码会比较大,不利于项目开发,建议使用按需加载的方式。

按需加载

1、安装react-app-rewired取代react-script,可以扩展webpack配置
npm install react-app-rewired@2.0.2-next.0 babel-plugin-import -D
2、根目录创建 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

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

export default class AntdTest extends Component {
    render(){
        return (
            <div>
                <Button type="primary">按钮</Button>
            </div>
        )
    }
}
5、因为修改了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(浅比较)

因为是浅比较,只会比较引用类型地址,只会比较一层。所以要么传值类型,要么引用类型地址不能发生变化并且地址只能有一层。大致源码解析如下:

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组件

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,也可能是函数,也可能是数组

1、对children作为函数解读

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>
    )
}

2、利用children是数组做滤器

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>
    )
}

3、修改children

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特性。

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种方式

1、第一种 消费者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>
    )
}

2、消费方法2、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'}}>
                {/* 消费方法2、Hook */}
                <Child2></Child2>
            </Provider>
        </div>
    )
}

3、消费方法3、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'}}>
                {/* 消费方法3、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 => {
            // 1、获取校验规则
            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

洁本佳人
86 声望3 粉丝

test


« 上一篇
React初探
下一篇 »
12道vue原理题