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
}
图文解释
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
Hook
是React16.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
,componentDidUpdate
和componentcomponentWillMount
具有相同的作用,只不过被合并成了一个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
useContext
、useReducer
、useCallback
、useMemo
组件跨层级通信-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
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。