「React」万字保姆级基础教程>1

595 阅读16分钟

🧑‍💻 友情链接

bilibili视频学习

官网文档

一、react入门

入门介绍

React:用于构建用户界面的JavaScript库。简单来说,就是一个将数据渲染为HTML视图的开源JavaScript库

React特点:

1)采用组件化模式、声明式编码,提高开发效率及组件复用率

2)在React Native中可以使用React语法进行移动端开发

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

React高效的原因:

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

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

学习React之前需要掌握的JS基础知识:判断this的指向、class类、ES6语法规范、npm包管理器、原型/原型链、数组常用方法、模块化...等

引入方式

学习时可直接使用官方提供的CDN地址(如果想要使用脚手架,请看第三章)

<!-- 加载 React。-->
<!-- 注意: 部署时,将 "development.js" 替换为 "production.min.js"。-->
<script src="https://unpkg.com/react@18/umd/react.development.js" crossorigin></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js" crossorigin></script>
<!-- 生产环境中不建议使用。用于将JSX转为JS的 -->
<script src="https://unpkg.com/babel-standalone@6.15.0/babel.min.js"></script>

这样就可以使用两个全局变量:React 和 ReactDOM

初始案例

<body>
    <!-- 准备容器 -->
    <div id="test"></div>

    <script type="text/babel">
        // 1、创建虚拟DOM
        const VDOM = <h1>Hello React</h1>
        // 2、渲染虚拟DOM到页面
        ReactDOM.render(VDOM, document.getElementById('test')) 
    </script>
</body>

虚拟DOM

第一种方式,使用JSX:

<script type="text/babel">
  // 1、创建虚拟DOM
  const VDOM = <h1 id="title">Hello React</h1>
  // 2、渲染虚拟DOM到页面
  ReactDOM.render(VDOM, document.getElementById('test')) 
</script>

第二种方式,不引入babel,使用纯JS原生方式:(不推荐使用此种方式)

<script type="text/javascript"> 
    // 1、创建虚拟DOM:<h1 id="title">Hello React</h1>
    const VDOM = React.createElement('h1', {id:'title'}, 'Hello React') 
    // 2、渲染虚拟DOM到页面
    ReactDOM.render(VDOM, document.getElementById('test')) 
</script>

由此可见,JSX其实就是原生写法的语法糖,可以让我们更简单的创建虚拟DOM。虚拟DOM其本质就是Object类型的对象,他最终会被React转化为真实DOM。

console.log(typeof VDOM) // 输出结果:object

JSX语法

JSX(JavaScript XML) ,是react定义的一种类似XML的JS扩展语法(JS+XML)。其本质是 React.createElement(component, props, ...children) 方法的语法糖。其作用是用来简化创建虚拟DOM。

JSX语法规则:

  • 定义虚拟DOM时,不要写引号。如const ele = <h1>Hello React</h1>
  • 标签中混入JS表达式时要用 {}。如 const ele = <h1>Hello {name}</h1>
  • 样式的类名指定不要用“class”,要用 className(驼峰的方式变量)
  • 内联样式,要用"style={{key: 'value', key: 'value'}}"的形式去写
  • 只能有一个根标签
  • 所有的标签必须要闭合
  • 对于标签的首字母:
    • 如果是小写字母开头,会将该标签转为html中的同名元素。如果html中没有该标签对应的同名元素,则会报错;
    • 如果是大写字母开头,react就会去渲染对应的组件。如果组件没有定义,则会报错;
<script type="text/babel">
    const h2_id = 'h2_ID'
    const span_data = 'Hello React'
    // 1、创建虚拟DOM
    const VDOM = (
        <div>
            <h2 className="title" id={h2_id}>
                <span style={{ color: 'red', fontSize: '20px' }}> {span_data} </span>
            </h2>
            <h3> {span_data+'www'} </h3>
            <input type="text"/>
            <Good></Good> // Good is not defined
        </div>
    )
    // 2、渲染虚拟DOM到页面
    ReactDOM.render(VDOM, document.getElementById('test'))
</script>

上面JSX规则中,如果要在标签中混入JS表达式需要用“{}” ,但是需要注意:表达式和语句不是同一个东西!

<script type="text/babel">
    // 模拟数据
    const data = ['React', 'Vue', 'Angular']
    // 1.创建虚拟DOM
    const element = (
        <div>
            <h1>前端JS框架</h1>
            <ul>
                { // 数组增强方法
                    data.map((item, index)=>{
                        // 这里用index作为key并不合理,后续说明
                        return <li key={index}> {item} </li>
                    })
                }
            </ul>
        </div>
    )
    // 2.渲染虚拟DOM到页面
    ReactDOM.render(element, document.getElementById('test'))
</script>

模块&组件

  • 模块:向外提供特定功能的JS程序,一般就是一个JS文件。复用JS简化编写
  • 组件:用来实现局部功能效果的代码和资源的集合。复用编码提高效率
  • 模块化:当应用的JS都以模块来编写的,这个应用就是一个模块化的应用
  • 组件化:当应用是以多组件的方式实现,这个应用就是一个组件化的应用

可以安装开发者工具:React Developer Tools

注意在开发者工具设置里下面这俩要取消勾选:

二、react组件

组件,从概念上类似于 JavaScript 函数。它接受任意的入参(即 “props”),并返回用于描述页面展示内容的 React 元素。

组件定义方式

1)函数式组件(推荐)

注意: 组件名称必须以大写字母开头, 渲染时要用标签的形式

// 1.创建函数式组件
function MyComponent(props){
  	return (<h2>用函数定义的组件。Hello {props.name}</h2>)
}
// 2.渲染组件到页面
ReactDOM.render(<MyComponent/>, document.getElementById('test')) 

2)类式组件

// 1.创建类式组件
class MyComponent extends React.Component {
    // render会放在MyComponent的原型对象上,供实例使用
    render() {
        return <h2>用类定义的组件.Hello {this.props.name}</h2>
    }
}
// 2.渲染组件到页面
// 执行此方法后,找到组件,随后new出来该类的实例,并调用原型对象上的render方法
ReactDOM.render(<MyComponent />, document.getElementById('test')) 

组件中的render会放在MyComponent的原型对象上,供实例使用:

组件中的render()中的this指的就是MyComponent组件实例对象:

组件核心属性(state)

state 代表了随时间会产生变化的数据,应当仅在实现交互时使用

如果组件中有状态(state),那么就是复杂组件。state是组件对象最重要的属性,值是对象(多个key-value的组合)。

组件被称为“状态机”,通过更新组件的state来更新对应的页面显示(重新渲染组件)。可以理解为:数据放在状态也就是state中,更改状态中的数据就可以引起页面的变化(状态更新时就会调用一次render()方法)。

state是私有的,并且完全受控于当前组件。

⚠️⚠️注意:

  • 组件中render()方法中的this为组件实例对象
  • 组件自定义的方法中this为undefined,如何解决?
    • 强制绑定this:通过函数对象的bind()
    • 箭头函数
  • 状态数据,不能直接修改或更新,需要使用setState方法

1)state初始化

构造函数是唯一可以给 this.state 赋值的地方

// 1.创建类式组件
class Weather extends React.Component {
    constructor(props) { // 参数props
        super(props)
        this.state = { isHot: true, wind: '大风' } //初始化状态
    }
    render() {
        const { isHot, wind } = this.state // 读取状态
        return <h2>今日天气:{isHot ? '炎热' : '凉爽'}, {wind}</h2>
    }
}
// 2.渲染组件到页面
ReactDOM.render(<Weather />, document.getElementById('test')) 

2)setState的使用

react事件绑定需要注意:onClick的大小写和函数绑定demo不加括号

// 1.创建类式组件
class Weather extends React.Component {
    constructor(props) { // 参数props
        super(props)
        // 初始化状态
        this.state = { isHot: true, wind: '大风' }
        // 解决changeWeather中的this指向问题
        this.changeWeather = this.changeWeather.bind(this) 
        // bind返回一个新函数
    }
    // 此方法会放在类的原型对象上,供实例使用。
    // onClick回调,非实例调用。其this为undefined
    changeWeather() {
        const isHot = this.state.isHot
        // 【状态不可直接更改】!!!!!!
        // this.state.isHot = !isHot
        // 【状态必须通过setState进行修改】!!!!!!
        this.setState({ isHot: !isHot })
        console.log('修改isHot的值,修改后是:', this.state.isHot)
    }
    // 状态更新时就会调用一次render()方法
    render() {
        const { isHot, wind } = this.state
        return <h2 onClick={this.changeWeather}>今日天气:{isHot ? '炎热' : '凉爽'}, 风向:{wind}</h2>
    }
}
// 2.渲染组件到页面
ReactDOM.render(<Weather />, document.getElementById('test')) 

3)state简写方式(推荐)

自定义方法:【需要使用赋值语句的形式+箭头函数】

// 1.创建类式组件
class Weather extends React.Component {
    state = { isHot: true, wind: '大风' }
    // 自定义方法:【需要使用赋值语句的形式+箭头函数】
    changeWeather = () => {
        const isHot = this.state.isHot
        this.setState({ isHot: !isHot })
    }
    render() {
        const { isHot, wind } = this.state
        return <h2 onClick={this.changeWeather}>今日天气:{isHot ? '炎热' : '凉爽'}, 风向:{wind}</h2>
    }
}
// 2.渲染组件到页面
ReactDOM.render(<Weather />, document.getElementById('test')) 
export default class Clock extends Component {
    constructor(props) {
        super(props);
        this.state = { date: new Date() }
    }

    componentDidMount() {
        this.timerID = setInterval(() => this.tick(), 1000);
    }
    componentWillUnmount() {
        clearInterval(this.timerID);
    }

    tick() {
        this.setState({ date: new Date() })
    }

    render() {
        return (
            <div>
                <h1>这里是类式组件Clock</h1>
                <h2>当前时间为:{this.state.date.toLocaleTimeString()}</h2>
            </div>
        )
    }
}

4)state更新

  1. 不要直接修改State, 需要使用setState方法
  2. State的更新可能是异步的
    1. 要解决这个问题,可以让 setState() 接收一个函数而不是一个对象。这个函数用上一个 state 作为第一个参数,将此次更新被应用时的 props 做为第二个参数:
this.setState((state, props) => ({
  counter: state.counter + props.increment
}));
  1. State的更新会被合并(浅合并)

组件核心属性(props)

props可以理解为传递参数使用的( props 是父组件向子组件传递数据的方式)

1)props基本使用

// 1.创建类式组件
class Person extends React.Component {
    render() {
        const { name, sex, age } = this.props;
        return (
            <ul>
                <li>姓名:{name}</li>
                <li>性别:{sex}</li>
                <li>年龄:{age}</li>
            </ul>
        )
    }
}
// 2.渲染组件到页面
ReactDOM.render(<Person name="桃子" age="18" sex="男" />, document.getElementById('test'))
ReactDOM.render(<Person name="苹果" age="19" sex="女" />, document.getElementById('test2'))

2)props批量传递

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

3)props限制

<!-- 引入prop-types.js -->
<script src="https://cdn.bootcdn.net/ajax/libs/prop-types/15.8.1/prop-types.js"></script>

...

<body>
    <!-- 准备容器 -->
    <div id="test"></div>

    <script type="text/babel">
        // 1.创建类式组件
        class Person extends React.Component {
            render() {
                const { name, sex, age } = this.props;
                return (
                    <ul>
                        <li>姓名:{name}</li>
                        <li>性别:{sex}</li>
                        <li>年龄:{age + 1}</li>
                    </ul>
                )
            }
        }
        // 属性规则限制
        Person.propTypes = {
            name: PropTypes.string.isRequired,
            sex: PropTypes.string,
            age: PropTypes.number,
            speak: PropTypes.func,
        }
        // 属性默认值
        Person.defaultProps = {
            sex: '男',
            age: 30,
        }
        // 2.渲染组件到页面
        ReactDOM.render(<Person name="桃子" speak={speak}/>, document.getElementById('test'))

        function speak(){
            console.log('说话方法');
        }
    </script>
</body>

以上props使用是只读的,无法修改

4)props简写方式

<script type="text/babel">
    // 1.创建类式组件
    class Person extends React.Component {
        // 属性规则限制
        static propTypes = {
            name: PropTypes.string.isRequired,
            sex: PropTypes.string,
            age: PropTypes.number,
        }
        // 属性默认值
        static defaultProps = {
            sex: '男',
            age: 20,
        }
        render() {
            const { name, sex, age } = this.props;
            return (
                <ul>
                    <li>姓名:{name}</li>
                    <li>性别:{sex}</li>
                    <li>年龄:{age + 1}</li>
                </ul>
            )
        }
    }
    // 2.渲染组件到页面
    ReactDOM.render(<Person name="桃子" />, document.getElementById('test'))
</script>

5)props与构造器

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

  • 通过给this.state赋值对象来初始化内部state(可以使用简写方式)
  • 为事件处理函数绑定实例(可以使用赋值语句加箭头函数方式)

在constructor()函数中不要调用setState()方法。如果组件需要使用内部state,可以直接在构造函数中为this.state赋值初始state:

constructor(props) {
  super(props);
  // 不要在这里调用 this.setState()
  this.state = { counter: 0 };
  this.handleClick = this.handleClick.bind(this);
}

【开发时一般不用构造器】

6)函数式组件使用props

⚠️注意:函数式组件只能使用props,无法使用state和refs(后面可以使用Hooks)

<script type="text/babel">
    // 1.创建函数式组件
    function Person(props) {
        const { name, sex, age } = props;
        return (
            <ul>
                <li>姓名:{name}</li>
                <li>性别:{sex}</li>
                <li>年龄:{age}</li>
            </ul>
        )
    }
    // 属性规则限制
    Person.propTypes = {...}
    // 属性默认值
    Person.defaultProps = {...}
    // 2.渲染组件到页面
    ReactDOM.render(<Person name="桃子"/>, document.getElementById('test'))
</script>

组件核心属性(refs)

refs可以理解为可以越过虚拟DOM去操作真实DOM

1)字符串形式的ref

注意:这种使用形式实际上不被推荐,很少使用

<script type="text/babel">
    // 1.创建类式组件 
    class Demo extends React.Component {
        showData = () => {
            console.log(this) // this指的是Demo组件的实例对象
            alert(this.refs.input111.value)
        }
        showData2 = () => {
            alert(this.refs.input222.value)
        }
        render() {
            return (
                <div>
                    <input ref="input111" type="text" placeholder="点击按钮提示数据" />
                    <button ref="button111"  onClick={this.showData}>点击提示左侧输入的数据</button> <br />
                    <input ref="input222" onBlur={this.showData2} type="text" placeholder="失去焦点提示数据" />
                </div>
            )
        }
    }
    // 2.渲染组件到页面
    ReactDOM.render(<Demo />, document.getElementById('test'))
</script>

2)回调形式的ref

<input ref={(currentNode) => { this.input111 = currentNode }} type="text" placeholder="点击按钮提示数据" />
<input ref={currentNode => this.input222 = currentNode} onBlur={this.showData2} type="text" placeholder="失去焦点提示数据" />

官方文档里的说明:

如果ref回调函数是以内联函数的方式定义的,在更新过程中它会被执行两次,第一次传入参数null,然后第二次会传入参数DOM元素。这是因为在每次渲染时会创建一个新的函数实例,所以React清空旧的ref并且设置新的。通过将ref的回调函数定义成class的绑定函数的方式可以避免上述问题,但是大多数情况下它是无关紧要的。

saveInput = (c) => {
	this.input1 = c
	console.log('调用:', c)
}

{/*这种内联函数的方式会导致更新过程被执行两次*/}
{/*<input ref={(currentNode) => { this.input1 = currentNode; console.log('调用:', currentNode) }} type="text" />*/}
{/*绑定class可以解决上述问题*/}
<input ref={this.saveInput} type="text" />

3)createRef(推荐)

React.createRef() 调用后会返回一个容器,该容器可以存储被ref所标识的节点。里面只能存入一个(使用比较繁琐一点。不过是官方推荐方式

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 (
            <div>
                <input ref={this.myRef} type="text" placeholder="点击按钮提示数据" />
                <button onClick={this.showData}>点击提示左侧输入的数据</button> <br />
                <input ref={this.myRef2} onBlur={this.showData2} type="text" placeholder="失去焦点提示数据" />
            </div>
        )
    }
}

4)事件处理

  • 通过onXxx属性指定事件处理函数(注意大小写,如原生是onclick,React中是onClick)
    • React使用的是自定义(合成)事件,而不是使用的原生DOM事件(为了兼容)
    • React中的事件是通过事件委托方式处理的(委托给组件最外层的元素)(为了高效)
  • 通过event.target得到发生事件的DOM元素对象
  • 如果需要阻止默认行为,必须显式的使用 preventDefault

如果此时发生事件的元素正好是要操作的元素,那么就可以不用ref

function showData(e) {
    e.preventDefault();
    console.log('You clicked showData.');
  }

<button onClick={this.showData} >AAA</button>

收集表单数据

1)非受控组件

表单中输入类DOM的值,现用现取,就是非受控组件

class Login extends React.Component {
    handleSubmit = (event) => {
        event.preventDefault() // 阻止表单提交
        const { username, password } = this
        alert(`您输入的用户名:${username.value},密码:${password.value}`)
    }
    render() {
        return (
            <form action="https://www.baidu.com" 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)受控组件(推荐)

渲染表单的 React 组件还控制着用户输入过程中表单发生的操作。被 React 以这种方式控制取值的表单输入元素就叫做“受控组件”。建议使用此种方式(不使用上面ref的方式,因为官方建议不过度使用ref

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 action="https://www.baidu.com" onSubmit={this.handleSubmit}>
                用户名:<input onChange={this.saveUsername} type="text" name="username" />
                密码:<input onChange={this.savePassword} type="password" name="password" />
                <button>登录</button>
            </form>
        )
    }
}

而对于<input type="text">, <textarea> 和 <select> 之类的标签都非常相似。它们都接受一个 value 属性来实现受控组件。

<input type="text" value={this.state.value} onChange={this.handleChange} />
<textarea value={this.state.value} onChange={this.handleChange} />
<select value={this.state.value} onChange={this.handleChange}>
    ...
</select>

高阶函数 | 函数柯里化

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

  1. 若A函数,接受的参数是一个函数,那么A就可以称为高阶函数。
  2. 若A函数,调用的返回值依然是一个函数,那么A就可以称为高阶函数。

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

saveFormData = (dataType) => {
    return (event) => {
        // 根据dataType存入对应的数据
        this.setState({ [dataType]: event.target.value })
    }
}

常见的高阶函数有:

  • Promise( 使用new Promise(()=>{}) )
  • setTimeout( 使用setTimeout(()=>{}) )
  • arr.map() ...等
class Login extends React.Component {
    state = {
        username: '',
        password: ''
    }
    saveFormData = (dataType) => {
        return (event) => {
            // 根据dataType存入对应的数据
            this.setState({ [dataType]: event.target.value })
        }
    }
    handleSubmit = (event) => {
        event.preventDefault() // 阻止表单提交
        const { username, password } = this.state
        alert(`您输入的用户名:${username},密码:${password}`)
    }
    render() {
        return (
            <form action="https://www.baidu.com" onSubmit={this.handleSubmit}>
                用户名:<input onChange={this.saveFormData('username')} type="text" name="username" />
                密码:<input onChange={this.saveFormData('password')} type="password" name="password" />
                <button>登录</button>
            </form>
        )
    }
}

如果不用柯里化方式:

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 action="https://www.baidu.com" 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>
        )
    }
}

组件生命周期

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 })
        }, 200);
    }
    // 组件将要被卸载时调用,只调用一次
    componentWillUnmount(){
        // 清除定时器
        clearInterval(this.timer)
    }
    // 第一次挂载,和后续状态修改的时候会调用
    render() {
        return (
            <div>
                <h2 style={{ opacity: this.state.opacity }}>Hello World</h2>
                <button onClick={this.death}>欢迎欢迎</button>
            </div>
        )
    }
}

2)生命周期流程(旧)

React 16.8以后的生命周期分为三个阶段,分别是挂载阶段、更新阶段、卸载阶段。

常用的勾子:

componentDidMount(一般做一些初始化的事情, 如开启监听, 发送请求请求

render(必须使用,渲染页面)

componentWillUnmount(一般做一些收尾的事情, 如:关闭定时器,取消订阅

新流程中即将要废弃的勾子:

componentWillMount、componentWillReceiveProps、componentWillUpdate

初始化过程: 由ReactDOM.render()触发,初次渲染

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

setState时:

forceUpdate时:

父组件重新render时:

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

3)生命周期流程(新)

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

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

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

4)getSnapshotBeforeUpdate

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

    <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
            }
            componentDidUpdate(preProps, preState, height) {
                this.refs.list.scrollTop += this.refs.list.scrollHeight - height
            }
            render() {
                return (
                    <div className="list" ref="list">
                        {this.state.newsArr.map((news, index) => {
                            return <div key={index} className='news'>{news}</div>
                        })}
                    </div>
                )
            }
        }
        ReactDOM.render(<NewsList />, document.getElementById('test'))
    </script>
</body>

虚拟DOM & DOM Diff算法

react中的key就是虚拟DOM对象的标识,在更新显示时起到很重要的作用。当状态中的数据发生变化时,react会根据新数据生成新的虚拟DOM,随后react进行新虚拟DOM旧虚拟DOM的diff比较,比较规则如下:

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

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

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

选择使用key的注意事项:

  • 最好使用每条数据的唯一标识作为key,比如id、手机号、身份证号、学号等信息
  • 如果确定只是简单的数据展示,也可以使用index
  • 在 map() 方法中的元素需要设置key属性
class Person extends React.Component {
    state = {
        persons: [
            { id: 1, name: '张三', age: 18 },
            { id: 2, name: '李四', age: 19 },
        ]
    }
    add = () => {
        const { persons } = this.state
        const wang = { id: persons.length + 1, name: '王五', age: 20 }
        this.setState({ persons: [wang, ...persons] })
    }
    render() {
        return (
            <div>
                <h2>人员列表</h2>
                <button onClick={this.add}>添加王五信息</button>
                <ul>
                    {this.state.persons.map((person, index) => {
                        return <li key={index}>{person.id}={person.name}={person.age} <input type="text"/></li>
                    })}
                </ul>
                <hr />
                <ul>
                    {this.state.persons.map((person) => {
                        return <li key={person.id}>{person.id}={person.name}={person.age} <input type="text"/></li>
                    })}
                </ul>
            </div>
        )
    }
}

条件渲染

  • 元素变量配合if进行展示
if (isLoggedIn) {
  button = <LogoutButton />;
} else {
  button = <LoginButton />;
}
  • 通过与运算符 &&
    • true && expression 总是会返回 expression, 而 false && expression 总是会返回 false。
{unreadMessages.length > 0 &&
  <h2> You have {unreadMessages.length} unread messages. </h2>
}
  • 三目运算符
{isLoggedIn? <LogoutButton /> : <LoginButton />}

三、react应用

react脚手架

xxx脚手架:是用来帮助程序员快速创建一个基于xxx库的模板项目。其中:包含了所有需要的配置(语法检查、JSX编译、devServer等)、下载好了所有相关的依赖、可以直接运行一个简单效果。

react提供了一个用于创建react项目的脚手架库:creat-react-app。项目的整体技术架构为:react+webpack+es6+eslint

使用脚手架开发项目的特点就是:模块化、组件化、工程化

创建项目并启动:

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

脚手架文件

  • public文件夹中一般存放静态资源文件

    • index.html中定义了容器:<div id="root">
  • src文件放的就是核心代码文件

    • App.js+App.css:函数式组件并暴露出去了, function App() { return() } 、 export default App;

    • index.js+index.css:入口文件

自定义组件流程

index.js作为入口文件:

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

App.js作为主页面,里面引入需要展示的组件:

export default class App extends Component { 
  render() {
    return (
      <div>
        <Hello />
        <Welcome />
      </div>
    )
  }
}

对应组件则可显示自己的内容:

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

组件开发知识点

  • 拆分组件,实现静态组件,需要注意:className、style的写法
  • 动态初始化列表,如何确定将数据放在哪个组件的state中呢?
    • 某个组件自己使用:就放在自身的state中
    • 某些组件都会使用:放在他们共同的父组件state中(状态提升
  • 关于父子组件之间的通信:
    • 【父组件】给【子组件】传递数据:通过props
    • 【子组件】给【父组件】传递数据:通过props,要求父组件提前给子组件传递一个函数
  • 注意 defaultChecked 和checked 的区别,类似的还有 defaultValue 和 value
  • 状态在哪里,操作状态的方法就在哪里

详细说明可以查看官方这片文章【React哲学】:zh-hans.reactjs.org/docs/thinki…

四、react Ajax

前置说明

React本身只关注界面,并不包含发送ajax请求的代码。前端需要通过ajax请求与后台进行交互。所以React应用中需要集成第三方ajax库。常见的ajax请求库有:

1、jQuery:比较重,不建议使用

2、axios:轻量级,基于promise可以用于浏览器和node.js的网络请求库

axios官方文档学习:axios-http.com/zh/docs/int…

每日一句案例

import React, { Component } from 'react';
import axios from 'axios';

export default class App extends Component {
  state = { info: '' }
  getStudentData = () => {
    axios.get('https://v.api.aa1.cn/api/yiyan/index.php').then(
      response => { this.setState({ info: response.data }) },
      error => { console.log('请求失败,错误信息为:', error) }
    )
  }
  render() {
    return (
      <div>
        <button onClick={this.getStudentData}>每日一句</button>
        <div dangerouslySetInnerHTML={{ __html: this.state.info }} />
      </div>
    )
  }
}

github搜索案例

import React, { Component } from "react";
import Search from "./components/Search";
import List from "./components/List";

export default class App extends Component {
  state = {
    users: [],
    isFirst: true, // 是否首次打开页面
    isLoading: false, // 是否搜索加载中
    err: '', // 存储请求错误信息
  };
  updateAppState = (stateObj) => {
    this.setState( stateObj );
  };
  render() {
    return (
      <div>
        <Search updateAppState={this.updateAppState} />
        <List {...this.state}/>
      </div>
    );
  }
}
import React, { Component } from "react";
import axios from 'axios';

export default class Search extends Component {
    handleSearch = () => {
        // 获取用户输入并重命名
        const { keyWordElement: { value: keyWord } } = this
        // 发送请求前更新状态:
        this.props.updateAppState({ isFirst: false, isLoading:true });
        // 发送网络请求
        axios.get(`https://api.github.com/search/users?q=${keyWord}`).then(
          (response) => {
            this.props.updateAppState({ isLoading: false, users: response.data.items });
          },
          (error) => {
            this.props.updateAppState({ isLoading: false, err: error.message });
          }
        );
    }
    render() {
        return (
            <section className="jumpbotron">
                <h3 className="jumpbotron-heading">搜索Github用户</h3>
                <div>
                    <input ref={c => this.keyWordElement = c} type="text" placeholder="输入昵称关键词" />
                    &nbsp;<button onClick={this.handleSearch}>搜索</button>
                </div>
            </section>
        );
    }
}
/* eslint-disable jsx-a11y/alt-text */
import React, { Component } from 'react'
import './index.css'

export default class List extends Component {
    render() {
        const { users, isFirst, isLoading, err } = this.props;
        return (
            <div className="row">
                {
                    isFirst ? <h2>初始化状态</h2> :
                        isLoading ? <h2>Loading...</h2> :
                            err ? <h2>{err}</h2> :
                                users.map((userObj) => {
                                    return (
                                        <div className="card" key={userObj.id}>
                                            <a href={userObj.html_url} target="_blank" rel="noreferrer">
                                                <img src={userObj.avatar_url} style={{ width: "100px" }} />
                                                <p className='card-text'>{userObj.login}</p>
                                            </a>
                                        </div>)
                                })
                }
            </div>
        )
    }
}

消息订阅发布机制

工具库:PubSubJS

项目git地址:github.com/WanderLV/Pu…

下载: npm install pubsub-js

import React, { Component } from "react";
import Search from "./components/Search";
import List from "./components/List";

export default class App extends Component {
  render() { 
    return (
      <div>
        <Search />
        <List />
      </div>
    );
  }
}
import React, { Component } from "react";
import PubSub from "pubsub-js";
import axios from "axios";

export default class Search extends Component {
    handleSearch = () => {
        // 获取用户输入并重命名
        const { keyWordElement: { value: keyWord } } = this;
        // 发送请求前通知List更新状态,发布消息
        PubSub.publish("github user", { isFirst: false, isLoading: true });

        // 发送网络请求
        axios.get(`https://api.github.com/search/users?q=${keyWord}`).then(
            (response) => {
                PubSub.publish("github user", {
                    isLoading: false,
                    users: response.data.items,
                });
            },
            (error) => {
                PubSub.publish("github user", {
                    isLoading: false,
                    err: error.message,
                });
            }
        );
    };
    render() {
        return (
            <section className="jumpbotron">
                <h3 className="jumpbotron-heading">搜索Github用户</h3>
                <div>
                    <input
                        ref={(c) => (this.keyWordElement = c)}
                        type="text"
                        placeholder="输入昵称关键词"
                    />
                    &nbsp;<button onClick={this.handleSearch}>搜索</button>
                </div>
            </section>
        );
    }
}
/* eslint-disable jsx-a11y/alt-text */
import React, { Component } from "react";
import PubSub from "pubsub-js";
import "./index.css";

export default class List extends Component {
  state = {
    users: [],
    isFirst: true, // 是否首次打开页面
    isLoading: false, // 是否搜索加载中
    err: "", // 存储请求错误信息
  };
  componentDidMount() {
    // 初始化操作-订阅消息(谁需要数据谁就订阅)
    this.token = PubSub.subscribe("github user", (_, stateObj) => {
      this.setState(stateObj);
    });
  }
  componentWillUnmount() {
    // 取消订阅消息
    PubSub.unsubscribe(this.token);
  }
  render() {
    const { users, isFirst, isLoading, err } = this.state;
    return (
      <div className="row">
        {isFirst ? (
          <h2>初始化状态</h2>
        ) : isLoading ? (
          <h2>Loading...</h2>
        ) : err ? (
          <h2>{err}</h2>
        ) : (
          users.map((userObj) => {
            return (
              <div className="card" key={userObj.id}>
                <a href={userObj.html_url} target="_blank" rel="noreferrer">
                  <img src={userObj.avatar_url} style={{ width: "100px" }} />
                  <p className="card-text">{userObj.login}</p>
                </a>
              </div>
            );
          })
        )}
      </div>
    );
  }
}

fetch发送请求

项目git地址:github.com/github/fetc…

fetch是原生函数,无需下载即可使用

import React, { Component } from "react";
import PubSub from "pubsub-js";
import axios from "axios";

export default class Search extends Component {
  handleSearch = async () => {
    // 获取用户输入并重命名
    const {
      keyWordElement: { value: keyWord },
    } = this;
    // 发送请求前通知List更新状态,发布消息
    PubSub.publish("github user", { isFirst: false, isLoading: true });

    // 发送网络请求
    try {
      const response = await fetch(
        `https://api.github.com/search/users?q=${keyWord}`
      );
      const data = await response.json();
      console.log("获取数据成功:", data);
      PubSub.publish("github user", {
        isLoading: false,
        users: data.items,
      });
    } catch (error) {
      console.log("获取数据失败", error);
      PubSub.publish("github user", {
        isLoading: false,
        err: error.message,
      });
    }
  };
  render() {
    return (
      <section className="jumpbotron">
        <h3 className="jumpbotron-heading">搜索Github用户</h3>
        <div>
          <input
            ref={(c) => (this.keyWordElement = c)}
            type="text"
            placeholder="输入昵称关键词"
          />
          &nbsp;<button onClick={this.handleSearch}>搜索</button>
        </div>
      </section>
    );
  }
}

网络请求知识点

1、ES6知识点:解构赋值+重命名

  • let obj = {a:{b:1}}
  • const {1} = obj; //传统解构赋值
  • const {a:{b}} = obj; //连续解构赋值
  • const {a:{b:value}} = obj; // 连续解构赋值+重命名

2、消息订阅与发布机制

  • 先订阅,再发布
  • 适用于任意组件之间的通信
  • 要在组件的 componentWillUnmount() 中取消订阅

3、fetch发送请求,要关注分离的设计思想

    // 发送网络请求
    try {
      const response = await fetch(
        `https://api.github.com/search/users?q=${keyWord}`
      );
      const data = await response.json();
      console.log("获取数据成功:", data);
    } catch (error) {
      console.log("获取数据失败", error);
    }

剩余内容请看下文:juejin.cn/post/721253…