阅读 7917
React快速暴力入门

React快速暴力入门

React快速暴力入门

React 作为如今三大框架之一,在进行了短期的学习后,整理了一些笔记,做一下分享:,如果有错误,欢迎指出。

一、React简介

1. 什么是React:

React 是一款由 Facebook开发的用于构造用户界面的Javascript库。
ReactVue相比,vue的语法简单易上手,适用于小而精的项目,但是 React在组件上的复用性与设计性上会胜于vue一筹,适用于大型的项目。
React 将页面以一个个组件的方式进行拆分与组装,重复使用提高效率(可见下图) React 对数据的处理与管理比原生更加的清晰。

2. React的特点

  1. 声明式设计 −React的每个组件都是通过声明创建,使得页面逻辑更加清晰

582_1.png 2. 虚拟DOM −React每次渲染页面时会创建一个虚拟DOM,与现有的DOM进行对比,有差异的才进行替换重新渲染,提高了效率。 584_1.png 3. JSX − JSXJavaScript 语法的扩展。React 开发不一定使用 JSX ,但我们建议使用它。

// 在 javascript中创建元素
const DOM = document.createElement("h1"); // 真实DOM
DOM.innerText = "这是h1标签";

// 在 jsx中创建元素
const VDOM = <h1>这是h1标签</h1> // 虚拟DOM
复制代码
  1. 组件 − 通过 React 构建组件,使得代码更加容易得到复用,能够很好的应用在大项目的开发中。
// 不必现在就看懂,仅需要知道每个组件都是需要进行声明后才能使用
import React, {PureCompoent} from "react";
export default class Header extends PureCompoent{
    render(){
        return <header>这是头部组件</header>
    }
}
// ------------------------------------------------
// 在需要使用 header组件时调用即可重复使用
import Header from "./Header";
<Header></Header>
复制代码
  1. 单向响应的数据流 − React 实现了单向响应的数据流,从而减少了重复代码,这也是它为什么比传统数据绑定更简单。(这个在后面会了解到)

3. 安装与使用

  1. 使用cdn
<script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
复制代码
  1. 通过下载导入 React包
<script src="../js/react.development.js"></script>
<script src="../js/react-dom.development.js"></script>
<script src="../js/babel.min.js"></script>
复制代码
  1. 使用 react的脚手架create-react-app 创建项目文件会自带react
npm i create-react-app -g
create-react-app 项目名称
复制代码

注意:使用 react必须使用 babel.jsjsx语法转为 js语法 ReactDOM 独立于 React(为了防止React太大太臃肿而拆开)

4. 虚拟DOM

(1) 关于虚拟DOM

虚拟DOM的本质就是一个对象,真实DOM也是个对象,但虚拟DOM的属性更少,更加轻量

// 虚拟DOM  
const VDOM = <h1>Hello World</h1>// 此处不需要引号,因为不是字符串
// 真实DOM
const TDOM = document.querySelector("#app");

console.log("虚拟DOM: ", VDOM);   // Object
console.log("真实DOM: ", TDOM);   // <div id="app"></div>
console.log("虚拟DOM类型: "typeof VDOM);  // object
console.log("真实DOM类型: "typeof TDOM);  // object
console.log(VDOM instanceof Object);    // true
console.log(TDOM instanceof Object);    // true
复制代码

虚拟DOM 最终会被 React转为 真实DOM呈现在页面上

(2) 创建虚拟DOM

  1. 通过 React的方法 createElement()方法创建虚拟DOM
// React.createElement(标签名称, 标签属性, 标签内容)
const VDOM1 = React.createElement("h1", {id: "title"}, "This is Title");
复制代码
  1. 使用方法一的语法糖创建虚拟DOM
const VDOM2 = <h1 id="title">This is Title</h1>;
复制代码

5. 关于JSX

jsx语法与javascript语法非常相似,只有一些需要注意的地方

// 虚拟DOM  
const VDOM = (  // 使用括号框住 jsx标签表示层级会更加美观些。
    <h1>
        <span>Hello World</span>
    </h1>
)
const myid = "HeLlO";
const content = "Happy New Year"
const students = [
{id: "001", name: "Tom", age: 18},
{id: "002", name: "Tim", age: 19},
{id: "003", name: "Jerry", age: 20},
];
const VDOM2 = (
    <div>
        <h2 className="title" id={myid.toLowerCase()}>
            <span style={{color: 'pink'}}>{content}</span>
        </h2>
        <ul>{/* 使用 ES6的 map() 函数进行列表渲染(将数据批量渲染到页面上) */}
        {
            students.map(student=>{
                return <li key={student.id}>{student.name}---{student.age}</li>
            }
        }
        </ul>
        <input type="text"/>    
    </div>
)
复制代码

jsx语法规则:

  1. 定义虚拟DOM时不要写引号
  2. 标签中混入 js表达式时要用 {}
    • 表达式会产生一个值,可以放在任何需要值的地方
    • 语句时一串代码,用于处理逻辑用的
  3. 标签中类名指定不用 class,要用 className
  4. 标签中使用内联样式时,要用双括号写法(使用小驼峰写法写css样式)
  5. 添加事件属性时(如onclick),on后面的单词首字母要大写(如onClick
  6. 虚拟DOM必须只有一个根标签
  7. 标签必须闭合
  8. 标签开头
    • 标签开头为小写时,会被 jsx编译为 html标签,若 html没有对应同名元素则报错
    • 标签开头为大写时,会被 jsx识别为组件,若没找到对应组件则

6. 渲染到页面上

使用 ReactDOMrender()方法进行渲染

const VDOM = <h1 id="title">This is a Title</h1>;    // 创建虚拟DOM
// ReactDOM.render( 组件(虚拟DOM),  要绑定到哪个元素上 );
ReactDOM.render(VDOM, document.querySelector("#root"));
复制代码

二、组件的使用

React中,有两种组件的创建方式,分别为函数式组件类式组件。其中 类式组件使用频率较高(React 16.8出现 Hook后函数式组件也多了起来)

函数式组件

顾名思义,该组件是由函数来写的

function Demo(props){    // 定义一个组件,名为 Demo
    return <h2>This is a component</h2>;    // 返回值为组件的DOM内容
}
ReactDOM.render(<Demo/>, document.querySelector("#root"))
复制代码

函数式组件定义:

  • 函数名称必须大写
  • 调用时以标签方式调用且开头大写
  • 函数式组件的参数为 props(后面会讲)

类式组件

该组件通过类来构建,但需要继承React自带的一个类Component

// 使用 ES6写法创建类式组件,并继承于 React.Component
class Demo extends React.Component{
    
    // 添加render() 函数(必须),返回值为组件的虚拟DOM
    render(){
        console.log(this);      // render() 函数的 this 指向组件的实例对象
        return <h1>This is a Title!</h1>
    }
}
ReactDOM.render(<Demo/>, document.querySelector("#root"));
复制代码

类式组件的定义:

  • 必须继承 React的内置类Component
  • 必须包含方法 render()
  • 构造函数 constructor()的参数为 props(后面会讲),如果需要使用constructor则必须调用父类的构造函数 super(props)

类式组件挂载时的执行情况:

  1. React解析组件标签,发现了 Demo组件
  2. 发现为类式组件,随后 new出该类的实例对象,通过实例调用原型对象上的render方法
  3. render方法返回的虚拟DOM转为真实DOM,随后呈现到页面中

组件定义的注意事项:

  1. 类式组件的render() 返回的组件标签与函数式组件返回的组件标签一定要有一个根标签
  2. 都必须以大写字母开头

组件的挂载与卸载

挂载已经看了很多个了,直接上代码:

// ReactDOM.render( 组件, 要绑定在哪个元素上 );
ReactDOM.render( <Demo/>, document.querySelector("#app") );
复制代码

卸载的代码长点,我也直接放上来了:

// ReactDOM.unmountComponentAtNode( 要卸载哪个元素上的组件 );
ReactDOM.unmountComponentAtNode( document.querySelector("#app") );
复制代码

其他小知识

  1. 包含表单元素的组件分为非受控租价受控组件
    • 受控组件:表单组件的输入组件随着输入并将内容存储到状态中(随时更新)
    • 非受控组件:表单组件的输入组件的内容在有需求的时候才存储到状态中(即用即取)

三、组件的三大属性

组件的实质就是个对象,而对象自然有属性,在组件里最常用的三个属性分别是 statepropsrefs

1. state

state即组件的状态,说的明白点就是该组件所存储的(所需要使用的)数据

类式组件中的使用:

class Weather extends React.Component{
    constructor(props){
        super(props);
        // this.state = {weather: "Spring"}     // 也可以在构造函数中定义 state
    }
    
    state = {   // 定义 state
        weather: "summer",
    }
    
    render(){
        // 当前季节为:summer
        return <h2>当前季节为:{ this.state.weather }</h2>
    }
}
复制代码

使用的时候通过 this.state调用 state里的值 类式组件定义 state:

  • 可以在构造函数中初始化 state
  • 可以在类中添加属性 state来初始化

函数式组件中的使用

  • React16.8 前,函数式组件并不能有自己的 state(因为会重复初始化数据)
  • React16.8 后,出现了 Hook方法,使得函数式组件也可以使用 state特性,先了解即可,后面会教学。
function Demo(){
    const [weather, setWeather] = React.useState("Winter");
    return <h2>当前季节为:{weather}</h2>     // 当前季节为:Winter
}
复制代码

修改 state

类式组件的函数中,你会发现直接修改 state的值,如:

this.state.count = 1
复制代码

如果你在页面中使用了 count这个值,你会发现,这页面咋没变呢?
梳理一下页面渲染靠的是哪个函数呢?不是靠的 render()函数吗?
如果你每次直接修改 state后在调用 render()函数的话,不会显得太麻烦了吗?
其实React也不建议 state不允许直接修改,而是通过特定的渠道来修改,便是使用在类的原型对象上的方法 setState()

setState()
this.setState(partialState, [callback]);
复制代码
  • partialState: 需要更新的状态的部分对象
  • callback: 更新完状态后的回调函数

setState 有两种写法: 写法1:

this.setState({
    count: 1,
})
复制代码

写法2:

// 传入一个函数,返回x需要修改成的对象,参数为当前的 state
this.setState(state => ({count: state.count+1});
复制代码

使用那种写法,取决于修改的状态是否需要动用到当前的状态

forceUpdate()

还有一种修改状态的方法,就是 forceUpdate,意思是强制更新,即强制更新状态。
参数为更新状态完成后的回调函数

this.forceUpdate([callback]);
复制代码

setState更新与forceUpdate更新都是一种合并操作,而不是替换操作


  • 在执行 setState操作后,React会自动帮我们调用一次 render()
  • render() 的执行次数便是 1+n (1 为初始化时的自动调用,n 为状态更新的次数(即调用 setStateforceUpdate的次数))
  • 尽量少用或不用 forceUpdate

2. props

state不同,state是组件自身的状态(数据),而 props则是外部传入给自己的状态(数据)

props在组件内修改,必须由谁传入的即由谁修改

类式组件中使用

class Person extends React.component{

    constructor(props){     // 还记得构造函数的参数吗,也能够获取 props
        super(props);
    }

    // 可以使用静态属性 propTyps 来限制props
    static propTypes = {
         // 在 16版本前,通过使用React自带的PropTypess属性传递值
        // name: React.PropTypes.string.isRequired,
        
        // 16版本后,PropTypes被单独移出 React作为单独的个体,需要另外导入
        name: PropTypes.string.isRequired, // 字符串类型,且必须传值
        sex: PropTypes.string,  // 字符串类型
        age: PropTypes.number,  // 数字类型
        speak: PropTypes.func,  // 函数类型
    }
    
    // 可以使用静态属性 defaultProps 来设置某些 prop 的默认值
    static defaultProps = {
        sex: "男",   // 设置 sex 的 prop 默认值为 “男”
    }

    render(){
        const {name, sex, age} = this.props;    // 从 props中获取值
        return (
            <ul>
                <li>姓名:{name}}</li>
                <li>性别:{sex}</li>
                <li>年龄:{age}</li>
            </ul>
        )
    }
}

// ReactDOM.render( <Person name="Tom" sex="男" age="16"/>, document.querySelector("#root"));    // 以类似属性的方式传递 props值

ReactDOM.render(<Person {...p} />, document.querySelector("#root"));  
// 可以用扩展运算符来传递 props值
复制代码

在使用的时候可以通过 this.props来获取值 类式组件的 props:

  1. 通过在组件标签上传递值,在组件中就可以获取到所传递的值
  2. 在构造函数的参数里可以获取到 props
  3. 可以分别设置 propTypesdefaultProps 两个属性来分别操作 props的规范和默认值,两者都是直接添加在类式组件的原型对象上的(所以需要添加 static

函数式组件中使用

还记得说函数式组件的参数是什么吗?便是 props
传入的方式与类式组件相同,都是在组件标签中传递值

function Person(props){   // 参数为 props
    const {name, sex, age} = props;     // 使用占位符获取 props的值
    return (
        <ul>
            <li>姓名:{name}}</li>
            <li>性别:{sex}</li>
            <li>年龄:{age}</li>
        </ul>
    )
}

Person.defaultProps = {     // 设置 props默认值
    sex"男",
    age18,
}
Person.propTypes = {    // 设置 props限制
    name: PropTypes.string.isRequired,
    sex: PropTypes.string,
    age: PropTypes.number,
}

const p = {name:"Jerry"sex:"女"age:16}
ReactDOM.render(<Person {...p}/>,document.querySelector("#app"));
复制代码

函数组件的 props定义:

  1. 同样也是在组件标签中传递 props的值
  2. 组件函数的参数为 props
  3. props的限制和设置默认值也同样实在原型对象上

3. refs

假设在你的组件里有着表单元素,然后你需要获取到其中的值,该怎么获取呢?有的人可能想给元素绑定 id后用 document.querySelector来查找元素,但这样就违背了 React的想法,又开始操作起了DOM元素,所有React提供了第三种特性 refs

React的历史中,共有三种操作refs的方法,分别为:

  • 字符串形式
  • 回调形式
  • createRef形式

让我们一一来了解一下:

类式组件中使用 refs

(1) 字符串形式的 refs
class Demo extends React.Component{
    
    showData(){
         // 通过自己设定的 refs名称获取对应的元素
        const {myInput} = this.refs;   // 返回该元素
        alert(myInput.value)
    }
    
    render(){
        return (
            <div>
                { /* 通过 ref属性绑定这个 input标签 */ }
                <input type="text" ref="myInput" placeholder="search something" />
                {/* 事件绑定会在下面讲 */}
                <button onClick={this.showData}>Show Data</button>
            </div>
        )
    }
}
复制代码
(2) 回调形式的 refs

其实很简单,但在后来的版本中,官方觉得这样做有问题,于是推出了全新的版本:使用回调函数来操作 refs

class Demo extends React.Component{
    
    showData(){
        const {myInput} = this;   // 返回该元素
        alert(myInput.value)
    }
    render(){
        return (
            <div>
                {/* 回调函数的参数为该元素本身,通过函数绑定在 this上 */}
                <input type="text" ref={ e => this.myInput = e } placeholder="search something" />
                <button onClick={this.showData}>Show Data</button>
            </div>
        )
    }
}
复制代码
(3) createRef形式(推荐)

后来的React发现,使用回调函数的形式还是会出现一些不可言喻的问题,所以,又一次推出了全新的的方法来使用 refs,便是使用 React身上的方法 createRef()

class Demo extends React.Component{

    // 使用 React.createRef() 创建一个 ref容器
    myRef = React.createRef();

    showData = ()=>{
        // 在容器中获取 DOM元素
        const {current} = this.myRef;
        alter(current.value);
    }

    render(){
        return (
            <div>
                {/* 将DOM元素绑定在容器中 */}
                <input ref={this.myRef} placeholder="点击提示数据" type="text"/>
                <button onClick={this.showData}>ShowData</button>    
            </div>
        )
    }
}
复制代码

注意:一个 ref容器,只能存储一个元素(专人专用),后加入的会把前一个顶出去

函数式组件中使用 refs

  • React16.8前,与 state相同,函数式组件也无法使用 refs特性
  • React16.8后,出现了 Hook函数,使得函数式组件也有了使用 refs特性的能力

以下代码现阶段无需看懂先,了解即可。

function Demo({
    const [count, setCount] = React.useState(0);
    function add(){
        setCount(count=> count+1);
    }
    return (
        <div>
            <h2>当前求和:{count}</h2>
            <button onClick={add}>点击加一</button>
        </div>
    )
}
复制代码

四、组件的事件绑定

1. 在函数式组件中进行事件绑定

可以在函数中定义另外一个函数(内置函数)然后进行调用即可

function Demo(props){
    function showData(){
        console.log(props);
        alert("触发了事件");
    }
    return (
        <div>
            <button onClick={showData}>点击触发</button>
        </div>
    )
}
复制代码

2. 在类式组件中进行事件绑定

在类式组件中可以通过类本身的方法来调用

class Demo extends React.Component{
    state = { count: 0 }    // 定义 state
    showData(){
        alert("触发了事件")
        console.log(this.state.count);; 报错
    }
    render(){
        return (
            <div>
                <button onClick={this.showData}></button>
            </div>
        )
        // 成功弹出弹窗,但是输出报错
    }
}
复制代码

这里你会发现,虽然成功 alter,但是输出 state会报错,这是为什么呢?

平时在组件内使用 this.state的时候,此时 this的指向都是作用于类的原型对象,即类本身。
而在函数中,因为使用了 babel.js的缘故,js默认开启了严格模式,所以函数体的 thisundefined,自然找不到在原型对象上的 state
此时有两种解决方法:

  • 使用 bindapply等方法改变 this指向
  • 使用箭头函数,改变 this指向(推荐)
class Demo extends React.Component{
    constractor(props){
        super(props);
        // this.showData = this.showData.bind(this);   // 方法1
    }
    
    state = { count: 0 }    // 定义 state
    showData = ()=>{    // 方法2
        alert("触发了事件")
        console.log(this.state.count);; 报错
    }
    render(){
        return (
            <div>
                <button onClick={this.showData}></button>
            </div>
        )
        // 成功弹出弹窗,但是输出报错
    }
}
复制代码

3. React的事件处理方法

  • 通过 onXxx属性指定事件处理函数(注意大小写
    • React使用的是自定义(合成)事件,而不是原生的 DOM事件(为了更好的兼容性)

    • React的事件是通过事件委托方式处理的(委托给组件最外层的元素)(为了更加的高效)

    • 可以通过事件的 event.target获取发生的DOM元素对象,可以尽量减少 refs的使用

    • 在绑定事件的时候不要加括号,这会被 jsx识别为执行函数

    • 在类式组件中绑定函数注意 this的指向问题(推荐绑定的函数都使用箭头函数

五、组件的生命周期

Vue熟悉的应该对生命周期并不陌生。
组件在挂载到页面上的过程中,期间发生了许多的事情,而生命周期,可以看作是一个组件的执行流程,我们可以通过在某个流程节点中进行一些操作,而这就是 生命周期钩子(函数)
注:生命周期函数只适用于类式组件,并不适用于函数式组件,但是在React16.8之后的 Hook,也可以让函数式组件实现类似于生命周期钩子的功能

1. React 16之前

592_1.png

以下是流程:

  • 自身组件挂载
    1. constructor: 构造函数(初始化)
    2. componentWillMount: 组件挂载前
    3. render: 组件挂载中
    4. componentDidMount: 组件挂载完成后
  • 拥有父组件且父组件状态进行更新(setState):
    1. 父组件 shouldComponentUpdate: 父组件是否进行状态更新
    2. 父组件 componentWillUpdate: 父组件状态更新前
    3. 父组件 render: 父组件更新挂载中
    4. 子组件 componentWillReceiveProps: 子组件将收到新的Props
    5. 子组件 shouldComponentUpdate: 子组件是否进行状态更新
    6. 子组件 componentWillUpdate: 子组件状态更新前
    7. 子组件 render: 子组件更新挂载中
    8. 子组件 componentDidMount: 子组件挂载完成
    9. 父组件 componentDidMount: 父组件挂载完成
  • 当组件要进行卸载时:
    1. componentWillUnmount: 组件卸载前

有几个需要注意的地方:

  • shouldComponentUpdate的意思是是否更新状态,所以他应该有个布尔类型返回值,而且默认为 true(不写的时候),当你写了这个钩子时别忘了写个返回值,这个横眉周期一般用于进行更新值的一些判断。
  • shouldComponentUpdate有两个参数,分别是 nextProps:新的propsnextState: 新的state,此时他们都还没更新到组件上。
  • 使用 setState时会经过生命周期 shouldComponentUpdate,但是使用 forceUpdate时则不会,而是直接到 componentWillUpdate生命周期

2. React 16之后

594_1.png

新旧生命周期对比:

  1. 在新的生命周期中,舍弃了(即将舍弃)三个生命周期函数:
    • componentWillMount
    • componentWillReceiveProps
    • componentWillUpdate
  2. 新增了两个生命周期函数:
    • getDerivedStateFromProps

    • getSnapshotBeforeUpdate

以下是流程:

  • 自身组件挂载:
    1. constructor: 构造函数(初始化)
    2. getDerivedStateFromProps: 从props中获取派生的state
    3. render: 组件挂载中
    4. componentDidMount: 组件完成挂载
  • 父组件更新时
    1. 父组件 getDerivedStateFromProps: 从 props中获取派生的state
    2. 父组件 shouldComponentUpdate: 判断是否进行状态更新
    3. 父组件 render: 父组件挂载中
    4. 子组件 getDerivedStateFromProps: 从 props中获取派生的state
    5. 子组件 shouldComponentUpdate: 判断是否进行状态更新
    6. 子组件 render: 子组件挂载中
    7. 子组件 getSnapshotBeforeUpdate: 子组件获取状态更新前的快照
    8. 子组件 componentDidUpdate: 子组件完成更新
    9. 父组件 getSnapshotBeforeUpdate: 父组件获取状态更新前的快照
    10. 父组件 componentDidUpdate: 父组件完成更新
  • 组件卸载时
    1. componentWillUnmount: 组件卸载前

也有几个需要注意的地方:

  • getDerivedStateFromProps 需要定义个实例本身,所以是静态方法
  • getDerivedStateFromProps 有两个参数,分别是当前的 propsstate
  • 若需要组件的状态任何时候都取决于 props则可以使用 getDerivedStateFromProps,但是使用场景比较罕见,可以在其中定义 state
  • getSnapshotBeforeUpdate 有两个参数,分别为状态更新前的 propsstate,并且有一个返回值(快照),一般用作本次更新情况的说明情况。
  • componentDidUpdate 有三个参数,分别为状态更新前的 propsstate,以及先前 getSnapshotBeforeUpdate返回的快照。
  • 其他的与旧的生命周期没有什么差别

六、组件列表渲染、条件渲染与DOM的Diffing算法

1. 组件列表渲染

在有时候我们需要批量的去创建一些DOM元素或组件,比如页面上的:新闻列表、推文列表、好友列表等等,你会发现在开发的过程中经常会使用到列表,但是自己一个个的去写DOM回十分繁琐。
我们可以通过数组存储数据,也可以使用数组来循环渲染数据。
举个例子:

class Demo extends React.Component{
    state = {
        arr: [a, b, c],
    }
    render(){
        // 输出 abc
        return (
            <div>
                <ul> {stus} </ul>
            </div>
        )
    }
}
复制代码

你会发现,React输出数组会把所有的元素直接循环输出出来,那么我们只要在每个元素左右添加上标签,那不就构成了列表渲染吗?
这里我们可以通过 jsx语法配合ES6语句来实现列表渲染。
直接上代码:

class Demo extends React.Component{
    state = {
        stus: [
            {id: "001", name: "小明", age: "28"},
            {id: "002", name: "小红", age: "26"},
        ],
    }
    render(){
        return (
            <div>
                <ul>
                    {
                        this.state.stus.map(item =>{
                            return <li key={item.id}>{item.name}---{item.age}</li>
                        })
                    }
                </ul>
            </div>
        )
    }
}
复制代码

598_1.png

2. key 的使用

有的人发现了写列表渲染的时候,我都会给每个标签加上一个 key属性,这是为什么呢?
其实 key的作用是给当前的标签添加一个唯一的标识,用于给React进行 diffing算法计算的时候使用

3. diffing 算法

当状态发生改变时,react会根据【新的状态】生成【新的虚拟DOM】
然后将新旧虚拟DOM进行 diff比较,比较规则如下:

  1. 旧虚拟DOM 中找到了与 新虚拟DOM 相同的 key
    • 若虚拟DOM中的内容没变,则直接使用之前的真实DOM
    • 若虚拟DOM中的内容变了,则生成新的真实DOM并进行替换           
  2. 旧虚拟DOM 中未找到与 新虚拟DOM 相同的 key,则根据数据创建新的真实DOM,然后渲染到页面

用index作为key可能引发的问题

  • 对数据进行 逆序添加、逆序删除 邓破坏顺序的操作 会产生没有必要的真实DOM更新,影响效率
  • 对结构中包含输入类的DOM 会产生错误的 DOM更新,同时界面渲染有问题
  • 若仅用于展示数据,那用 index作为 key则没有问题 

七、React脚手架

恭喜,终于熬到了脚手架这里了,终于可以一键生成所有东西而不是自己一个个引用了。
什么是脚手架呢? 脚手架可以说是建房子时的构架,其他人已经帮你吧房子的架构搭好了,不需要自己动手。
React就有自己的脚手架,开发团队通过 webpackbabelnpm等方法,帮你搭建好了一个使用React开发软件的环境,不再需要自己去创建文件夹,导入React等重复的操作,更加利于我们编写 SPA应用(单页面富应用)。

1. 安装React脚手架

在自己的命令行窗口中输入(需要现有 node环境):

npm i create-react-app -g
复制代码

全局安装 create-react-app脚手架

2. 创建React应用

create-create-app 应用名称
复制代码
  • 应用名称不应该出现大写字母和特殊字符
  • 使用英文命名而不是中文命名

3. 文件解析

项目文件中比较常用的文件就以下这些:

  • node_modules ------ npm包的存放位置
  • public ------ 用于存放静态文件
  • src ------ 项目的代码存放位置
    • components ------ 用于存放公用组件的文件夹
    • page ------ 用于存放页面的文件夹
    • App.js ------ 根组件
    • App.css ------ 根组件的样式
    • index.js ------ 项目入口文件
    • index.css ------ 项目的公用样式
  • .gitgnore ------ 编写git的配置文件
  • package.json ------ 项目配置文件
  • README.md ------ 项目信息

还记得我们的讲组件化的图片吗

582_1.png

其中的 APP就是根组件,通过我们编写其他的组件如 HeaderAside等组件,都加装在根组件上。

4. npm 指令

npm start       // 使用 webpack-dev-server 启动服务查看应用
npm build       // 打包生成生产文件
npm test        // 进行软件测试(不常用)
npm eject       // 将所有的配置文件暴漏出来(不常用且不建议用)
复制代码

八、react-router的使用

1. 什么是 react-router

react-router 是为了React编写SPA应用操作前端路由而诞生的。

(1) 前端路由(可能有点直白)

前端路由是通过HTML5的新API History来操作的,其原理就是url地址的地址发生改变,但是并不会触发重新加载,同时javascript可以监听到改变。
有两种类型:

  • HashRouter: 利用url地址栏# 后面的哈希值

604_1.png

  • BrowserRouter: 利用浏览器的History API,地址栏中不包含 # ,显得更加美观

606_2.png

以上两种情况都不会触发 url的跳转功能

(2) SPA 应用

SPA 全称 Single Page web Application,顾名思义是只有一个页面的应用,通过javascript进行实时的渲染来更新页面。
即通过当前的路由来判断页面应该加载什么组件,从而呈现不同的页面与效果。

2. 使用 react-router

(1) 安装与使用

在项目文件夹中打开命令行窗口进行 npm下载

npm i react-router-dom -S
复制代码

注意:我们下载的是 react-router-dom 而不是 react-router 两者区别:

  • react-router:提供了router的核心 API。如RouterRouteSwitch等,但没有提供有关dom操作进行路由跳转的API
  • react-router-dom:提供了BrowserRouterRouteLink等api,可以通过dom操作触发事件控制路由

react-router-dom中包含了react-router,所以我们选择下 react-router-dom

(2) 常用组件

a. 路由跳转

在多页面应用中,通常都是使用 a标签进行页面跳转

<a href="http://localhost:3000">跳转页面</a>
复制代码

使用单页面富应用中使用react-router则使用路由跳转组件

import {Link, NavLink} from "react-router";
复制代码
<Link activeClassName="nav-active" className="nav" to="/about">About</Link>
<NavLink activeClassName="nav-active" className="nav" to="/home">Home</NavLink>
复制代码
  • activeClassName: 处于当前路由时,对应的组件会自动添加该类
  • className: 当前组件类名
  • to: 当前组件所对应的路由

Link组件与 NavLink组件都可以进行路由的跳转,区别在于:当前路由对应的NavLink会自动添加class: active,而 Link不会。

b. 注册路由
import {Route} from "react-router";
复制代码
<Route path="/home" component={Home}></Route>
<Route exact path="/about" component={About}></Route>
复制代码
  • path: 所要监听的路由
  • component: 该路由要绑定的组件
  • exact: 可选,不写时为 false,是否选择严格匹配

当当前路由对应上了路由组件所绑定的路由时,则会展示所绑定的组件。

(a) 路由严格匹配与模糊匹配

路由不仅仅只有一级,有的时候是有多级嵌套的,比如以下这张:

606_2.png
模糊匹配严格匹配,都是指当前的组件对当前路由的匹配模式:

  • 模糊匹配: 如果当前路由与匹配的路由成相等或包含(注意层级)的情况,则启用该组件
    • http://localhost:3000/home/a/b/c 则为包含路由 /home
    • http://localhost:3000/a/b/home/c 则为不包含路由 /home层级不对
  • 严格匹配: 如果当前路由与匹配的路由相等的话,才启用该组件
    • http://localhost:3000/home 则为与路由 /home 相等
    • http://localhost:3000/home/a 则为与路由 /home 不相等
c. 重定向路由

你明明设置好了路由 /home,但是有的用户就喜欢对着干,在地址栏输入了 /nothing,而你没有注册这个路由,那该怎么办呢?
这个时候你就可以东涌道重定向路由了,对于没有注册过的路由,都会被跳转到你指定的某个路由去,这就是重定向路由
经常可以用作一些404页面丢失等情况的路由跳转方式。

import {Redirect, Route} from "react-router";
复制代码
<Route ....../>
<Route ....../>
<Redirect to="/home"/>
复制代码
  • to: 需要重定向到哪个路由?

Redirect需放在所有Route下面,当上面的 Route都没有匹配到时,则路由将重定向到指定的路由。

d. Switch 路由

你想想,如果你的路由中,出现了 /home/home/abc/home/a/b/c 等这样的路由,当路由为 /home时则会三个路由都同时渲染,但是你又只想要渲染其中的一条,这个时候我们就可以使用 Switch组件。
使用 Switch组件包裹住所有的 RouteRedirect,当出现多个匹配的路由时,只会渲染第一个匹配的组件。

import {Switch, Route, Redirect} from "react-router";
复制代码
<Switch>
    <Route ..../>
    <Route ..../>
    <Redirect to="..."/>
</Switch>
复制代码
e. 路由器

你想要使用路由跳转组件和路由组件,还差一个路由器组件,同时路由器组件必须包裹着这两个组件。

import {HashRouter, BrowserRouter} from "react-router";
复制代码

一般为了使整个React应用都可以使用到路由组件,所以一般我们都是把路由器包裹在根组件上的。

ReactDOM.render( 
    <BrowserRouter>
        <App/>
    </BrowserRouter>,
    document.querySelector("#root")
);
复制代码

有两种路由器组件,分别是HashRouterBrowserRouter,分别对应者两种路由方式。

(3) 路由组件

路由组件与一般组件

  1. 写法不同
    • 一般组件:<Demo></Demo>
    • 路由组件:<Route path="/demo" component={Demo}/>
  2. 存放位置不同
    • 一般组件:components文件夹
    • 路由组件:page 文件夹
  3. 接受到的 props 不同
    • 一般组件:根据组件标签传递了上面,就收到了什么
    • 路由标签:会收到三个固定的属性
{
  "history": {
    "length"18,
    "action""PUSH",
    "location": {
      "pathname""/home",
      "search""",
      "hash""",
      "key""tvfyve"
    }
  },
  "location": {
    "pathname""/home",
    "search""",
    "hash""",
    "key""tvfyve"
  },
  "match": {
    "path""/home",
    "url""/home",
    "isExact"true,
    "params": {}
  }
} 
复制代码

3.嵌套路由

假设我们有个路由组件为 Home,在根组件中使用 Link跳转到了该路由,当前路由为 /home,可在组件 Home 中还有两个 Link,分别导向路由 /home/message/home/news 然后在组件中还有其他的路由组件。这就时嵌套路由的使用。
如下面的代码:

class Home extends Component{
    render(){
        return (
            <div>
                <Link to="/home/message">Message</Link>
                <Link to="/home/news">News</Link>
                <hr/>
                <Route path="/home/message" component={Message} />
                <Route path ="/home/news" component={News} />
            </div>
        )
    }
}
复制代码

4. 编程式路由

如果说,我们想要做用户点击按钮登陆后,如果他是老师就去老师页面,如果是学生就去学生页面,这个显然单靠 Link 无法完成,我们可以通过 js进行路由的跳转(也是 react-router 基于 History API 编写的)

class Message extends Component {
    state = {
        messageArr:[
            {id:"01"title"消息1"},
            {id:"02"title"消息2"},
            {id:"03"title"消息3"},
        ]
    }
    // 编程式路由导航
    pushShow = (id, title)=>{
        // push跳转 + 携带 params参数
        this.props.history.push(`/home/message/detail/${id}/${title}`);
        
        // push跳转 + 携带 search参数
        // this.props.history.push(`/home/message/detail?id=${id}&title=${title}`);
        
        // push跳转 + 携带 state参数
        // this.props.history.push(`/home/message/detail`, {id,title});
    }
    replaceShow = (id, title)=>{
        // replace跳转 + 携带 params参数
        this.props.history.replace(`/home/message/detail/${id}/${title}`);
        
        // replace跳转 + 携带 search参数
        // this.props.history.replace(`/home/message/detail?id=${id}&title=${title}`);
        
        // replace跳转 + 携带 state参数
        // this.props.history.replace(`/home/message/detail`, {id, title});
    }
    // 后退
    goBack = ()=>{
        this.props.history.goBack();
    }
    // 前进
    goForward = ()=>{
        this.props.history.goForward();
    }
    // 跳转指定位置
    go = ()=>{
        // 向前两步
        this.props.history.go(2);
        
        // 后退两步
        this.props.history.go(-2);
    }
    render() {
        const {messageArr} = this.state;
        return (
            <div>
                <ul>
                    {
                        messageArr.map(item=>{
                            return (
                            <li key={item.id}>
                                <Link to={`/home/message/detail/${item.id}/${item.title}`}>{item.title}</Link>
                                &nbsp;<button onClick={() => this.pushShow(item.id, item.title)}>push查看</button>
                                &nbsp;<button onClick={() => this.replaceShow(item.id, item.title)}>replace查看</button>
                            </li>
                            )
                        })
                    }
                </ul>
                <hr/>
                <Route path="/home/message/detail/:id/:title" component={Detail} />
                <button onClick={this.goBack}>goBack</button>
                &nbsp;
                <button onClick={this.goForward}>goForward</button>
                &nbsp;
                <button onClick={this.go}>go</button>
            </div>
        )
    }
}
复制代码

总结一下上面的代码:

  1. 编程式路由都是通过 props中的 history对象进行操作(都是该对象身上的方法,调用方式为:this.props.history.xxx
  2. 常用方法:
    • push(route[, state]): 跳转到指定路由(带有历史记录)

    • replace(route[, state]): 跳转到指定路由(不带有历史记录)

    • goBack(): 后退一个

    • goForward(): 前进一个

    • go(num): 前往指定步数,当 num为正数时,为前进,当 num为负数时则为后退。

5. withRouter 组件

有的时候,我们想要在其他组件中也使用路由组件的功能,比如导航栏,应该属于公用组件,但是里面的导航链接的功能却是路由组件的功能,我们应该怎么解决呢?
react-router 中,提供了这么一种方法,可以让一般组件具有路由组件的功能,则就是 withRouter() 方法。
看看演示:

import {withRouter} from "react-router-dom";

class Header extends Component {
    // withRouter后该组件也有了路由组件的功能
    goBack = ()=>{
        this.props.history.goBack();
    }
    go = ()=>{
        this.props.history.go(2);
    }
    goForward = ()=>{
        this.props.history.goForward();
    }
    render() {
        return (
            <div>
                <h1>This is a React-router-dom Test!</h1>
                <button onClick={this.goBack}>goBack</button>
                &nbsp;
                <button onClick={this.goForward}>goForward</button>
                &nbsp;
                <button onClick={this.go}>go</button>
            </div>
        )
    }
}

// withRouter 用于给一般组件添加上路由组件特有的功能,返回一个新组件
export default withRouter(Header);
复制代码

九、路由组件之间的参数传递

父子组件之间的内容传递,可以通过 props来进行参数传递,但是路由组件却没有自己的标签,那么该如何进行参数的传递呢?
还记得路由组件特有的 props吗,我们可以利用 History API的一些特性来进行路由组件之间的参数传递,有三种方法。

  • params
  • search
  • state

1. 传递 params 参数

// 父组件
class Parent extends Component {
    state = {
        messageArr:[
            {id:"01"title"消息1"},
            {id:"02"title"消息2"},
            {id:"03"title"消息3"},
        ]
    }
    render() {
        const {messageArr} = this.state;
        return (
            <div>
                <ul>{
                 messageArr.map(item=>{
                    // 向路由组件传递 params参数
                    return <li key={item.id}><Link to={`/home/message/detail/${item.id}/${item.title}`}>{item.title}</Link></li>
                    })
                }</ul>
                <hr/>
                {/* 声明接受 params参数,可以在 props中的 match属性的 params属性里获得 */}
                <Route path="/home/message/detail/:id/:title" component={Child} />
            </div>
        )
    }
}
复制代码
// 子组件
class Child extends Component {
    state = {
        contentArr:[
            {id:"01"content"你好中国"},
            {id:"02"content"你好世界"},
            {id:"03"content"你好帅哥"},
        ]
    }
    render() {
        console.log(this.props);
        // 获取 params参数
        const {id, title} = this.props.match.params
        const findResult = this.state.contentArr.find(obj=>obj.id == id).content;
        return (
            <div>
                <ul>
                    <li>ID: {id}</li>
                    <li>TITLE: {title}</li>
                    <li>CONTENT: {findResult}</li>
                </ul>
            </div>
        )
    }
}
复制代码

在使用 params传递参数时,你会清楚的看到参数以路由的形式展现了出来,例如:

  • http://localhost:3000/home/message/用户1/文章32

类式上面的用户1文章32,这就是传递的参数。
params方式传递的参数,在路由中需要声明接受才可以使用。

<Route path="/home/message/detail/:id/:title" component={Child} />
复制代码

后面的 :id:title 就是可变路由,可以通过propsmatch接受接收这里的内容.

const {id, title} = this.props.match.params
复制代码

2. 传递 search 参数

这个就是依赖 get的请求方式。

  • http://localhost/home/message?id=1&title=abc

javascript 可以获取到 url? 后面的请求体。 所以我们可以吧上面的 map 中返回的标签修改以下

<li key={item.id}><Link to={`/home/message/detail?id=${item.id}&title=${item.title}`}>{item.title}</Link></li>
复制代码

且这种方式不需要在路由中声明接收。
通过 props 中的 location 进行接收

const search = this.props.location.search;
// 获取到的格式时: ?id=xxx&title=xxx 所以还需要加工一下
const {id, title} = qs.parse(search.slice(1));
复制代码

3. 传递 state 参数

通过 HistoryAPI来进行数据传输:

<li key={item.id}><Link to={{pathname: `/home/message/detail`, state:{id:item.idtitle:item.title}}}>{item.title}</Link></li>
复制代码

不需要在路由链接中添加任何东西,也不需要路由进行声明接收。更加的美观。
props中的 location 里获取 state属性

const {id, title} = this.props.location.state;
复制代码

4. 三者对比

路由组件传参其实用的较少,因为他们都有对某些东西的依赖:

  • params: 依赖于路由链接,链接不美观
  • search: 依赖于路由链接,链接不美观
  • state: 依赖于历史记录 (HistoryAPI),链接美观,但是当直接输入链接时则会报错(没有历史记录)

所以尽量使用其他的传参方式,如果非要使用的话,有限度比较为:

  1. params (最为常用)
  2. search (较为常用)
  3. state (比较少用)

十、redux 的使用

614_1.png

假设我们的组件如同这张图一般,组件之间相互嵌套着。
这时候提出一个要求,在组件E中的数据,要给组件A组件F使用,要怎么处理呢?

  • 方法1:通过不断的 props进行传参,但是这非常的费时费力
  • 方法2:使用 Pubsub.js等进行消息发布/订阅功能
  • 方法3:使用 react-redux进行数据集中式管理

redux可以看作一个管家,负责帮忙存储公共的数据。

1. 安装 redux

npm i redux -S
复制代码

2. 核心概念

616_1.png

redux 有着三个核心概念

  1. action:
    • 动作的对象(操作内容)
    • 包含两个属性: a. type: 表示属性,值为字符串,唯一,必要属性(要干嘛) b. data: 数据属性,值为任意类型,可选属性(怎么干)
    • 例如:{ type: "ADD_STUDENT", data: { name: "tom", age: 18 } }
  2. reducer:
    • 用于初始化状态和加工状态(对数据进行初始化和操作数据的)
    • 加工时,根据旧的 stateaction,产生新的 state的纯函数
    • 有两个参数,一个为之前的状态(prevstate)与动作对象(action
  3. store:
    • stateactionreducer联系在一起的对象(大脑)

我们大致可以把 redux的想象成一家餐厅,而我们就是顾客( component),我们通过叫服务员( action)进行点餐等操作,服务员转达经理( store)后,经理吩咐后厨( reducer)进行做菜,然后把菜做好后由经理传递给顾客

3. 基本使用

(1) 创建文件夹

src文件夹中创建 redux文件夹,用于存放 redux的相关内容

  • Compnoent ------ 存放组件相关的文件夹
  • redux ------ 存放 redux相关内容的文件夹
    • actions ------ 存放 action相关内容的文件夹
    • reducers ------ 存放 reducer相关内容的文件夹
    • constant.js ------ 存放规范命名的文件
    • store.js ------ 编写 store的文件

由我来一个个带你们解析。先写一个最简单的 redux

/**
 * store.js
 * 该文件专门用于暴漏一个 store对象,整个应用只有一个 store对象
 */
// 引入 createStore,专门用于创建 redux中最为核心的 store
import {createStore} from "redux";
// 引入为 Count组件服务的 reducer
import countReducer from "./count_reducer";
const store = createStore(countReducer)
// 暴露 store对象
export default store;
复制代码
/**
 * / reducer / count.js
 * 1. 该文件是用于创建一个为 Count组件服务的 reducer,reducer的本质就是一个函数
 * 
 * 2. reducer函数会收到两个参数,分别为:之前的状态(preState),动作对象(action)
 * 
 * 3. 会自动调用一次 reducer(初始化)
 */
// 初始化的状态
const initState = 0;
export default function countReducer(preState = initState, action){
    if(preState === undefined) preState = 0; 
    // 从 action对象中获取 type,data
    const {type, data} = action;
    // 根据 type觉得如何加工数据
    switch (type) {
        case 'increment'// data
            return preState + data;
        case 'decrement'// 如果是减
            return preState - data;
        default:
            return preState;
    }
}
复制代码
// / Component / Count.js 
import React, { Component } from 'react'
// 引入store
import store from "../../redux/store";
export default class Count extends Component {
    // 加法
    increment = ()=>{
        const {value} = this.selectNumber;
        // 发送动作对象给 store
        store.dispatch({
            type"increment",
            data: value*1
        })
    }
    render() {
        return (
            <div>
                <h1>当前求和为:{store.getState()}</h1>
                <select ref={c=>this.selectNumber = c}>
                    <option value="1">1</option>
                    <option value="2">2</option>
                    <option value="3">3</option>
                </select>
                <button onClick={this.increment}>+</button>
            </div>
        )
    }
}
复制代码

以上就是最精简的 redux,让我们分析一下流程:

  1. 编写 Count组件,创建 Count组件对应的 Reducer文件
  2. 编写 Reducer的代码并抛出::
    • Reducer是个纯函数
    • 通常使用 Switch进行 action中的 type的判断
    • 函数返回值为修改后的值
  3. 创建 store并编写代码:
    • 使用方法 createStore()方法来创建一个 store,参数是一个 reducer
    • 把编写好的 Count组件的 reducer导入
  4. 在组件中引入 store并进行调用:
    • 在方法中通过使用 dspatch()方法向 store传递 action
    • dispatch的参数时一个对象(即 action动作对象)

(2) 使用流程

以上使创建的大致流程,而使用的大致流程是这样的:

  1. store 初始化时自动调用了一次 reducer进行了值的初始化
  2. 组件发出动 actionstorestore进行判断后分发给对应的 reducer
  3. reducer根据 action里的 type对数据进行相应的处理后返回新的值
  4. store接收返回的新的值,并将旧的值替换掉
  5. 通过方法 store.getState() 获取当前 store身上的值

(3) 使用异步redux

如果我们需要使用异步的 redux的话,还需要借助另一款插件: redux-thunk

npm i redux-thunk -S
复制代码

这是一个中间件,用于帮忙处理异步的 redux
异步的 action的值为一个函数 在函数中进行普通的 dispatch() 操作

export const createIncrementAsyncAction = (data, time=500)=>{
    // 返回一个 action
    return ()=>{
        setTimeout(()=>{
            store.dispatch(createIncrementAction(data));
        }, time);
    }
}
复制代码

同时我们要在 store处设置让他支持执行中间件,通过 reduxapplyMiddleware() 方法就可以加载中间件,他的参数就是中间件,然后 applyMiddleware() 将作为 createStore() 的第二个参数引入。

// store.js
// 引入 applyMiddleware,专门用于执行中间件
import {createStore, applyMiddleware} from "redux";
// 引入为 Count组件服务的 reducer
import countReducer from "./count_reducer";
// 引入 redux-thunk,用于支持异步 action
import thunk from "redux-thunk";
// 暴露 store对象
export default createStore(countReducer, applyMiddleware(thunk));
复制代码

(4) 监听状态变化

你写着写着有没有发现,虽然 redux里面的状态确实更新了,但是页面并没有变化啊?
还记得页面渲染使用的是哪个函数吗?render()函数。可是在 redux状态发生变化时,并不会帮助我们调用 render()函数,所以我们需要手动实现实时渲染页面。
在这里我们使用到了 reduxstore上的 subscribe() 方法,用于监听 redux上状态的变化,参数是一个函数,便于我们进行操作。
一般我们都写在根标签上(精简些,不用再每个使用 redux的组件中都写一遍)

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import store from "./redux/store"
ReactDOM.render(
    <App />,
  document.getElementById('root')
);
// 精简写法
store.subscribe(()=>{
  ReactDOM.render(
      <App />,
    document.getElementById('root')
  );
})
复制代码

4. 注意事项

  1. 一般使用到 redux的话,需要使用的组件肯定不止一个,所以创建 actionsreducers用来存储多个 actionreducer
  2. 同上,store肯定不止加载一个 reducer,所以我们使用 reduxcombineReducers()方法来整合所有的 reducer
    • combineReducers()方法的参数是一个对象,里面存放着所有的 reducer
import {createStore, applyMiddleware, combineReducers} from "redux";
import countReducer from "./reducers/count";
import personReducer from "./reducers/person";
import thunk from "redux-thunk";
// 汇总所有的 reducer
const allReducer = combineReducers({
    count: countReducer,
    persons: personReducer,
});
export default createStore(allReducer, applyMiddleware(thunk));


复制代码
  1. 因为 redux在设计上还有许多的问题,例如:
    • 单个组件需要做:与 store打交道,获取数据,监听数据变化,派发 action对象等,一个组件负责的事情太多了。
    • 需要另外监听 redux的状态变化来更新状态并渲染页面。
    • 所以有人对 redux进行了优化,推出了另一个库 react-redux(放心,没啥不同,就是多了点优化,后面会讲)
  2. 能不使用 redux,就不要使用 redux(不管是 redux 还是 react-redux
  3. 能不使用 redux,就不要使用 redux(不管是 redux 还是 react-redux
  4. 能不使用 redux,就不要使用 redux(不管是 redux 还是 react-redux

十一、了解与使用 react-redux

前面也说了,react-redux 其实就是 redux的升级版,对许多地方进行了优化,但在学习他之前,需要我们进行一些对 redux的优化知识。

620_1.png

1. 使用容器组件和UI组件

其目的就是为了把组件身上太多的活进行拆分,分为UI组件(内组件)容器组件(外组件),两个组件之间使用 props进行通信,对 store那边的请求状态,更改状态的活交给容器组件来干,而通过状态来编写页面,更新渲染等活,就交给 UI组件来干。

了解了这个后,就可以开始使用 react-redux

2. 安装 react-redux

npm i react-redux -S
复制代码

这个就不多说了。

3. 创建文件夹

对于容器组件,我们都是使用 containers文件夹进行存储。

  • containers ------ 用于存储容器组件的文件夹
  • redux ------ 用于存储 react-redux相关的文件夹

4. 创建容器组件

容器组件通过 react-reduxconnect() 方法进行创建。

// 引入 Count的 UI组件
import Count from "../../components/Count";
// 引入 connect用于连接 UI组件与 redux
import { connect } from 'react-redux'

// 该函数的返回值作为状态传递给 UI组件
function mapStateToProps(state){
    return {
        count: state,
    }
}
// 该函数的返回值作为操作状态的方法传递给 UI组件
function mapDispatchToProps(dispatch){
    return {
        add: data=>{
            console.log(1234);
        }
    }
}

export default connect(mapStateToProps, mapDispatchToProps)(Count);
复制代码

使用 connect( )( )创建并暴露一个 Count组件的容器组件。
调用 connect时有两个参数,且必须时函数。

  • mapStateToProps( state ): 该函数的返回值会作为 状态 传递给 UI组件
    • state: 参数 statereact-redux 默认操作好的 store.getState()
  • mapDispatchToProps( dispatch ): 该函数的返回值会作为 操作状态的方法 传递给 UI组件,有语法糖写法,传入个对象即可(看下面代码)。
    • dispatch: 参数 dispatchreact-redux 默认给的 store.dispatch 方法

这里只是为了讲解方便,使用时可以使用语法糖的。

注意的几个地方:

  1. 一般都是把 UI组件容器组件 写在一个文件中,至于存在哪个文件夹中看公司需求(一般都是 containers
  2. 容器组件里的 store不是通过引入使用,而是作为 props传递给容器组件的标签的。<Count store={store} />
  3. 语法糖写法:
// 引入 Count的 UI组件
import Count from "../../components/Count";
// 引入 connect用于连接 UI组件与 redux
import { connect } from 'react-redux'

// 精简写法
export default connect(
    state => ({count: state}),
    {
        add: data=> console.log(1234, data)
    }
)(Count);
复制代码

5. Provider 组件

如果你有许多个容器组件,那么每个容器组件都要传入 store,那么是不是觉得太繁琐了呢?所以 react-redux 提供了 Provider组件用于处理这个问题,只需要在根标签处使用并把 store传递进去,他就可以自动判断哪些组件需要使用 store并自动传递给它。

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import store from "./redux/store"
// 优化3: 使用自带的 Provider自动判断哪些组件需要使用 store,从而自动导入
import { Provider } from "react-redux";
ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
);
复制代码

6. 监听状态变化

有没有发现,我上面的代码已经没有再写 store.subscribe() 监听状态变化了,是因为我们创建容器组件的 connect已经帮我们进行监听了。

7. 容器组件之间通信

其实这个很简单,还记的 connect的第一个函数的第一个参数吗?传递给 UI组件的是 props,他的参数是 state,这个 statestore.getState()
可是此时你的 store.getState() 不再是一个单纯的值,而是所有 reducer的对象,所以我们可以在里面获取到其他容器组件的值。

export default connect(
    state => ({count: state.count, personLength: state.persons.length}),
    {
        increment: createIncrementAction,
        decrement: createDecrementAction,
        asyncIncrement: createIncrementAsyncAction,
    }
)(Count);
复制代码

8. 文件规范

创建了那么多东西,有的可以整合一下的,让我们来修整修整:

  • containers ------ 用于存放容器组件的文件夹(UI组件与容器组件写在一起)
    • Count.js ------ Count 的容器组件
    • Person.js ------ Person 的容器组件
  • redux ------ 用于存放 react-redux相关的文件夹
    • actions ------ 用于存放所有 action的文件夹

      • count.js ------ 用于存储 Count组件的 action
      • person.js ------ 用于存储 Person组件的 action
    • reducers ------ 用于存放所有 reducer的文件夹

      • count.js ------ 用于存储 Count组件的 reducer
      • person.js ------ 用于存储 Person组件的 reducer
      • index.js ------ 用于存储汇总的 reducer的文件(combineReducers()方法)
    • constant.js ------ 用于存储一些公用的命名的文件

    • store.js ------ react-reduxstore文件

9. 再说一遍,能不用就别用这东西!!

十二、React16.8 与一些扩展

1. lazy() 与 Suspense

之前的组件,都是一并加载的,这样会给服务器带来较大的负担,所以 react推出了 lazy()懒加载,进行组件的按需加载。
与之一起出来的是 Suspense,他解决的是组件在加载过程中还没加载出来时的白屏,用于展示其他的内容。

import React, { Component, lazy, Suspense } from 'react'
import {Route, Link} from "react-router-dom"

// import Home from "./Home";
// import About from "./About";
import Loading from "./Loading";
// lazy() 方法的参数是一个函数,返回需要加载的组件
const Home = lazy(()=>import("./Home"))
const About = lazy(()=>import("./About"))
export default class index extends Component {
    render() {
        return (
            <div>
                <Link to="/home">Home</Link>
                <Link to="/about">About</Link>
                <hr/>
                {/* Suspense 用于解决加载组件时的白屏,可以显示其他的内容,而其他内容不允许使用 lazy加载 */}
                <Suspense fallback={<Loading/>}>
                    <Route path="/home" component={Home}></Route>
                    <Route path="/about" component={About}></Route>
                </Suspense>
            </div>
        )
    }
}
复制代码
  • lazy 的参数是一个函数,使用 import 导入一个组件并返回
  • Suspense 的属性 fallback属性的属性值是 组件标签 而不是组件
  • Suspense所使用的组件不能使用 lazy进行懒加载。

2. Hook

React16.8可以说是给函数式组件一次春天,因为他有了Hook,可以实现一些 state、生命周期函数,refs等特性。
让我们一个个来看:

(1) stateHook

可以让函数式组件实现使用 state的特性:


export default function Demo({
    // useState返回一个数组,只有两个元素(只有两个元素)
    // 元素1 为状态,元素2 为更新状态的方法
    // 第一次调用时以及将count进行底层存储,所以 Demo重复渲染不会重置count数据
    const [count, setCount] = React.useState(0); // 初始值赋为 0
    const [name, setName] = React.useState("Tom");
    function add(){
        // 进行状态赋值
        // setCount(count + 1); // 写法1,直接将原来的状态值覆盖
        setCount(count=> count+1); // 写法2,参数为函数,接受原本的状态值,返回新的状态值,覆盖原来的状态
    }
    return (
        <div>
            <h3>名字:{name}</h3>
            <h2>当前求和:{count}</h2>
            <button onClick={add}>点击加一</button>    
        </div>
    )
}
复制代码

(2) EffectHook

可以让函数式组件实现类似生命周期钩子的特性:

export default function Demo({
    const [count, setCount] = React.useState(0);
    const [name, setName] = React.useState("Tom");
    function add(){
        setCount(count=> count+1);
    }
    function updName(){
        setName(name=>"Jerry");
    }
    function unmount(){
        ReactDOM.unmountComponentAtNode(document.querySelector("#root"));
    }
    // useEffect接收两个参数,第一个为函数体,第二个为检测的对象(数组),当检测的对象状态发生改变,就会触发函数
    // 不填写第二参数时,检测所有元素,相当于 componentDidUpdate生命周期函数
    React.useEffect(()=>{
        // console.log("asdf")
        let timer = setInterval(()=>{
            setCount(count=>count+1);
        },1000);
        return ()=>{        // 在 useEffect中的函数体里返回的函数,相当于 componentWillUnmount生命周期函数
            console.log("unmount")
            clearInterval(timer);
;        }
    },[]) // 数组为空是谁也不检测,只执行一次函数,相当于生命周期函数的 componentDidMount
    return (
        <div>
            <h2>当前求和:{count}</h2>
            <button onClick={add}>点击加一</button>
            <h2>当前名字:{name}</h2>
            <button onClick={updName}>修改名字为Jerry</button>
            <button onClick={unmount}>卸载组件</button>
        </div>
    )
}
复制代码

可以把 useEffect Hook 看作三个函数的结合:

  • componentDidMount
  • componentDidUpdate
  • componentWillUnmount

(3) refHook

refHook 可以让函数式组件实现类似 ref的特性

export default function Demo({
    const [count, setCount] = React.useState(0);
    function add(){
        setCount(count=> count+1);
    }
    function show(){
        // 获取文本框内容
        alert(myInput.current.value);
    }
    // 生成一个容器
    const myInput = React.useRef();
    return (
        <div>
            <h2>当前求和:{count}</h2>
            {/* 绑定容器 */}
            <input type="text" ref={myInput}/>
            <button onClick={add}>点击加一</button>
            <button onClick={show}>点击展示数据</button>
        </div>
    )
}
复制代码

这个就没啥难度。

3. Fragment

react渲染组件的时候,当你的组件越来越多时,你有没有发现你的 DOM层级越来越多,有些不美观,所以就出现了 Fragment
可以使用 Fragment标签代替组件的根标签,在 React解析的时候会被处理掉, 从而让生成出来的代码的层级更加简洁。

export default class Demo extends Component {
    render() {
        return (
            // 使用空标签可以达到一样的效果,但是空标签不允许包含任何的属性
            <Fragment key={1}>
                <input type="text"/>
                <input type="text"/>
            </Fragment>
        )
    }
}
复制代码

4. Context

Context是一种新的组件通信方式,常用于【祖组件】和【后代组件】之间通信。

// 1. 创建一个 Context容器对象
const UserNameContext = React.createContext();
// 1.1 拿到 Provider与 Consumer属性
const {Provider, Consumer} = UserNameContext;
export default class A extends Component {
    state={username"Tom"}
    render() {
        return (
            <div className="a">
                <h1>我是A组件</h1>
                <p>我的用户名是:{this.state.username}</p>
                {/* 2 使用组件,后代组件都能收到来自 value的值,就在 this上的 context属性上(需提前声明)  */}
                <Provider value={this.state.username}>
                    <B/>
                </Provider>
            </div>
        )
    }
}
class B extends Component {
    // 3. 声明接受 Context
    static contextType = UserNameContext;   // 此方法只适用于 类组件
    render() {
        console.log(this.context);  // Tom
        return (
            <div className="b">
                <h2>我是B组件</h2>
                <p>A的用户名是:{this.context}</p>
                <C/>
            </div>
        )
    }
}
function C({
    return (
        <div>
             <div className="c">
                <h3>我是C组件</h3>         
                {/* 3.2 使用 Consumer组件进行声明接受(类组件和函数式组件都可以)  */}
                <Consumer>
                    {value=> ("A的用户名是:" + value)}
                </Consumer>
             </div>
        </div>
    )
}
复制代码

5. PureComponent

在组件中,只要执行了 setState(),即使没更新状态数据,组件也会重新 render()
只要组件重新 render(),就会自动重新 render()子组件,纵使子组件没有使用到父组件任何数据。
这两种情况都会导致重复渲染,使得效率低下。

效率高的做法:只有组件的stateprops发生变化时菜重新render()

  • 解决方法1:通过生命周期函数 shouldComponentUpdate() 进行数据判断在进行重新渲染
  • 解决方法2:类组件通过继承 PureComponent组件,自动进行数据判断(浅对比---判断地址值)(常用)
import React, { Component, PureComponent } from 'react'
export default class Parent extends PureComponent {
    state = {carName"奔驰"}
    changeCar = ()=>{
        this.setState(state=>({carName:"迈巴赫"}));
    }
    // shouldComponentUpdate有两个参数,分别是准备修改的 props和 state
    // shouldComponentUp date(nextProps, nextState){
    //     console.log(nextProps, nextState);  // 目标要修改的props和state
    //     console.log(this.props, this.state);   // 还未修改原本的props和state
    //     return !this.state.carName === nextState.carName;
    // }
    render() {
        console.log("parent render");
        return (
            <div className="parent">
                <h1>Parent</h1>
                <p>我的车是:{this.state.carName}</p>
                <button onClick={this.changeCar}>点击换车</button>
                <Child/>
            </div>
        )
    }
}
class Child extends PureComponent {
    
    // shouldComponentUpdate(nextProps){
    //     return !this.props.carName === nextProps.carName
    // }
    render() {
        console.log("child render");
        return (
            <div className="child">
                <h2>Child</h2>
                {/* <p>父亲的车是:{this.props.carName}</p> */}
            </div>
        )
    }
}
复制代码

6. 父子组件

不多说,直接上代码:

import React, { Component, PureComponent } from 'react'
import "./index.css"

export default class Parent extends PureComponent {
    state = {carName: "奔驰"}
    changeCar = ()=>{
        this.setState(state=>({carName:"迈巴赫"}));
    }
    render() {
        console.log("parent render");
        return (
            <div className="parent">
                <h1>Parent</h1>
                <p>我的车是:{this.state.carName}</p>
                {/* A组件 与 B组件 形成父子组件的第二种方法 */}
                {/* <A>
                    <B/>
                </A> */}
                {/* 类似于 Vue的插槽 */}
                <A render={(name)=><B name={name}/>}/>  
            </div>
        )
    }
}

class A extends PureComponent {
    render() {
        console.log("A render");
        return (
            <div className="a">
                <h2>A</h2>
                {/* A组件 与 B组件 形成父子组件的第一种方式 */}
                {/* <B/> */}
                {/* {this.props.children} */}
                {this.props.render("Tom")}
            </div>
        )
    }
}
class B extends PureComponent {
    render() {
        console.log("B render");
        return (
            <div className="b">
                <h2>B</h2>
                <p>{this.props.name}</p>
            </div>
        )
    }
}
复制代码

7. ErrorBoundary

当你的组件存在父子组件关系时,如果说你的子组件出现了错误,那么会导致父组件一并崩掉,那有没有什么办法,可以把错误控制在一个组件里,不让他扩散呢?
答案是有的,有两个函数:

  • getDerivedStateFromError(error)
  • componentDidCatch(error, info)

上代码:

import React, { Component, Fragment } from 'react'
import Child from "./Child";
// 错误边界即把组件的错误信息控制在一个组件中,不使他扩散而导致程序崩溃
export default class Person extends Component {
    state = {
        hasError"",   // 用于标识子组件是否产生错误
    }
    // 当子组件发生错误时会触发该生命周期函数,且参数为错误信息
    // 只适用于生产环境,只能捕获后代组件生命周期产生的错误
    static getDerivedStateFromError(error){
        // 一般用于处理错误出现时返回给用户展示的东西
        console.log("出错了");
        console.log(error);
    }
    // 组件渲染过程中出错就会触发该生命周期函数
    componentDidCatch(error, info){
        // 一般用于统计错误,反馈给雾浮起,用于通知程序员进行bug修改
        console.log("渲染组件出错");
        console.log(error, info)
    }
    render() {
        return (
            <Fragment>
                <h2>我是Parent组件</h2>
                {
                    this.state.hasError ? 
                        <h2>当前网络不大行,建议买高级网络套餐好吧</h2> :
                        <Child/>                
                }
            </Fragment>
        )
    }
}
复制代码

8. 组件通信方式总结

  • 组件之间的关系:
    1. 父子组件
    2. 兄弟组件
    3. 祖孙组件(跨级组件)
  • 几种通信方式:
    1. props
      1. children props
      2. render props
    2. 消息订阅 pubsub, event
    3. 集中式管理 redux, dva, react-redux
    4. conText 生产者, 消费者模式
  • 较好的搭配方式:
    • 父子组件:props
    • 兄弟组件: 消息订阅发布,集中式管理
    • 祖孙组件:消息订阅发布,集中式管理,conText

十三、最后

这些就是我学习React的一些学习笔记了,日后如果还有其他的内容的话应该会深究后写成单独的文章了。

新人上路,还请多多包含。
我是MoonLight,一个初出茅庐的小前端。

文章分类
前端
文章标签