React学习笔记

一、React 简介

React 是用于构建用户界面的 JavaScript 库,是一个将数据渲染为 HTML 视图的开源 JavaScript 库,只关注操作 DOM 和呈现页面

React 由 Facebook 开发且开源

1、原生 JS 的痛点

(1)原生 JavaScript 操作 DOM 繁琐、效率低(DOM-API 操作 UI),

(2)使用 JavaScript 直接操作 DOM,浏览器会进行大量的重绘重排

(3)原生 JavaScript 没有组件化编码方案,代码复用率低

2、React 的特点

(1)采用组件化模式,声明式编码,关注结果,过程能自动完成(而非命令式编码,需要关注中间过程),提高开发效率及组件复用率

(2)在 React Native 中可以使用 React 语法进行移动端原生应用开发(利用 js 完成安卓和 ios 移动端开发)

(3)使用虚拟 DOM + 优秀的 Diffing 算法,尽量减少与真实 DOM 的交互

3、React 高效的原因

(1)使用虚拟 DOM,不总是直接操作页面真实 DOM

(2)DOM Diffing 算法,最小化页面重绘

4、相关 js 库

babel.min.js:解析 JSX 代码语法转为 JS 代码的库

react.development.js:React 核心库

react-dom.development.js:提供操作 DOM 的 react 扩展库

要先引入 react-dom.development.js,后引入 react.development.js

5、虚拟 DOM 和 真实 DOM

虚拟 DOM 本质是 Object 类型的对象(一般对象)

虚拟 DOM 比较 “轻”,属性较少,而真实 DOM 较 “重”,因为虚拟 DOM 是 React 内部在用,无需真实 DOM 上那么多的属性(如 style 等)

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

6、创建虚拟 DOM

方式一:纯 js(一般不用)

<body>
    <div id="test"></div>
    <script type="text/javascript" src="./react.development.js"></script>
    <script type="text/javascript" src="./react-dom.development.js"></script>
    <script type="text/JavaScript"> /* 此处写 JavaScript */
        //1.使用 js 创建虚拟 DOM
        const VDOM = React.createElementt('h1',{id:'title'},'Hello')
        //2.渲染虚拟 DOM 到页面
        ReactDOM.render(VDOM, document.getElementById('test'))
    </script>
</body>

方式二:JSX

<body>
    <div id="test"></div>
    <script type="text/javascript" src="./react.development.js"></script>
    <script type="text/javascript" src="./react-dom.development.js"></script>
    <script type="text/javascript" src="./babel.min.js"></script>
    <script type="text/babel"> /* 此处一定要写 babel */
        //1.使用 jsx 创建虚拟 DOM
        const VDOM = <h1>Hello</h1> /* 此处一定不要写引号 */
        //2.渲染虚拟 DOM 到页面
        ReactDOM.render(VDOM, document.getElementById('test'))
    </script>
</body>

当要创建嵌套标签的虚拟 DOM 时,使用 js 很繁琐,而使用 jsx 就像写 html 一样很方便,babel 编译后依然是使用 js 创建虚拟 DOM 时的语法,因此 JSX 创建虚拟 DOM 就是 js 创建虚拟 DOM 写法的语法糖

7、JSX

JSX(JavaScript XML)是 react 定义的一种类似于 XML 的 JS 扩展语法(JS + XML)

(XML 早期用于存储和传输数据,后来用 JSON 用的更多更方便简单)

JSX 本质是 React.createElementt(标签名,{属性名:属性值},'标签体内容') 方法的语法糖

作用:用来简化创建虚拟 DOM,var ele = <h1>Hello</h1>,注意它不是字符串也不是 HTML/XML 标签,它最终产生的就是一个 JS 对象

JSX 中标签名任意,可以是 HTML 标签或其他标签

jsx 语法规则

(1)定义虚拟 DOM 时不要写引号

(2)标签中混入 JS 表达式时要用 {}

(3)样式的类名指定不要用 class,要用 className

(4)内联样式要用 style={{key:value}} 的形式写

(5)虚拟 DOM 必须只有一个根标签

(6)标签必须闭合,如 <input /><input></input>

(7)标签首字母

1)若是小写字母开头,则将该标签转为 html 中同名元素,若 html 中无该编起对应的同名元素,则报错
2)若大写字母开头,react 就去渲染对应的组件,若组件没有定义,则报错

(8)给一个{数组} react 会自动遍历,但给一个对象不会

<head>
    <style>
        .title{
            background-color:red;
            width: 100px;
        }
    </style>
</head>

<body>
    <div id="test"></div>

    <script type="text/javascript" src="./react.development.js"></script>
    <script type="text/javascript" src="./react-dom.development.js"></script>
    <script type="text/javascript" src="./babel.min.js"></script>
    <script type="text/babel">
        const myId = 'xXx';
        const myData = '123aBc';
        const VDOM = (
            <h1 className="title" id={myId.toLowerCase()}>
                <span style={{color:'white',fontSize:'20px'}}>{{myData.toLowerCase()}}</span>
            </h1>
        )
        ReactDOM.render(VDOM, document.getElementById('test'))
    </script>
</body>

区分 js 语句(代码)与 js 表达式

表达式:一个表达式会产生一个值(即通过 const x = 来接能接到值就是表达式,否则不是),可放在任何一个需要值的地方,如

a
a+b
demo(1) //函数调用表达式
arr.map()
function test(){}
console.log()

而语句(代码)如

if(){}
for(){}
switch(){case:xxx}

二、模块与组件、模块化与组件化

模块

模块是向外提供特定功能的 js 程序,一般就是一个 js 文件

为什么要拆成模块?:随着业务逻辑增加,代码越来越多且复杂

作用:复用 js,简化 js 的编写(每个 js 文件不那么庞大),提高 js 运行效率

组件

组件用来实现局部功能效果的代码和资源的集合(html/css/js/image等)

为什么用组件?:一个界面的功能更复杂

作用:复用编码,简化项目编码,提高运行效率

模块化

当应用的 js 都以模块来编写的,这个应用就是一个模块化的应用

组件化

当应用是以多组件的方式实现,这个应用就是一个组件化的应用

三、React 面向组件编程

可现在 Chrome 浏览器中安装插件 React Developer Tools

简单组件与复杂组件

若组件中有状态 state 就是复杂组件

组件的数据存在 state 里,组件的状态 state 驱动页面

定义组件

方式一:函数式组件

用函数定义的组件适用于简单组件

<body>
    <div id="test"></div>

    <script type="text/javascript" src="./react.development.js"></script>
    <script type="text/javascript" src="./react-dom.development.js"></script>
    <script type="text/javascript" src="./babel.min.js"></script>
    <script type="text/babel">
        //1.创建函数式组件
        function Demo(){ //组件首字母要大写
            console.log(this); //此处的 this 是 undefined,因为 babel 编译后开启了严格模式(禁止自定义函数中 this 指向 window)
            return <h1>xxx</h1>
        }
        //2.渲染组件到页面
        ReactDOM.render(<Demo/>, document.getElementById('test'))
    </script>
</body>

执行了 ReactDOM.render(<demo/>...) 后发生了什么?

— 1.React 解析组件标签,找到了 Demo 组件

— 2.发现组件是使用函数定义的,随后调用该函数,将返回的虚拟 DOM 转为真实 DOM,随后呈现在页面中

方式二:类式组件

用类定义的组件适用于复杂组件

构造器 constructor 调用 1 次

render 调用 1+n 次(其中 1 是初始化的那次,n 是状态 state 更新的次数)

事件函数触发几次(如点击)调用几次

复习类相关知识

类中的构造器不是必须写的,要对实例进行初始化操作时(如添加指定属性)才写

类中的构造器方法中的 this 是类的实例对象

类中的一般方法是放在了类型的原型对象上(__proto__),供实例使用,通过实例对象调用类中的方法式,方法中的 this 就是实例对象

若 A 类继承了 B 类,且 A 类中写了构造器,则 A 类构造器中的 super 是必须调用的

类中的方法默认开启了局部的严格模式,所以方法中的 this 为 undefined

且在外部通过 const x = 实例对象.方法,然后 x() 进行调用此时依然不是通过实例对象调用,而是直接调用,因此调用的方法中的 this 不是实例对象

类中可以直接写赋值语句 `a:1`,相当于给实例对象自身上添加一个属性 a 值为 1

类中箭头函数中没有this,但若使用this不报错,而是去外部找this

创建类式组件

<body>
    <div id="test"></div>

    <script type="text/javascript" src="./react.development.js"></script>
    <script type="text/javascript" src="./react-dom.development.js"></script>
    <script type="text/javascript" src="./babel.min.js"></script>
    <script type="text/babel">
        //1.创建类式组件
        class MyComponent extends React.Component{ //必须继承React.Component
            render(){  //必须有 render 函数,render 是放在 MyComponent 类的原型对象上,供实例使用,render 中的 this 是 MyComponent 实例对象(也叫 MyComponent 实例对象)
                return <h1>xxx</h1>
            }
        }
        //2.渲染组件到页面
        ReactDOM.render(<MyComponent/>, document.getElementById('test'))
    </script>
</body>

执行了 ReactDOM.render(<MyComponent/>...) 后发生了什么?

— 1.React 解析组件标签,找到了 MyComponent 组件

— 2.发现组件是使用类定义的,随后 new 出来该类的实例,并通过该实例调用到原型上的 render 方法

— 3.将 render 返回的虚拟 DOM 转为真实 DOM,随后呈现在页面中

组件实例(用类定义的组件)的三大核心属性

1、state

state 是组件实例对象最重要的属性,值是对象,可包含多个 key-value 的组合

组件被称为“状态机”,通过更新组件的 state 来更新对应的页面显示(重新渲染组件)

注意:(1)组件中 render 方法中的 this 为组件实例对象

(2)组件自定义的方法中 this 为 undefined 如何解决?

a)强制绑定 this:通过函数对象的 bind()
b)赋值语句+箭头函数

(3)状态数据不能直接修改或更新,需要使用 this.setState({state中属性:值}),且更新是一种合并(重名的覆盖掉),不是替换

<script type="text/babel">
    //1.创建类式组件
    class Weather extends React.Component{
        constructor(props){
            super(props)
            //初始化状态
            this.state={isHot:true}
        }
        render(){
            return <h1>今天天气{this.state.isHot?'炎热':'凉爽'}</h1>
        }
    }
    //2.渲染组件到页面
    ReactDOM.render(<Weather/>, document.getElementById('test'))
</script> 

state 的简写方式

<script type="text/babel">
    //1.创建类式组件
    class Weather extends React.Component{
        //constructor(props){
            //super(props)
            //this.state={isHot:true}
        //}
        state={isHot:true}
        render(){
            return <h1 onClick={this.demo}>今天天气{this.state.isHot?'炎热':'凉爽'}</h1>
        }
        //自定义方法:用赋值语句的形式+箭头函数,因为类中箭头函数中没有this,但若使用this不报错,而是去外部找this(这里即实例对象),这样在定义事件方法时就不要再this.demo=this.demo.bind(this)
        demo=()=>{
            console.log('哈哈哈')
            this.setState({isHot:!isHot})
        }

    }
    //2.渲染组件到页面
    ReactDOM.render(<Weather/>, document.getElementById('test'))
</script> 

1、props

每个组件对象都会有 props(properties的简写)属性

组件标签的所有属性都保存在 props 中

作用:

—(1)通过标签属性从组件外向组件内传递变化的数据

—(2)注意:组件内部不要修改 props 数据

<script type="text/babel">
    //1.创建类式组件
    class Person extends React.Component{
        render(){
            return (
                <ul>
                    <li>姓名:{this.props.name}</li>
                    <li>性别:{this.props.ex</li>
                </ul>
            )
        }
    }
    //2.渲染组件到页面
    ReactDOM.render(<Person name='xxx' sex='男'/>, document.getElementById('test'))
    ReactDOM.render(<Person name='xxxxx' sex='女'/>, document.getElementById('test1'))
</script> 

注意:props 是只读的

props 的批量传递

在标签属性中使用 {...对象},react + babel 允许使用展开运算符展开对象,...对象 这也仅适用于标签属性的传递,如在 console.log(...对象) 就无效,啥也不输出,注意这里的花括号表示里面表达式,和 js 中拷贝对象的含义不同

const p ={name:'xxx',sex:'男'}
ReactDOM.render(<Person {...p}/>, document.getElementById('test'))

对 props 进行限制

需要引入 prop-types,用于对组件标签属性进行类型、必要性、默认值限制,引入后全局多了个对象 PropTypes

<script type="text/javascript" src="./react.development.js"></script>
<script type="text/javascript" src="./react-dom.development.js"></script>
<script type="text/javascript" src="./babel.min.js"></script>
<script type="text/javascript" src="./prop-types.js"></script>

<script type="text/babel">
    //1.创建类式组件
    class Person extends React.Component{
        render(){
            return (
                <ul>
                    <li>姓名:{this.props.name}</li>
                    <li>性别:{this.props.ex</li>
                </ul>
            )
        }
    }

    Person.propTypes = {
        name:PropTypes.string.isRequired, //使得 name 必传且为字符串。在 React v15.5 之前使用 name:React.PropTypes.string.isRequired,且不需要引入 prop-types,v15.5 之后单独封装成 prop-types,减轻 React 重量
        sex:PropTypes.string,
        age:PropTypes.number,
        speak:PropTypes.func
    }
    Person.defaultProps = { //设置不传某属性时使用的默认值
        sex:'男',
        age:18
    }
    //2.渲染组件到页面
    ReactDOM.render(<Person name='xxx' sex='男' speak={speak}/>, document.getElementById('test'))

    function speak(){
        console.log('xxx')
    }
</script> 

props 的简写方式

组件对象.propTypes组件对象.defaultProps 放到类内部,前面加上 static 关键字

<script type="text/javascript" src="./react.development.js"></script>
<script type="text/javascript" src="./react-dom.development.js"></script>
<script type="text/javascript" src="./babel.min.js"></script>
<script type="text/javascript" src="./prop-types.js"></script>

<script type="text/babel">
    //1.创建类式组件
    class Person extends React.Component{
        static propTypes = {
            name:PropTypes.string.isRequired, //使得 name 必传且为字符串
            sex:PropTypes.string,
            age:PropTypes.number,
            speak:PropTypes.func
        }
        static defaultProps = { //设置不传某属性时使用的默认值
            sex:'男',
            age:18
        }
        render(){
            return (
                <ul>
                    <li>姓名:{this.props.name}</li>
                    <li>性别:{this.props.ex</li>
                </ul>
            )
        }
    }
    //2.渲染组件到页面
    ReactDOM.render(<Person name='xxx' sex='男' speak={speak}/>, document.getElementById('test'))

    function speak(){
        console.log('xxx')
    }
</script> 

类式组件中的构造器与 props

在 React 中构造函数仅用于以下两种情况:

—(1)通过 this.state = {...} 来初始化 state(但这种初始化方式可在构造器外直接使用 state = {} 来赋值)

—(2)为事件处理函数绑定实例 this.demo=this.demo.bind(this)(但这种绑定可直接在定义函数时使用箭头函数来省略这步)

因此构造器不是必须的

但是有时需要接收和访问实例对象的 props 值,需要在构造器中接收 props 并传递给 super(props),若要访问 this.props 则必须将 props 传给 super

即构造器是否接收 props,是否传递给 super 取决于是否希望在构造器中通过 this 访问 props

constructor(props){
    super(props)
    console.log(this.props)
}

函数式组件使用 props

若不是通过类创建组件,而是通过函数定义的组件,则无法使用 state 和 refs(除非使用最新版 React 中的 hooks),因为没有类就没有实例就没有 this,但可以使用 props

<script type="text/babel">
    //1.创建函数式组件
    function Person(props){
        const {name,age,sex} = props
        return (
            <ul>
                <li>姓名:{this.props.name}</li>
                <li>性别:{this.props.ex</li>
            </ul>
        )
    }
    Person.propTypes = {
        name:PropTypes.string.isRequired, //使得 name 必传且为字符串
        sex:PropTypes.string,
        age:PropTypes.number,
        speak:PropTypes.func
    }
    Person.defaultProps = { //设置不传某属性时使用的默认值
        sex:'男',
        age:18
    }
    //2.渲染组件到页面
    ReactDOM.render(<Person name="x"/>, document.getElementById('test'))
</script>

3、refs

组件内的标签可以定义 ref 属性来标识自己,这样就可以不使用 id 和 getElementById,而是使用 this.refs.ref名 来替代(注意这样拿到的是真实 DOM 节点,而不是虚拟 DOM 节点)

定义形式:

—(1)字符串形式的 ref <input ref="input1"/>(因为存在效率问题,这种方式以不被官方推荐使用,可能在未来 React 版本中移除)

<script type="text/babel">
    //1.创建类式组件
    class Demo extends React.Component{
        showData=()=>{
            const {input1} = this.refs
            console.log(input1.value)
        }
        showData2=()=>{
            const {input2} = this.refs
            console.log(input2.value)
        }
        render(){
            return(
                <input ref="input1" type="text"/>
                <button onClick={this.showData}>点击</button>
                <input ref="input2" onBlur={this.showData2} type="text"/>
            )
        }

    }
    //2.渲染组件到页面
    ReactDOM.render(<Weather/>, document.getElementById('test'))
</script> 

—(2)回调形式的 ref <input ref={(c)=>{this.input1=c}}/> 执行 render 函数时就会执行该回调函数,其中参数 c 表示当前所处 DOM 节点,把当前 DOM 节点挂在实例自身(this)上,并取名为 input1

<script type="text/babel">
    //1.创建类式组件
    class Demo extends React.Component{
        showData=()=>{
            const {input1} = this
            console.log(input1.value)
        }
        showData2=()=>{
            const {input2} = this
            console.log(input2.value)
        }
        render(){
            return(
                <input ref={(c)=>{this.input1 = c}} type="text"/>
                <button onClick={this.showData}>点击</button>
                <input ref={(c)=>{this.input2 = c}} onBlur={this.showData2} type="text"/>
            )
        }

    }
    //2.渲染组件到页面
    ReactDOM.render(<Weather/>, document.getElementById('test'))
</script> 

注意:若 ref 回调函数是以内联函数的方式定义,在更新过程中(第二次触发 render() 开始)它会被执行两次,第一次传入参数 null,第二次才传入 DOM 元素,因为每次渲染时会创建一个新的函数实例,所以 React 会传入 null 清空旧的 ref 再设置新的

通过 ref 回调函数定义或 class 的绑定函数的方式可避免上述问题,但大多数情况下它是无关紧要的,可以使用内联方式

<script type="text/babel">
    //1.创建类式组件
    class Demo extends React.Component{
        showData=()=>{
            const {input1} = this
            console.log(input1.value)
        }
        saveInput=(c)=>{
            this.input1 = c
            console.log(c)
        }
        render(){
            return(
                <input ref={this.saveInput} type="text"/>
                <button onClick={this.showData}>点击</button>
                <input ref={(c)=>{this.input2 = c}} onBlur={this.showData2} type="text"/>
            )
        }

    }
    //2.渲染组件到页面
    ReactDOM.render(<Weather/>, document.getElementById('test'))
</script> 

—(3)createRef 创建 ref 容器 myRef = React.createRef(),调用后会返回一个容器,该容器可存储被 ref 标识的节点,把当前 ref 所在 DOM 节点直接存储到容器中(官方推荐使用)

注意:该容器是 “专人专用” 的,如果两个 DOM 节点都放在同一个容器中,后放入的会覆盖前面的

<script type="text/babel">
    //1.创建类式组件
    class Demo extends React.Component{
        myRef = React.createRef() //把这个容器挂在组件实例自身上
        myRef2 = React.createRef()
        showData=()=>{
            console.log(this.myRef.current.value)
        }
        showData2=()=>{
            console.log(this.myRef2.current.value)
        }
        render(){
            return(
                <input ref={this.myRef} type="text"/>
                <button onClick={this.showData}>点击</button>
                <input onBlur={this.showData2} ref={this.myRef2} type="text"/>
            )
        }
    }
    //2.渲染组件到页面
    ReactDOM.render(<Weather/>, document.getElementById('test'))
</script> 

注意:不要过度使用 ref,如当发生事件的元素和当前操作的元素是同一个就可以不使用 ref,而是 event.target 来获取,如 <input onBlur={this.showData2} ref={this.myRef2} type="text"/> 可换成 <input onBlur={this.showData2} type="text"/> 在 showData2 中定义如下

showData2=(event)=>{
    console.log(event.target.value)
}

事件绑定

在 React 中使用onClickonBlur 等绑定事件,注意在原生 js 中是 onclickonblur 

方式一:

<script type="text/babel">
    //1.创建类式组件
    class Weather extends React.Component{
        constructor(props){
            super(props)
            this.state={isHot:true}
        }
        render(){
            return <h1 onClick={demo}>今天天气{this.state.isHot?'炎热':'凉爽'}</h1>
        }
    }
    //2.渲染组件到页面
    ReactDOM.render(<Weather/>, document.getElementById('test'))

    function demo(){
        console.log('哈哈哈')
    }
</script> 

方式二:

<script type="text/babel">
    //1.创建类式组件
    class Weather extends React.Component{
        constructor(props){
            super(props)
            this.state={isHot:true}
            this.demo1=this.demo.bind(this)//生成一个新函数,这个新函数的this为实例对象,并把这个新函数取名为demo1挂在实例上,解决 demo 中 this 指向问题
        }
        render(){
            return <h1 onClick={this.demo1}>今天天气{this.state.isHot?'炎热':'凉爽'}</h1> //这里 demo 在 Weather 原型对象上,供实例使用,由于 demo 是作为 onClick 的回调,所以不是通过实例调用的,是直接调用,且类中的方法默认开启局部的严格模式,因此 demo 中的 this 为 undefined,所以需要 this.demo=this.demo.bind(this)
        }
        demo(){
            console.log('哈哈哈')
            this.setState({isHot:!isHot})
        }
    }
    //2.渲染组件到页面
    ReactDOM.render(<Weather/>, document.getElementById('test'))
</script> 

方式三:

<script type="text/babel">
    //1.创建类式组件
    class Weather extends React.Component{
        //constructor(props){
            //super(props)
            //this.state={isHot:true}
        //}
        state={isHot:true}
        render(){
            return <h1 onClick={this.demo}>今天天气{this.state.isHot?'炎热':'凉爽'}</h1>
        }
        //自定义方法:用赋值语句的形式+箭头函数,因为类中箭头函数中没有this,但若使用this不报错,而是去外部找this(这里即实例对象),这样在定义事件方法时就不要再this.demo=this.demo.bind(this)
        demo=()=>{
            console.log('哈哈哈')
            this.setState({isHot:!isHot})
        }

    }
    //2.渲染组件到页面
    ReactDOM.render(<Weather/>, document.getElementById('test'))
</script> 

事件处理

(1)通过 onXxx 属性指定事件处理函数(注意大小写)

— React 使用的是自定义(合成)事件(如 onClick),而不是使用原生 DOM 事件(如 onclick),这是为了更好的兼容性

— React 中的事件是通过事件委托(冒泡)方式处理的(委托给组件最外层的元素),这是为了更高效

(2)通过 event.target 得到发生事件的 DOM 元素对象,这可以解决过度使用 ref 的情况

非受控组件

页面中所有输入类的 DOM 节点(如 checkbox、radio 等)现用现取就是非受控组件

<script type="text/babel">
    //1.创建类式组件
    class Login extends React.Component{
        handleSubmit=(event)=>{
            event.preventDefault() //阻止表单提交这一默认事件
            const {username,password}=this
            alert(${username.value},${password.value})
        }
        render(){
            return(
                <form onSubmit={this.handleSubmit}>
                    用户名:<input ref={c=>this.username=c} type="text" name="username"/>
                    密码:<input ref={c=>this.password=c} type="password" name="password"/>
                    <button>登录</button>
                </form>
            )
        }    
    }
    //2.渲染组件到页面
    ReactDOM.render(<Login/>, document.getElementById('test'))
</script> 

受控组件

页面中所有输入类的 DOM 节点,随着输入的变化存入维护到状态 state 中,需要时从 state 中取,这种就属于受控组件

受控组件的优势在于可以省略掉 ref

<script type="text/babel">
    //1.创建类式组件
    class Login extends React.Component{
        state={
            username:''.
            password:''
        }
        saveUsername=(event)=>{
            this.setState({username:event.target.value})
        }
        savePassword=(event)=>{
            this.setState({password:event.target.value})
        }
        handleSubmit=(event)=>{
            event.preventDefault() //阻止表单提交这一默认事件
            const {username,password}=this.state
            alert(${username},${password})
        }
        render(){
            return(
                <form onSubmit={this.handleSubmit}>
                    用户名:<input  onChange={this.saveUsername} type="text" name="username"/>
                    密码:<input  onChange={this.savePassword} type="password" name="password"/>
                    <button>登录</button>
                </form>
            )
        }    
    }
    //2.渲染组件到页面
    ReactDOM.render(<Login/>, document.getElementById('test'))
</script> 

注意上述代码中是把 this.saveUsername 函数传给 onChange 作为事件回调

高阶函数

高阶函数:若一个函数符合下面两个规范中的任何一个,那么该函数就是高阶函数

(1)若 A 函数接收的参数是个函数,那么 A 就可称之为高阶函数

(2)若 A 函数调用的返回值依然是个函数,那么 A 就可称之为高阶函数

常见的高阶函数有:Promise、setTimeout、setInterval、arr.map() 等

函数柯里化

函数的柯里化:通过函数调用继续返回函数的方式,实现多次接收参数最后统一处理的函数编码形式,如

function sum(a){
    return (b)=>{
        return (c)=>{
            return a+b+c
        }
    }
}

例子:

<script type="text/babel">
    //1.创建类式组件
    class Login extends React.Component{
        state={
            username:''.
            password:''
        }
        saveFormData=(dataType)=>{
            return (event)=>{
                this.setState({[dataType]:event.target.value})
            }
        }
        handleSubmit=(event)=>{
            event.preventDefault() //阻止表单提交这一默认事件
            const {username,password}=this.state
            alert(${username},${password})
        }
        render(){
            return(
                <form onSubmit={this.handleSubmit}>
                    用户名:<input  onChange={this.saveFormData('username')} type="text" name="username"/>
                    密码:<input  onChange={this.saveFormData('password')} type="password" name="password"/>
                    <button>登录</button>
                </form>
            )
        }    
    }
    //2.渲染组件到页面
    ReactDOM.render(<Login/>, document.getElementById('test'))
</script> 

注意上述代码中 onChange={this.saveFormData('username') 的 saveFormData 后加了括号,所以表示把 saveFormData 的返回值传给 onChange,而不是把 saveFormData 函数传给 onChange

上述代码也可不用函数柯里化实现

<script type="text/babel">
    //1.创建类式组件
    class Login extends React.Component{
        state={
            username:''.
            password:''
        }
        saveFormData=(dataType,event)=>{
            this.setState({[dataType]:event.target.value})
        }
        handleSubmit=(event)=>{
            event.preventDefault() //阻止表单提交这一默认事件
            const {username,password}=this.state
            alert(${username},${password})
        }
        render(){
            return(
                <form onSubmit={this.handleSubmit}>
                    用户名:<input  onChange={event => this.saveFormData('username',event)} type="text" name="username"/>
                    密码:<input  onChange={event => this.saveFormData('password',event)} type="password" name="password"/>
                    <button>登录</button>
                </form>
            )
        }    
    }
    //2.渲染组件到页面
    ReactDOM.render(<Login/>, document.getElementById('test'))
</script> 

组件的生命周期

组件对象从创建到死亡会经历特定阶段

React 组件对象包含一系列勾子函数(生命周期回调函数),在特定的时刻调用

在定义组件时,在特定的生命周期回调函数中做特定工作

生命周期钩子中的 this 都是组件实例对象

render:初始化渲染、状态更新之后调用

componentDidMount:组件挂在页面之后调用

componentWillReceiveProps(props):子组件将要接收新的 props 的钩子,也可以不传参,但是父子组件第一次 render 时不会调用该函数,当父组件第二次 render 时给子组件传 props 才会调用该函数

shouldComponentUpdate:控制组件更新的 “阀门” 表示组件是否应该被更新,调用 setState 后会调用 shouldComponentUpdate,返回布尔值,默认返回 true,若写了 shouldComponentUpdate 函数就要写返回值

componentWillUpdate:组件将要更新的钩子

componentDidUpdate:组件更新完毕的钩子

componentWillUnmount:组件将要卸载时调用

旧版的 React 生命周期

旧版生命周期

生命周期的三个阶段(旧版):

(1)初始化阶段:由 ReactDOM.render() 触发—初次渲染

1. constructor()
2. componentWillMount()
3. render()
4. componentDidMount() ==> 常用,一般在这个钩子中做一些初始化的事,如开启定时器、发送网络请求、订阅消息

(2)更新阶段:由组件内部 this.setState() 或父组件重新 render 触发

1. shouldComponentUpdate()
2. componentWillUpdate()
3. render()  ==> 必须使用的一个
4. componentDidUpdate()

(3)卸载组件:由 ReactDOM.unmountComponentAtNode() 触发

1. componentWillUnmount() ==> 常用,一般在这个钩子中做一些收尾的事,例如:关闭定时器、取消订阅消息

例子:

h2 标签中的文字2s逐渐变淡直到完全透明后又变黑再逐渐变淡,而当点击按钮时删除 h2

<script type="text/babel">
    //1.创建类式组件
    class Life extends React.Component{
        state={opacity:1}
        death=()=>{
            //卸载组件
            ReactDOM.unmountComponentAtNode(document.getElementById('test'))
        }
        //组件挂载完毕
        componentDidMount(){
            this.timer = setInterval(()=>{
                let {opacity} = this.state
                opacity -= 0.1
                if(opacity <= 0) opacity = 1
                this.setState({opacity}) //这里相当于 this.setState({opacity:opacity}),因为俩 opacity 同名所以可以简写
            },200)
        }
        //组件将要卸载
        componentWillUnmount(){
            clearInterval(this.timer)
        }
        render(){

            return(
                <div>
                    <h2 style={{opacity:this.state.opacity}}>xxx</h2>
                    <button onClick={this.death}>xx</button>
                </div>
            )
        }    
    }
    //2.渲染组件到页面
    ReactDOM.render(<Life/>, document.getElementById('test'))
</script> 

例子:

每点击一次按钮就 + 1

<script type="text/babel">
    //1.创建类式组件
    class Count extends React.Component{
        constructor(props){
            console.log('constructor')
            super(props)
            this.state={count:0}
        }

        add=()=>{
            const {count} = this.state
            this.setState({count:count+1})
        }
        force=()=>{
            this.forceUpdate() //强制更新
        }
        //组件将要挂载
        componentWillMount(){
            console.log('componentWillMount')
        }
        //组件挂载完毕
        componentDidMount(){
            console.log('componentDidMount')
        }
        //组件将要卸载
        componentWillUnmount(){

        }
        render(){
            console.log('render')
            return(
                <div>
                    <h2>当前求和为:{count}</h2>
                    <button onClick={this.add}>点击+1</button>
                    <button onClick={this.force}>强制更新</button>
                </div>
            )
        }    
    }
    //2.渲染组件到页面
    ReactDOM.render(<Count/>, document.getElementById('test'))
</script> 

新版的 React 生命周期(v17.0.1之后)

与旧版本不同的是 componentWillReceiveProps、componentWillMount、componentWillUpdate 重命名为 UNSAFE_componentWillReceiveProps、UNSAFE_componentWillMount、UNSAFE_componentWillUpdate,且 v18.x 版本开始必须加 UNSAFE_ 才行

注意:UNSAFE_ 不是指安全性,而是表示使用这些生命周期的代码在 React 的未来版本中更有可能出现 bug,尤其是在启用异步渲染后

新旧版本对比:新版本废弃了三个钩子 componentWillReceiveProps、componentWillMount、componentWillUpdate,新引入了两个钩子 getDerivedStateFromProps 和 getSnapshotBeforeUpdate

getDerivedStateFromProps(props,state):返回一个对象来更新 state,或返回 null 则不更新任何内容,并且这个更新的 state 无法修改,值只能是 props 的值

static getDerivedStateFromProps(props){
    return props
}

注意:getDerivedStateFromProps 适用于 state 的值在任何时候都取决于 props 值的情况

getDerivedStateFromProps 会导致代码冗余,并使组件难以维护

getSnapshotBeforeUpdate:在最近一次渲染输出(提交到 DOM 节点)之前调用,它使得组件能在发生更改之前从 DOM 中捕获一些信息(如滚动位置),此生命周期的任何返回值将作为参数传给 componentDidUpdate,返回 snapshot 或 null

getSnapshotBeforeUpdate(){
    return 'xx' //或 return null
}

此用法不常见,可能出现在 UI 处理中,如需要以特殊方式处理滚动位置的聊天线程等

如随着时间 list 的顶部一直增加一定高度的内容,但下拉至某位置后滚动条位置不动且不影响上面继续添加

getSnapshotBeforeUpdate(){
    return this.refs.list.scrollHeight
}
componentDidUpdate(preProps,preState,height){
    this.refs.list.scrollTop += this.refs.list.scrollHeight - height
}

componentDidUpdate(preProps,preState):

componentDidUpdate(preProps,preState,snapshotValue){
    console.log('上一个props'+preProps+'上一个state'+preState+'getSnapshotBeforeUpdate return来的'+snapshotValue)
}

新版生命周期

生命周期的三个阶段(新版):

(1)初始化阶段:由 ReactDOM.render() 触发—初次渲染

1. constructor()
2. getDerivedStateFromProps()
3. render()
4. componentDidMount() ==> 常用,一般在这个钩子中做一些初始化的事,如开启定时器、发送网络请求、订阅消息

(2)更新阶段:由组件内部 this.setState() 或父组件重新 render 触发

1. getDerivedStateFromProps
2. shouldComponentUpdate()
3. render()
4. getSnapshotBeforeUpdate()
5. componentDidUpdate()

(3)卸载组件:由 ReactDOM.unmountComponentAtNode() 触发

1. componentWillUnmount() ==> 常用,一般在这个钩子中做一些收尾的事,例如:关闭定时器、取消订阅消息

DOM 的 Diffing 算法

验证 Diffing 算法

key

问题1: react/vue 中 key 有什么作用?(key 的内部原理是什么?)

虚拟 DOM 中 key 的作用:

1)简单的说:key 是虚拟 DOM 对象的标识,在更新显式时 key 起着极其重要的作用
2)详细的说:当状态中数据发生变化时,react 会根据【新数据】生成【新的虚拟 DOM】,随后 react 进行【新虚拟 DOM】与【旧虚拟 DOM】的 diff 对比,比较规则如下:
    a.旧虚拟 DOM 中找到了与新虚拟 DOM 相同的 key
        若虚拟 DOM 中内容没变,直接使用之前的真实 DOM
        若虚拟 DOM 中的内容变了,则生成新的真实 DOM,随后替换掉页面中之前的真实 DOM
    b.旧虚拟 DOM 中未找到与新虚拟 DOM 相同的 key,则根据数据创建新的真实 DOM,随后渲染到页面

问题2:为什么遍历列表时 key 最好不要用 index?

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

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

问题3:开发中如何选择 key?

1)最好使用每条数据的唯一标识作为 key,如 id、手机号、身份证号、学号等唯一值

2)若确定只是简单的展示数据,用 index 也是可以的

例子:点击按钮后在数组头部增加一条数据并显示

add=()=>{
    const {personArr} = this.state
    const p = {id:personArr.length+1,name:'c',age:20}
    this.setState({personArr:[p,...personArr]})
}

render(){
    return(
        <ul>
            {
                this.state.personArr.map((personObj,index)=>{  //这里用 index 做 key
                    return <li key={index}>{personObj.name}---{personObj.age}</li>
                })
            }
        </ul>
    )
}

当使用 index 作为 key 时

初始数据:
    {id:1,name:'a',age:18}
    {id:2,name:'b',age:18}
初始的虚拟 DOM
    <li key=0>a---18<input type="text"/></li>
    <li key=1>b---19<input type="text"/></li>
更新后的数据:
    {id:3,name:'c',age:20}
    {id:1,name:'a',age:18}
    {id:2,name:'b',age:18}
更新后的虚拟 DOM
    <li key=0>c---20<input type="text"/></li>
    <li key=1>b---19<input type="text"/></li>
    <li key=2>a---18<input type="text"/></li>

这种情况下增加数据后,头两条数据的 input 框中内容和之前一样,这是已导致了混乱

当使用 id 作为 key 时

初始数据:
    {id:1,name:'a',age:18}
    {id:2,name:'b',age:18}
初始的虚拟 DOM
    <li key=1>a---18<input type="text"/></li>
    <li key=2>b---19<input type="text"/></li>
更新后的数据:
    {id:3,name:'c',age:20}
    {id:1,name:'a',age:18}
    {id:2,name:'b',age:18}
更新后的虚拟 DOM
    <li key=30>c---20<input type="text"/></li>
    <li key=1>b---19<input type="text"/></li>
    <li key=2>a---18<input type="text"/></li>

这种情况下增加数据后,第一条数据后的 input 框中为空白,后两条数据的 input 框中内容和之前一样

四、React 应用

1、react 脚手架

xxx 脚手架是用来帮助程序员快速创建一个基于 xxx 库的模板项目

脚手架中包含了所有需要的配置(语法检查、jsx 编译、devServer…)、下载好了所有相关依赖、可以直接运行一个简单的效果

react 提供了一个用于创建 react 项目的脚手架库:create-react-app

项目的整体技术架构为 react + webpack + es6 + eslint

使用脚手架开发的项目的特点:模块化、组件化、工程化(工程化就是写好代码后会自动进行语法检查、代码压缩、编译、兼容性处理等一系列操作)

2、使用脚手架创建 react 项目并启动

(1)全局安装 create-react-app 库

npm install -g create-react-app

(2)切换到想创建项目的目录,并创建项目 create-react-app 项目名

(3)进入项目文件夹,并启动项目 npm start

3、react 脚手架项目结构

public — 静态资源文件夹

facion.icon --- 网站页签图标
index.html --- 主页面
logo192.png --- logo 图
logo512.png --- logo 图
manifest.json --- 应用加壳(将网络加壳成安卓或 ios 应用)的配置文件
robots.txt --- 爬虫协议文件

src — 源码文件夹

App.css --- App 组件的样式
App.js --- App 组件
App.test.js --- 用于给 App 做测试
index.css --- 样式
index.js --- 入口文件
logo.svg ---logo 图
reportWebVitals.js --- 页面性能分析文件(需要 web-vitals 库的支持)
setupTests.js --- 用于做应用的整体测试或组件单元测试(需要 jest-dom 库支持)

4、样式模块化

若组件内都直接使用 import ‘./组件.css’ 引入样式,不同组件间可能定义了 className 相同的标签而设置的样式不同就会产生冲突

要解决这种冲突的一种方式是使用 less

另一种方式是组件的 css 文件都命名为 xxx.module.css,在 组件.js 文件中引入样式时使用 import xxx from './xxx.module.css',使用样式时如 <h1 className={xxx.类名}></h1>

5、vscode 中 react 插件

ES7 React/Redux/GraphQL/React-Native snippets 插件便于生成代码模板,如 rcc + 回车 即可生成类式组件代码模板,rfc + 回车 即可生成函数式组件代码模板

6、功能界面的组件化编码流程

(1)拆分组件:拆分界面,抽取组件

(2)实现静态组件:使用组件实现静态页面效果

(3)实现动态组件:

— 1)动态显示初始化数据:数据类型、数据名称、保存在哪个组件

— 2)交互(从绑定事件监听开始)

7、如何确定将数据放在哪个组件的 state 中?

若某个组件使用,则放在其自身的 state 中

若某些组件使用,则放在他们共同的父组件 state 中(也称为状态提升)

状态在哪,操作状态的方法就在哪

8、父子组件间数据传递

【父组件】给【子组件】传数据可在子组件传属性,在子组件中通过 props 获取数据

【子组件】给【父组件】传数据,在父组件定义函数并传给子组件,在子组件中调用该函数 this.props.函数()

五、react ajax

React 本身只关注于界面,并不包含发送 ajax 请求的代码

前端应用需要通过 ajax 请求与后台进行交互(json 数据)

react 应用中需要集成第三方 ajax 库(或自己封装)

1、常用的 ajax 请求库

(1)jQuery:比较重,若需要另外引入不建议使用

(2)axios:轻量级,建议使用

—a)封装 XmlHttpRequest 对象的 ajax

—b)promise 风格

—c)可以用在浏览器端和 node 服务器端

2、跨域问题

ajax 引擎会拦截不同源返回的数据,所以需要一个中间代理服务器,该服务器的和前端 ajax 引擎是同源的,前端给服务器发请求时是给代理服务器发送请求,代理服务器再转发至服务器,然后服务器将数据返回给代理服务器,最后再返回给前端

解决方式一:在脚手架中配置代理解决跨域

在 package.json 文件中添加

"proxy":"服务器端地址:服务器端口"
但这种方式会把服务器端口写死,只能向这个地址这个端口发请求

在代码中发送请求时地址写前端运行的端口

axios.get('http://localhost:前端端口/获取后端数据的api')

因为在前端找不到相应 api,所以代理服务器会转发至后端

优点:配置简单,前端请求资源时可以不加任何前缀

缺点:不能配置多个代理

工作方式:当请求了前端如 3000 端口不存在的资源时,该请求会转发给配置的服务器如 5000 端口(优先匹配前端资源)

解决方式二:也是在脚手架中配置代理解决跨域

新加文件 src/setupProxy.js 文件,并添加如下内容,React 脚手架会自动找到这个文件进行代理服务器配置

const proxy = require('http-proxy-middleware')
module.exports = function(app){
    app.use(
        proxy('/api1',{  //遇见 /api1 前缀的请求,就会触发该代理配置
            target:'http://localhost:5000', //请求转发给哪个服务器
            changeOrigin: true, //控制服务器收到的请求头中 Host 字段的值,设为 true 后服务器端输出请求头的 Host 值时就不是真正前端地址,而是服务器自身地址,这样服务器设置一些限制时不会有问题
            pathRewrite:{'^/api1':''} //重写请求路径,删掉前面的 /api1
        }),
        /*changeOrigin 为 true 时,服务器收到的请求头中 host 为如 localhost:5000(服务器自身地址)
        changeOrigin 为 false 时,服务器收到的请求头中 host 为如 localhost:3000(前端地址)
        changeOrigin 默认为 false,一般将其设为 true
        */
        proxy('/api2',{
            target:'http://localhost:5001',
            changeOrigin: true,
            pathRewrite:{'^/api2':''}
        })
    )
}

在代码中发送请求时写的地址要加上 /api1/api2 这样会去相应服务器中获取数据

优点:可以配置多个代理,可灵活的控制请求是否走代理

缺点:配置繁琐,前端请求资源时必须加前缀,若不写前缀就不走代理

3、fetch 发送请求

关于 fetch 可查看 github思否 的相关文档

之前发送请求都是通过 XMLHttpRequest,即使是使用 jQuery、axios、zepto 等,这些其实是对 XMLHttpRequest 的封装

此外有另一种发送请求的方式 fetch(因为兼容性问题所以不常用)

fetch 是原生函数,不再使用 XMLHttpRequest 对象提交 ajax 请求,注意老版本浏览器可能不支持

fetch 体现了 “关注分离” 的原则(即不是一步到位,而是分步走)

fetch(url, options).then(
    response => {
        console.log('联系服务器成功了',response.json()) //response.json() 返回 Promise 实例对象,若联系服务器成功且获取数据成功则里面存着获取到数据,若联系服务器成功但获取数据失败则里面存着失败的原因
        return response.json()  //返回 Promise 实例对象,因此才能有下面的 .then 链式调用
    },
    error => {
        console.log('联系服务器失败了')
        return new Promise(()=>{}) //返回初始化状态的 Promise 来阻断往下走执行下面的 then,若没有这个 return 则会返回 undefined,且当前这个 then 对应的 Promise 返回是成功状态,值为 undefined
    }
).then(
    response => {console.log('获取数据成功了',response);},
    error => {console.log('获取数据失败了',error)}
)

对上面的代码进行优化

fetch(url, options).then(
    response => {
        console.log('联系服务器成功了',response.json())
        return response.json()
    },
).then(
    response => {console.log('获取数据成功了',response);},
).catch(
    error => {console.log('请求出错',error)}
)

对上面代码继续优化

事件响应函数 = async() => {
    try{
        const response = await fetch(url) //此时等到的就是上面未优化代码中 Promise 成功时返回的 response.json()
        const data = await response.json()
        console.log(data)
    }catch(error){
        console.log('请求出错',error)
    }
}

六、任意组件间的通信:消息订阅——发布机制

需要借助工具库 PubSubJS

下载 npm install pubsub-js --save

使用

import PubSub from 'pubsub-js' //引入
let token = PubSub.subscribe('订阅的消息',回调函数(消息名,收到的数据)=>{...})  //在需要获取消息的组件中订阅
PubSub.unsubscribe(token) //取消订阅
PubSub.publish('消息名',携带的数据)  //发布消息

如在订阅消息的组件中

componentDidMount(){
    this.token = PubSub.subscribe('xx',(_,data){
        conosle.log(data)
    })
}
//取消订阅
componentWillUnmount(){
    PubSub.unsubscribe(this.token)
}

在发布消息的组件中

PubSub.publish('xx',{name:'tom',age:18})

七、React 路由

1、SPA 的理解

单页 Web 应用(single page web application, SPA)中整个应用只有一个完整的页面,react、vue 等写的都是单页面多组件应用

点击页面中的链接不会刷新页面,只会做页面的局部更新

数据都需要通过 ajax 请求获取,并在前端异步展现

2、路由的理解

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

路由的分类

(1)后端路由:

后端路由的 value 是 function,用来处理客户端提交的请求

注册路由:router.get(path,function(req,res))

工作过程:当 node 接收到一个请求时,根据请求路径找到匹配的路由,调用路由中的函数来处理请求,返回响应数据

(2)前端路由:

浏览器路由 value 是 component,用于展示页面的内容

原生 html 中,靠 <a> 跳转不同的页面,在 React 中靠路由链接实现切换组件

注册路由:<Route path="/test" component={Test}>

工作过程:点击某组件引起路由跳转,路径改变,当浏览器的 path 变为 /test 时,被前端路由器监测到进行匹配组件,当前路由组件就会变为 Test 组件

history

在 BOM 对象(window 也在 BOM 上)上有个属性 history 专门管理浏览器的路径、历史记录等,这是前端路由的基石

<script type="text/javascript" src="https://cdn.bootcss.com/history/4.7.2/history.js"></script>
<script type="text/javascript">
    //方法一
    let history = History.createBrowserHistory() //这种方式是直接使用 H5 推出的 BOM 上 history 身上的 API,这行代码里定义的 history 最终操作的就是 BOM 上的 history
    //方法二
    //let history = History.createHashHistory() //这种方式是使用 hash 值(锚点),路由为 http://xx.xxx.xx.x/xx#/xxx,其中 path 放在 # 后面,# 后的资源都不会发给服务器,且锚点跳转不会引起页面刷新,但会留下历史记录,且这种方式对浏览器兼容性极佳
    function push(path){
        history.push(path)
    }
    function replace(path){
        history.replace(path)
    }
    function back(){
        history.goBack()
    }
    function forward(){
        history.goForward()
    }
    history.listen((location)=>{
        conosle.log('请求路由变化了',location)
    })
</script>

3、react 中路由的基本使用

react-router-dom

react-router 库有三种实现,分别给三种平台使用,一种是 react-router-dom 给网页开发应用人员,一种是 native 给 React Native 用 React 做原生应用开发,还有一种是 any 在哪都能用,虽然通用性更强但学习它的相关 API 没有前两种方便

react-router-dom 是 react 的一个插件库,专门用来实现一个 SPA 应用

基于 react 的项目基本都会用到此库

react-router 相关 API

内置组件

<BrowserRouter>
<HashRouter>
<Route>
<Redirect>
<Link>
<NavLink>
<Switch>

其它

history 对象
match 对象
withRouter 函数

react-router-dom 基本使用

需要下载 npm install react-router-dom 这样下载的是最新版本(目前是版本6),也可指定版本下载 npm install react-router-dom@5

在项目的入口文件 index.js 中引入路由器(BrowserRouter 或 HashRouter),该文件内容如下

//引入 react 核心库
import React from 'react'    
//引入 ReactDOM
import ReactDOM from 'react-dom'
//引入路由器    
import {BrowserRouter} from 'react-router-dom'
//引入 App 组件
import App from './App'

ReactDOM.render(
    <BrowserRouter>
        <App/>
    </BrowserRouter>,
    document.getElementById('root')
)

在组件中引入 Link 和 Route

import {Link,Route} from 'react-router-dom'

在组件中使用,导航区的 a 标签改为 Link 标签,展示区写 Route 标签进行路径的匹配

{/*编写路由链接*/}
<Link to="/xxx">xxx</Link>
{/*注册路由*/}
<Route path="/xxx" component={组件名}/>

路由组件和一般组件

(1)写法不同

一般组件是直接通过 <xxx/> 使用

而路由组件是通过 <Route path="/xxx" component={组件名}/> 进行路由匹配

(2)存放位置不同

一般组件放在 components 文件夹下

路由组件放在 pages 文件夹下

(3)接收到的 props 不同

一般组件写组件标签时传递了什么就能收到什么,若没传属性,this.props 中就为空

而路由组件会收到三个固定的属性,是路由器发送的三个 props,分别是 history、location、match

history:
    action:
    block:
    createHref:
    go:
    goBack:
    goForward:
    length:
    listen:
    location:
    push:
    replace:

location:
    hash:""
    key:"随机生成"
    pathname:"/xx"
    search:
    state:

match:
    isExact:
    params:{}
    path:"/xx"
    url:"/xx"

其中 history.location 和 直接获取 location 相同

若要给 Link 标签对应的导航栏中当前所在路由对应的按钮添加高亮显示,可把 Link 标签替换为 NavLink,在 NavLink 中可添加属性 activeClassName 表示给要高亮显示的标签追加有个样式类(不写该属性的话默认是给 className 中添加 active),然后自己定义这个类对应的高亮样式即可

需要现引入

import {NavLink,Route} from 'react-router-dom'

再使用

{/*编写路由链接*/}
<NavLink activeClassName="xxx" to="/xxx1">组件1</NavLink>
<NavLink activeClassName="xxx" to="/xxx2">组件2</NavLink>
{/*注册路由*/}
<Route path="/xxx1" component={组件1名}/>
<Route path="/xxx2" component={组件2名}/>

可定义一个组件如 MyNavLink

import React, { Component } from 'react'
import {NavLink} from 'react-router-dom'
export default class MyNavLink extends Component{
    render(){
        return(
            <NavLink activeClassName="xxx" {...this.props}/>
        ) //这里显式写标签体是因为标签体是特殊的标签属性,它其实是标签的 children 属性,外面传来的所有属性都在 this.props 中
    }    
}

在其他组件中使用自己定义的 MyNavLink

import MyNavLink from './components/MyNavLink/MyNavLink'

<MyNavLink to="/xxx">标签体</MyNavLink>
<Route path="/xxx" component={组件名}/>

Switch 的使用

使用 <Route path="/xxx" component={组件名}/> 进行路由匹配,若有多个组件匹配同一个路由地址,默认情况下路由地址匹配到一个组件后会继续往下匹配,会都展示,但是若这两行代码中间有很多个其他路由匹配代码,那么效率就很低

因此可利用 Switch 提高路由匹配效率(单一匹配)

在 Route 外包裹 <Switch>,那么匹配到当前路由的组件后就不会继续往下匹配了

<Switch>
    <Route path="/xxx1" component={组件1名}/>
    <Route path="/xxx2" component={组件2名}/>
</Switch>

解决样式丢失问题

对于有多级路由的地址(如 /xx1/xx2),若点击浏览器的刷新按钮后会出现 index.html 中引入的 css 文件(如 <link rel="stylesheet" href="./css/bootstrap.css">)失效的问题

假设 bootstrap.css 文件存放在 public 文件夹(该文件夹其实就是对应 http://localhost:3000 这个地址)下,正常情况下要获取样式访问的是 http://localhost:3000/css/bootstrap.css,而刷新后样式失效是因为此时访问的是 http://localhost:3000/xx1/css/bootstrap.css

解决方式一:修改 public/index.html 中引入样式的地址,去掉前面的 .,使用根目录(在浏览器中根目录就是 public 文件夹,且对应 http://localhost:3000)

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

解决方式二:在 public/index.html 中引入样式的地址使用绝对路径,%PUBLIC_URL% 表示的就是 public 文件夹所在路径

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

解决方式三:修改路由模式,使用 HashRouter(这种解决方式比较少见),在入口文件 index.js 中将 BrowserRouter 换成 HashRouter,这样会在根路径后添加 #,如 http://localhost:3000/#/xx1/xx2,# 后表示都是前端资源

//引入 react 核心库
import React from 'react'    
//引入 ReactDOM
import ReactDOM from 'react-dom'
//引入路由器    
import {HashRouter} from 'react-router-dom'
//引入 App 组件
import App from './App'

ReactDOM.render(
    <HashRouter>
        <App/>
    </HashRouter>,
    document.getElementById('root')
)

路由的模糊匹配与严格匹配

默认是模糊匹配,即输入的路径必须包含要匹配的路径,且顺序要一致

<NavLink activeClassName="xxx" to="/xxx1">组件1</NavLink>
<Route path="/xxx1/a/b" component={组件1名}/>
此时访问 /xx1 不能显示

<NavLink activeClassName="xxx" to="/xxx1/a/b">组件1</NavLink>
<Route path="/xxx1" component={组件1名}/>
此时访问 /xx1 可正常显示

<NavLink activeClassName="xxx" to="/a/xxx1/b">组件1</NavLink>
<Route path="/xxx1" component={组件1名}/>
此时访问 /xx1 也不能显示

在注册路由时添加属性 exact={true}exact 就可以开启精准匹配

<Route exact path="/xxx1" component={组件1名}/>
或
<Route exact={true} path="/xxx1" component={组件1名}/>

注意:若模糊匹配时页面可正常显示就不要开启严格匹配,有时开启严格匹配会导致无法继续匹配二级路由

Redirect 的使用

Redirect 是 react-router-dom 的内置组件

一般把 Redirect 写在所有路由注册的最下方,当所有路由都无法匹配时,跳转到 Redirect 指定的路由

引入

import {NavLink,Route,Redirect} from 'react-router-dom'

使用

<NavLink activeClassName="xxx" to="/xxx1">组件1</NavLink>
<NavLink activeClassName="xxx" to="/xxx2">组件2</NavLink>
<Route path="/xxx1" component={组件1名}/>
<Route path="/xxx2" component={组件2名}/>
<Redirect to="/xxx1"/>

当路由谁都匹配不上时就去 Redirect 设置的地址

4、嵌套路由(二级路由)的使用

注册子路由时要写上父路由的 path 值

路由的匹配是按照注册路由的顺序进行的,每次路由匹配都是从最开始注册的路由开始过一遍

如一级路由下组件先注册

<NavLink activeClassName="xxx" to="/xxx1">组件1</NavLink>
<NavLink activeClassName="xxx" to="/xxx2">组件2</NavLink>
<Route path="/xxx1" component={组件1名}/>
<Route path="/xxx2" component={组件2名}/>
<Redirect to="/xxx1"/>

二级路由下组件后注册

<NavLink activeClassName="xxx" to="/xxx11">组件11</NavLink>
<NavLink activeClassName="xxx" to="/xxx12">组件12</NavLink>
<Route path="/xxx1/xx11" component={组件11名}/>
<Route path="/xxx1/xx12" component={组件12名}/>
<Redirect to="/xxx1/xx11"/>

当点击【组件1】时路由会自动跳转到 /xxx1/xx11,因为 /xxx1 匹配后,相同一级路由下的【组件11】和【组件12】就会挂载,此时这两个组件都未点击,所以 redirect 到 /xxx1/xx11

当访问 /xxx1/xx11 时,先匹配先注册的 /xxx1,再匹配到 /xxx1/xx11,所以【组件1】和【组件11】的内容都会渲染

5、向路由组件传递参数数据

方式一:向路由组件传递 params 参数

{/*向路由组件传递 params 参数*/}
<Link to={`/xxx1/xx11/x111/${变量1}/${变量2}`}>xxx</Link>
{/*声明接收 params 参数*/}
<Route path="/xxx1/xx11/x111/:id/:title" component={组件名}/>

这些变量在组件中通过 this.props.match.params 获取这些传来的变量

const {id,title} = this.props.match.params

方式二:向路由组件传递 search 参数

{/*向路由组件传递 search 参数*/}
<Link to={`/xxx1/xx11/x111/?id=${变量1}&title=${变量2}`}>xxx</Link>
{/*search 参数无需声明接收,正常注册路由即可*/}
<Route path="/xxx1/xx11/x11" component={组件名}/>

这些变量在组件中通过 this.props.location.search 获取这些传来的search 参数,但收到的是 "?id=xxx&title=xxx" (key=value&key=value 这种形式称为 urlencoded 编码)的字符串

const {search} = this.props.location

可借助 querystring 库解析,对 urlencoded 编码和对象进行互相转换

import qs from 'querystring'
const {search} = this.props.location
cosnt {id,title} = qs.parse(search.slice(1)) //urlencoded => 对象形式
qs.stringfy(对象) //对象形式 => urlencoded

方式三:向路由组件传递 state 参数

注意这里的 state 是路由组件上独有的 state

{/*向路由组件传递 state 参数*/}
<Link to={{pathname:'/xxx1/xx11/x111',state:{id:变量1,title:变量2}}}>xxx</Link>
{/*state 参数无需声明接收,正常注册路由即可*/}
<Route path="/xxx1/xx11/x11" component={组件名}/>

这些变量在组件中通过 this.props.location.state 接收 state 参数

const {id,title} = this.props.location.state || {} // || {} 是为了若第一次直接访问 /xxx1/xx11/x111 时没有传参时不报错

这种方式在地址栏中没有体现,地址栏中都是 /xxx1/xx11/x111,但刷新也可以保留住参数,因为都在 history 中维护

6、路由跳转的两种模式 push 和 replace

默认是使用 push 模式

可给 Link 标签添加 replace={true} replace 开启 replace 模式,使用 replace 不会留下痕迹

<Link replace={true} to={`/xxx1/xx11/x111`}>xxx</Link>
或
<Link replace to={`/xxx1/xx11/x111`}>xxx</Link>

7、编程式路由导航

在按钮上绑定 onClick,并定义响应函数如下

replaceShow = (id,title){
    //replace 跳转 + 携带 params 参数
    //this.props.history.replace(`/xxx1/xx11/x111/${id}/${title}`)
    //replace 跳转 + 携带 search 参数
    //this.props.history.replace(`/xxx1/xx11/x111/?ID=${id}&title=${title}`)
    //replace 跳转 + 携带 state 参数
    //this.props.history.replace(`/xxx1/xx11/x111`,{id,title})
}
pushShow = (id,title){
    //push 跳转 + 携带 params 参数
    //this.props.history.push(`/xxx1/xx11/x111/${id}/${title}`)
    //push 跳转 + 携带 search 参数
    //this.props.history.push(`/xxx1/xx11/x111/?ID=${id}&title=${title}`)
    //push 跳转 + 携带 state 参数
    //this.props.history.push(`/xxx1/xx11/x111`,{id,title})
}
back = () => {
    this.props.history.goBack()
}
forward = () => {
    this.props.history.goForward()
}
go = () => {
    this.props.history.go(整数) //正整数表示前进 n 步,负整数表示后退 n 步
}

注意使用 push 或 replace 中携带不同参数则对应的传参方式和接收参数的方式也要改

8、withRouter 的使用

withRouter 可以加工一般组件,让一般组件上也有路由组件中特有的 API(history、location、match)

withRouter 的返回值是一个新组件

在一般组件中

import React, {Component} from 'react'
import {withRouter} form 'react-router-dom'
class 组件名 extends Component {...}
export default withRouter(组件名)

9、BrowserRouter 与 HashRouter 的区别

(1)底层原理不一样

BrowserRouter 使用的是 H5 的 history(this.props.histyor 是 react 对 H5 的 history 的二次封装) API,不兼容 IE9 及以下版本

HashRouter 使用的是 URL 的哈希值

(2)path 表现形式不一样

BrowserRouter 的路径中没有 #,例如 localhost:3000/xxx/xx

HashRouter 的路径包含 #,例如 localhost:3000/#/xxx/xx

(3)刷新后对路由 state 参数的影响

a. BrowserRouter 没有任何影响,因为 state 保存在 history 对象中

b. HashRouter 刷新后会导致路由 state 参数丢失

(4)HashRouter 可用于解决一些路径错误相关的问题(如样式丢失问题)

八、React UI 组件库

(1)国外的 material-ui,可访问官网GitHub

(3)国内蚂蚁金服 ant-design,可访问官网GitHub

(3)饿了么团队出的 Element UI for react,Element UI 原先是基于 vue 的

(4)有道团队推出的 vantUI,主要针对移动端(基于 vue)

antd

antd 的基本使用

下载 ant-design

npm install antd --save-dev

如要使用 antd 中封装好的按钮:

要先引入该组件

import {Button} from 'antd' //引入 Button 组件

粘贴官方文档中的使用代码,若要修改属性可查看文档中的 API

<Button type="primary">Primary Button</Button>

引入样式

import 'antd/dist/antd.css'

优化:按需引入样式

高级配置

可参考官方文档

先下载 react-app-rewired(靠这个库启动)和 customize-cra(靠这个库执行规则的修改)

npm install react-app-rewired customize-cra -D

修改 package.json 文件中的 “scripts”

"scripts": {
    "start": "react-app-rewired start",
    "build": "react-app-rewired build",
    "test": "react-app-rewired test",
}

然后在项目根目录创建一个 config-overrides.js 文件用于修改默认配置

module.exports = function override(config, env) {
  //修改或做一些 webpack 配置...
  return config;
};

按需引入样式

babel-plugin-import 是一个用于按需加载组件代码和样式的 babel 插件

下载

npm install babel-plugin-import -D

将 config-overrides.js 文件改为

const { override, fixBabelImports } = require('customize-cra');

module.exports = override(
    fixBabelImports('import', {
        libraryName: 'antd',
        libraryDirectory: 'es',
        style: 'css',
    }),
);

此时不要在组件文件中 import 'antd/dist/antd.css' 引入样式

antd 自定义主题

antd 中的样式最初是使用 less 写的然后编译成 css,自定义主题需要用到 less 变量覆盖功能

首先需要下载 less 和 less-loader

npm install less less-loader -D

将 config-overrides.js 文件改为

const { override, fixBabelImports, addLessLoader } = require('customize-cra');

module.exports = override(
    fixBabelImports('import', {
        libraryName: 'antd',
        libraryDirectory: 'es',
        style: true,
    }),
    addLessLoader({
        lessOptions:{
            javascriptEnabled: true,
            modifyVars: { '@primary-color': '#1DA57A' },
        }
    }),
);

在 addLessLoader 的 lessOptions 中的 modifyVars 里设置主题颜色

九、redux

redux 可参考相关英文文档中文文档GitHub

redux 介绍

redux 是一个专门用于做状态管理的 JS 库(不是 react 插件库)

它可以用在 react、angular、vue 等项目中,但基本与 react 配合使用

作用:集中式管理 react 应用中多个组件共享的状态

什么情况下需要使用 redux

(1)某个组件的状态需要让其他组件可以随时拿到(共享)

(2)一个组件需要改变另一个组件的状态(通信)

(3)总体原则:能不用就不用,若不用比较吃力才考虑使用

redux 工作流程图

redux工作流程

Action Creators 负责把动作包装成一个对象

Reducers 负责初始化状态和加工状态

redux 三个核心概念

action

action 是动作的对象,包含两个属性

type:标识属性,值为字符串,唯一,必要属性
data:数据属性,值类型任意,可选属性

{type:'ADD_STUDENT',data:{name:'xx',age:18}}

reducer

reducer 用于初始化状态、加工状态

加工时,根据旧的 state 和 action,产生新的 state 的纯函数

store

store 是将 state、action、reducer 联系在一起的对象

如何得到 store 对象

(1)import {createStore} from 'redux'
(2)import reducer from './reducers'
(3)const store = createStore(reducer)

store 对象的功能

(1)getState():得到 state
(2)dispatch(action):分发 action,触发 reducer 调用,产生新的 state
(3)subscribe(listener):注册监听,当产生了新的 state 时,自动调用

redux 的简单使用

下载 npm install redux -D

例子:下拉框中选择数字,按钮有加、减、奇数时加、异步加

(1)创建 src/redux 文件夹

(2)创建 reducers/reducer.js(可以不为此名) 文件,用于创建一个为某组件服务的 reducer

const initState = 0 //初始化状态
export default function countReducer(preState=initState,action){
    const {type,data} = action //从 action 对象中获取 type、data
    switch(type){
        case 'increment':
            return preState + data
        case 'decrement':
            return preState - data
        default:
            return preState
    }
}

reducer 的本质是一个函数,reducer 函数会接收到两个参数:之前的状态(preState)和动作对象(action)

reducer 第一次被调用是 store 自动触发的,传递的 preState 为 undefined,action 为 {type:@@REDUX/INIT.x.x.x.x}

(3)创建 store.js 文件,该文件专门用于暴露一个 store 对象,整个应用只有一个 store 对象

import {createStore} from 'redux' //引入 createStore 专门用于创建 redux 中最核心的 store 对象
import countReducer from './reducer.js' //引入为 Count 组件服务的 reducer
export default createStore(countReducer) //暴露 store

(4)在要使用 store 的组件中

引入 store

import store form '../../redux/store.js'

在要使用 store 调用 reducer 的地方

store.dispatch({type:'increment',data:1})

在要读取 store 中状态值的地方

store.getState()

监测 redux 中状态的变化,只要变化,就调用 render

componentDidMount(){
    store.subscribe(()=>{
        this.setState({}) //只要调用 setState 就会自动触发 render
    })
}

(5)优化:上面最后一步监测 redux 中状态变化调用 render 可不写在组件中,直接写在入口文件 index.js 中,这样写一次就行不用每个组件都写,在 index.js 中添加如下代码

import store from './redux/store'
store.subscribe(()=>{
    ReactDOM.render(<App/>,document.getElementById('root'))
})

只要 store 中状态有一个发生改变就调用 render,因为 react 渲染时使用 diff 算法所以这样效率也不会低

注意:redux 只负责状态管理,状态发生变化时不会自动调用 render 重新渲染,需要自己监测并手动调用 render

redux 的完整写法

相比于上述 redux 的简单使用,增加如下部分

(1)在 src/redux 文件夹下创建 constant.js 文件用于定义 action 对象中 type 类型的常量值,便于管理的同时防止程序员单词写错

export const INCREMENT = 'increment'
export const DECREMENT = 'decrement'

(2)在 src/redux 文件夹下创建 actions/count_action.js(也可以不为此名),该文件专门为 Count 组件生成 action 对象

import {INCREMENT,DECREMENT} from './constant'
export const createIncrementAction = data => {
    return {type:INCREMENT,data}
}
export const createDecrementAction = data => ({type:DECREMENT,data}) //这种写法相当于上面的写法,返回的是个对象

(3)在 reducer.js 中同样使用 constant.js 中定义的常量

import {INCREMENT,DECREMENT} from '../constant'
const initState = 0 //初始化状态
export default function countReducer(preState=initState,action){
    const {type,data} = action //从 action 对象中获取 type、data
    switch(type){
        case INCREMENT:
            return preState + data
        case DECREMENT:
            return preState - data
        default:
            return preState
    }
}

(4)在组件中

引入 actionCreator(专门用于创建 action 对象)

import {createIncrementAction,createDecrementAction} from '../../redux/count_action'

在要使用 store 调用 reducer 的地方

store.dispatch(createIncrementAction(1))

异步 action

若 action 的值为 Object 类型的一般对象就是同步 action

若 action 的值是个函数(因为只有函数才能开启异步任务,而数字、数组等不行)就是异步 action,该函数由 store 调用

异步 action 中一般都会调用同步 action

何时需要异步 action?

当延迟的动作不想交给组件自身,想交给 action
想要对状态进行操作,但具体的数据靠异步任务返回时

异步 action 不是必须的,也可以自己在组件中等待异步任务的结果再去分发同步 action

使用异步 action

要使用异步 action 需要安装中间件

npm install redux-thunk

修改 store.js 文件

import {createStore,applyMiddleware} from 'redux' //引入 createStore 专门用于创建 redux 中最核心的 store 对象
import countReducer from './reducer.js' //引入为 Count 组件服务的 reducer
import thunk from 'redux-thunk' //引入 redux-thunk 用于支持异步 action
export default createStore(countReducer,applyMiddleware(thunk)) //执行中间件 thunk 并暴露 store

在 action 文件中

import store from './store'
//异步 action
export const createIncrementAsyncAction = (data,time) => {
    return () => {
        setTimeout(() => {
            store.dispatch(createIncrementAction(data))
        },time)
    }
}
或
因为异步 action 返回的函数由 store 调用,所以,可不手动引入 store
//异步 action
export const createIncrementAsyncAction = (data,time) => {
    return (dispatch) => {
        setTimeout(() => {
            dispatch(createIncrementAction(data))
        },time)
    }
}

在组件中使用异步 action

store.createIncrementAsyncAction(1,1000)

react-redux

react-redux 是 Facebook 出的插件库

react-redux 的模型图如下

react-redux模型图

所有的 UI 组件都应该包裹一个容器组件,他们是父子关系

容器组件是真正和 redux 打交道的,里面可以随意使用 redux 的 api

UI 组件中不能使用任何 redux 的 api

容器组件会通过 props 传给 UI 组件(1)redux 中保存的状态(2)用于操作状态的方法

利用 react-redux 后一个组件要和 redux 打交道要经过哪几步?

(1)定义 UI 组件 --- 不暴露
(2)引入 connect 生成一个容器组件,并暴露,写法如下
    connect(
        state => ({key:value}), //映射状态
        {key:xxxAction}  //映射操作状态的方法
    )(UI组件)
(3)在 UI 组件中通过 this.props.xxx 读取和操状态

连接容器组件与 UI 组件

下载 npm install react-redux -D

新建 src/containers 用于存放容器组件,UI 组件依然存放在 src/components 里

在 containers 文件夹中新建一个容器组件的文件夹(如 Count),Count 文件夹下新建 index.js

//引入 Count 的 UI 组件
import CountUI from '../../components/Count'
//引入 action
import {createIncrementAction,createDecrementAction,createIncrementAsyncAction} from '../../redux/count_action'
//引入 connect 用于连接 UI 组件与 redux
import {connect} from 'react-redux'
//mapStateToProps 函数返回的是个对象,返回的对象中的 key 就作为传递给 UI 组件 props 的 key,value 就作为传递给 UI 组件 props 的 value,mapStateToProps 用于传递状态
function mapStateToProps(state){ //react-redux 调用的 a 函数,调用时已经 store.getState() 获取到状态,并作为参数传给 a 函数
    return {count:state} //这样相当于 <CountUI count=xxx/>
}
//mapDispatchToProps 函数返回的是个对象,返回的对象中的 key 就作为传递给 UI 组件 props 的 key,value 就作为传递给 UI 组件 props 的 value,mapDispatchToProps 用于传递操作状态的方法
function mapDispatchToProps(dispatch){
    return {
        inc:number => dispatch(createIncrementAction(number)),
        dec:number => dispatch(createDecrementAction(number)),
        incAsync:(number,time) => dispatch(createIncrementAsyncAction(number,time))
    }
}
//使用 connect()() 创建并暴露一个 Count 的容器组件
export defualt connect(mapStateToProps,mapDispatchToProps)(CountUI)

最后应渲染容器组件,在使用 Count 组件的组件(如 App.js)中

import React,{ Component } from 'react'
import store from './redux/store'
import Count from './containers/Count'
export default class App extends Component{
    render(
        return(
            <div>
                <Count store={store}/>
            </div>
        )
    )
}

注意:容器组件中的 store 是靠 props 传进去的,不是在容器组件中直接 import 引入的

优化程序编写

优化 mapDispatchToProps

mapDispatchToProps 可以是个函数也可以是个对象

import CountUI from '../../components/Count'
import {createIncrementAction,createDecrementAction,createIncrementAsyncAction} from '../../redux/count_action'
//引入 connect 用于连接 UI 组件与 redux
import {connect} from 'react-redux'
//使用 connect()() 创建并暴露一个 Count 的容器组件
export defualt connect(
    state => ({count:state}),
    //mapDispatchToProps 的一般写法
    /*dispatch => (
        {
            inc:number => dispatch(createIncrementAction(number)),
            dec:number => dispatch(createDecrementAction(number)),
            incAsync:(number,time) => dispatch(createIncrementAsyncAction(number,time))
        }
    )*/
    //mapDispatchToProps 的简写
    {
        inc:createIncrementAction, //这是因为 react-redux 收到传来的 action 对象能自动掉 dispatch
        dec:createDecrementAction,
        incAsync:createIncrementAsyncAction
    }
)(CountUI)

无需对 store 中状态进行监测重新 render

使用 react-redux 后可删除之前在入口文件 index.js 文件中写的

import store from './redux/store'
store.subscribe(()=>{
    ReactDOM.render(<App/>,document.getElementById('root'))
})

因为 react-redux 中使用 connect()() 创建容器组件就会自动监测 store 中的状态变化

Provider 组件的使用

之前需要给容器组件传递 store,有几个容器组件就要传几次 store

<Count store={store}/>

利用 Provider 只需写一次即可给所有容器组件传 store,在入口文件 index.js 中

import React from 'react'    
import ReactDOM from 'react-dom'
import {HashRouter} from 'react-router-dom'
import App from './App'
import store from './redux/store'
import {Provider} from 'react-redux'

ReactDOM.render(
    <Provider store={store}> /*Provider 让该组件的所有后代容器组件都能接收到 store*/
        <App/>
    </Provider>,
    document.getElementById('root')
)

整合 UI 组件与容器组件

有多少需要和 redux 关联的 UI 组件就需要写多少容器组件,目前文件太多,从文件层面进行优化,可以把 UI 组件和容器组件写在一个文件中

import React, {Component} from 'react'    
import {
    createIncrementAction,
    createDecrementAction,
    createIncrementAsyncAction
} from '../../redux/count_action'
//引入 connect 用于连接 UI 组件与 redux
import {connect} from 'react-redux'

//定义 UI 组件
class CountUI extends Component{...} //UI 组件中通过 this.props.xxx 来使用容器组件中从 redux 获取的状态和操作状态的方法

//使用 connect()() 创建并暴露一个 Count 的容器组件
export defualt connect(...)(CountUI)

react-redux 使用的最终版

(1)新建 src/containers/组件名/index.js,

import React,{ Component } from 'react'
import { createIncrementAction } from '../../redux/count_action'
import {connect} from 'react-redux'
class Count extends Component{
    add = () => {
        this.props.inc(1)
    }
    render(
        return(
            <div>
                <h1>{this.props.num}</h1>
                <button onClick={this.add}>加一</button>
            </div>
        )
    )
}
export defualt connect(
    state => ({num:state}), //映射状态
    {inc:createIncrementAction}//映射状态的方法
)(Count)

多个组件间的数据共享

假设有两个组件 Count 和 Person,且已写好两个组件相关的 action 和 reducer,Count 组件中可点击+1,Person 组件可添加一个人的信息

新建 src/reducers/index.js 文件用于汇总所有的 reducer 为一个总的 reducer

import {combineReducers} from 'redux'
//引入为 Count、Person 组件服务的 reducer
import count from './reducers/count'
import person from './reducers/person'

//汇总所有 reducer 变为一个总的 reducer
export default combineReducers({ //这里传入的对象就是 store 中存的对象
    num:count,
    person:person
})

修改 store.js 文件

import {createStore,applyMiddleware} from 'redux'
import reducer from './reducers/index.js'
import thunk from 'redux-thunk'

export default createStore(reducer,applyMiddleware(thunk)) //执行中间件 thunk 并暴露 store

两个 reducer 进行状态传递时应进行修改,如 Count 中

export default connect(
    state => ({count:state.num,person:state.person})//注意这里不是之前的 state => ({count:state}),因为之前 state 中存只是个数字,现在 state 中存的是个对象,用来存放很多组件的状态
)(Count)

纯函数

纯函数:只要同样的输入(实参),必定得到同样的输出(返回)

纯函数必须遵守以下一些约束:

(1)不得改写参数数据,如 function func(a){a=1}
(2)不会产生任何副作用,例如网络请求(可能会断网等),输入和输出设备
(3)不能调用 Date.now() 或 Math.random() 等不纯的方法

redux 的 reducer 函数必须是一个纯函数,如

export default function personReducer(preState=initState,action){
    const {type,data} = action
    switch(type){
        case ADD_PERSON:
            return [data,...preState] //注意这里不能使用 preState.unshift(data),因为这样导致 preState 被改写了,personReducer 就不是纯函数了
        default:
            return preState
    }
}

redux 开发者工具

在 Chrome 浏览器中安装插件 Redux DevTools

在项目中下载 redux-devtools-extension

npm install redux-devtools-extension -D

在 src/redux/store.js 文件中添加

//引入
import {composeWithDevTools} from 'redux-devtools-extension'
//修改原先的 export default...
export default createStore(reducer,composeWithDevTools(applyMiddleware(thunk)))

十、项目打包运行

执行 npm run build 生成文件夹 build

全局安装 serve 来查看打包项目的运行效果

npm i serve -g
serve build

十一、扩展

setState 更新状态的两种写法

setState 是同步的,setState 引起 React 后续更新状态的动作是异步的

若更改完 state 状态后要马上输出查看新的 state 要在回调函数中查看,若在外部查看输出依然是旧值

(1)setState(stateChange, [callback])————对象式的 setState

stateChange 为状态改变对象(该对象可体现出状态的更改)
callback 是可选的回调函数,它在状态更新完毕、界面也更新后(render 调用后)才被调用

如 this.setState({count:count+1})

(2)setState(updater, [callback])————函数式的 setState

updater 为返回 stateChange 对象的函数,可接收 state 和 props
callback 是可选的回调函数,它在状态更新完毕、界面也更新后(render 调用后)才被调用

如 this.setState((state,props) => {
    console.log(state,props)
    return {count:state.count+1}
})

总结:对象式的 setState 是函数式的 setState 的简写方式(语法糖)

使用原则:

(1)若新状态不依赖原状态————使用对象方式
(2)若新状态依赖于原状态————使用函数/对象方式
(3)若需要在 setState() 执行后获取最新状态数据,要在第二个 callback 函数中读取

lazyLoad

做懒加载最多的是路由组件 lazyLoad

import React, { Component,lazy } from 'react'
//通过 React 的 lazy 函数配合 import() 函数动态加载路由组件 ==> 路由组件代码会被分开打包
const Login = lazy(()=>import('@/pages/Login'))
//通过 <Suspense> 指定在加载得到路由打包文件前显示一个自定义 loading 界面,当网速慢时会先显示 fallback 中的东西
<Suspense fallback={<h1>Loading...</h1>}>
    <Switch>
        <Route path="/xx" component={Xxx}>
        <Redirect to="/login/">
    </Switch>
</Suspense>

Hooks

Hook 是 React 16.8.0 版本增加的新特性/新语法,使得在函数组件中也可以使用 state 以及其他 React 特性

三个常用的 Hook

State Hook:React.useState()

State Hook 让函数组件也可以有 state 状态,并进行状态数据的读写操作

语法:const [xxx,setXxx] = React.useState(initValue)

useState() 中

参数:第一次初始化指定的值在内部作缓存
返回值:包含 2 个元素的数组,第 1 个为内部当前状态值,第 2 个为更新状态值的函数

setXxx() 两种写法

setXxx(newValue):参数为非函数值,直接指定新的状态值,内部用其覆盖原来的状态值
setXxx(value => newValue):参数为函数,接收原本的状态值,返回新的状态值,内部用其覆盖原来的状态值

例子

import React from 'react'
function Demo(){
    const [count,setCount] = React.useState(0)
    function add(){
        setCount(count+1)//或 setCount(count => count+1)
    }
    return(
        <h1>{count}</h1>
        <button onClick={add}>加一</button>
    )
}
export default Demo

Effect Hook:React.useEffect()

Effect Hook 让函数组件也可以执行副作用操作(副作用操作用于模拟类组件中的生命周期钩子)

React 中的副作用操作:

发 ajax 请求数据获取
设置订阅、启动定时器
手动更改真实 DOM

语法:

React.useEffect(()=>{
    //在此可以执行任何带副作用操作
    return () => { //组件卸载前执行
        //在此做一些收尾工作,如清除定时器/取消订阅
    }
},[stateValue]) //第二个参数表示监测的值,若为 [],回调函数只会在第一次 render() 后执行

可以把 useEffect Hook 看作三个函数的组合:componentDidMount、componentDidUpdate、componentWillUnmount

React.useEffect() 该函数相当于 componentDidMount 和 componentDidUpdate,具体是哪个取决于传入的第二个参数,若为空数组[],则相当于 componentDidMount,若数组中有要监测变化的值[xxx],则相当于 componentDidUpdate

React.useEffect() 中若返回函数,返回的函数相当于 componentWillUnmount

例子

import React from 'react'
function Demo(){
    const [count,setCount] = React.useState(0)
    React.useEffect(()=>{
        let timer = setInterval(()=>{
            setCount(count => count + 1)
        },1000)
        return ()=>{
            clearInterval(timer)
        }
    },[]) 
    function unmount(){
        ReactDOM.unmountComponentAtNode(document.getElementById('root'))
    }
    return(
        <h1>{count}</h1>
        <button onClick={unmount}>卸载组件</button>
    )
}
export default Demo

Ref Hook:React.useRef()

Ref Hook 可在函数组件中存储/查找组件内的标签或任意其他数据

语法:const refContainer = useRef()

作用:保存标签对象,功能与 React.createRef() 一样

例子

import React from 'react'
function Demo(){
    const [count,setCount] = React.useState(0)
    const myRef = React.useRef()
    function show(){
        alert(myRef.current.value)
    }
    return(
        <input type="text" ref={myRef}>
        <h1>{count}</h1>
        <button onClick={show}>点击提示input中输入的数据</button>
    )
}
export default Demo

Fragment

在组件中 jsx 语法一定要求只能有一个根标签,这样使得在组件嵌套后会有很多没用的根标签,而使用 Fragment 作为根标签在实际渲染时会忽略

import React, { Component,Fragment } from 'react'
export deffault clss Demo extends Component{
    render(
        return(
            <Fragment>
                <input type="text"/>
                <input type="text"/>
            </Fragment>
        )
    )
}
<Fragment></Fragment>
或
<></>

<Fragment> 可以添加 key 属性,用于遍历

可以不用必须有一个真实的 DOM 根标签了,而 <> 空标签里不能添加任何属性

Context

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

使用:

(1)在祖孙组件都能访问到的地方创建 Context 容器对象

const xxxContext = React.createContext()  //xxxContext 首字母需要大写,因为后面它还会作为组件标签使用

(2)在祖组件渲染子组件时外面包裹 xxxContext.Provider,通过 value 属性给后代组件传递数据

<xxxContext.Provider value={数据}>
    子组件
</xxxContext.Provider>

(3)后代组件读取数据

//方式一:仅适用于类组件
static contextType = xxxContext //声明接收 context
this.context  //读取 context 中的value 数据

//方式二:函数组件与类组件都可以
<xxxContext.Consumer>
    {
        value => {  //value 就是 context 中的 value 数据
            //要显示的内容
        }
    }
</xxxContext.Consumer>

注意:在应用开发中一般不用 context,一般都用它的封装 react 插件(react-redux 中的 Provider)

例子:A 组件是 B 组件的父组件,B 是 C 的父组件,要让 C 不通过 B,直接拿到 A 的数据

import React, { Component } from 'react'

//创建 Context 对象
const MyContext = React.createContext()
const {Provider,Consumer} = MyContext

export default class A extends Component{
    state = {username:'xx',age:18}
    render(
        const {username} = this.state
        return(
            <div>
                <Provider value={{username:username,age:age}}>
                    <B/>
                </Provider>
            </div>
        )
    )
}
class B extends Component{
    render(
        return(
            <div>
                <C/>
            </div>
        )
    )
}
class C extends Component{
    render(
        static contextType = MyContext
        return(
            <div>
                <h1>{this.context.username}</h1>
            </div>
        )
    )
}

//若 C 是函数式组件
function C(){
    return (
        <div>
            <Consumer>
                {
                    value => {
                        return `${value.username},${value.age}`
                    }
                }
            </Consumer>
        </div>
    )
}

PureComponent

Component 的 2 个问题:

(1)只要执行 setState() 即使不改变状态数据,组件也会重新 render()
(2)只当前组件重新 render(),就会自动重新 render 子组件,就算子组件没有用到父组件的任何数据也会触发子组件重新 render,这会导致效率低

效率高的做法:只有当组件的 state 或 props 数据发生改变时才重新 render()

原因:Component 的 shouldComponentUpdate() 总是返回 true

解决:

办法1:重写 shouldComponentUpdate() 方法
比较新旧 state 或 props 数据,若有变化才返回 true,若没有返回 false
在父子组件中重写:
shouldComponentUpdate(nextProps,nextState){
    console.log(this.props,this.state)  //目前的 props 和 state
    console.log(nextProps,nextState)  //接下来要变化的新 props 和 state、
    return !this.state.xx === nextState.xxx
}

办法2:使用 PureComponent
PureComponent 重写了 shouldComponentUpdate(),只有 state 或 props 数据有变化才返回 true
注意:只是进行 state 和 props 数据的浅比较,只有对象地址改变时返回 true,若只是数据对象内部数据变了,返回 false,所以不要直接修改 state 数据,而是要产生新数据
用法如下:
import React, { PureComponent } from 'react'
class 组件 extends PureComponent{...}

项目中一般使用 PureComponent 来优化

render props

html 标签里的标签体内容能直接展示,而自定义组件中的标签体(如 <自定义组件>xxx</自定义组件>)不能直接展示,而是作为 this.props.children 属性的值

如何想组件内部动态传入带内容的结构(标签)?

在 Vue 中,使用 slot 技术,即通过组件标签体传入结构 <A><B/></A>

在 React 中
    使用 children props:通过父组件标签体传入结构(子组件)
    使用 render props:通过组件标签属性传入结构,来形成父子关系,,且可携带数据,一般用 render 的函数属性

(1)childer props

在 A 的父组件中形成 AB 的父子关系
<A>
    <B>xxx</B>
</A>
在 A 组件中通过如下代码展示 B 组件
{this.props.children}

但是若 B 组件需要 A 组件内的数据则做不到

(2)render props

在 A 的父组件中形成 AC 的父子关系
<A render={(data)=><C data={data}></C>}></A>
A 组件中渲染 C 组件并给 C 传数据
{this.props.render(要给 C 传的数据)}
C 组件中读取 A 组件传入的数据显示 
{this.props.data}

ErrorBoundary 错误边界

错误边界(ErrorBoundary)用来捕获后代组件错误,渲染出备用页面

错误边界就是让错误控制在一定范围内不要往外扩散,子组件出错父组件依然能正常渲染

错误边界只适用于生产环境

特点:只能捕获后代组件生命周期函数中产生的错误,普通函数中不行,不能捕获自己组件产生的错误和其他组件在合成事件、定时器中产生的错误

使用方式:getDerivedStateFromError 配合 componentDidCatch

state = {
    hasError:''  //用于标识子组件是否产生错误
}
//声明周期函数,一旦后代组件报错,就会触发,并携带错误信息
static getDerivedStateFromError(error){
    console.log(error)
    //在 render 之前触发
    //返回新的 state
    return {hasError: true}
}
render(){
    return(
        <div>
            {this.state.hasError?<h2>当前网络不稳定,稍后再试</h2>:<子组件/>}
        </div>
    )
}
componentDidCatch(error,info){  //当页面中子组件出现问题时该钩子会被调用
    //统计页面的错误,发送请求发送到后台去,用于通知编码人员解决 bug
    console.log(error,info)
}

十二、组件通信方式总结

组件间的关系:

父子组件
兄弟组件(非嵌套组件)
祖孙组件(跨级组件)

通信方式:

(1)props
    children props
    render props
(2)消息订阅-发布
    pubs-sub、event(C#中用的多) 等
(3)集中式管理
    redux、dva 等
(4)conText
    生产者-消费者模式

比较好的搭配方式:

父子组件:props
兄弟组件:消息订阅-发布、集中式管理
祖孙组件(跨级组件):消息订阅-发布、集中式管理、conText(开发用的少,封装插件用的多)

十三、ReactRouter 6

React Router 以三个不同的包发布到 npm 上,分别为:

(1)react-router:路由的核心库,提供很多组件、钩子
(2)react-router-dom:包含 react-router 所有内容,并添加到一些专门用于 DOM 的组件,如 <BrowserRouter> 等
(3)react-router-native:包括 react-router 所有内容,并添加一些专门用于 ReactNative 的 API,如 <NativeRouter> 等

相比 ReactRouter 5.x 的变化

(1)内置组件的变化:移除 <Switch/>,新增 <Routes/>

(2)语法的变化:component={组件} 变为 element={<组件名/>}

(3)新增多个 hook:useParams、useNavigate、useMatch 等

(4)官方明确推荐函数式组件了

<Routes/><Route/>

(1)v6 版本中移除了 <Switch>,引入了替代者 <Routes/>

(2)<Routes/><Route/> 要配合使用,且必须要用 <Routes/> 包裹 <Route/>

(3)<Route/> 相当于一个 if 语句,若其路径与当前 URL 匹配,则程序对应组件

(4)<Route caseSensitive/> 属性用于指定匹配时是否区分大小写(默认为 false)

(5)当 URL 发生变化时,<Routes/> 都会查看其所有子 <Route/> 元素以找到最佳匹配并呈现组件

(6)<Route/> 也可以嵌套使用,且可配合 useRoutes() 配置“路由表”,但需要通过 <Outlet/> 组件来渲染其子路由

import {NavLink,Routes,Route,Navigate} from 'react-router-dom'

<Routes> {/*功能和 Switch 一样(路由一旦匹配到就不继续往下匹配),但必须包裹 Routes*/}
    <Route path='/about' element={<About/>}>
        <Route path='/xx' element={<Xxx/>}>  {/*嵌套路由,/about/xx*/}
    </Route>
    <Route path='/' element={<Navigate to="/about"/>}/> {/*没有 Redirect 了,而是换成 Navigate,且 Navigate 组件一旦被渲染一定会触发视图切换*/}
</Routes>

<Navigate/> 重定向

作用:只要 <Navigate/> 组件被渲染,就会修改路径,切换视图

replace 属性用于控制跳转模式(push 或 replace,默认是 false)

import React,{useState} from 'react'
import {NavLink,Routes,Route,Navigate} from 'react-router-dom'

export default function About(){
    const [sum,setSum] = useState(1)
    return(
        <div>
            {sum === 1 ? <h2>xxx</h2> : <Navigate to="about" replace={true}/>}
            <button onClick={()=>setSum(2)}>点击后sum变为2</button>
        </div>
    )
}

在 v5 中高亮是使用 <NavLink activeClassName="xxx"/> 来控制标签高亮

在 v6 中废弃了 activeClassName 属性,将要高亮的类也添加在 className 中,值为一个函数

<NavLink className={({isActive})=>isActive?'定义的高亮样式类':''} to="/about">About</NavLink>

上述方式在每个有可能高亮的组件中都要使用较多重复代码,所以可以单独定义成一个函数

function computedClassName({isActive}){
    return isActive?'定义的高亮样式类':''
}

在 NavLink 标签中使用
<NavLink className={computedClassName} to="/about">About</NavLink>

useRoutes 路由表

新建 src/routes/index.js

import {Navigate} from 'react-router-dom'
import About from '../pages/About'
import Home from '../pages/Home'
export default[
    {
        path:'/about',
        element:<About/>
    },
    {
        path:'/home',
        element:<Home/>
    },
    {
        path:'/',
        element:<Navigate to="/about"/>
    }
]

在组件中

import React from 'react'
import {NavLink,useRoutes} from 'react-router-dom'
import routes from './routes'
export default function App(){
    //根据路由表生产对应的路由规则
    const element = useRoutes(routes)
    return (
        <div>
            {element}
        </div>
    )
}

嵌套路由

<Route> 产生嵌套时,渲染其对于的后续子路由使用 <Outlet />

假设 <News/><Message/><Home/> 的子组件,他们在 <Home/> 所在路由的二级路由下

修改 src/routes/index.js 文件

import {Navigate} from 'react-router-dom'
import About from '../pages/About'
import Home from '../pages/Home'
import News from '../pages/News'
import Message from '../pages/Message'
export default[
    {
        path:'/about',
        element:<About/>
    },
    {
        path:'/home',
        element:<Home/>,
        children:[
            {
                path:'news',
                element:<News/>
            },
            {
                path:'message',
                element:<Message/>
            }
        ]
    },
    {
        path:'/',
        element:<Navigate to="/about"/>
    }
]

<Home/> 组件中

import {NavLink,Outlet} from 'react-router-dom'

{/* 路由链接,end 属性表示若子级路由匹配上则父级路由不高亮 */}
<NavLink to="news">News</NavLink>{/* News 组件对应的是 /home/news 路径,这里要么两级路由写完整,要么只写第二级且不加 /,若加 / 表示从根目录开始,或使用 ./news 表示从当前路径开始加 */}
<NavLink end to="message">Message</NavLink>
{/* 指定路由组件呈现位置 */}
<Outlet />

路由的 params 参数

在 src/routes/index.js 文件中

import {Navigate} from 'react-router-dom'
import About from '../pages/About'
import Home from '../pages/Home'
import News from '../pages/News'
import Message from '../pages/Message'
export default[
    {
        path:'/about',
        element:<About/>
    },
    {
        path:'/home',
        element:<Home/>,
        children:[
            {
                path:'news',
                element:<News/>
            },
            {
                path:'message',
                element:<Message/>
                children:[
                    {
                        path:'detail/:id/:title',  //携带params 参数形式
                        element:<Detail/>
                    },
                    {
                        path:'message',
                        element:<Message/>
                    }
                ]
            }
        ]
    },
    {
        path:'/',
        element:<Navigate to="/about"/>
    }
]

在组件中

<NavLink to={`detail/${变量1}/${变量2}`}>xxx</NavLink>
<Outlet />

在接收 params 路由参数的组件中(使用函数式定义的情况下)可使用 useParams 或 useMatch

import {useParams,useMatch} from 'react-router-dom'

export default function Detail(){
    //方式一:
    const {id,title} = useParams()
    //方式二:
    const x = useMatch('/home/message/detail/:id/:title')
    console.log(x)
    return (
        <ul>
            <li>{id}</li>
            <li>{title}</li>
        </ul>
    )
}

路由的 search 参数

在组件中

<NavLink to={`detail?id=${变量1}&title=${变量2}`}>xxx</NavLink>
<Outlet />

在接收 search 路由参数的组件中(使用函数式定义的情况下),可使用 useSearchParams(返回包含两个值的数组[search参数,更新search的函数]) 或 useLocation

import {useSearchParams,useLocation} from 'react-router-dom'

export default function Detail(){
    //方式一:
    const [search,setSearch] = useSearchParams()
    const id =search.get('id')
    const title = search.get('title')
    //方式二:
    const x = useLocation()
    console.log(x)
    return (
        <ul>
            <li>{id}</li>
            <li>{title}</li>
            <li><button onClick={()=>setSearch('id=111&title=xxx')}>点击更新收到的 search 参数</button></li>
        </ul>
    )
}

路由的 state 参数

在组件中

<NavLink to="detail" state={{id:变量1,title:变量2}}>xxx</NavLink>
<Outlet />

在接收 state 路由参数的组件中(使用函数式定义的情况下)

import {useLocation} from 'react-router-dom'

export default function Detail(){
    const {state:{id,title}} = useLocation()
    return (
        <ul>
            <li>{id}</li>
            <li>{title}</li>
        </ul>
    )
}

编程式路由导航

使用 useNavigate 实现路由跳转

import React from 'react'
import {Link,Outlet,useNavigate} from 'react-router-dom'

export default function Message(){
    const navigate = useNavigate()
    function showDetail(m){
        navigate('detail',{
            replace:false,
            state:{
                id:m.id, //假设 m 传来是个对象
                title:m.title
            }
        }) //触发showDetail函数就会发生路由跳转
    }
    return (
        <ul>
            <li><button onClick={()=>showDetail(数据)}>查看详情</button></li>
        </ul>
    )
}

对于一般组件

import React from 'react'
import {useNavigate} from 'react-router-dom'

export default function Header(){
    const navigate = useNavigate()
    function back(m){
        navigate(-1)
    }
    function forward(m){
        navigate(1)
    }
    return (
        <div>
            <button onClick={()=>back(数据)}>后退</button>
            <button onClick={()=>forward(数据)}>前进</button>
        </div>
    )
}

一些其他较不常用的 Hooks

(1)useInRouterContext()

作用:若组件在 <Router> 的上下文中呈现,则 useInRouterContext 钩子返回 true,否则返回 false

(2)useNavigationType()

作用:返回当前的导航类型(用户是如何来到当前页面的)

返回值:POP、PUSH、REPLACE

备注:POP 是指在浏览器中直接打开了这个路由组件(是刷新当前页面)

(3)useOutlet()

作用:用来呈现当前组件中要渲染的嵌套路由组件

const result = useOutlet()
console.log(result)
//若嵌套路由没有挂载,则 result 为 null
//若嵌套路由已经挂载,则显示嵌套的路由对象

(4)useResolvedPath()

作用:给定一个 URL 值,解析其中的 path、search、hash 值

其他

defaultChecked 和 checked

勾选框 checkbox 初始时是否勾选可使用 defaultChecked(只能在显示初始值时起作用),这样设置后可修改值

<input type="checkbox" defaultChecked=xxx?true:false>
第一次显示时有效,但当 xxx 变量值发生改变时,checkbox 的勾选情况不会随着改变

或者使用 checked + onChange 来初始和监听是否勾选,这样可实时修改勾选情况

React 中定义的一些事件

onClick:点击
onBlur:失去焦点
onKeyUp:键盘按下松开
event.KeyCode:按下键盘按键的编码(回车的 KeyCode 是 13)
onMouseLeave:鼠标移入
onMouseEnter:鼠标移出

id 生成

生成数据 id,可通过 uuid 库(比较大)或 nanoid 库(较小,推荐)

npm i nanoid
import {nanoid} from 'nanoid' //暴露的 nanoid 是个函数,可生成全世界唯一的字符串
const Obj = {id:nanoid(),name:'xx'}

确认对话框

若点击按钮需要弹出确认对话框当用户点击“确定”后方可执行按钮事件可利用 window 下的 confirm 函数

if(window.confirm('提示语句')){执行相应代码}

Chrome 中插件

FeHelper 可帮助整理 json 数据缩进显示

解构赋值

解构赋值

let {对象中的属性} = 对象

连续解构赋值

let {对象中的属性对象:{属性对象中的属性}} = 对象

let obj = {a:{b:{c:1}}}
const {a:{b:{c}}} = obj
console.log(c) //输出 1

连续解构赋值并重命名

let {对象中的属性:新名字} = 对象

let obj = {a:{b:1}}
const {a:{b:data}} = obj
console.log(data)  //也能输出 1