本文所用代码链接:https://oss.rainsheep.cn/save/react%E5%85%A8%E5%AE%B6%E6%A1%B6%E8%B5%84%E6%96%99.zip

1. React 入门

1.1 React 简介

1.1.1 官网

  1. 英文官网: https://reactjs.org/
  2. 中文官网: https://react.docschina.org/

1.1.2 介绍描述

  1. 用于动态构建用户界面的 JavaScript 库(只关注于视图)
  2. 由Facebook开源

1.1.3 React的特点

  1. 声明式编码
  2. 组件化编码
  3. React Native 编写原生应用
  4. 高效(优秀的Diffing算法)

1.1.4 React 高效的原因

  1. 使用虚拟 (virtual)DOM, 不总是直接操作页面真实 DOM。
  2. DOM Diffing 算法, 最小化页面重绘。

1.2 React 基本使用

1.2.1 效果

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>hello_react</title>
</head>
<body>
<!-- 准备好一个“容器” -->
<div id="test"></div>

<!-- 引入react核心库 -->
<script type="text/javascript" src="../js/react.development.js"></script>
<!-- 引入react-dom,用于支持react操作DOM -->
<script type="text/javascript" src="../js/react-dom.development.js"></script>
<!-- 引入babel,用于将jsx转为js -->
<script type="text/javascript" src="../js/babel.min.js"></script>

<script type="text/babel"> /* 此处一定要写babel */
//1.创建虚拟DOM
const VDOM = <h1>Hello,React</h1> /* 此处一定不要写引号,因为不是字符串 */
//2.渲染虚拟DOM到页面
ReactDOM.render(VDOM, document.getElementById('test'))
</script>
</body>
</html>

react 效果

1.2.2 相关 js 库

  1. react.js:React 核心库,如 props、state、refs。
  2. react-dom.js:提供操作 DOM 的 react 扩展库。
  3. babel.min.js:解析 JSX 语法代码转为 JS 代码的库。

1.2.3 创建虚拟 DOM 的两种方式

  1. 纯JS方式(一般不用)

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>2_使用js创建虚拟DOM</title>
    </head>
    <body>
    <!-- 准备好一个“容器” -->
    <div id="test"></div>
    
    <!-- 引入react核心库 -->
    <script type="text/javascript" src="../js/react.development.js"></script>
    <!-- 引入react-dom,用于支持react操作DOM -->
    <script type="text/javascript" src="../js/react-dom.development.js"></script>
    
    <script type="text/javascript">
        //1.创建虚拟DOM
        const VDOM = React.createElement('h1', {id: 'title'}, React.createElement('span', {}, 'Hello,React'))
        //2.渲染虚拟DOM到页面
        ReactDOM.render(VDOM, document.getElementById('test'))
    </script>
    </body>
    </html>
    

    js 创建虚拟 DOM

  2. JSX方式

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>1_使用jsx创建虚拟DOM</title>
    </head>
    <body>
    <!-- 准备好一个“容器” -->
    <div id="test"></div>
    
    <!-- 引入react核心库 -->
    <script type="text/javascript" src="../js/react.development.js"></script>
    <!-- 引入react-dom,用于支持react操作DOM -->
    <script type="text/javascript" src="../js/react-dom.development.js"></script>
    <!-- 引入babel,用于将jsx转为js -->
    <script type="text/javascript" src="../js/babel.min.js"></script>
    
    <script type="text/babel"> /* 此处一定要写babel */
    //1.创建虚拟DOM
    const VDOM = (  /* 此处一定不要写引号,因为不是字符串 */
        <h1 id="title">
            <span>Hello,React</span>
        </h1>
    )
    //2.渲染虚拟DOM到页面
    ReactDOM.render(VDOM, document.getElementById('test'))
    </script>
    </body>
    </html>
    

    js 创建虚拟 DOM

1.2.4 虚拟 DOM 与真实 DOM

  1. React 提供了一些 API 来创建一种 “特别” 的一般 js 对象,下面创建的就是一个简单的虚拟 DOM 对象

    const VDOM = React.createElement('xx', {id: 'xx'}, 'xx')
    
  2. 虚拟 DOM 对象最终都会被 React 转换为真实的 DOM

  3. 我们编码时基本只需要操作 react 的虚拟 DOM 相关数据, react 会转换为真实 DOM。

  4. 关于虚拟 DOM

    1. 本质是 Object 类型的对象(一般对象)
    2. 虚拟 DOM 比较“轻”,真实 DOM 比较“重”,因为虚拟 DOM 是 React 内部在用,无需真实 DOM 上那么多的属性。
    3. 虚拟 DOM 最终会被 React 转化为真实 DOM,呈现在页面上。
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>3_虚拟DOM与真实DOM</title>
</head>
<body>
<!-- 准备好一个“容器” -->
<div id="test"></div>
<div id="demo"></div>

<!-- 引入react核心库 -->
<script type="text/javascript" src="../js/react.development.js"></script>
<!-- 引入react-dom,用于支持react操作DOM -->
<script type="text/javascript" src="../js/react-dom.development.js"></script>
<!-- 引入babel,用于将jsx转为js -->
<script type="text/javascript" src="../js/babel.min.js"></script>

<script type="text/babel"> /* 此处一定要写babel */
//1.创建虚拟DOM
const VDOM = (  /* 此处一定不要写引号,因为不是字符串 */
    <h1 id="title">
        <span>Hello,React</span>
    </h1>
)
//2.渲染虚拟DOM到页面
ReactDOM.render(VDOM, document.getElementById('test'))

const TDOM = document.getElementById('demo')
console.log('虚拟DOM', VDOM);
console.log('真实DOM', TDOM);

console.log(typeof VDOM);
console.log(VDOM instanceof Object);
</script>
</body>
</html>

虚拟 DOM vs 真实 DOM

1.3 React JSX

1.3.1 效果

JSX 效果

1.3.2 JSX

  1. 全称: JavaScript XML
  2. react 定义的一种类似于 XML 的 JS 扩展语法: JS + XML本质是 React.createElement(component, props, ...childen) 方法的语法糖
  3. 作用: 用来简化创建虚拟 DOM
    • 写法:var ele = <h1>Hello JSX!</h1>
    • 注意1:它不是字符串, 也不是 HTML/XML 标签
    • 注意2:它最终产生的就是一个 JS 对象
  4. 标签名任意: HTML 标签或其它标签
  5. 标签属性任意: HTML 标签属性或其它
  6. 基本语法规则
    • 遇到 < 开头的代码, 以标签的语法解析
      • 若小写字母开头,则将该标签转为 html 中同名元素,若 html 中无该标签对应的同名元素,则报错。
      • 若大写字母开头,react 就去渲染对应的组件,若组件没有定义,则报错。
    • 遇到以 { 开头的代码,以 JS 语法解析: 标签中的 js 表达式必须用 { } 包含
    • 定义虚拟 DOM 时,不要写引号。
    • 样式的类名指定不要用 class,要用 className。
    • 内联样式,要用 style={{key:value}} 的形式去写。
    • 只有一个根标签 (<></><div></div> 一致)
    • 标签必须闭合

1.3.3 渲染虚拟 DOM(元素)

  1. 语法:ReactDOM.render(virtualDOM, containerDOM)
  2. 作用: 将虚拟 DOM 元素渲染到页面中的真实容器 DOM 中显示
  3. 参数说明
    • 参数一: 纯 js 或 jsx 创建的虚拟 dom 对象
    • 参数二: 用来包含虚拟 DOM 元素的真实 dom 元素对象(一般是一个 div)
<script type="text/babel">
	const myId = "aTgUiGu";
	const myData = "HeLlo,rEaCt";

	//1.创建虚拟DOM
	const VDOM = (
		<div>
			<h2 className="title" id={myId.toLowerCase()}>
				<span style={{ color: "white", fontSize: "29px" }}>
					{myData.toLowerCase()}
				</span>
			</h2>
			<h2 className="title" id={myId.toUpperCase()}>
				<span style={{ color: "white", fontSize: "29px" }}>
					{myData.toLowerCase()}
				</span>
			</h2>
			<input type="text" />
		</div>
	);
	//2.渲染虚拟DOM到页面
	ReactDOM.render(VDOM, document.getElementById("test"));
</script>

1.3.4 js 语句与表达式

js 表达式

表达式:一个表达式会产生一个值,可以放在任何一个需要值的地方

  1. a
  2. a+b
  3. demo(1)
  4. arr.map()
  5. function test () {}

js 语句

  1. if(){}
  2. for() {}
  3. switch() {case: xxxx}

1.3.5 JSX 练习

需求: 动态展示如下列表

JSX 练习

<script type="text/babel" >
    //模拟一些数据
    const data = ['Angular','React','Vue']
    //1.创建虚拟DOM
    const VDOM = (
        <div>
            <h1>前端js框架列表</h1>
            <ul>
                {
                    data.map((item,index)=>{
                        return <li key={index}>{item}</li>
                    })
                }
            </ul>
        </div>
    )
    //2.渲染虚拟DOM到页面
    ReactDOM.render(VDOM,document.getElementById('test'))
</script>

1.4 模块与组件、模块化与组件化的理解

1.4.1 模块

  1. 理解:向外提供特定功能的 js 程序, 一般就是一个 js 文件
  2. 为什么要拆成模块:随着业务逻辑增加,代码越来越多且复杂。
  3. 作用:复用 js, 简化 js 的编写, 提高 js 运行效率

1.4.2 组件

  1. 理解:用来实现局部功能效果的代码和资源的集合 (html/css/js/image等等)
  2. 为什么要用组件: 一个界面的功能更复杂
  3. 作用:复用编码, 简化项目编码, 提高运行效率

1.4.3 模块化

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

1.4.4 组件化

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

组件化

2. React 面向组件编程

2.1 基本理解与使用

2.1.1 使用 React 开发者工具调试

Chrome 插件:React Developer Tools

安装完后开发者工具会多两个工具

开发者工具

当项目为开发状态(未打包),插件为橘色

插件

线上为蓝色

Components 能展示组件及属性,Profiler 能展示网站的性能。

蓝色

Components

2.1.2 函数式组件和类式组件

函数式组件:

<script type="text/babel">
    //1.创建函数式组件
    function MyComponent() {
        console.log(this); //此处的 this 是 undefined,因为 babel 编译后开启了严格模式
        return <h2>我是用函数定义的组件(适用于【简单组件】的定义)</h2>
    }

    //2.渲染组件到页面
    ReactDOM.render(<MyComponent/>, document.getElementById('test'))
    /*
        执行了ReactDOM.render(<MyComponent/>.......之后,发生了什么?
                1.React解析组件标签,找到了MyComponent组件。
                2.发现组件是使用函数定义的,随后调用该函数,将返回的虚拟DOM转为真实DOM,随后呈现在页面中。
    */
</script>

函数式组件

类式组件:

<script type="text/babel">
    //1.创建类式组件
    class MyComponent extends React.Component {
        render() {
            // render 是放在哪里的?—— MyComponent 的原型对象上,供实例使用。
            // render 中的 this 是谁?—— MyComponent 的实例对象 <=> MyComponent 组件实例对象。
            console.log('render中的this:', this);
            return <h2>我是用类定义的组件(适用于【复杂组件】的定义)</h2>
        }
    }

    //2.渲染组件到页面
    ReactDOM.render(<MyComponent/>, document.getElementById('test'))
    /* 
        执行了ReactDOM.render(<MyComponent/>.......之后,发生了什么?
                1.React 解析组件标签,找到了 MyComponent 组件。
                2.发现组件是使用类定义的,随后 new 出来该类的实例,并通过该实例调用到原型上的 render 方法。
                3.将 render 返回的虚拟 DOM 转为真实 DOM,随后呈现在页面中。
    */
</script>

类式组件

为什么 render 中的 this 组件实例对象?

因为调用 render 的实例是组件实例,React 帮创建的。

2.1.3 注意

  1. 组件名必须首字母大写
  2. 虚拟 DOM 元素只能有一个根元素
  3. 虚拟 DOM 元素必须有结束标签

2.1.4 渲染类组件标签的基本流程

  1. React 内部会创建组件实例对象
  2. 调用 render() 得到虚拟 DOM, 并解析为真实 DOM
  3. 插入到指定的页面元素内部

2.2 组件三大核心属性1:state

2.2.1 效果

需求: 定义一个展示天气信息的组件

  1. 默认展示天气炎热或凉爽
  2. 点击文字切换天气

Weather 组件

<script type="text/babel">
    //1.创建组件
    class Weather extends React.Component {

        //构造器调用几次? ———— 1次
        constructor(props) {
            console.log('constructor');
            super(props)
            //初始化状态
            this.state = {isHot: false, wind: '微风'}
            //解决changeWeather中this指向问题
            this.changeWeather = this.changeWeather.bind(this)
        }

        //render调用几次? ———— 1+n次 1是初始化的那次 n是状态更新的次数
        render() {
            console.log('render');
            //读取状态
            const {isHot, wind} = this.state
            return <h1 onClick={this.changeWeather}>今天天气很{isHot ? '炎热' : '凉爽'},{wind}</h1>
        }

        //changeWeather调用几次? ———— 点几次调几次
        changeWeather() {
            //changeWeather放在哪里? ———— Weather的原型对象上,供实例使用
            //由于changeWeather是作为onClick的回调,所以不是通过实例调用的,是直接调用,所以 changeWeather 中 this 不是实例本身
            //类中的方法默认开启了局部的严格模式,所以changeWeather中的this为undefined

            console.log('changeWeather');
            //获取原来的isHot值
            const isHot = this.state.isHot
            //严重注意:状态必须通过setState进行更新,且更新是一种合并,不是替换。
            this.setState({isHot: !isHot})
            console.log(this);

            //严重注意:状态(state)不可直接更改,下面这行就是直接更改!!!
            //this.state.isHot = !isHot //这是错误的写法
        }
    }

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

</script>

state 简写方式

<script type="text/babel">
    //1.创建组件
    class Weather extends React.Component {
        //初始化状态
        state = {isHot: false, wind: '微风'}

        render() {
            const {isHot, wind} = this.state
            return <h1 onClick={this.changeWeather}>今天天气很{isHot ? '炎热' : '凉爽'},{wind}</h1>
        }

        //自定义方法————要用赋值语句的形式+箭头函数
        changeWeather = () => {
            const isHot = this.state.isHot
            this.setState({isHot: !isHot})
        }
    }

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

</script>

2.2.2 理解

  1. state 是组件对象最重要的属性, 值是对象(可以包含多个 key-value 的组合)
  2. 组件被称为"状态机", 通过更新组件的 state 来更新对应的页面显示(重新渲染组件)

2.2.3 强烈注意

  1. 组件中 render 方法中的 this 为组件实例对象

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

    • 强制绑定 this: 通过函数对象的 bind()
    • 箭头函数
  3. 状态数据,不能直接修改或更新

2.3 组件三大核心属性2:props

2.3.1 效果

需求: 自定义用来显示一个人员信息的组件

  1. 姓名必须指定,且为字符串类型;
  2. 性别为字符串类型,如果性别没有指定,默认为男
  3. 年龄为字符串类型,且为数字类型,默认值为18

props

2.3.2 理解

  1. 每个组件对象都会有 props(properties的简写) 属性
  2. 组件标签的所有属性都保存在 props 中

2.3.3 作用

  1. 通过标签属性从组件外向组件内传递变化的数据
  2. 注意: 组件内部不要修改 props 数据

2.3.4 编码操作

  1. 内部读取某个属性值 this.props.name

  2. 对 props 中的属性值进行类型限制和必要性限制

    第一种方式(React v15.5 开始已弃用)

    Person.propTypes = {
     name: React.PropTypes.string.isRequired,
     age: React.PropTypes.number
    }
    

    第二种方式(新):使用 prop-types 库进限制(需要引入 prop-types 库)

    Person.propTypes = {
      name: PropTypes.string.isRequired,
      age: PropTypes.number. 
    }
    
  3. 扩展属性: 将对象的所有属性通过 props 传递

    <Person {...person}/>
    
  4. 默认属性值:

    Person.defaultProps = {
      age: 18,
      sex:'男'
    }
    
  5. 组件类的构造函数

    constructor(props){
      super(props)
      console.log(props)//打印所有属性
    }
    

2.3.5 实例

2.3.5.1 props 基本使用

<script type="text/babel">
    //创建组件
    class Person extends React.Component {
        render() {
            // console.log(this);
            const {name, age, sex} = this.props
            return (
                <ul>
                    <li>姓名:{name}</li>
                    <li>性别:{sex}</li>
                    <li>年龄:{age + 1}</li>
                </ul>
            )
        }
    }

    //渲染组件到页面
    ReactDOM.render(<Person name="jerry" age={19} sex="男"/>, document.getElementById('test1'))
    ReactDOM.render(<Person name="tom" age={18} sex="女"/>, document.getElementById('test2'))

    const p = {name: '老刘', age: 18, sex: '女'}
    // console.log('@',...p);
    // ReactDOM.render(<Person name={p.name} age={p.age} sex={p.sex}/>,document.getElementById('test3'))
    ReactDOM.render(<Person {...p}/>, document.getElementById('test3'))
</script>

2.3.5.2 对 props 进行限制

<script type="text/babel">
    //创建组件
    class Person extends React.Component {
        render() {
            // console.log(this);
            const {name, age, sex} = this.props
            //props是只读的
            //this.props.name = 'jack' //此行代码会报错,因为props是只读的
            return (
                <ul>
                    <li>姓名:{name}</li>
                    <li>性别:{sex}</li>
                    <li>年龄:{age + 1}</li>
                </ul>
            )
        }
    }

    //对标签属性进行类型、必要性的限制
    Person.propTypes = {
        name: PropTypes.string.isRequired, //限制name必传,且为字符串
        sex: PropTypes.string,//限制sex为字符串
        age: PropTypes.number,//限制age为数值
        speak: PropTypes.func,//限制speak为函数
    }
    //指定默认标签属性值
    Person.defaultProps = {
        sex: '男',//sex默认值为男
        age: 18 //age默认值为18
    }
    //渲染组件到页面
    ReactDOM.render(<Person name={100} speak={speak}/>, document.getElementById('test1'))
    ReactDOM.render(<Person name="tom" age={18} sex="女"/>, document.getElementById('test2'))

    const p = {name: '老刘', age: 18, sex: '女'}
    // console.log('@',...p);
    // ReactDOM.render(<Person name={p.name} age={p.age} sex={p.sex}/>,document.getElementById('test3'))
    ReactDOM.render(<Person {...p}/>, document.getElementById('test3'))

    function speak() {
        console.log('我说话了');
    }
</script>

2.3.5.3 props 的简写方式

<script type="text/babel">
    //创建组件
    class Person extends React.Component {

        constructor(props) {
            //构造器是否接收props,是否传递给super,取决于:是否希望在构造器中通过this访问props
            // console.log(props);
            super(props)
            console.log('constructor', this.props);
        }

        //对标签属性进行类型、必要性的限制
        static propTypes = {
            name: PropTypes.string.isRequired, //限制name必传,且为字符串
            sex: PropTypes.string,//限制sex为字符串
            age: PropTypes.number,//限制age为数值
        }

        //指定默认标签属性值
        static defaultProps = {
            sex: '男',//sex默认值为男
            age: 18 //age默认值为18
        }

        render() {
            // console.log(this);
            const {name, age, sex} = this.props
            //props是只读的
            //this.props.name = 'jack' //此行代码会报错,因为props是只读的
            return (
                <ul>
                    <li>姓名:{name}</li>
                    <li>性别:{sex}</li>
                    <li>年龄:{age + 1}</li>
                </ul>
            )
        }
    }

    //渲染组件到页面
    ReactDOM.render(<Person name="jerry"/>, document.getElementById('test1'))
</script>

2.3.5.4 函数组件使用 props

<script type="text/babel">
    //创建组件
    function Person(props) {
        const {name, age, sex} = props
        return (
            <ul>
                <li>姓名:{name}</li>
                <li>性别:{sex}</li>
                <li>年龄:{age}</li>
            </ul>
        )
    }

    Person.propTypes = {
        name: PropTypes.string.isRequired, //限制name必传,且为字符串
        sex: PropTypes.string,//限制sex为字符串
        age: PropTypes.number,//限制age为数值
    }

    //指定默认标签属性值
    Person.defaultProps = {
        sex: '男',//sex默认值为男
        age: 18 //age默认值为18
    }
    //渲染组件到页面
    ReactDOM.render(<Person name="jerry"/>, document.getElementById('test1'))
</script>

2.4 组件三大核心属性3:refs 与事件处理

2.4.1 效果

需求: 自定义组件, 功能说明如下:

  1. 点击按钮, 提示第一个输入框中的值
  2. 当第 2 个输入框失去焦点时, 提示这个输入框中的值

refs

2.4.2 理解

组件内的标签可以定义 ref 属性来标识自己

2.4.3 编码

  1. 字符串形式 ref <input ref="input1"/>

    <script type="text/babel">
        //创建组件
        class Demo extends React.Component {
            //展示左侧输入框的数据
            showData = () => {
                const {input1} = this.refs
                alert(input1.value)
            }
            //展示右侧输入框的数据
            showData2 = () => {
                const {input2} = this.refs
                alert(input2.value)
            }
    
            render() {
                return (
                    <div>
                        <input ref="input1" type="text" placeholder="点击按钮提示数据"/>&nbsp;
                        <button onClick={this.showData}>点我提示左侧的数据</button>
                        &nbsp;
                        <input ref="input2" onBlur={this.showData2} type="text" placeholder="失去焦点提示数据"/>
                    </div>
                )
            }
        }
    
        //渲染组件到页面
        ReactDOM.render(<Demo a="1" b="2"/>, document.getElementById('test'))
    </script>
    
  2. 回调形式的 ref <input ref={(c)=>{this.input1 = c}}

    <script type="text/babel">
        //创建组件
        class Demo extends React.Component {
            //展示左侧输入框的数据
            showData = () => {
                const {input1} = this
                alert(input1.value)
            }
            //展示右侧输入框的数据
            showData2 = () => {
                const {input2} = this
                alert(input2.value)
            }
    
            render() {
                return (
                    <div>
                        <input ref={c => this.input1 = c} type="text" placeholder="点击按钮提示数据"/>&nbsp;
                        <button onClick={this.showData}>点我提示左侧的数据</button>
                        &nbsp;
                        <input onBlur={this.showData2} ref={c => this.input2 = c} type="text"
                               placeholder="失去焦点提示数据"/>&nbsp;
                    </div>
                )
            }
        }
    
        //渲染组件到页面
        ReactDOM.render(<Demo a="1" b="2"/>, document.getElementById('test'))
    </script>
    

    如果 ref 回调函数是以内联函数的方式定义的,在更新过程中它会被执行两次,第一次传入参数 null,然后第二次会传入参数 DOM 元素。

    这是因为在每次渲染时会创建一个新的函数实例,所以 React 清空旧的 ref 并且设置新的。

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

    <script type="text/babel">
     //创建组件
     class Demo extends React.Component {
    
         state = {isHot: false}
    
         showInfo = () => {
             const {input1} = this
             alert(input1.value)
         }
    
         changeWeather = () => {
             //获取原来的状态
             const {isHot} = this.state
             //更新状态
             this.setState({isHot: !isHot})
         }
    
         saveInput = (c) => {
             this.input1 = c;
             console.log('@', c);
         }
    
         render() {
             const {isHot} = this.state
             return (
                 <div>
                     <h2>今天天气很{isHot ? '炎热' : '凉爽'}</h2>
                     {/*<input ref={(c)=>{this.input1 = c;console.log('@',c);}} type="text"/><br/><br/>*/}
                     <input ref={this.saveInput} type="text"/><br/><br/>
                     <button onClick={this.showInfo}>点我提示输入的数据</button>
                     <button onClick={this.changeWeather}>点我切换天气</button>
                 </div>
             )
         }
     }
    
     //渲染组件到页面
     ReactDOM.render(<Demo/>, document.getElementById('test'))
    </script>
    
  3. createRef 创建 ref 容器

    myRef = React.createRef() 
    <input ref={this.myRef}/>
    
    <script type="text/babel">
        //创建组件
        class Demo extends React.Component {
            /* 
                React.createRef调用后可以返回一个容器,该容器可以存储被ref所标识的节点,该容器是“专人专用”的
             */
            myRef = React.createRef()
            myRef2 = React.createRef()
            //展示左侧输入框的数据
            showData = () => {
                alert(this.myRef.current.value);
            }
            //展示右侧输入框的数据
            showData2 = () => {
                alert(this.myRef2.current.value);
            }
    
            render() {
                return (
                    <div>
                        <input ref={this.myRef} type="text" placeholder="点击按钮提示数据"/>&nbsp;
                        <button onClick={this.showData}>点我提示左侧的数据</button>
                        &nbsp;
                        <input onBlur={this.showData2} ref={this.myRef2} type="text" placeholder="失去焦点提示数据"/>&nbsp;
                    </div>
                )
            }
        }
    
        //渲染组件到页面
        ReactDOM.render(<Demo a="1" b="2"/>, document.getElementById('test'))
    </script>
    

2.4.4 事件处理

  1. 通过 onXxx 属性指定事件处理函数(注意大小写)
    • React 使用的是自定义(合成)事件, 而不是使用的原生 DOM 事件
    • React 中的事件是通过事件委托方式处理的(委托给组件最外层的元素)
  2. 通过 event.target 得到发生事件的 DOM 元素对象
<script type="text/babel">
    //创建组件
    class Demo extends React.Component {
        /* 
            (1).通过onXxx属性指定事件处理函数(注意大小写)
                    a.React使用的是自定义(合成)事件, 而不是使用的原生DOM事件 —————— 为了更好的兼容性
                    b.React中的事件是通过事件委托方式处理的(委托给组件最外层的元素) ————————为了的高效
            (2).通过event.target得到发生事件的DOM元素对象 ——————————不要过度使用ref
         */
        //创建ref容器
        myRef = React.createRef()
        myRef2 = React.createRef()

        //展示左侧输入框的数据
        showData = (event) => {
            console.log(event.target);
            alert(this.myRef.current.value);
        }

        //展示右侧输入框的数据
        showData2 = (event) => {
            alert(event.target.value);
        }

        render() {
            return (
                <div>
                    <input ref={this.myRef} type="text" placeholder="点击按钮提示数据"/>&nbsp;
                    <button onClick={this.showData}>点我提示左侧的数据</button>
                    &nbsp;
                    <input onBlur={this.showData2} type="text" placeholder="失去焦点提示数据"/>&nbsp;
                </div>
            )
        }
    }

    //渲染组件到页面
    ReactDOM.render(<Demo a="1" b="2"/>, document.getElementById('test'))
</script>

2.5 收集表单数据

2.5.1 效果

需求: 定义一个包含表单的组件
输入用户名密码后, 点击登录提示输入信息

收集表单数据

2.5.2 理解

包含表单的组件分类

  1. 非受控组件(输入类的,没有用状态维护,现用现取)

    <script type="text/babel">
        //创建组件
        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>
                )
            }
        }
    
        //渲染组件
        ReactDOM.render(<Login/>, document.getElementById('test'))
    </script>
    
  2. 受控组件(输入类的维护至状态中,随用随取,双向绑定)

    <script type="text/babel">
        //创建组件
        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>
                )
            }
        }
    
        //渲染组件
        ReactDOM.render(<Login/>, document.getElementById('test'))
    </script>
    

    ref 尽量少用,受控组件可以减少 ref 的使用

2.6 组件的生命周期

2.6.1 效果

需求:定义组件实现以下功能:

  1. 让指定的文本做显示 / 隐藏的渐变动画
  2. 从完全可见,到彻底消失,耗时2S
  3. 点击“不活了”按钮从界面中卸载组件

component 生命周期

<script type="text/babel">
    //创建组件
    //生命周期回调函数 <=> 生命周期钩子函数 <=> 生命周期函数 <=> 生命周期钩子
    class Life extends React.Component {

        state = {opacity: 1}

        death = () => {
            //卸载组件
            ReactDOM.unmountComponentAtNode(document.getElementById('test'))
        }

        //组件挂完毕
        componentDidMount() {
            console.log('componentDidMount');
            this.timer = setInterval(() => {
                //获取原状态
                let {opacity} = this.state
                //减小0.1
                opacity -= 0.1
                if (opacity <= 0) opacity = 1
                //设置新的透明度
                this.setState({opacity})
            }, 200);
        }

        //组件将要卸载
        componentWillUnmount() {
            //清除定时器
            clearInterval(this.timer)
        }

        //初始化渲染、状态更新之后
        render() {
            console.log('render');
            return (
                <div>
                    <h2 style={{opacity: this.state.opacity}}>React学不会怎么办?</h2>
                    <button onClick={this.death}>不活了</button>
                </div>
            )
        }
    }

    //渲染组件
    ReactDOM.render(<Life/>, document.getElementById('test'))
</script>

2.6.2 理解

  1. 组件从创建到死亡它会经历一些特定的阶段。
  2. React 组件中包含一系列勾子函数(生命周期回调函数), 会在特定的时刻调用。
  3. 我们在定义组件时,会在特定的生命周期回调函数中,做特定的工作。

2.6.3 生命周期流程图(旧)

react生命周期(旧)

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

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

  1. constructor()
  2. componentWillMount()
  3. render()
  4. componentDidMount()

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

  1. shouldComponentUpdate()
  2. componentWillUpdate()
  3. render()
  4. componentDidUpdate()

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

  1. componentWillUnmount()

componentWillReceiveProps 在父组件更新(非第一次 render) 时会被调用。

<script type="text/babel">
    /*
            1. 初始化阶段: 由ReactDOM.render()触发---初次渲染
                                1.	constructor()
                                2.	componentWillMount()
                                3.	render()
                                4.	componentDidMount() =====> 常用
                                                一般在这个钩子中做一些初始化的事,例如:开启定时器、发送网络请求、订阅消息
            2. 更新阶段: 由组件内部this.setSate()或父组件render触发
                                1.	shouldComponentUpdate()
                                2.	componentWillUpdate()
                                3.	render() =====> 必须使用的一个
                                4.	componentDidUpdate()
            3. 卸载组件: 由ReactDOM.unmountComponentAtNode()触发
                                1.	componentWillUnmount()  =====> 常用
                                                一般在这个钩子中做一些收尾的事,例如:关闭定时器、取消订阅消息
    */
    //创建组件
    class Count extends React.Component {

        //构造器
        constructor(props) {
            console.log('Count---constructor');
            super(props)
            //初始化状态
            this.state = {count: 0}
        }

        //加1按钮的回调
        add = () => {
            //获取原状态
            const {count} = this.state
            //更新状态
            this.setState({count: count + 1})
        }

        //卸载组件按钮的回调
        death = () => {
            ReactDOM.unmountComponentAtNode(document.getElementById('test'))
        }

        //强制更新按钮的回调
        force = () => {
            this.forceUpdate()
        }

        //组件将要挂载的钩子
        componentWillMount() {
            console.log('Count---componentWillMount');
        }

        //组件挂载完毕的钩子
        componentDidMount() {
            console.log('Count---componentDidMount');
        }

        //组件将要卸载的钩子
        componentWillUnmount() {
            console.log('Count---componentWillUnmount');
        }

        //控制组件更新的“阀门”
        shouldComponentUpdate() {
            console.log('Count---shouldComponentUpdate');
            return true
        }

        //组件将要更新的钩子
        componentWillUpdate() {
            console.log('Count---componentWillUpdate');
        }

        //组件更新完毕的钩子
        componentDidUpdate() {
            console.log('Count---componentDidUpdate');
        }

        render() {
            console.log('Count---render');
            const {count} = this.state
            return (
                <div>
                    <h2>当前求和为:{count}</h2>
                    <button onClick={this.add}>点我+1</button>
                    <button onClick={this.death}>卸载组件</button>
                    <button onClick={this.force}>不更改任何状态中的数据,强制更新一下</button>
                </div>
            )
        }
    }

    //父组件A
    class A extends React.Component {
        //初始化状态
        state = {carName: '奔驰'}

        changeCar = () => {
            this.setState({carName: '奥拓'})
        }

        render() {
            return (
                <div>
                    <div>我是A组件</div>
                    <button onClick={this.changeCar}>换车</button>
                    <B carName={this.state.carName}/>
                </div>
            )
        }
    }

    //子组件B
    class B extends React.Component {
        //组件将要接收新的props的钩子
        componentWillReceiveProps(props) {
            console.log('B---componentWillReceiveProps', props);
        }

        //控制组件更新的“阀门”
        shouldComponentUpdate() {
            console.log('B---shouldComponentUpdate');
            return true
        }

        //组件将要更新的钩子
        componentWillUpdate() {
            console.log('B---componentWillUpdate');
        }

        //组件更新完毕的钩子
        componentDidUpdate() {
            console.log('B---componentDidUpdate');
        }

        render() {
            console.log('B---render');
            return (
                <div>我是B组件,接收到的车是:{this.props.carName}</div>
            )
        }
    }

    //渲染组件
    ReactDOM.render(<A/>, document.getElementById('test'))
</script>

2.6.4 声明周期流程图(新)

react生命周期新

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

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

  1. constructor()
  2. getDerivedStateFromProps
  3. render()
  4. componentDidMount()

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

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

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

  1. componentWillUnmount()
<script type="text/babel">
    //创建组件
    class Count extends React.Component {
        // change the world
        /*
            1. 初始化阶段: 由ReactDOM.render()触发---初次渲染
                            1.	constructor()
                            2.	getDerivedStateFromProps
                            3.	render()
                            4.	componentDidMount() =====> 常用
                                        一般在这个钩子中做一些初始化的事,例如:开启定时器、发送网络请求、订阅消息
            2. 更新阶段: 由组件内部this.setSate()或父组件重新render触发
                            1.	getDerivedStateFromProps
                            2.	shouldComponentUpdate()
                            3.	render()
                            4.	getSnapshotBeforeUpdate
                            5.	componentDidUpdate()
            3. 卸载组件: 由ReactDOM.unmountComponentAtNode()触发
                            1.	componentWillUnmount()  =====> 常用
                                        一般在这个钩子中做一些收尾的事,例如:关闭定时器、取消订阅消息
        */

        //构造器
        constructor(props) {
            console.log('Count---constructor');
            super(props)
            //初始化状态
            this.state = {count: 0}
        }

        //加1按钮的回调
        add = () => {
            //获取原状态
            const {count} = this.state
            //更新状态
            this.setState({count: count + 1})
        }

        //卸载组件按钮的回调
        death = () => {
            ReactDOM.unmountComponentAtNode(document.getElementById('test'))
        }

        //强制更新按钮的回调
        force = () => {
            this.forceUpdate()
        }

        //若state的值在任何时候都取决于props,那么可以使用getDerivedStateFromProps
        static getDerivedStateFromProps(props, state) {
            console.log('getDerivedStateFromProps', props, state);
            return null
        }

        //在更新之前获取快照
        getSnapshotBeforeUpdate() {
            console.log('getSnapshotBeforeUpdate');
            return 'atguigu'
        }

        //组件挂载完毕的钩子
        componentDidMount() {
            console.log('Count---componentDidMount');
        }

        //组件将要卸载的钩子
        componentWillUnmount() {
            console.log('Count---componentWillUnmount');
        }

        //控制组件更新的“阀门”
        shouldComponentUpdate() {
            console.log('Count---shouldComponentUpdate');
            return true
        }

        //组件更新完毕的钩子
        componentDidUpdate(preProps, preState, snapshotValue) {
            console.log('Count---componentDidUpdate', preProps, preState, snapshotValue);
        }

        render() {
            console.log('Count---render');
            const {count} = this.state
            return (
                <div>
                    <h2>当前求和为:{count}</h2>
                    <button onClick={this.add}>点我+1</button>
                    <button onClick={this.death}>卸载组件</button>
                    <button onClick={this.force}>不更改任何状态中的数据,强制更新一下</button>
                </div>
            )
        }
    }

    //渲染组件
    ReactDOM.render(<Count count={199}/>, document.getElementById('test'))
</script>

getDerivedStateFromProps 例子

getDerivedStateFromProps 是 React 组件生命周期中的一个静态方法,用于在组件接收到新的属性(props)时更新组件的状态(state)。它在组件的每次渲染之前调用,并返回一个对象,表示需要更新的状态,或者返回 null 表示不需要更新状态。

以下是一个使用 getDerivedStateFromProps 的简单示例:

import React from 'react';

class MyComponent extends React.Component {
  state = {
    prevPropsValue: null,
  };

  static getDerivedStateFromProps(nextProps, prevState) {
    // 在这个方法中,您可以根据新的 props 更新状态
    if (nextProps.value !== prevState.prevPropsValue) {
      return { prevPropsValue: nextProps.value };
    }
    return null; // 不需要更新状态
  }

  render() {
    return <div>Previous Props Value: {this.state.prevPropsValue}</div>;
  }
}

export default MyComponent;

在这个示例中,当组件接收到新的 value 属性时,getDerivedStateFromProps 方法会将这个新属性的值与之前的状态进行比较。如果不同,就会返回一个新的状态对象,将新的属性值存储在 prevPropsValue 中。如果相同,就返回 null,表示不需要更新状态。

请注意,getDerivedStateFromProps 是一个静态方法,因此它没有访问组件实例的 this。它接收两个参数:nextProps(新的属性值)和prevState(之前的状态值)。您可以使用这些参数来判断是否需要更新状态。

然而,需要注意的是,尽管 getDerivedStateFromProps 在某些情况下可以用于更新状态,但在大多数情况下,更推荐使用受控组件或其他状态管理方法,以保持组件的可预测性和可维护性。

getSnapShotBeforeUpdate 的使用场景

<script type="text/babel">
    class NewsList extends React.Component {

        state = {newsArr: []}

        componentDidMount() {
            setInterval(() => {
                //获取原状态
                const {newsArr} = this.state
                //模拟一条新闻
                const news = '新闻' + (newsArr.length + 1)
                //更新状态
                this.setState({newsArr: [news, ...newsArr]})
            }, 1000);
        }

        getSnapshotBeforeUpdate() {
            return this.refs.list.scrollHeight
        }

        // height 为 getSnapshotBeforeUpdate 的返回值
        componentDidUpdate(preProps, preState, height) {
            this.refs.list.scrollTop += this.refs.list.scrollHeight - height
        }

        render() {
            return (
                <div className="list" ref="list">
                    {
                        this.state.newsArr.map((n, index) => {
                            return <div key={index} className="news">{n}</div>
                        })
                    }
                </div>
            )
        }
    }

    ReactDOM.render(<NewsList/>, document.getElementById('test'))
</script>

2.6.5 重要的钩子

  1. render:初始化渲染或更新渲染调用
  2. componentDidMount:开启监听, 发送 ajax 请求
  3. componentWillUnmount:做一些收尾工作, 如: 清理定时器

2.6.6 即将废弃的钩子

  1. componentWillMount
  2. componentWillReceiveProps
  3. componentWillUpdate

现在使用会出现警告,下一个大版本需要加上 UNSAFE_ 前缀才能使用,以后可能会被彻底废弃,不建议使用。

2.7 虚拟 DOM 与 DOM Diffing 算法

2.7.1 效果

需求:验证虚拟DOM Diffing算法的存在

component 虚拟 DOM

<script type="text/babel">
    class Time extends React.Component {
        state = {date: new Date()}

        componentDidMount() {
            setInterval(() => {
                this.setState({
                    date: new Date()
                })
            }, 1000)
        }

        render() {
            return (
                <div>
                    <h1>hello</h1>
                    <input type="text"/>
                    <span>
                        现在是:{this.state.date.toTimeString()}
                        <input type="text"/>
                    </span>
                </div>
            )
        }
    }

    ReactDOM.render(<Time/>, document.getElementById('test'))
</script>

2.7.2 基本原理图

diff 原理图

2.7.3 key 的作用

key 的作用

虚拟 DOM 中 key 的作用:

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

遍历列表时,key 最好不要用 index,用 index 作为 key 可能会引发的问题:

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

开发中如何选择key?:

  1. 最好使用每条数据的唯一标识作为 key, 比如 id、手机号、身份证号、学号等唯一值。
  2. 如果确定只是简单的展示数据,用 index 也是可以的。
<script type="text/babel">
    /*
        慢动作回放----使用index索引值作为key

            初始数据:
                    {id:1,name:'小张',age:18},
                    {id:2,name:'小李',age:19},
            初始的虚拟DOM:
                    <li key=0>小张---18<input type="text"/></li>
                    <li key=1>小李---19<input type="text"/></li>

            更新后的数据:
                    {id:3,name:'小王',age:20},
                    {id:1,name:'小张',age:18},
                    {id:2,name:'小李',age:19},
            更新数据后的虚拟DOM:
                    <li key=0>小王---20<input type="text"/></li>
                    <li key=1>小张---18<input type="text"/></li>
                    <li key=2>小李---19<input type="text"/></li>

    -----------------------------------------------------------------

    慢动作回放----使用id唯一标识作为key

            初始数据:
                    {id:1,name:'小张',age:18},
                    {id:2,name:'小李',age:19},
            初始的虚拟DOM:
                    <li key=1>小张---18<input type="text"/></li>
                    <li key=2>小李---19<input type="text"/></li>

            更新后的数据:
                    {id:3,name:'小王',age:20},
                    {id:1,name:'小张',age:18},
                    {id:2,name:'小李',age:19},
            更新数据后的虚拟DOM:
                    <li key=3>小王---20<input type="text"/></li>
                    <li key=1>小张---18<input type="text"/></li>
                    <li key=2>小李---19<input type="text"/></li>


     */
    class Person extends React.Component {

        state = {
            persons: [
                {id: 1, name: '小张', age: 18},
                {id: 2, name: '小李', age: 19},
            ]
        }

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

        render() {
            return (
                <div>
                    <h2>展示人员信息</h2>
                    <button onClick={this.add}>添加一个小王</button>
                    <h3>使用index(索引值)作为key</h3>
                    <ul>
                        {
                            this.state.persons.map((personObj, index) => {
                                return <li key={index}>{personObj.name}---{personObj.age}<input type="text"/></li>
                            })
                        }
                    </ul>
                    <hr/>
                    <hr/>
                    <h3>使用id(数据的唯一标识)作为key</h3>
                    <ul>
                        {
                            this.state.persons.map((personObj) => {
                                return <li key={personObj.id}>{personObj.name}---{personObj.age}<input type="text"/>
                                </li>
                            })
                        }
                    </ul>
                </div>
            )
        }
    }

    ReactDOM.render(<Person/>, document.getElementById('test'))
</script>

3. React 应用(基于 React 脚手架)

3.1 使用 create-react-app 创建 react 应用

3.1.1 react 脚手架

  1. xxx 脚手架: 用来帮助程序员快速创建一个基于 xxx 库的模板项目
    • 包含了所有需要的配置(语法检查、jsx 编译、devServer…)
    • 下载好了所有相关的依赖
    • 可以直接运行一个简单效果
  2. react 提供了一个用于创建 react 项目的脚手架库: create-react-app
  3. 项目的整体技术架构为: react + webpack + es6 + eslint
  4. 使用脚手架开发的项目的特点: 模块化, 组件化, 工程化

3.1.2 创建项目并启动

  1. 全局安装:npm i -g create-react-app
  2. 切换到想创项目的目录,使用命令:create-react-app hello-react
  3. 进入项目文件夹:cd hello-react
  4. 启动项目:npm start

3.1.3 react 脚手架项目结构

public ---- 静态资源文件夹
		favicon.icon ------ 网站页签图标
		index.html -------- 主页面
		logo192.png ------- logo图
		logo512.png ------- logo图
		manifest.json ----- 应用加壳的配置文件
		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库的支持)

3.1.4 样式的模块化

样式为什么需要模块化?看一个例子

Hello 组件(Hello/index.jsx)

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

export default class Hello extends Component{
  render(){
    return <h2 className="title">Hello,React!</h2>
  }
}

Welcom 组件(Welcome/index.jsx)

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

export default class Welcome extends Component {
  render() {
    return <h2 className="title">Welcome</h2>
  }
}

两个组件都引入了各自目录下的 index.css ,且两个样式文件存在 class 冲突,最后组件会统一打包成一个文件,其中一个样式会被另外一个样式覆盖,所以样式文件需要模块化。

解决样式冲突,我们可以利用 less 的嵌套关系来解决,或者使用以下的模块化方式,两种方式都可以

样式文件模块化方式

  1. 先将模块下的 index.css 更名为 index.module.css (必须加 module)
  2. import React,{Component} from 'react'
    import  hello from'./index.module.css'
    
    export default class Hello extends Component{
      render(){
        return <h2 className={hello.title}>Hello,React!</h2>
      }
    }
    

3.1.5 功能界面的组件化编码流程(通用)

  1. 拆分组件: 拆分界面,抽取组件
  2. 实现静态组件: 使用组件实现静态页面效果
  3. 实现动态组件
    • 动态显示初始化数据
      • 数据类型
      • 数据名称
      • 保存在哪个组件
    • 交互(从绑定事件监听开始)

3.2 组件的组合使用 - TodoList

功能: 组件化实现此功能

1. 显示所有 todo 列表

2. 输入文本, 点击按钮显示到列表的首位, 并清除输入的文本

component

App.jsx

import React, {Component} from 'react'
import Header from './components/Header'
import List from './components/List'
import Footer from './components/Footer'
import './App.css'

export default class App extends Component {
  //状态在哪里,操作状态的方法就在哪里

  //初始化状态
  state = {
    todos: [
      {id: '001', name: '吃饭', done: true},
      {id: '002', name: '睡觉', done: true},
      {id: '003', name: '打代码', done: false},
      {id: '004', name: '逛街', done: false}
    ]
  }

  //addTodo用于添加一个todo,接收的参数是todo对象
  addTodo = (todoObj) => {
    //获取原todos
    const {todos} = this.state
    //追加一个todo
    const newTodos = [todoObj, ...todos]
    //更新状态
    this.setState({todos: newTodos})
  }

  //updateTodo用于更新一个todo对象
  updateTodo = (id, done) => {
    //获取状态中的todos
    const {todos} = this.state
    //匹配处理数据
    const newTodos = todos.map((todoObj) => {
      if (todoObj.id === id) {
        return {...todoObj, done}
      } else {
        return todoObj
      }
    })
    this.setState({todos: newTodos})
  }

  //deleteTodo用于删除一个todo对象
  deleteTodo = (id) => {
    //获取原来的todos
    const {todos} = this.state
    //删除指定id的todo对象
    const newTodos = todos.filter((todoObj) => {
      return todoObj.id !== id
    })
    //更新状态
    this.setState({todos: newTodos})
  }

  //checkAllTodo用于全选
  checkAllTodo = (done) => {
    //获取原来的todos
    const {todos} = this.state
    //加工数据
    const newTodos = todos.map((todoObj) => {
      return {...todoObj, done}
    })
    //更新状态
    this.setState({todos: newTodos})
  }

  //clearAllDone用于清除所有已完成的
  clearAllDone = () => {
    //获取原来的todos
    const {todos} = this.state
    //过滤数据
    const newTodos = todos.filter((todoObj) => {
      return !todoObj.done
    })
    //更新状态
    this.setState({todos: newTodos})
  }

  render() {
    const {todos} = this.state
    return (
        <div className="todo-container">
          <div className="todo-wrap">
            <Header addTodo={this.addTodo}/>
            <List todos={todos} updateTodo={this.updateTodo}
                  deleteTodo={this.deleteTodo}/>
            <Footer todos={todos} checkAllTodo={this.checkAllTodo}
                    clearAllDone={this.clearAllDone}/>
          </div>
        </div>
    )
  }
}

Header.jsx

import React, {Component} from 'react'
import PropTypes from 'prop-types'
import {nanoid} from 'nanoid'
import './index.css'

export default class Header extends Component {

  //对接收的props进行:类型、必要性的限制
  static propTypes = {
    addTodo: PropTypes.func.isRequired
  }

  //键盘事件的回调
  handleKeyUp = (event) => {
    //解构赋值获取keyCode,target
    const {keyCode, target} = event
    //判断是否是回车按键
    if (keyCode !== 13) {
      return
    }
    //添加的todo名字不能为空
    if (target.value.trim() === '') {
      alert('输入不能为空')
      return
    }
    //准备好一个todo对象
    const todoObj = {id: nanoid(), name: target.value, done: false}
    //将todoObj传递给App
    this.props.addTodo(todoObj)
    //清空输入
    target.value = ''
  }

  render() {
    return (
        <div className="todo-header">
          <input onKeyUp={this.handleKeyUp} type="text"
                 placeholder="请输入你的任务名称,按回车键确认"/>
        </div>
    )
  }
}

Item.jsx

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

export default class Item extends Component {

  state = {mouse: false} //标识鼠标移入、移出

  //鼠标移入、移出的回调
  handleMouse = (flag) => {
    return () => {
      this.setState({mouse: flag})
    }
  }

  //勾选、取消勾选某一个todo的回调
  handleCheck = (id) => {
    return (event) => {
      this.props.updateTodo(id, event.target.checked)
    }
  }

  //删除一个todo的回调
  handleDelete = (id) => {
    if (window.confirm('确定删除吗?')) {
      this.props.deleteTodo(id)
    }
  }

  render() {
    const {id, name, done} = this.props
    const {mouse} = this.state
    return (
        <li style={{backgroundColor: mouse ? '#ddd' : 'white'}}
            onMouseEnter={this.handleMouse(true)}
            onMouseLeave={this.handleMouse(false)}>
          <label>
            <input type="checkbox" checked={done}
                   onChange={this.handleCheck(id)}/>
            <span>{name}</span>
          </label>
          <button onClick={() => this.handleDelete(id)}
                  className="btn btn-danger"
                  style={{display: mouse ? 'block' : 'none'}}>删除
          </button>
        </li>
    )
  }
}

List.jsx

import React, { Component } from 'react'
import PropTypes from 'prop-types'
import Item from '../Item'
import './index.css'

export default class List extends Component {

  //对接收的props进行:类型、必要性的限制
  static propTypes = {
    todos:PropTypes.array.isRequired,
    updateTodo:PropTypes.func.isRequired,
    deleteTodo:PropTypes.func.isRequired,
  }

  render() {
    const {todos,updateTodo,deleteTodo} = this.props
    return (
        <ul className="todo-main">
          {
            todos.map( todo =>{
              return <Item key={todo.id} {...todo} updateTodo={updateTodo} deleteTodo={deleteTodo}/>
            })
          }
        </ul>
    )
  }
}

Footer.jsx

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

export default class Footer extends Component {

  //全选checkbox的回调
  handleCheckAll = (event) => {
    this.props.checkAllTodo(event.target.checked)
  }

  //清除已完成任务的回调
  handleClearAllDone = () => {
    this.props.clearAllDone()
  }

  render() {
    const {todos} = this.props
    //已完成的个数
    const doneCount = todos.reduce((pre, todo) => pre + (todo.done ? 1 : 0), 0)
    //总数
    const total = todos.length
    return (
        <div className="todo-footer">
          <label>
            <input type="checkbox" onChange={this.handleCheckAll}
                   checked={doneCount === total && total !== 0 ? true : false}/>
          </label>
          <span>
					<span>已完成{doneCount}</span> / 全部{total}
				</span>
          <button onClick={this.handleClearAllDone}
                  className="btn btn-danger">清除已完成任务
          </button>
        </div>
    )
  }
}

4. React ajax

5. React 路由

6. React UI 组件库

流行的开源 React UI 组件库有以下两个:

material-ui (国外)

  1. 官网: http://www.material-ui.com/#/
  2. github: https://github.com/callemall/material-ui

ant-design (国内蚂蚁金服)

  1. 偏向于后台系统的开发
  2. 官网: https://ant.design/index-cn
  3. Github: https://github.com/ant-design/ant-design/

7. redux

8. 项目打包运行

npm run start 运行的是开发服务器。

npm run build 会将项目打包,打包后会生成 build 文件夹。

serve

可以根据某个文件夹,作为服务器运行,端口 5000

  1. 安装:npm i serve -g
  2. 启动:serve [目录]

9. React 扩展

10. ReactTouter6