React入门

React 基础

    • 初步创建
      • React特点
      • Hello World
      • 创建虚拟DOM的两种方法
      • 关于虚拟DOM
      • JSX语法规则
      • ES6结构赋值
    • 组件
      • 函数式组件
      • 类组件
      • 组件实例的三大核心
        • 状态state
        • 组件传值props
        • refs获取节点
      • React事件处理
      • 收集表单数据
      • 高阶函数
      • 函数柯里化
    • 生命周期
      • 旧版本react生命周期钩子函数
      • 新版本react生命周期钩子函数
      • 重要的钩子函数
      • 即将废弃的钩子函数
    • Diffing算法和Key的作用
      • Diffing算法
      • key的作用
    • React脚手架
      • 创建项目
      • 项目结构
    • React 请求数据
      • axios
      • 跨域处理办法
      • fetch
    • 组件通信
      • 父子组件通信:Props
      • 任意组件通信:PubSubJS
    • React 路由
      • SPA 的理解
      • 路由的理解
      • react-router-dom
      • 路由组件与一般组件(V5)
      • NavLink二次封装
      • BrowserRouter下使用相对路径问题
      • 精准匹配exact
      • Redirect重定向
      • 嵌套路由
      • 路由传参(三种方式)
      • push和replace
      • 编程式路由导航
      • withRouter
      • BrowserRouter 和 HashRouter的区别
    • Redux状态管理
      • Redux三大核心
        • action
        • reducer
        • store
      • Redux应用
      • react-redux
      • 多个reducer处理
      • redux开发者工具
    • 拓展
      • setState更新状态的两种写法
      • Hooks
        • State Hook
        • Effect Hook
        • Ref Hook
      • Fragment
      • Context
      • 组件优化
      • render props
      • 错误边界
      • 组件通信方式总结

初步创建

React特点

  • 采用组件化模式、声明式编码,提高开发效率及组件复用率
  • 在React Native中可以使用React语法进行移动端开发
  • 使用虚拟DOM + 优秀的Diffing算法,经量减少与真实DOM的交互

Hello World

<body>
    <div id="root">div>
    
    <script src="./react.development.js">script>
    
    <script src="./react-dom.development.js">script>
    
    <script src="./babel.min.js">script>
    <script type="text/babel">
        // 创建虚拟DOM
        const VDOM = <div>Hello World</div>
        // 渲染虚拟DOM
        ReactDOM.render(VDOM, document.getElementById('root'));
    script>
body>

创建虚拟DOM的两种方法

  • jsx语法
<script type="text/babel">
    // 创建虚拟DOM
    const VDOM = <div>Hello World</div>
    // 渲染虚拟DOM
    ReactDOM.render(VDOM, document.getElementById('root'));
script>
  • js语法
<script type="text/javascript">
    // 创建虚拟DOM
    const VDOM = React.createElement('div', {id: 'title'}, 'Hello World')
    // 渲染虚拟DOM
    ReactDOM.render(VDOM, document.getElementById('root'));
script>

关于虚拟DOM

  • 本质是Object类型对象
  • 虚拟DOM比较“轻”,真实DOM比较“重”。虚拟DOM上的属性较真实DOM要少
  • 虚拟DOM会被React转化为真实DOM,在页面上渲染

JSX语法规则

  • 定义虚拟DOM时,不能用引号包括
  • 标签中混入js表达式时,要用{}
  • 样式的类名指定,不用class而用className
  • 内联样式写法:style={{key: value}}
  • 虚拟DOM只能有一个跟标签
  • 标签必须闭合
  • 标签首字母
    • 若小写字母开头,则将标签转为html同名元素。若html中无该对应同名标签,则报错
    • 若大写字母开头,react会将标签作为组件渲染

ES6结构赋值

const obj = {a: {b: 1}};
const {a} = obj; // 传统结构赋值
const {a: {b}} = obj; // 连续结构赋值
const {a: {b: newName}} = obj; // 连续结构赋值 + 重命名

组件

函数式组件

  • React解析组件标签,找到了MyComponent组件。
  • 发现组件是使用函数定义的,随后调用该函数,将返回的虚拟DOM转为真是DOM,并渲染到页面
<script type="text/babel">
    // 创建函数式组件
    function MyComponent() {
        console.log(this); // 因为bable编译开启了严格模式,此处的this是undefined
        return <h2>我是函数式组件</h2>
    }
    // 渲染组件到页面
    ReactDOM.render(<MyComponent />, document.getElementById('root'));
script>

类组件

  • React解析组件标签,找到了MyComponent组件。
  • 发现组件是使用类定义的,随后new出该类的实例对象,并通过该实例调用到原型上的render方法
  • 将render返回的虚拟DOM转为真实DOM,并渲染到页面
<script type="text/babel">
    // 创建类式组件
    class MyComponent extends React.Component {
        // render方法是放在MyComponent的原型对象上的,供实例使用
        render() {
            // render中的this是MyComponent的实例对象
            console.log(this);
            return <div>这是一个类组件</div>
        }
    }
    // 渲染组件到页面
    ReactDOM.render(<MyComponent />, document.getElementById('root'));
script>

组件实例的三大核心

状态state
  • state是组件对象的一个重要属性,他的值是对象。
  • 组件被称为“状态机”,通过更新组件的state来更新对应的页面显示
  • 注意:
    • 组件中render方法中的this为组件实例对象
    • 组件自定义方法中的this为undefined
      • 解决方法1:在构造函数中bind函数this指向
      • 解决方法2:赋值语句定义箭头函数
    • state数据不能直接修改,直接修改不会触发render方法更新页面。需要调用内置API修改:setState()
class Weather extends React.Component {

    state = {
        isHot: false
    }

    constructor(props) {
        super(props);
        this.demo = this.demo.bind(this); // 修改demo方法中的this指向
    }

    render() {
        const {isHot} = this.state;
        // 此处changeWeather方法只是赋值给了onClick,并未调用
        return <h1 onClick={this.changeWeather}>今天天气很{isHot ? '炎热' : '凉爽'}</h1>;
    }

    // 箭头函数没有自己的this, 它的this是继承而来; 默认指向在定义它时所处的对象(宿主对象)
    changeWeather = () => {
        // 内置API修改state
        this.setState({isHot: !this.state.isHot});
    }
    // 事件中调用自定义函数时,this为undefined。
        // 原因1:demo方法并非由Weather实例对象调用
        // 原因2:类中的方法,局部开启了严格模式,导致this为undefined
    demo() {
        console.log(this);
    }
}
组件传值props
  • props属性为只读
  • 类组件
    class Person extends React.Component {
        // 限定标签属性类型、是否必传
        static propTypes = {
            // PropTypes需要引入prop-types.js文件
            name: PropTypes.string.isRequired, // name必传,且为字符串
            age: PropTypes.number, // 限制为数值
            sex: PropTypes.string, // 限制为字符串
            speak: PropTypes.func // 限制为函数
        }
        // 指定标签属性默认值
        static defaultProps = {
            sex: 'man',
            age: 18
        }
    
        // react基本不用构造函数
        constructor(props) {
            super(props);
            // 是否接受props和是否给super传递props,取决于constructor中石否用到this.props。不传则取不到
            console.log(this.props);
        }
    
        render() {
            const {name, age, sex} = this.state;
            return (
                <ul>
                    <li>{name}</li>
                    <li>{age}</li>
                    <li>{sex}</li>
                </ul>
            );
        }
    }
    const data = {
        name: '张三',
        age: 18,
        sex: '男'
    }
    
    function speak() {
        console.log('speak');
    }
    // 渲染组件到页面
    ReactDOM.render(<Person name={data.name} age={data.age} sex={data.sex} speak={speak}/>, document.getElementById('root1'));
    // 此处的{...data}并非构造字面量对象时使用的展开语法。{}表示嵌入js表达式,jsx标签中可以直接使用...obj展开一个对象
    ReactDOM.render(<Person {...data}/>, document.getElementById('root2'));
    
  • 函数组件
    function Person(props) {
        const {name, age, sex} = props;
        return (
            <ul>
                <li>{name}</li>
                <li>{age}</li>
                <li>{sex}</li>
            </ul>
        );
    }
    
    // 限定标签属性类型、是否必传
    Person.propTypes = {
        name: PropTypes.string.isRequired, // name必传,且为字符串
        age: PropTypes.number, // 限制为数值
        sex: PropTypes.string, // 限制为字符串
        speak: PropTypes.func // 限制为函数
    }
    // 指定标签属性默认值
    Person.defaultProps = {
        sex: 'man',
        age: 18
    }
    const data = {
        name: '张三',
        age: 18,
        sex: '男'
    }
    
    function speak() {
        console.log('speak');
    }
    // 渲染组件到页面
    ReactDOM.render(<Person name={data.name} age={data.age} sex={data.sex} speak={speak}/>, document.getElementById('root1'));
    ReactDOM.render(<Person {...data}/>, document.getElementById('root2'));
    
refs获取节点
  • 字符串形式(较老版本中的写法,不推荐使用)
    class Demo extends React.Component {
        render() {
            return (
                <div>
                    <div>
                        <input ref="input" type="text"/>
                        <button onClick={this.getValue}>按钮</button>
                    </div>
                </div>
            );
        }
    
        getValue = () => {
            const { input } = this.refs;
            console.log(input.value);
        }
    }
    
  • 回调形式
    // 内联回调函数 和 绑定回调函数 实际使用中并没有什么影响,推荐使用内联回调
    class Demo extends React.Component {
        render() {
            return (
                <div>
                    <div>
                        { /* 内联回调函数:在state更新的时候,内联回调会被调用两次,第一次c为null,第二次c为节点 */ }
                        <input ref={element => this.input = element} type="text"/>
                        <button onClick={this.getValue}>按钮</button>
                    </div>
                    <div>
                        { /* 绑定回调函数:在state更新的时候,绑定的函数不会被再次调用 */ }
                        <input ref={this.getInput} type="text"/>
                        <button onClick={this.getValue}>按钮</button>
                    </div>
                </div>
            );
        }
    
        getInput = (element) => {
            this.input = element;
        }
    
        getValue = () => {
            const { input } = this;
            console.log(input.value);
        }
    }
    
  • createRef API形式 (React最推荐的形式)
    • React.createRef()调用后返回一个容器,该容器可以存储被ref标识的节点
    • 每一个容器只能存一个
    class Demo extends React.Component {
        myRef = React.createRef();
    
        render() {
            return (
                <div>
                    <div>
                        <input ref={this.myRef} type="text"/>
                        <button onClick={this.getValue}>按钮</button>
                    </div>
                </div>
            );
        }
    
        getValue = () => {
            console.log(this.myRef.current.value);
        }
    }
    

React事件处理

  • 通过onXxx属性指定事件处理函数(注意大小写)
    • React使用的是自定义事件,而不是使用原生的DOM事件。—为了更好的兼容性
    • React中的事件是通过事件委托方式处理的(委托给组件最外层的元素) —为了高效
  • 通过event.target得到发生事件的DOM元素对象 — 勿过度使用ref

收集表单数据

  • 非受控组件:现用现取(使用ref)
    // 在提交的时候才取input中的值
    class Demo extends React.Component {
        handleSubmit = (e) => {
            e.preventDefault();
            console.log(`用户名:${this.userName.value}---密码:${this.password.value}`)
        };
    
        render() { 
            return (
                <form onSubmit={this.handleSubmit}>
                    用户名:<input type="text" ref={c => this.userName = c} />
                    密码:<input type="password" ref={c => this.password = c} />
                    <button>提交</button>   
                </form>
            );
        }
    }
    
  • 受控组件:通过状态进行数据存储(避免使用ref)
    // 页面输入类的DOM,随着输入将数据维护到state中。用的时候从状态中取
    class Demo extends React.Component {
        state = {
            userName: '',
            password: ''
        }
    
        handleSubmit = (e) => {
            e.preventDefault();
            const {userName, password} = this.state;
            console.log(`用户名:${userName}---密码:${password}`)
        };
    
        saveUserName = (e) => {
            this.setState({ userName: e.target.value });
        };
    
        savePassword = (e) => {
            this.setState({ password: e.target.value });
        };
    
        render() { 
            return (
                <form onSubmit={this.handleSubmit}>
                    用户名:<input type="text" onChange={this.saveUserName}/>
                    密码:<input type="password" onChange={this.savePassword}/>
                    <button>提交</button>   
                </form>
            );
        }
    }
    

高阶函数

  • 满足一下两个条件之一,即是高阶函数
    • 函数A,接收的参数是一个函数,那么A是高阶函数:Promise,setTimeout,setInterval,map,forEach…
    • 函数A,调用后的返回值是一个函数,那么A是高阶函数:

函数柯里化

  • 通过函数的调用继续返回一个函数,实现多次接收参数最后统一处理的函数编码形式
    class Demo extends React.Component {
        state = {
            userName: '',
            password: ''
        }
        // 高阶函数(返回值是一个函数)
        saveFormData1 = (type) => {
            return event => {
                this.setState({[type]: event.target.value});
            }
        };
    
        saveFormData2(type, event) {
            this.setState({[type]: event.target.value});
        }
        render() { 
            return (
                <div>
                    { /*用函数柯里化实现*/ }
                    <form>
                        用户名:<input type="text" onChange={this.saveFormData1('userName')}/>
                        密码:<input type="password" onChange={this.saveFormData1('password')}/>
                        <button>提交</button>   
                    </form>
                    { /*不用函数柯里化实现*/ }
                    <form onSubmit={this.handleSubmit}>
                        用户名:<input type="text" onChange={event => this.saveFormData2('userName', event)}/>
                        密码:<input type="password" onChange={event => this.saveFormData2('password', event)}/>
                        <button>提交</button>   
                    </form>
                </div>
                
            );
        }
    }
    

生命周期

旧版本react生命周期钩子函数

  • 初始化阶段
    • constructor() {}:构造函数
    • componentWillMount() {}:组件将要挂载
    • render() {}:组件挂载 (必用)
    • componentDidMount() {}:组件挂载完毕 (常用)
  • 更新阶段:组件内部this.setState 或 父组件render触发
    • componentWillReceiveProps() {}:父组件render更新。子组件将要接受新的props时触发(第一次接受props不触发)
    • shouldComponentUpdate() {}:setState()调用后,控制是否更新组件的阀门。必须要有一个返回值,返回值类型为布尔值
    • componentWillUpdate() {}:组件将要更新
    • render() {}:组件更新
    • componentDidUpdate() {}:组件更新完毕
  • 卸载组件:ReactDOM.unmountComponentAtNode(节点)
    • componentWillUnmount() {}:组件将要卸载 (常用)
      React入门_第1张图片

新版本react生命周期钩子函数

  • 区别:
    • 废弃(或即将废弃)三个钩子函数:componentWillMount、componentWillUpdate、componentWillReceiveProps。需要使用时,需要加前缀UNSAFE_
    • 新增两个钩子函数(并不常用):getDerivedStateFormProps、getSnapsshotBeforeUpdate
  • getDerivedStateFormProps:状态更新时,在render之前调用
    // 此方法调用,state的值在任何时候都取决于props。state将无法进行修改
    // props为组件传入的参数,state为组件定义的state
    static getDerivedStateFromProps(props, state) {
        console.log('new-----getDerivedStateFormProps');
        return props;
    }
    
    ReactDOM.render(<NewReact count={123}/>, document.getElementById('root'))
    
  • getSnapsshotBeforeUpdate:在最近一次渲染输出(提交到DOM节点之前)调用,它使得组件能在发生更改之前从DOM中捕获一些信息。此声明周期的任何返回值将最为参数传递给componentDidUpdate
    getSnapshotBeforeUpdate(prevProps, prevState) {
        console.log(prevProps, prevState)
        return '123';
    }
    
    componentDidUpdate(prevProps, prevState, snapshot) {
        console.log(prevProps, prevState, snapshot); // snapshot = '123'
    }
    

React入门_第2张图片

重要的钩子函数

  • render
  • componentDidMount
  • componentWillUnmount

即将废弃的钩子函数

  • componentWillMount
  • componentWillUpdate
  • componentWillReceiveProps

Diffing算法和Key的作用

Diffing算法

  • React在将虚拟DOM转成真实DOM的时候进行一个比较,已经渲染过一次的节点,再次渲染的时候,会对相同的key值的节点进行比较,如果内容相同,则复用原来的DOM

key的作用

  • key是虚拟DOM对象的一个标识

  • 当状态中的数据发生变化,React会根据新数据生成新的虚拟DOM。随后React将进行新的虚拟DOM与旧的虚拟DOM的Diffing算法比较。规则如下

    • 旧虚拟DOM中找到了与新虚拟DOM相同的key:
      1. 若虚拟DOM中内容没变,直接使用之前的真实DOM;
      2. 若虚拟DOM中内容变了,则生成新的真实DOM,随后替换掉页面中之前的真实DOM
    • 旧虚拟DOM中未找到与新虚拟DOM相同的key:根据数据创建新的真实DOM,随后渲染到到页面
  • index作为key可能引发的问题:

    • 若对数据进行:逆序添加、逆序删除等破坏顺序操作: 会产生没有必要的真实DOM更新 ==> 界面效果没问题, 但效率低。
    • 如果结构中还包含输入类的DOM:会产生错误DOM更新 ==> 界面有问题。
    • 注意!如果不存在对数据的逆序添加、逆序删除等破坏顺序操作,仅用于渲染列表用于展示,使用index作为key是没有问题的。
  • 开发中如何选择key?:

    • 最好使用每条数据的唯一标识作为key, 比如id
    • 如果确定只是简单的展示数据,用index也是可以的。

React脚手架

  • 用于快速创建React库的模板项目
  • 项目整体技术架构:react + webpack + es6 + eslint
  • 使用脚手架开发项目特点:模块化,组件化,工程化

创建项目

  1. 全局安装脚手架:npm install -g create-react-app
  2. 创建项目:create-react-app my-project
  3. 启动项目:npm start

项目结构

  • public ---- 静态资源文件夹
    • favicon.icon ------ 网站页签图标
    • index.html -------- 主页面
      <head>
          <meta charset="utf-8" />
          
          <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
          
          <meta name="viewport" content="width=device-width, initial-scale=1" />
          
          <meta name="theme-color" content="red" />
          
          <meta name="description" content="Web site created using create-react-app" />
          
          <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
          
          <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
          <title>React Apptitle>
      head>
      
      <body>
          
          <noscript>You need to enable JavaScript to run this app.noscript>
          <div id="root">div>
      body>
      
    • manifest.json ----- 应用加壳的配置文件
    • robots.txt -------- 爬虫协议文件
  • src ---- 源码文件夹
    • App.css -------- App组件的样式
    • App.js --------- App组件
    • App.test.js ---- 用于给App做测试
    • index.css ------ 样式
    • index.js ------- 入口文件
      ReactDOM.render(
          // 用于检测内部包裹的所有子组件中,react使用是否合理(例如字符串形式的ref)
          <React.StrictMode>
              <App />
          </React.StrictMode>,
          document.getElementById('root')
      );
      // 用于记录页面性能,需要在reportWebVitals.js文件中进行相应配置
      reportWebVitals();
      
    • logo.svg ------- logo图
    • reportWebVitals.js — 页面性能分析文件(需要web-vitals库的支持)
    • setupTests.js ---- 组件单元测试的文件(需要jest-dom库的支持)

React 请求数据

  • react本身只关注界面,并不包含发送ajax请求的代码
  • react中需要继承第三方ajax库(或自己封装)

axios

  • 安装axios:yarn add axios
  • 使用
    import axios from 'axios';
    
    axios.get('http://localhost:5000/students').then(
        res => {},
        err => {}
    )
    

跨域处理办法

  1. package.json配置代理
    缺点:只能指定一个代理
    // package.json
    {
        "proxy": "http://localhost:5000"
    }
    // 使用注意
    // 浏览器不再直接给5000端口发送请求,而是像3000端口请求
    // 3000服务器收到地址,先在当前端口找是否有匹配的地址,如果没有再向5000端口进行请求
    axios.get('http://localhost:3000/students').then();
    
  2. 配置跨域文件:setupProxy.js
    // 1.在src目录下新增setupProxy.js文件
    // 2.配置setupProxy.js文件
    const proxy = require('http-proxy-middleware');
    module.exports = function (app) {
        app.use(
            proxy('/api', { // 匹配到'/api'前缀的请求,就会触发代理配置
                target: 'http://localhost:5000', // 跨域目标服务器
                // changeOrigin 控制服务器收到的请求头中HOST的值
                // true => 与服务区地址一致:http://localhost:5000
                // false => 真实的当前地址:http://localhost:3000
                changeOrigin: true,
                pathRewrite: {'^/api': ''} // 重写请求路径,将'/api'替换为''
            }),
            proxy('/master', { // 可以传多个参数
                target: 'http://localhost:5001',
                changeOrigin: true,
                pathRewrite: {'^/master': ''}
            })
        )
    }
    

fetch

  • window内置方法,不同于XMLHttpRequest的ajax请求
  • 采用了关注分离的方式:连接服务器正常与否,与获取数据分离
// 关注分离
fetch(`https://api.github.com/search/users?q=${this.state.keyword}`)
    .then( // 联系服务器成功与否
        response => {
            console.log('联系服务器成功', response);
            return response.json();
        }
    )
    .then(data => console.log(data)) // 获取数据
    .catch(err => console.log(err))
// 代码优化
getData = async () => {
	try {
	     const response = await fetch(`https://api.github.com/search/users?q=${this.state.keyword}`);
	     const data = await response.json();
	     console.log(data);
	 } catch (error) {
	     console.log(error);
	 }
}

组件通信

父子组件通信:Props

  • 父传子:父组件给子组件标签添加属性传值,子组件通过props接收
  • 子传父:父组件给子组件传递一个函数,子组件调用父组件函数,同时想函数内进行传参
// 父组件
export default class App extends Component {
    state = {
        list: [],
        isFirst: true,
        isLoading: false,
        err: ''
    }

    render() {
        return (
            <div className="container">
                { /* 传递函数 */ }
                <Search updateState={this.updateState}/>
                { /* 批量传参 */ }
                <List {...this.state}/>
            </div>
        )
    }

    updateState = (state) => {
        this.setState(state)
    }
}
// Search子组件
export default class Search extends Component {
    getData = () => {
        const {updateState} = this.props;
        updateState({isFirst: false, isLoading: true, err: ''})
        axios.get(`https://api.github.com/search/users?q=${this.state.keyword}`).then(
            res => {
                updateState({isLoading: false, list: res.data.items});
            },
            err => {
                updateState({isLoading: false, err: err.message})
            }
        )
    }
}
// List 子组件
export default class List extends Component {
    render() {
        const { list, isFirst, isLoading, err } = this.props;
        return (
            <div className="row">
                {
                    isFirst ? <h2>第一次进入</h2> :
                    isLoading ? <h2>Loading......</h2> :
                    err ? <h2>err</h2> :
                    list.map(item => {
                        return (
                            <div className="card" key={item.id}>
                                <a href={item.html_url} target="_blank" rel="noreferrer">
                                    <img alt="header_pic" src={item.avatar_url} style={{ width: '100px' }} />
                                </a>
                                <p className="card-text">{item.login}</p>
                            </div>
                        )
                    })
                }
            </div>
        )
    }
}

任意组件通信:PubSubJS

  • 消息订阅与发布模式
    // 订阅消息
    import PubSub from 'pubsub-js';
    export default class List extends Component {
        state = {
            list: [], 
            isFirst: true,
            isLoading: false,
            err: ''
        }
    
        componentDidMount() {
            // 订阅
            this.token = PubSub.subscribe('updateList', (msg, data) => this.setState(data))
        }
        componentWillUnmount() {
        	// 取消订阅
        	PubSub.unsubscribe(this.token);
        }
    }
    
    // 发布消息
    export default class Search extends Component {
        getData = () => {
            // 发布
            PubSub.publish('updateList', {isFirst: false, isLoading: true, err: ''});
            axios.get(`https://api.github.com/search/users?q=${this.state.keyword}`).then(
                res => {
                    PubSub.publish('updateList', {isLoading: false, list: res.data.items});
                },
                err => {
                    PubSub.publish('updateList', {isLoading: false, err: err.message});
                }
            )
        }
    }
    

React 路由

SPA 的理解

  • 单页面Web应用(single page web application, SPA)
  • 整个应用只有一个完整的页面
  • 点击页面中的跳转连接不会刷新页面,只会做页面的局部更新
  • 数据都需要通过ajax请求获取,并在前端异步展现

路由的理解

  • 路由是一个映射关系(key: value)
  • key为路径,value是function或component

react-router-dom

  • 基本使用:
    1. 导航区的标签:
      <Link to="/home">HomeLink>
      
      <NavLink activeClassName="active" to="/about">NavLink>
      
    2. 展示区写Route标签进行路径的匹配:
      
      <Route path="/about" component={ About }/>
      
      <Routes>
          <Route path="/home" element={  /> }>Route>
      Routes>
      
    3. 所有的路由组件都需要包裹在或者标签内

路由组件与一般组件(V5)

  • 写法不同
    • 一般组件:
    • 路由组件:
  • 存放位置不同:
    • 一般组件:components
    • 路由组件:pages
  • 接收到的props不同:
    • 一般组件:写组件标签时,传递什么就能收到什么
    • 路由组件:收到三个固定属性
      1. history:
        go: ƒ go(n)
        goBack: ƒ goBack()
        goForward: ƒ goForward()
        listen: ƒ listen(listener)
        push: ƒ push(path, state)
        replace: ƒ replace(path, state)
      2. location:
        pathname: “/home”
        search: “”
        state: undefined
      3. match:
        params: {}
        path: “/home”
        url: “/home”

NavLink二次封装

props接收标签传递的属性参数,同时接收标签体内容在props.children中。
使用children时可以直接赋值给标签属性children,即展示标签体内容

// 封装
export default class MyNavLink extends Component {
    render() {
        return (
            <NavLink activeClassName="active-class" className="list-group-item" {...this.props} />
            // 等同于
            <NavLink activeClassName="active-class" className="list-group-item" to={this.props.to} children={this.props.children} />
            // 等同于
            <NavLink activeClassName="active-class" className="list-group-item" to={this.props.to}>{this.props.children}</NavLink>
        )
    }
}
// 使用
<MyNavLink to="/about">About</MyNavLink>

BrowserRouter下使用相对路径问题

BrowserRouter下使用多层级路由,会导致使用相对路径的文件丢失(找不到文件,默认返回index.html)。


<link rel="stylesheet" href="./css/bootstrap.css">


<Route path="/api/home" component={ Home }/>

解决方法:

  1. 相对路径改为绝对路径
    <link rel="stylesheet" href="/css/bootstrap.css">
    
  2. 使用%PUBLIC_URL%作为根路径
    <link rel="stylesheet" href="%PUBLIC_URL%/css/bootstrap.css">
    
  3. 使用HashRouter
    ReactDOM.render(<HashRouter><App /></HashRouter>, document.getElementById('root'));
    

精准匹配exact

  • 模糊匹配:起始路径包含Route需要的路径,即可匹配。url可以多写
    
    <MyNavLink to="/home/a/b">homeMyNavLink>
    
    <MyNavLink to="/a/home/b">homeMyNavLink>
    
    <Switch>
        <Route path="/home" component={ Home }/>
    Switch>
    
  • 精准匹配:路径需要完全匹配才能使用
    <MyNavLink to="/home/a/b">homeMyNavLink>
    <Switch>
        <Route exact path="/home" component={ Home }/>
    Switch>
    
  • 精准匹配不要随意开启,需要的时候再开。有时候开启精准匹配会导致无法匹配二级路由

Redirect重定向

  • 一般写在所有路由的最下方,当所有路由无法匹配时,跳转到Redirect指定的路由
  • 属性:
    1. to:需要重定向到哪个路径
    2. from:当匹配到from中的路径时,重定向到to后的路径。不写from默认为"/"
    3. exact:开启精准匹配
    4. push:将新路径push进history中,而不是替换
    <Switch>
        <Route path="/about" component={ About }/>
        <Route exact path="/home" component={ Home }/>
        <Redirect push from="/test" to="/home">Redirect>
        <Redirect exact to="/about">Redirect>
    Switch>
    

嵌套路由

  • "/home/news"路径匹配过程:
    1. /home/news 分为 home 和 news。在app.js中进行路由匹配。匹配到Home组件,进行渲染
    2. Home组件加载,home中注册了新的路由。路径/home/news继续进行匹配
    3. 匹配到/home/news路径,加载News组件
  • 注册子路由时,要写上父路由的完整路径
  • 路由的匹配时按照注册路由的顺序进行的
    
    
    <Switch>
        <Route path="/about" component={ About }/>
        <Route path="/home" component={ Home }/>
        <Redirect to="/about">Redirect>
    Switch>
    
    
    
    <Switch>
        <Route path="/home/news" component={ News } />
        <Route path="/home/message" component={ Message } />
        <Redirect to="/home/news">Redirect>
    Switch>
    

路由传参(三种方式)

  1. params参数
    • 路由链接(携带参数):链接
    • 注册路由(声明接收):
    • 接收参数:const {id, title} = this.props.match.params;
  2. search传参
    • 路由链接(携带参数):连接
    • 注册路由(声明接收):
    • 接收参数:
      import qs from 'querystring'; // 转换查询字符串的库,react自带。qs.parse <=> qs.stringify
      const search = this.props.location.search;  
      const {id, title} = qs.parse(search.slice(1));
      
    • 备注:获取到的search是urleccoded编码字符串,需要借助querystring解析
  3. state传参
    • 路由链接(携带参数):连接
    • 注册路由(声明接收):
    • 接收参数:const {id, title} = this.props.location.state;
    • 备注:刷新也能保留住参数,原理是state在history中有存储

push和replace

  • 默认为push.是一个压栈操作,显示的是栈顶的路由
    <Route push path="/home/message/detail" component={ Detail } />
    
  • replace是替换当前路由,history中不再保留当前路由
    <Route replace path="/home/message/detail" component={ Detail } />
    

编程式路由导航

  • history中的方法:
    1. go(num):参数为num,正数表示前进几步,负数表示后退几步
    2. goForward():前进一步
    3. goBack():后退一步
    4. replace(url, state):替换路由
    5. push(url, state):压栈路由
      // 携带params参数
      this.props.history.push(`/home/message/detail/${id}/${title}`);
      // 携带search参数
      this.props.history.push(`/home/message/detail?id=${id}&title=${title}`);
      // 携带state参数
      this.props.history.push(`/home/message/detail`, {id, title});
      

withRouter

  • 可以让非路由组件的props拥有history/location/match三个对象
  • 通过history实现一般组件的编程式路由导航
import {withRouter} from 'react-router-dom';
class Header extends Component {
    forward = () => {
        this.props.history.goForward();
    }    
    render() {
        return (
            <div>
                <h2>React Router Demo</h2>
                <button onClick={this.forward}>forward</button>
            </div>
        )
    }
}
export default withRouter(Header);

BrowserRouter 和 HashRouter的区别

  1. 底层原理不一样
    • BrowserRouter使用的是H5的history api,不兼容IE9一下版本浏览器
    • HashRouter使用的是URL的哈希值
  2. url表现不一样
    • BrowserRouter的路径中没有#:localhost:3000/home/news
    • HashRouter中包含#:localhost:3000/#/home/news
  3. 刷新后对state参数的影响
    • BrowserRouter没有任何影响,因为state存储在history中
    • HashRouter刷新后会导致state数据丢失
  4. 备注:HashRouter可以解决一些路径错误相关的问题(即上方提及的,引入文件的相对路径问题)

Redux状态管理

  • 什么是redux:redux是专门用于做状态管理的JS库。可以集中式管理react应用中多个组件共享的状态
  • 适用场景:
    1. 某个组件的状态,需要与其它组件共享
    2. 一个组件需要改变另一个组件的状态
    3. 总体原则:能不用就不用,不用的情况下开发吃力才考虑使用
      React入门_第3张图片

Redux三大核心

action
  • 动作的对象
  • 包含2个属性~
    • type:标识属性, 值为字符串, 唯一, 必要属性
    • data:数据属性, 值类型任意, 可选属性
reducer
  • 用于初始化状态、加工状态。
  • 加工时,根据旧的state和action, 产生新的state的纯函数。
store
  • 将state、action、reducer联系在一起的对象
  • 如何得到此对象?
    1. import {createStore} from ‘redux’
    2. import reducer from ‘./reducers’
    3. const store = createStore(reducer)
  • 此对象的功能?
    1. getState(): 得到state
    2. dispatch(action): 分发action, 触发reducer调用, 产生新的state
    3. subscribe(listener): 注册监听, 当产生了新的state时, 自动调用

Redux应用

  1. src下建立:

    • redux文件夹
    • store.js
    • count_reducer.js
    • count_action.js
  2. store.js:

    • 引入redux中的createStore函数,创建一个store
    • createStore调用时要传入一个为其服务的reducer
    • 第二个参数为引入中间件,来自第三方库,用于支持异步action
    • 记得暴露store对象
    import { createStore, applyMiddleware } from 'redux';
    import { countReducer } from './counter_reducer';
    import thunk from 'redux-thunk';
    
    export default createStore(countReducer, applyMiddleware(thunk))
    
  3. count_reducer.js:

    • reducer的本质是一个函数,接收:preState,action,返回加工后的状态
    • reducer有两个作用:初始化状态,加工状态
    • reducer被第一次调用时,是store自动触发的,
      • 传递的preState是undefined,
      • 传递的action是:{type:’@@REDUX/INIT_a.2.b.4}
    const countInit = 0;
    export default function countReducer(preState = countInit, action) {
        const { type, data } = action;
        switch(type) {
            case 'increment':
                return preState + data;
            case 'decrement':
                return preState - data;
            default:
                return preState;
        }
    }
    
  4. count_action.js

    • 用于提供创建action对象的方法
    • 异步action需要创建一个方法作为参数传给dispatch.同时store需要通过applyMiddleware方法引入thunk
    // 同步action
    export function createIncrementAction(data) {
        return {type: 'increment', data};
    }
    // 异步action
    export function createIncrementAsyncAction(data, time) {
        // store.dispatch调用此方法时,会传入dispatch方法
        return (dispatch) => {
            setTimeout(() => {
                dispatch(createIncrementAction(data))
            }, time);
        }
    }
    
  5. 在index.js中监测store中状态的改变,一旦发生改变重新渲染

    • 备注:redux只负责管理状态,至于状态的改变驱动着页面的展示,要靠我们自己写。
    import store from './store';
    store.subscribe(ReactDOM.render(<App />, document.getElementById('root')));
    

react-redux

  • react-redux是react官方提供的库
  • 它将所有组件分成两大类:
    • 容器组件:
      1. 负责管理数据和业务逻辑,不负责UI的呈现
      2. 可以使用 Redux 的 API
      3. 一般保存在containers文件夹下
    // 容器组件完整写法
    import Count from '../../components/Count'; // UI组件
    import { connect } from 'react-redux'; // 连接UI组件与redux,并返回一个容器组件
    import { createDecrementAction, createIncrementAction, createIncrementAsyncAction } from '../../redux/count/count_action'
    
    // 给UI组件传递props属性
    const mapStateToProps = (state) => {
        return {count: state};
    }
    
    // 给UI组件传递方法
    const mapDispatchToProps = (dispatch) => {
        return {
            increment: data => dispatch(createIncrementAction(data)),
            decrement: data => dispatch(createDecrementAction(data)),
            asyncIncrement: data => dispatch(createIncrementAsyncAction(data))
        }
    }
    // 连接UI组件与redux,并返回一个容器组件
    export default connect(mapStateToProps, mapDispatchToProps)(Count);
    
    // 容器组件简写
    export default connect(
        state => ({count: state}),
        {
            increment: createIncrementAction,
            decrement: createDecrementAction,
            asyncIncrement: createIncrementAsyncAction
        }
    )(Count);
    
    • UI组件:
    1. 只负责 UI 的呈现,不带有任何业务逻辑
    2. 通过props接收数据(一般数据和函数)
    3. 不使用任何 Redux 的 API
    4. 一般保存在components文件夹下
  • 使用react-redux后,无需再检测状态更新
  • Provider给所有容器组件提供store
import ReactDOM from 'react-dom'
import App from './App';
import store from './redux/store';
import { Provider } from 'react-redux';

ReactDOM.render(
    <Provider store={ store }>
        <App />
    </Provider>
    , document.getElementById('root'));
  • 容器组件和UI组件可以合并到一个文件中

多个reducer处理

  • store.js文件合并多个reducer
    import { createStore, combineReducers } from 'redux';
    import countReducer from './reducers/count';
    import personReducer from './reducers/person';
    const reducers = combineReducers({
        count: countReducer,
        persons: personReducer
    });
    export default createStore(reducers);
    
  • react-redux容器组件引用
    export default connect(
        state => ({count: state.count, persons: state.persons}),
        {increment: createIncrementAction}
    )(Count) 
    

redux开发者工具

  • chrome商店:Redux DevTools
  • 项目安装库:yarn add redux-devtools-extension
  • store.js引入库
    import { createStore, combineReducers, applyMiddleware } from 'redux';
    import { composeWithDevTools } from 'redux-devtools-extension';
    import countReducer from './reducers/count'
    import personReducer from './reducers/person';
    import thunk from 'redux-thunk';
    const allReducer = combineReducers({
        count: countReducer,
        persons: personReducer
    })
    export default createStore(allReducer, composeWithDevTools(applyMiddleware(thunk)))
    

拓展

setState更新状态的两种写法

  • 对象式setState:setState(stateChange, [callback])
    1. stateChange为状态改变对象
    2. callback为状态更新完毕的回调函数,setState操作是异步的,该回调会在render调用后才会被调用
  • 函数式setState:setState(updater, [callback])
    1. updater为返回stateChange对象的函数
    2. updater可以接收到state和props
    3. callback同上
  • 注意:
    1. setState()方法有时候是同步的,有时候是异步的
      • 异步:由React控制的事件处理程序(比如onClick/onChange等),以及生命周期函数调用setState不会同步更新state。
      • 同步:React控制之外的事件中调用setState是同步更新的。比如原生js绑定的事件,setTimeout/setInterval等。
      • 原因:假如setState为同步执行,则每次调用都会完整的执行complementShouldUpdate/componentWillUpdate/render/componentDidUpdate这些生命周期函数,虽然有diff算法,但也会一定程度影响性能。通过异步执行,等待多个setState都执行完毕,将最终的state状态进行渲染,则各个生命周期函数都只要执行一次。
      • 原理:在 React 的 setState 函数实现中,会根据一个变量 isBatchingUpdates 判断是直接更新 this.state 还是放到队列中延时更新,而 isBatchingUpdates 默认是 false,表示 setState 会同步更新 this.state;但是,有一个函数 batchedUpdates,该函数会把 isBatchingUpdates 修改为 true,而当 React 在调用事件处理函数之前就会先调用这个 batchedUpdates将isBatchingUpdates修改为true,这样由 React 控制的事件处理过程 setState 不会同步更新 this.state。
    2. 多个setState调用会合并处理。下面程序调用了两次setState,但render只会调用一次
      render() {
          console.log('render')
      }
      hanldeClick() {
          this.setState({ name: 'jack' })
          this.setState({ age: 12 })
      }
      
    3. 对象式setState与函数式setState区别
      hanldeClick() {
          // 连续调用三次对象式setState,结果是count只+1。
          // 因为setState异步调用,每次调用完,this.state.count状态并未更新,三次调用取到的count值是一样的
          this.setState({ count: this.state.count + 1 })
          this.setState({ count: this.state.count + 1 })
          this.setState({ count: this.state.count + 1 })
          // 连续调用三次函数式setState,结果是count+3
          // 多次调用函数式setState的情况,React会保证调用每次函数,state都已经合并了之前的状态修改结果。
          this.setState(state => ({count: state.count + 1}));
          this.setState(state => ({count: state.count + 1}));
          this.setState(state => ({count: state.count + 1}));
      }
      
    4. 在同一个事件处理程序中不要混用对象式setState和函数式setState

Hooks

  • 支持在函数组建中使用state和其它一些特性
  • 三个常用的Hook
State Hook

React.useState()

    // state更新,Hooks函数会重新调用。React在第一次调用useState后,会对创建的state进行缓存,后续执行不会覆盖原有结果
    export default function Hooks () {
        // 数组结构赋值,第一个为定义的state属性,第二个为修改属性的方法
        const [count, setCount] = React.useState(0);
        function add() {
            // 同setState方法
            setCount(count + 1);
            setCount(count => ++count);
        }
        return (
            <div>
                <div>Count: { count }</div>
                <button onClick={add}>+1</button>
            </div>
        )
    }
Effect Hook

React.useEffect()

    export default function Demo () {
        const [count, setCount] = React.useState(0);
        // 参数一(回调函数):初始化会执行一次,后续更新state是否调用,取决于参数二
        // 参数二(数组):不传第二参数时,相当于检测所有state,任何state更新,参数一回调都会调用。传空数组,相当于不检测任何state。数组中指定检测count,则在初始化加载和count更新时,调用参数一的回调函数。
        React.useEffect(() => {
            const timer = setInterval(() => {
                setCount(count => ++count);
            }, 1000);
            // 参数一的回调函数返回值,相当于componentWillUnmount钩子函数
            return () => {
                clearInterval(timer);
            }
        }, [count])
        function add() {
            setCount(count => ++count);
        }
        function unmount() {
            ReactDOM.unmountComponentAtNode(document.getElementById('root'));
        }
        return (
            <div>
                <div>Count: { count }</div>
                <button onClick={add}>+1</button>
                <button onClick={unmount}>卸载</button>
            </div>
        )
    }
Ref Hook

React.useRef()

   export default function Demo () {

   const input = React.useRef();
   
   function getRef() {
       console.log(input.current.value);
   }

   return (
       <div>
           <input type="text" ref={input} />
           <button onClick={getRef}>getRef</button>
       </div>
   )
}

Fragment

  • 在react渲染时会被忽略的一个标签。相当于Angular中的标签
  • 也可以写<>空标签,但空标签无法带key值
import React, { Component, Fragment } from 'react'
export default class Demo extends Component {
    render() {
        return (
            <Fragment key={1}>
                <div>demo</div>
            </Fragment>
        )
    }
}

Context

  • 一种组件通讯方式,常用语祖组件与后代组件之间
import React, { Component } from 'react';
// 创建Context容器对象。祖组件与后代组件公用
const MyContext = React.createContext();
// 提取Provider,方便祖组件包裹后代组件
const {Provider} = MyContext;

export default class A extends Component {
    state = {name: '张三'}
    render() {
        return (
            <div>
                <h3>A组件</h3>
                <h4>state: { this.state.name }</h4>
                <hr />
                { /*
                    也可以用
                    value中的传值,后代组件都可以使用。value可以传值,也可以传对象
                */ }
                <Provider value={ this.state.name }>
                    <B />
                </Provider>
            </div>
        )
    }
}
// 子组件传值一般不用Context,因为provider更便捷
// 取值方式一:
class B extends Component {
    // 使用context中的数据之前,需要进行申明
    // 声明后,this中的context才会有值
    static contextType = MyContext;
    render() {
        return (
            <div>
                <h3>B组件</h3>
                <h4>state:{this.context}</h4>
                <hr />
                <C />
            </div>
        )
    }
}
// 取值方式二:
// 函数组件:只能通过方式二取值
class C extends Component {
    render() {
        return (
            <div>
                <h3>C组件</h3>
                <MyContext.Consumer>
                    { value => (<h4>state:{value}</h4>) }
                </MyContext.Consumer>
                <hr />
            </div>
        )
    }
}

组件优化

  • component 存在两个问题:
    1. 只要执行setState(),即使不改变状态数据,组件也会重新render()
    2. 只要当前组件重新render(),就会自动重新render子组件
  • 解决思路:只有当组件state或者props数据发生变化,才重新render
  • 解决方法:
    1. 手动编写shouldComponentUpdate钩子函数
          shouldComponentUpdate(nextProps, nextState) {
              return nextState.name !== this.state.name
          }
          shouldComponentUpdate(nextProps, nextState) {
              return nextProps.name !== this.props.name;
          }
      }
      
    2. 将组件继承的Compoent改为PureComponent
          import React, { PureComponent } from 'react'
          export default class Parent extends PureComponent {}
      

render props

  • 向组件内动态传入组件
    1. 组件标签嵌套使用
          export default class A extends Component {
              render() {
                  return (
                      <div>
                          <h3>A组件</h3>
                          <B><C /></B>
                      </div>
                  )
              }
          }
          class B extends Component {
              render() {
                  return (
                      <div>
                          <h3>B组件</h3>
                          {this.props.children}
                      </div>
                  )
              }
          }
      
    2. 通过props传参:可以给嵌入的组件进行传参
          export default class A extends Component {
              render() {
                  return (
                      <div>
                          <h3>A组件</h3>
                          <B render={name => <C name={name}/>}/>
                      </div>
                  )
              }
          }
      
          class B extends Component {
              render() {
                  return (
                      <div>
                          <h3>B组件</h3>
                          {this.props.render('张三')}
                      </div>
                  )
              }
          }
      

错误边界

  • 用于捕获后代组件错误,渲染出备用页面。将错误控制在一定范围内
  • 只能捕获后代组件生命周期产生的错误,不能捕获自己组件产生的错误和其它组件在合成事件、定时器中产生的错误
  • 错误边界的控制,只会在生产环境生效
  • 使用方式
    state = {
        hasError: ''
    }
    
    // 生命周期函数,一旦后代组件包错,就会出发
    static getDerivedStateFromError(error) {
        return {hasError: error}
    }
    
    // 统计页面的错误,发送请求的后台
    componentDidCatch(error, info) {
        console.log(error, info)
    }
    
    render() {
        return (
            <div>{this.state.hasError ? '页面出错' : <Child />}</div>
        )
    }
    

组件通信方式总结

  • 组件关系:
    1. 父子组件
    2. 兄弟组件
    3. 跨级组件
  • 几种通信方式:
    1. props:
      - children props
      - render props
    2. 消息订阅发布
      - pubsub、event
    3. 状态集中式管理
      - redux、dva等
    4. Context
      - 生产者-消费者模式
  • 搭配方式:
    1. 父子组件:props
    2. 兄弟组件:消息订阅-发布、集中式管理
    3. 跨级组件:消息订阅-发布、集中式管理、Context(开发用的少,封装插件用的多)

你可能感兴趣的:(react,jsx,redux,react.js,javascript,前端)