React学习笔记03-React组件

young 499 2022-09-30

React组件化

组件化思想的应用:

  • 尽可能将页面拆分成一个个小的、可复用的组件
  • 这样可以让代码更便于组织和管理,扩展性也强

React的组件相对于Vue更加的灵活和多样,可以按照不同的方式分成很多类组件

  • 根据组件的定义方式,可以分为函数式组件(Functional Component)和类组件(Class Component)
  • 根据组件内部是否有状态需要维护,可以分成:无状态组件(Stateless Component)和有状态组件(Stateful Component)
  • 根据组件的不同职责,可以分成:展示型组件(Presentational Component)和容器型组件(Container Component)

这些组件,最主要的是关注数据逻辑和UI展示的分离

  • 函数组件、无状态组件、展示型组件主要关注UI的展示
  • 类组件、有状态组件、容器型组件主要关注数据逻辑

类组件

类组件的定义有如下要求

  • 组件的名称是大写字符开头(无论类组件还是函数组件)
  • 类组件必须继承自React.Component
  • 类组件必须实现render函数

在ES6之前,可以通过create-react-class模块来定义类组件,但是目前官方建议使用ES6的class类定义

使用class定义一个组件

  • constructor是可选的,我们通常在constructor中初始化一些数据
  • this.state中维护的是组件内部的数据结构
  • render()方法是class组件中唯一必须实现的方法
export default class App extends React.Component {
  render(){
    return [
      <div>111</div>,
      <div>222</div>,
      <div>333</div>,
    ]
  }
}

render函数的返回值

当render函数被调用时,它会检查this.props和this.state的变换,并返回以下类型之一

  • React元素
    • 通常通过JSX创建
    • 例如<div/>会被React渲染为DOM节点,<MyComponent/>会被React渲染为自定义组件
    • 无论是<div/>还是<MyComponent/>都是React元素
  • 数组或者fragments:是的render方法可以返回多个元素
  • Portals:可以渲染子节点到不同的DOM子树中
  • 字符串或数值类型:他们在DOM中会被渲染为文本节点
  • 布尔类型或null:什么都不渲染

函数组件

函数组件是使用function来进行定义的函数,只是这个函数会返回和类组件中render函数返回一样的内容

函数组件有自己的特点(后面使用hooks就不一样了)

  • 没有声明周期,会被更新并挂载,但是没有生命周期函数
  • 没有this(组件实例)
  • 没有内部状态(state)
export default function App(){
  return (
  	<div>Hello World</div>
  )
}

生命周期

认识生命周期

事物从创建到销毁的过程称之为生命周期

React组件也有自己的生命周期,理解组件的生命周期可以让我们在最合适的地方王城自己想要的功能呢

生命周期与生命周期函数的关系:

  • 声明周期是一个抽象的概念,在生命周期的整个过程,分成了很多个阶段
    • 比如装载阶段(Mount),组件第一次在DOM树中被渲染的过程
    • 比如更新过程(Update),组件状态发生变化,重新更新渲染的过程
    • 比如卸载过程(Unmount),组件从DOM树中被移除的过程

React内部为了告诉我们当前处于哪些阶段,会对我们组件内部实现的某些函数进行回调,这些函数就是生命周期函数

  • 比如实现componentDidMount函数:组件已经挂载到DOM上时,就会回调
  • 比如实现componentDidUpdate函数:组件已经发生了更新时,就会回调
  • 比如实现componentWillUnmount函数:组件即将被移除时,就会回调
  • 我们可以在这些回调用实现自己的逻辑代码,来完成自己的需求功能

我们谈React生命周期时,主要谈的是类的生命周期,因为函数式组件是是没有生命周期函数的,后面可以通过hooks来模拟一些生命周期的回调

生命周期解析

https://zh-hans.reactjs.org/docs/react-component.html

https://projects.wojtekmaj.pl/react-lifecycle-methods-diagram/
react常用生命周期函数

常用生命周期函数

constructor

如果不初始化state或者不进行方法绑定,则不需要为React组件实现构造函数

constructor中通常只做两件事情

  • 通过给this.state赋值对象来初始化内部的state
  • 为事件绑定实例(this)
export default class App extends React.Component{
  constructor(props){
   	 super(props)
     this.state = {
       counter: 0
     }
     console.log('调用constructor方法')
  }
}

componentDidMount

componentDidMount函数会在组件挂载后(插入DOM树中)立即调用

componentDidMount中通常的操作

  • 依赖于DOM的操作可以在这里进行
  • 在此处发送网络请求最好的地方(官方建议)
  • 可以在此处添加一些订阅(在componentWillUnmount取消订阅)
componentDidMount(prevProps,prevState,snapshot){
  console.log("调用componentDidMount方法")
}

componentDidUpdate

componentDidUpdate会在更新后立即被调用,首次渲染时不会执行此方法

  • 当组件更新后,可以在此处对DOM进行操作
  • 如果你对更新前后的props进行了比较,也可以选在此处进行网络请求,如当props未发生变化时。则不会执行网络请求
componentDidUpdate(prevProps,prevState,snapshot){
  console.log('调用componentDidUpdate方法')
}
componentDidUpdate(prevProps,prevState,snapshot){
  if(this.props.userId!==prevProps.userId){
    this.fetchData(this.props.userId)
  }
}

componentWillUnmount

componentWillUnmount会在组件卸载及销毁之前直接调用

  • 在此方法中执行必要的清理操作
  • 比如清楚timer,取消网络请求或清除在componentDidMount中创建的订阅等

示例

import React, { Component } from 'react';

class Cpn extends Component {
  render() {
    return <h2>我是Cpn组件</h2>
  }

  componentWillUnmount() {
    console.log("调用了Cpn的componentWillUnmount方法");
  }
}

export default class App extends Component {

  constructor() {
    super();

    this.state = {
      counter: 0,
      isShow: true
    }

    console.log("执行了组件的constructor方法");
  }

  render() {
    console.log("执行了组件的render方法");

    return (
      <div>
        我是App组件
        <h2>当前计数: {this.state.counter}</h2>
        <button onClick={e => this.increment()}>+1</button>
        <hr/>
        <button onClick={e => this.changeCpnShow()}>切换</button>
        {this.state.isShow && <Cpn/>}
      </div>
    )
  }

  increment() {
    this.setState({
      counter: this.state.counter + 1
    })
  }

  changeCpnShow() {
    this.setState({
      isShow: !this.state.isShow
    })
  }

  componentDidMount() {
    console.log("执行了组件的componentDidMount方法");
  }

  componentDidUpdate(prevProps, prevState, snapshot) {
    console.log("执行了组件的componentDidUpdate方法");
  }
}

不常用生命周期函数

react不常用生命周期函数

除了常用的生命周期函数之外,还有一些不常用的生命周期函数

  • getDrivedStateFromProps:state的值在任何时候都依赖于props时使用,该方法返回一个对象来更新state
  • getSnapshotBeforeUpdate:在React更新DOM之前回调的一个函数,可以获取DOM更新前的一些信息(比如滚动位置)
  • shouldComponentUpdate:该生命周期函数很常用,常用语调优,返回一个boolean,用于判断是否调用render

另外React中还提供了一些过期的生命周期函数,这些函数已经不推荐使用

组件嵌套

组件之间存在嵌套关系

  • 之前的案例中,只创建了一个组件App
  • 如果一个应用程序将所有的逻辑都放在一个组件中,那么这个组件就会变的非常的臃肿以及难以维护
  • 所以组件化的核心思想就是对组件进行拆分,拆分成一个个小的组件
  • 再将这些组件组合嵌套在一起,最终形成我们需要的应用程序

react-组件嵌套

上图嵌套逻辑如下

  • App组件是Header,Footer,Main组件的父组件
  • Main组件是Banner,ProductList组件的父组件
import React, { Component } from 'react';

// Header
function Header() {
  return <h2>我是Header组件</h2>
}

// Main
function Banner() {
  return <h3>我是Banner组件</h3>
}

function ProductList() {
  return (
    <ul>
      <li>商品列表1</li>
      <li>商品列表2</li>
      <li>商品列表3</li>
      <li>商品列表4</li>
      <li>商品列表5</li>
    </ul>
  )
}

function Main() {
  return (
    <div>
      <Banner/>
      <ProductList/>
    </div>
  )
}

// Footer
function Footer() {
  return <h2>我是Footer组件</h2>
}


export default class App extends Component {
  render() {
    return (
      <div>
        <Header/>
        <Main/>
        <Footer/>
      </div>
    )
  }
}

组件间的通信

开发的过程中,经常会需要组件之间相互进行通信的场景

  • 比如App可能使用了多个Header,每个地方的Header展示的内容不同,那么我们就需要使用者传递给Header一些数据,让其进行展示
  • 比如我们在Main中一次性请求了Banner数据和ProductList数据,那么就需要传递给他们来进行展示
  • 也可能是子组件中发生了事件,需要由父组件来完成某些操作,那就需要子组件向父组件传递事件

父组件传递子组件

  • 父组件通过属性=值的形式来传递给子组件
  • 子组件通过props参数来获取父组件传递过来的值

类组件

import React, { Component } from 'react';


class ChildCpn extends Component {
  constructor(props) {
    super(props);
  }

  componentWillMount() {

  }

  componentDidMount() {
    console.log(this.props, "componentDidMount");
  }

  render() {
    // console.log(this.props, "render");
    const {name, age, height} = this.props;
    return (
      <h2>子组件展示数据: {name + " " + age + " " + height}</h2>
    )
  }
}

export default class App extends Component {
  render() {
    return (
      <div>
        <ChildCpn name="why" age="18" height="1.88"/>
        <ChildCpn name="kobe" age="40" height="1.98"/>
      </div>
    )
  }
}

函数组件

import React, { Component } from 'react';

function ChildCpn(props) {
  const { name, age, height } = props;

  return (
    <h2>{name + age + height}</h2>
  )
}

export default class App extends Component {
  render() {
    return (
      <div>
        <ChildCpn name="why" age="18" height="1.88" />
        <ChildCpn name="kobe" age="40" height="1.98" />
      </div>
    )
  }
}

参数校验propTypes及默认值

对于传递给子组件的数据,有时候我们可能希望进行验证,特别是对于大型项目来说

  • 如果项目中默认继承了Flow或者TypeScript,那么就可以直接进行类型验证
  • 如果没有使用Flow或者TypeScript,也可以通过prop-types库来进行参数验证

从React V15.5开始,React.PropTypes已经移入另一个包中:prop-types库

官网地址:https://zh-hans.reactjs.org/docs/typechecking-with-proptypes.html

import React, {Component} from 'react';

import PropTypes from 'prop-types';

function ChildCpn(props) {
    const {name, age, height} = props;
    console.log(name, age, height);
    const {names} = props;

    return (
        <div>
            <h2>{name + age + height}</h2>
            <ul>
                {
                    names.map((item, index) => {
                        return <li>{item}</li>
                    })
                }
            </ul>
        </div>
    )
}

class ChildCpn2 extends Component {
    // es6中的class fields写法
    // 属性定义 必须加static
    // 等同于 ChildCpn2.propTypes
    static propTypes = {}
    // 默认值 必须加static
    // 等同于 ChildCpn2.defaultProps
    static defaultProps = {}
}

// 属性类型定义
ChildCpn.propTypes = {
    name: PropTypes.string.isRequired,
    age: PropTypes.number,
    height: PropTypes.number,
    names: PropTypes.array
}

// 默认值
ChildCpn.defaultProps = {
    name: "why",
    age: 30,
    height: 1.98,
    names: ["aaa", "bbb"]
}

export default class App extends Component {
    render() {
        return (
            <div>
                <ChildCpn name="why" age={18} height={1.88} names={["abc", "cba"]}/>
                <ChildCpn name="kobe" age={40} height={1.98} names={["nba", "mba"]}/>
                <ChildCpn/>
            </div>
        )
    }
}

子组件传递父组件

某些情况,我们也需要子组件向父组件传递消息

  • 在vue中是通过自定义事件来完成的
  • 在React中同样是通过props传递消息,只是让父组件给子组件传递一个回调幻术

案例

  • 将计数器案例进行拆解
  • 将按钮封装到子组件中:CounterButton
  • CounterButton发生点击事件,将内容传递到父组件中,修改counter的值
import React, {Component} from 'react';
import PropTypes from 'prop-types';

class CounterButton extends Component {
    constructor(props, context) {
        super(props, context);
    }

    render() {
        const {btnClick} = this.props
        return (
            <button onClick={btnClick}>+1</button>
        )
    }
}

CounterButton.propTypes = {
    btnClick: PropTypes.func
}

class App extends Component {
    constructor(props) {
        super(props);
        this.state = {
            counter: 0
        }
    }

    render() {
        const {counter} = this.state
        return (
            <div>
                <h2>当前计数:{counter}</h2>
                <CounterButton btnClick={() => this.incr()}/>
            </div>
        )
    }

    incr() {
        this.setState({
            counter: this.state.counter + 1
        })
    }
}

export default App;

组件通信案例

App.js

import React, {Component} from 'react';
import TableControl from "./TableControl";

class App extends Component {
    constructor(props) {
        super(props);

        this.titles = ['流行', '新款', '精选']

        this.state = {
            currentTitle: '流行'
        }
    }

    render() {
        const {currentTitle} = this.state
        return (
            <div>
                <TableControl titles={this.titles} titleCheck={(index)=>this.titleCheck(index)}/>
                <h2>{currentTitle}</h2>
            </div>
        );
    }

    titleCheck(index) {
        this.setState({
            currentTitle: this.titles[index]
        })
    }
}

export default App;

TableControl.js

import React, {Component} from 'react';
import PropTypes from "prop-types";
import style from "./style.css"

class TableControl extends Component {
    constructor(props) {
        super(props);
        this.state = {
            currentIndex: 0
        }
    }


    render() {
        const {titles} = this.props
        const {currentIndex} = this.state
        return (
            <div className={"tab-control"}>
                {
                    titles.map((item, index) => {
                        return (
                            <div className={"tab-item " + (index === currentIndex ? "active" : "")}
                                 onClick={() => this.changeCurrentIndex(index)}>
                                <span>{item}</span>
                            </div>
                        )
                    })
                }
            </div>
        );
    }

    changeCurrentIndex(index) {
        const {titleCheck} = this.props
        this.setState({
            currentIndex: index
        })
        titleCheck(index)
    }
}

TableControl.propTypes = {
    titles: PropTypes.array,
    titleCheck: PropTypes.func
};

export default TableControl;

style.css

.tab-control {
    display: flex;
    height: 44px;
    line-height: 44px;
}

.tab-item {
    flex: 1;
    text-align: center;
}

.tab-item span {
    padding: 5px 8px;
}

.tab-item.active {
    color: red;
}

.tab-item.active span {
    border-bottom: 3px solid red;
}

React实现vue的slot

App.js

import React, {Component} from 'react';
import NavBar from "./NavBar"
import style from "./style.css"
import NavBar2 from "./NavBar2";
class App extends Component {
    render() {
        const left = (
            <span>aaa</span>
        )
        return (
            <div>
                <NavBar>
                    <span>aaa</span>
                    <strong>bbb</strong>
                    <a href="#">ccc</a>
                </NavBar>
                <NavBar2 left={left}
                         center={<strong>bbb</strong>}
                         right={<a href="#">ccc</a> }/>
            </div>
        );
    }
}

export default App;

NavBar.js

import React, {Component} from 'react';

class NavBar extends Component {

    render() {
        return (
            <div className="nav-item nav-bar">
                <div className="nav-left">
                    {this.props.children[0]}
                </div>
                <div className="nav-center">
                    {this.props.children[1]}
                </div>
                <div className="nav-right">
                    {this.props.children[2]}
                </div>
            </div>
        );
    }
}

export default NavBar;

NavBar2.js

import React, {Component} from 'react';

class NavBar2 extends Component {

    render() {
        return (
            <div className="nav-item nav-bar">
                <div className="nav-left">
                    {this.props.left}
                </div>
                <div className="nav-center">
                    {this.props.center}
                </div>
                <div className="nav-right">
                    {this.props.right}
                </div>
            </div>
        );
    }
}

export default NavBar;

style.css

body {
    padding: 0;
    margin: 0;
}

.nav-bar {
    display: flex;
}

.nav-item {
    height: 44px;
    line-height: 44px;
    text-align: center;
}

.nav-left, .nav-right {
    width: 70px;
    background-color: red;
}

.nav-center {
    flex: 1;
    background-color: blue;
}

Context

Context应用场景

非父子组件数据的共享

  • 开发中,比较常见的数据传递方式是通过属性自上而下(由父到子)进行传递
  • 但是对于一些场景,比如一些数据需要在多个组件中个进行共享(地区偏好、UI主题、用户登录状态、用户信息等)
  • 如果我们在顶层的App中定义这些信息,之后一层层传递下去,那么对于一些中间层不需要数据的组件来说,是一种冗余的操作。

属性展开语法:https://zh-hans.reactjs.org/docs/jsx-in-depth.html#spread-attributes

import React, { Component } from 'react';

function ProfileHeader(props) {
  return (
    <div>
      <h2>用户昵称: {props.nickname}</h2>
      <h2>用户等级: {props.level}</h2>
    </div>
  )
}

function Profile(props) {
  return (
    <div>
      {/* <ProfileHeader nickname={props.nickname} level={props.level}/> */}
      <ProfileHeader {...props}/>
      <ul>
        <li>设置1</li>
        <li>设置2</li>
        <li>设置3</li>
        <li>设置4</li>
      </ul>
    </div>
  )
} 

export default class App extends Component {
  constructor(props) {
    super(props);

    this.state = {
      nickname: "kobe",
      level: 99
    }
  }

  render() {
    // const {nickname, level} = this.state;

    return (
      <div>
        <Profile {...this.state}/>
      </div>
    )
  }
}

如果层级更多的话,一层层的传递是非常麻烦的,并且代码是非常冗余的

React提供了一个API:Context

Context提供了一种在组件之间共享此类值的方式,而不必显式地通过组件树的逐层传递props

Context设计目的是为了共享那些对于子一个组件树而言是全局的数据,例如当前认证的用户、主题或者首选语言等

Context相关API

React.createContext

创建一个需要共享的Context对象

如果一个组件订阅了Context,那么这个组件会从离自身最近的那个匹配的Provider中读取到当前的context值

defaultValue是组件在顶层查找过程中没有找到对应的Provider时使用的默认值

Context.Provider

每个Context对象都会返回一个Provider React组件,它允许消费组件订阅context的变化

Provider接收一个value属性,传递给消费组件

一个Provider可以和多个消费组件有对应关系

多个Provider可以嵌套使用,内层的会覆盖外层的数据

当Provider的value值发生变化的时候,它内部的所有消费组件都会重新渲染

Class.contextType

挂载在class上的context属性会被重赋值为一个React.createContext创建的Context对象

这能让你使用this.context来消费最近的Context上的那个值

你可以在任何生命周期中访问到他,包括render函数中

Context.Consumer

React组件可以订阅到context变更。这能让你在函数式组件中完成订阅context

这里需要函数作为子元素(function as child)这种做法

这个函数接收当前的context值,返回一个React节点

案例

类组件

import React, {Component} from 'react';

const UserContext = React.createContext({
    nickName: "young",
    level: -1
})

class ProfileHeader extends Component {
    render() {
        return (
            <div>
                <h2>用户昵称:{this.context.nickName}</h2>
                <h2>用户等级:{this.context.level}</h2>
            </div>
        )
    }
}
ProfileHeader.contextType=UserContext

class Profile extends Component {
    render() {
        return (
            <div>
                <ProfileHeader/>
                <ul>
                    <li>设置1</li>
                    <li>设置2</li>
                    <li>设置3</li>
                    <li>设置4</li>
                </ul>
            </div>
        )
    }
}

class App extends Component {
    constructor(props) {
        super(props);
        this.state={
            nickName:"kobe",
            level:99
        }
    }
    render() {
        return (
            <div>
                {/* 不将Profile组件放在Context中间,会使用默认值*/}
                <UserContext.Provider value={this.state}>
                    <Profile/>
                </UserContext.Provider>
            </div>
        );
    }
}

export default App;

函数组件

函数组件要使用Context.comsumer进行订阅

import React, {Component} from 'react';

const UserContext = React.createContext({
    nickName: "young",
    level: -1
})

function ProfileHeader() {
    return (
        <UserContext.Consumer>
            {
                value => {
                    return (
                        <div>
                            <h2>用户昵称:{value.nickName}</h2>
                            <h2>用户等级:{value.level}</h2>
                        </div>)
                }
            }

        </UserContext.Consumer>
    )
}

class Profile extends Component {
    render() {
        return (
            <div>
                <ProfileHeader/>
                <ul>
                    <li>设置1</li>
                    <li>设置2</li>
                    <li>设置3</li>
                    <li>设置4</li>
                </ul>
            </div>
        )
    }
}

class App extends Component {
    constructor(props) {
        super(props);
        this.state = {
            nickName: "kobe",
            level: 99
        }
    }

    render() {
        return (
            <div>
                {/* 不将Profile组件放在Context中间,会使用默认值*/}
                <UserContext.Provider value={this.state}>
                    <Profile/>
                </UserContext.Provider>
            </div>
        );
    }
}

export default App;

多个context

import React, { Component } from 'react';

// 创建Context对象
const UserContext = React.createContext({
  nickname: "aaaa",
  level: -1
})

const ThemeContext = React.createContext({
  color: "black"
})

function ProfileHeader() {
  // jsx -> 嵌套的方式
  return (
    <UserContext.Consumer>
      {
        value => {
          return (
            <ThemeContext.Consumer>
              {
                theme => {
                  return (
                    <div>
                      <h2 style={{color: theme.color}}>用户昵称: {value.nickname}</h2>
                      <h2>用户等级: {value.level}</h2>
                      <h2>颜色: {theme.color}</h2>
                    </div>
                  )
                }
              }
            </ThemeContext.Consumer>
          )
        }
      }
    </UserContext.Consumer>
  )
}

function Profile(props) {
  return (
    <div>
      <ProfileHeader />
      <ul>
        <li>设置1</li>
        <li>设置2</li>
        <li>设置3</li>
        <li>设置4</li>
      </ul>
    </div>
  )
}

export default class App extends Component {
  constructor(props) {
    super(props);

    this.state = {
      nickname: "kobe",
      level: 99
    }
  }

  render() {
    return (
      <div>
        <UserContext.Provider value={this.state}>
          <ThemeContext.Provider value={{ color: "red" }}>
            <Profile />
          </ThemeContext.Provider>
        </UserContext.Provider>
      </div>
    )
  }
}

setState的使用

为什么要使用setState

开发过程中,我们并不能通过直接修改state的值来让界面发生更新

  • 修改了state之后,希望React根据最新的State来重新渲染界面,但是这种修改方式React并不能知道数据发生了变化
  • React中没有实现类似于Vue2中的Object.defineProperty或者Vue3中的Proxy的方式来监听数据的变化
  • 我们必须通过setState来告知React数据已经发生了变化

我们实现的组件中并没有实现setState方法,setState方法是从Component中继承过来的

Component.prototype.setState = function(partialState,callback) {
  invariant(
  	typeof partialState === 'object' ||
			typeof partialState === 'function' ||
    		partialState == null,
    'setState(...): takes an object of state variables to update or a'  +
    	'function which returns an object of state variables.',
  );
  this.updater.enqueueSetState(this, partialState, callback, 'setState';)
}

setState异步更新

changeText() {
  this.setState({
    message: "Hello Message"
  })
  console.log(this.state.message); // Hello World
}

最终打印结果是Hello World,可见setState是异步的操作,我们并不能在执行完setState之后立马拿到最新的state结果。

为什么setState设计成异步的,React的核心成员(Redux的作者)在github上做了回应

https://github.com/facebook/react/issues/11527#issuecomment-360199710

简单的可以总结为:

  • setState设计为异步的,可以显著的提升性能
    • 如果每次调用setState都进行一次更新,那么意味着render函数会被频繁的调用,界面重新渲染,这样的话性能很低
    • 最好的办法是获取到多个更新,之后进行批量更新
  • 如果通过更新了state,但是还没有执行render函数,那么state和props不能保持同步
    • state和props不能保持一致性,会在开发中产生很多问题

如何获取异步的结果

方式一:setState的回调函数

  • setState接收两个参数,第二个参数是一个回调函数,这个回调函数会在更新更新之后执行
  • setState(partialState, callback)
changeText() {
  this.setState({
    message: "Hello Message"
  }, ()=>{
    console.log(this.state.message); // Hello Message
  })
}

方式二:在声明周期函数中

componentDidUpdate(prevProps, provState, snapshot){
  console.log(this.state.message)
}

setState一定是异步的吗

验证一:在setTimeout中更新

changeText() {
  setTimeout(()=>{
  	this.setState({
      message: "Hello Message"
    })  
    console.log(this.state.message); // Hello Message
  }, 0);
}

验证二:原生DOM事件

componentDidMount() {
  const btnEl = document.getElementById("btn")
  btnEl.addEventListener('click',()=>{
    this.setState({
      message: "Hello Message"
    })  
    console.log(this.state.message); // Hello Message
  })
}
  • 在组件生命周期或者React合成事件中,setState是异步的
  • 在setTimeout或者原生DOM事件中,setState是同步的

数据的合并

比如现在我定义的state为

this.state = {
  message: Hello World,
  name: Young
}

那么,我通过setState去修改message

this.setState({
  message: "Hello Message"
})

是否会对name产生影响

其实是不会的,在源码中使用了Object.assign的方法进行处理,对原有对象和新对象进行合并

Object.assign({}, prevState, partialState);

源码在ReactUpdateQueue.js的processUpdateQueue方法中

多个state的合并

比如计数器的案例中,我们对increment方法做如下操作

increment() {
  this.setState({
    counter: this.state.counter +1
  })
  this.setState({
    counter: this.state.counter +1
  })
  this.setState({
    counter: this.state.counter +1
  })
}

那么此时,我们点击了按钮之后,实现显示还是从0变成了1,并没有变成3

如果采用如下方式,那么将会变成3

increment() {
  this.setState((state, props)=>{
    return {
      counter: state.counter +1
    }   
  })
  
  this.setState((state, props)=>{
    return {
      counter: state.counter +1
    }   
  })
  
  this.setState((state, props)=>{
    return {
      counter: state.counter +1
    }   
  })
}

React的更新机制

React的渲染流程

JSX–>虚拟DOM–>真实DOM

React的更新流程

props/state改变–>render函数重新执行–>产生新的DOM树–>新旧DOM树进行diif–>计算出差异进行更新–>更新到真实的DOM

React的更新流程

React在props或者state发生改变时,会调用React的render方法,会创建一颗不同的树。

React需要基于这两颗不同的树之间的差别来判断如何更有效的更新UI

  • 如果一棵树参考另外一棵树进行比较更新,那么即使是最现金的算法,改算法的复杂程度为O(n3)
  • https://grfia.dlsi.ua.es/ml/algorithms/references/editsurvey_bille.pdf
  • 如果在React中使用了该算法。那么展示100个元素所需执行的计算量将在十亿的量级范围
  • 这会使React的更新性能变的非常的低效

于是React对算法进行了优化,将其优化程了O(n)

  • 同层节点之间互相比较,不会跨节点比较
  • 不同类型的节点,产生不同的树结构
  • 开发中,可以通过key来指定哪些节点在不同的渲染下保持稳定

情况一:对比不同类型的元素

当节点为不同的元素时,React会拆卸原有的树,并建立起新的树

  • 当一个元素从<a>变成<img>,从<Article>变成<Comment>,或者从<Button>变成<div>都会触发一个完整的重建流程

  • 当卸载一棵树时,对应的DOM节点也会被销毁,组件实例将执行componentWillUnmound()方法

  • 当建立一颗新的树时,对应的ODM节点会被创建以及插入到DOM中,组件实例将执行componentWillMount()方法,紧接着componentDidMount()方法

  • 例如下面代码更改,React会销毁Counter组件并且重新组装一个新的组件,而不会对Counter进行复用

    <div>
      <Counter/>
    </div>
    <span>
      <Counter/>
    </span>
    

情况二:对比同一类型的元素

当对比两个相同类型的React元素时,React会保留DOM节点,仅对比及更新有改变的属性

比如下面的代码改变,通过对比这两个元素,React知道只需要修改DOM元素上的className属性

<div className="before" title="stuff"/>
<div className="after" title="stuff"/>

比如下面代码的更改,当更新style属性时,react仅更新有所改变的属性,通过对比这两个元素,React知道主需要修改DOM元素上的color样式,无需修改fontWeigh

<div style={{color: 'red',fontWeight: 'blod'}} />
<div style={{color: 'green',fontWeight: 'blod'}} />

如果是同类型的组件元素,组件会保持不变,React会更新改组件的props,并且调用componentWillReceiveProps()和componentWillUpdate()方法,下一步,调用render()方法,diff算法将在之前的结果以及新的结果中进行递归

情况三:对子节点进行递归

默认条件下,当递归DOM节点的子元素时,React会同时遍历两个子元素列表,当产生差异时,生成一个mutation

<ul>
  <li>first</li>
	<li>second</li>
</ul>
<ul>
  <li>first</li>
	<li>second</li>
	<li>third</li>
</ul>

在最后插入一条数据的情况

  • 前面两个比较是完全相同的,所以不会产生mutation
  • 最后一个比较,产生一个mutation,将其插入到新的DOM树中即可
<ul>
  <li>星际穿越</li>
	<li>盗梦空间</li>
</ul>
<ul>
  <li>大话西游</li>
	<li>星际穿越</li>
	<li>盗梦空间</li>
</ul>

如果是在中间插入一条

  • React会对每一个子元素产生一个mutation,而不是保持<li>星际穿越</li><li>盗梦空间</li>的不变
  • 这种低效的比较方式会带来一定的性能问题

keys的优化

方式一:在最后位置插入数据,这种情况下,有无key意义并不大

方式二:在前面插入数据,这种做法,在没有key的情况下,所有的li都需要进行修改

当子元素(这里的li)拥有key时,React使用Key来匹配原有书上的子元素以及最新树上的子元素

  • 将key为111和222的元素仅仅进行位移,不需要进行任何修改
  • 将key为333的元素插入到最前面的位置即可

key的注意事项:

  • key应该是唯一的
  • key不要使用随机数
  • 使用index作为key,对性能是没有优化的

render函数的调用

react-组件嵌套

在之前的案例中,在App中,增加一个计数器的代码,当点击+1时,会重新调用App的render函数,而当App的render函数被调用时,所有的子组件的render函数都会被重新调用

import React, { Component } from 'react';

// Header
function Header() {
  console.log("Header被调用");
  return <h2>我是Header组件</h2>
}

// Main
class Banner extends Component {
  render() {
    console.log("Banner render函数被调用");
    return <h3>我是Banner组件</h3>
  }
}

function ProductList() {
  console.log("ProductList被调用");
  return (
    <ul>
      <li>商品列表1</li>
      <li>商品列表2</li>
      <li>商品列表3</li>
      <li>商品列表4</li>
      <li>商品列表5</li>
    </ul>
  )
}

class Main extends Component {
  render() {
    console.log("Main render函数被调用");
    return (
      <div>
        <Banner/>
        <ProductList/>
      </div>
    )
  }
}

// Footer
function Footer() {
  console.log("Footer被调用");
  return <h2>我是Footer组件</h2>
}


export default class App extends Component {
  constructor(props) {
    super(props);

    this.state = {
      counter: 0
    }
  }

  render() {
    console.log("App render函数被调用");
    return (
      <div>
        <h2>当前计数: {this.state.counter}</h2>
        <button onClick={e => this.increment()}>+1</button>
        <Header/>
        <Main/>
        <Footer/>
      </div>
    )
  }

  increment() {
    this.setState({
      counter: this.state.counter + 1
    })
  }
}

如果在开发中,我们只要是修改了App中的数据,所有的组件都需要重新render,进行diff算法,那性能是很低的

事实上,很多的组件没有必要重新render

他们调用的render应该有一个前提,就是依赖的数据(state、props)发生改变时,再调用自己的render方法

shouldComponentUpdate

React提供了一个生命周期方法shouldComponentUpdate,简称为SCU,这个方法接收参数,并且需要有返回值

  • 该方法的两个参数
    • nextProps,修改之后,最新的props属性
    • nextState,修改之后,最新的state属性
  • 该方法的返回值为一个boolean类型
    • 返回值为true,那么调用render方法
    • 返回值为false,那么不需要调用render方法
    • 默认返回是true,也就是只要state发生改变,就会调用render方法

比如在App中增加一个message属性,jsx中并没有依赖这个message属性,那么它的改变不应该引起重新的渲染,但是因为render监听到state的改变,就会重新render,所以最后render方法还是被重新调用了

import React, {Component} from 'react';

export default class App extends Component {
  constructor(props) {
    super(props);

    this.state = {
      counter: 0,
      message: "Hello World"
    }
  }

  render() {
    console.log("App render函数被调用");
    return (
      <div>
        <h2>当前计数: {this.state.counter}</h2>
        <button onClick={e => this.increment()}>+1</button>
        <button onClick={e => this.changeText()}>改变文本</button>
      </div>
    )
  }

  shouldComponentUpdate(nextProps, nextState) {
    if (this.state.counter !== nextState.counter) {
      return true;
    }
    
    return false;
  }

  increment() {
    this.setState({
      counter: this.state.counter + 1
    })
  }

  changeText() {
    this.setState({
      message: "你好啊,李银河"
    })
  }
}

PureComponent

如果所有的类,我们都需要手动实现shouldComponentUpdate,那么就会给我们增加非常多的工作量

React中已经有了对应的实现,只需要将class继承自PureComponent即可

在源码的react-reconciler下的ReactFiberClassComponent.js中,checkShouldComponentUpdate方法

如果有自己实现的shouldComponentUpdate方法,则调用自己实现的,否则调用PureComponent的实现shallowEqual方法

import React, { PureComponent } from 'react';

// Header
function Header() {
  console.log("Header被调用");
  return <h2>我是Header组件</h2>
}

// Main
class Banner extends PureComponent {
  render() {
    console.log("Banner render函数被调用");
    return <h3>我是Banner组件</h3>
  }
}

function ProductList() {
  console.log("ProductList被调用");
  return (
    <ul>
      <li>商品列表1</li>
      <li>商品列表2</li>
      <li>商品列表3</li>
      <li>商品列表4</li>
      <li>商品列表5</li>
    </ul>
  )
}

class Main extends PureComponent {
  render() {
    console.log("Main render函数被调用");
    return (
      <div>
        <Banner/>
        <ProductList/>
      </div>
    )
  }
}

// Footer
function Footer() {
  console.log("Footer被调用");
  return <h2>我是Footer组件</h2>
}


export default class App extends PureComponent {
  constructor(props) {
    super(props);

    this.state = {
      counter: 0
    }
  }

  render() {
    console.log("App render函数被调用");
    return (
      <div>
        <h2>当前计数: {this.state.counter}</h2>
        <button onClick={e => this.increment()}>+1</button>
        <Header/>
        <Main/>
        <Footer/>
      </div>
    )
  }

  increment() {
    this.setState({
      counter: this.state.counter + 1
    })
  }
}

shallowEqual方法

这个方法是一个渐层比较

高阶组件memo

对函数组件,我们需要使用高阶组件memo,对之前的函数组件进行包裹

import React, { PureComponent, memo } from 'react';

// Header
const MemoHeader = memo(function Header() {
  console.log("Header被调用");
  return <h2>我是Header组件</h2>
})


// Main
class Banner extends PureComponent {
  render() {
    console.log("Banner render函数被调用");
    return <h3>我是Banner组件</h3>
  }
}

const MemoProductList = memo(function ProductList() {
  console.log("ProductList被调用");
  return (
    <ul>
      <li>商品列表1</li>
      <li>商品列表2</li>
      <li>商品列表3</li>
      <li>商品列表4</li>
      <li>商品列表5</li>
    </ul>
  )
})

class Main extends PureComponent {
  render() {
    console.log("Main render函数被调用");
    return (
      <div>
        <Banner/>
        <MemoProductList/>
      </div>
    )
  }
}

// Footer
const MemoFooter = memo(function Footer() {
  console.log("Footer被调用");
  return <h2>我是Footer组件</h2>
})


export default class App extends PureComponent {
  constructor(props) {
    super(props);

    this.state = {
      counter: 0
    }
  }

  render() {
    console.log("App render函数被调用");
    return (
      <div>
        <h2>当前计数: {this.state.counter}</h2>
        <button onClick={e => this.increment()}>+1</button>
        <MemoHeader/>
        <Main/>
        <MemoFooter/>
      </div>
    )
  }

  increment() {
    this.setState({
      counter: this.state.counter + 1
    })
  }
}

setState不可变数据

setState时,要保证state的数据不可变,否则,使用SCU进行优化时,就无法判断出数据是否更改了

import React, { PureComponent } from 'react';

export default class App extends PureComponent {
  constructor(props) {
    super(props);

    // 引用类型
    this.state = {
      friends: [
        { name: "lilei", age: 20 },
        { name: "lily", age: 25 },
        { name: "lucy", age: 22 }
      ]
    }
  }

  // shouldComponentUpdate(newProps, newState) {
  //   if (newState.friends !== this.state.friends) {
  //     return true;
  //   }

  //   return false;
  // }

  render() {
    return (
      <div>
        <h2>好友列表:</h2>
        <ul>
          {
            this.state.friends.map((item, index) => {
              return (
                <li key={item.name}>
                  姓名: {item.name} 
                  年龄: {item.age}
                  <button onClick={e => this.incrementAge(index)}>age+1</button>
                </li>
              )
            })
          }
        </ul>
        <button onClick={e => this.insertData()}>添加数据</button>
      </div>
    )
  }

  insertData() {
    // 1.在开发中不要这样来做
    // const newData = {name: "tom", age: 30}
    // this.state.friends.push(newData);
    // this.setState({
    //   friends: this.state.friends
    // });

    // 2.推荐做法
    const newFriends = [...this.state.friends];
    newFriends.push({ name: "tom", age: 30 });
    this.setState({
      friends: newFriends
    })
  }

  incrementAge(index) {
    const newFriends = [...this.state.friends];
    newFriends[index].age += 1;
    this.setState({
      friends: newFriends
    })
  }
}

事件总线

前面通过Context主要实现的是数据共享,但是在开发过程中,如果有跨组件之间的事件传递,应该如何操作

  • 在Vue中,我们可以通过Vue实例,快速实现一个事件总线(EventBus)来完成操作
  • 在React中,我们可以依赖一个使用的比较多的库events来完成对应的操作

可以使用npm或者yarn来安装一个events

yarn add events

events常用的API

  • 创建EventEmitter对象:eventBus对象
  • 发出事件:eventBus.emit(“事件名称”,参数列表)
  • 监听时间:eventBus.addListener(“事件名称”,监听函数)
  • 移除事件:eventBus.removeListener(“事件名称”,监听函数)
import React, { PureComponent } from 'react';

import { EventEmitter } from 'events';

// 事件总线: event bus
const eventBus = new EventEmitter();

class Home extends PureComponent {
  componentDidMount() {
    eventBus.addListener("sayHello", this.handleSayHelloListener);
  }

  componentWillUnmount() {
    eventBus.removeListener("sayHello", this.handleSayHelloListener);
  }

  handleSayHelloListener(num, message) {
    console.log(num, message);
  }

  render() {
    return (
      <div>
        Home

      </div>
    )
  }
}

class Profile extends PureComponent {
  render() {
    return (
      <div>
        Profile
        <button onClick={e => this.emmitEvent()}>点击了profile按钮</button>
      </div>
    )
  }

  emmitEvent() {
    eventBus.emit("sayHello", 123, "Hello Home");
  }
}

export default class App extends PureComponent {
  render() {
    return (
      <div>
        <Home/>
        <Profile/>
      </div>
    )
  }
}

ref

如何使用ref

在React的开发模式中,通常情况下不需要、也不剑姬直接操作DOM原生,但是某些特殊的情况,确实需要获取到DOM进行操作

  • 管理交单,文本选择或者媒体播放
  • 触发强制动画
  • 集成第三方DOM库

创建refs来获取对应DOM的方式有三种

方式一:传入字符串

  • 使用时通过this.refs.传入的字符串格式获取对应的元素

方式二:传入一个对象

  • 对象是通过React.createRef()方式创建出来的
  • 使用时获取到创建的对象其中有一个current属性就是对应的元素

方式三:传入一个函数

  • 该函数会在DOM被挂载时进行回调,这个函数会传入一个元素对象,我们可以自己保存
  • 使用时,直接拿到之前保存的元素对象即可

ref的类型

ref的值根据节点的类型而有所不同

  • 当ref属性用于HTML元素时,构造函数中使用React.createRef()创建的ref接收底层DOM元素作为其current属性
  • 当ref属性用于自定义class组件时,ref对象接收组件的挂载实例作为其current属性
  • 不能在函数组件上使用ref属性,因为它们没有实例

函数组件可以通过React.forwardRef,以及在hooks中使用ref

import React, { PureComponent, createRef, Component } from 'react';

class Counter extends PureComponent {
  constructor(props) {
    super(props);

    this.state = {
      counter: 0
    }
  }

  render() {
    return (
      <div>
        <h2>当前计数: {this.state.counter}</h2>
        <button onClick={e => this.increment()}>+1</button>
      </div>
    )
  }

  increment() {
    this.setState({
      counter: this.state.counter + 1
    })
  }
}

export default class App extends PureComponent {

  constructor(props) {
    super(props);

    this.titleRef = createRef();
    this.counterRef = createRef();
    this.titleEl = null;
  }

  render() {
    return (
      <div>
        {/* <h2 ref=字符串/对象/函数>Hello React</h2> */}
        <h2 ref="titleRef">Hello React</h2>
        {/* 目前React推荐的方式 */}
        <h2 ref={this.titleRef}>Hello React</h2>
        <h2 ref={arg => this.titleEl = arg}>Hello React</h2>
        <button onClick={e => this.changeText()}>改变文本</button>
        <hr/>
        <Counter ref={this.counterRef}/>
        <button onClick={e => this.appBtnClick()}>App按钮</button>
      </div>
    )
  }

  changeText() {
    // 1.使用方式一: 字符串(不推荐, 后续的更新会删除)
    this.refs.titleRef.innerHTML = "Hello Coderwhy";
    // 2.使用方式二: 对象方式
    this.titleRef.current.innerHTML = "Hello JavaScript";
    // 3.使用方式三: 回调函数方式
    this.titleEl.innerHTML = "Hello TypeScript";
  }

  appBtnClick() {
    this.counterRef.current.increment();
  }
}

class App extends Component {
  constructor(props) {
    super(props);
  }

  render() {
    return (
      <div>
        <Counter title={}></Counter>
      </div>
    )
  }
}

受控组件

在React中,HTML表单的处理方式和普通的DOM元素不太一样,表单元素通常会保存在一些内部的state

比如下面的HTML表单元素

  • 这个处理方式是DOM默认处理HTML表单的行为,在用户点击提交时会提交到某个服务器中,并且刷新页面
  • 在React中,并没有静止这个行为,它依然是有效的
  • 但是通常情况下会使用JavaScript函数来方便的处理表单提交,同时还可以访问用户填写的表单数据
  • 实现这种效果的标准方式是受用”受控组件“
<form>
  <lable>
  	名字:
    <input type="text" name="name"/>
  </lable>
  <input type="submit" value="提交"/>
</form>

受控组件的基本演练

在HTML元素中,表单元素(如<input/>、<textarea/>、<select/>)之类的扁担元素通常自己维护state,并根据用户输入进行更新

而在React中,可变状态(mutable state)通常保存在组件state属性中,并且只能通过setState来更新

  • 我们将二者结合起来,是React的state成为唯一数据源
  • 渲染表单的React组件还控制着用户输入构成中表单发生的操作
  • 被React以这种方式控制取值的表单输入元素就叫做受控组件

由于在表单上设置了value属性,因此显示的值将使用为this.state.value,这是的React的state成为唯一数据源

由于handleUsernameChange在每次按键时都会执行并更新React的state,因此显示的值锁着用户输入而更新

Element Value property Change callback New value in the callback
<input type=“text”/> value=“string” onChange event.target.value
<input type=“checkbox”/> value= onChange event.target.checked
<input type=“radio”/> value= onChange event.target.checked
<textarea/> value=“string” onChange event.target.value
<select/> value=“option value” onChange event.target.value

受控组件的其他演练

textarea:textarea标签与input比较相似

select:select标签需要通过selectecd属性来控制哪一个被选中,它可以匹配state中的value来选中

处理多个输入:多处理方式可以像单处理方式那样进行操作,但是需要更多监听方法,可以使用ES6的一个语法:计算属性名(Computed property names)

import React, { PureComponent } from 'react'

export default class App extends PureComponent {
  constructor(props) {
    super(props);

    this.state = {
      username: ""
    }
  }

  render() {
    return (
      <div>
        <form onSubmit={e => this.handleSubmit(e)}>
          <label htmlFor="username">
            用户: 
            {/* 受控组件 */}
            <input type="text" 
                   id="username" 
                   onChange={e => this.handleChange(e)}
                   value={this.state.username}/>
          </label>
          <input type="submit" value="提交"/>
        </form>
      </div>
    )
  }

  handleSubmit(event) {
    event.preventDefault();
    console.log(this.state.username);
  }

  handleChange(event) {
    this.setState({
      username: event.target.value
    })
  }
}
import React, { PureComponent } from 'react'

export default class App extends PureComponent {
  constructor(props) {
    super(props);

    this.state = {
      fruits: "orange"
    }
  }

  render() {
    return (
      <div>
        <form onSubmit={e => this.handleSubmit(e)}>
          <select name="fruits" 
                  onChange={e => this.handleChange(e)}
                  value={this.state.fruits}>
            <option value="apple">苹果</option>
            <option value="banana">香蕉</option>
            <option value="orange">橘子</option>
          </select>
          <input type="submit" value="提交"/>
        </form>
      </div>
    )
  }

  handleSubmit(event) {
    event.preventDefault();
    console.log(this.state.fruits);
  }

  handleChange(event) {
    this.setState({
      fruits: event.target.value
    })
  }
}
import React, { PureComponent } from 'react'

export default class App extends PureComponent {
  constructor(props) {
    super(props);

    this.state = {
      username: "",
      password: "",
      valid: ""
    }
  }

  render() {
    return (
      <div>
        <form onSubmit={e => this.handleSubmit(e)}>
          <label htmlFor="username">
            用户: 
            <input type="text" 
                   id="username"
                   name="username" 
                   onChange={e => this.handleChange(e)}
                   value={this.state.username}/>
          </label>
          <br/>
          <label htmlFor="password">
            密码: 
            <input type="text" 
                   id="password" 
                   name="password" 
                   onChange={e => this.handleChange(e)}
                   value={this.state.password}/>
          </label>
          <br/>
          <label htmlFor="valid">
            验证: 
            <input type="text" 
                   id="valid" 
                   name="valid" 
                   onChange={e => this.handleChange(e)}
                   value={this.state.valid}/>
          </label>
          <br/>
          <input type="submit" value="提交"/>
        </form>
      </div>
    )
  }

  handleSubmit(event) {
    event.preventDefault();
    const {username, password, valid} = this.state;
    console.log(username, password, valid);
  }

  handleChange(event) {
    this.setState({
      // 计算属性名
      [event.target.name]: event.target.value
    })
  }

  // handleUsernameChange(event) {
  //   this.setState({
  //     username: event.target.value
  //   })
  // }

  // handlePasswordChange(event) {
  //   this.setState({
  //     password: event.target.value
  //   })
  // }

  // handleValidChange(event) {
  //   this.setState({
  //     valid: event.target.value
  //   })
  // }
}

非受控组件

React推荐大多数情况下使用受控组件来处理表单数据:

  • 一个受控组件中,表单数据是由React组件来管理的
  • 另一种替代方案是使用非受控组件,这时表单数据将交由DOM节点来处理

如果要使用非受控组件中的数据,那么我们需要使用ref来从DOM节点中获取表单数据

在非受控组件中,通常使用defaultValue来设置默认值

import React, { PureComponent, createRef } from 'react'

export default class App extends PureComponent {
  constructor(props) {
    super(props);

    this.usernameRef = createRef();
  }

  render() {
    return (
      <div>
        <form onSubmit={e => this.handleSubmit(e)}>
          <label htmlFor="username">
            用户: 
            <input type="text" id="username" ref={this.usernameRef}/>
          </label>
          <input type="submit" value="提交"/>
        </form>
      </div>
    )
  }

  handleSubmit(event) {
    event.preventDefault();
    console.log(this.usernameRef.current.value);
  }
}

高阶组件

高阶函数的定义:至少满足以下条件之一

  • 接受一个或多个函数作为输入
  • 输出一个函数

JavaScript中常见的filter,map,reduce都是高阶函数

高阶组件的英文是Higher-Order Components,简称HOC

官方定义:高阶组件是参数为组件,返回值为新组件的函数

高阶组件本事不是一个组件,而是一个函数,这个函数的参数是一个组件,返回值也是一个组件

高阶组件的定义

高阶组件的调用过程类似于

const EnhanceComponent = higherOrderComponent(WrappedComponent);

高阶函数的编写过程类似于

function higherOrderComponent(WrapperComponenet){
  class NewComponent extends PureComponent {
    render(){
      return <WrapperComponenet/>
    }
  }
  newComponent.displayName = "Young"
  return NewComponent
}

组件名称问题:

  • 在ES6中,类表达式中类名是可以省略的
  • 组件的名称都可以通过displayName来修改

应用一:props增强

在不改动原有代码的情况下,添加新的props

function enhanceProps(WrappedCpn, otherProps) {
  return props => <WrapperCpn {...props}, {...otherProps}/>
}

利用高阶组件来共享Context

function withUser(WrappedCpn) {
  return props =>{
    return (
    	<UserContext.Consumer>
      	{
          value => {
          	return <WrapperCpn {...props} {...value}/>
        	}
        }
      </UserContext.Consumer>
    )
  }
}

案例:增强props

import React, { PureComponent } from 'react';

// 定义一个高阶组件
function enhanceRegionProps(WrappedComponent) {
  return props => {
    return <WrappedComponent {...props} region="中国"/>
  }
}

class Home extends PureComponent {
  render() {
    return <h2>Home: {`昵称: ${this.props.nickname} 等级: ${this.props.level} 区域: ${this.props.region}`}</h2>
  }
}


class About extends PureComponent {
  render() {
    return <h2>About: {`昵称: ${this.props.nickname} 等级: ${this.props.level} 区域: ${this.props.region}`}</h2>
  }
}


const EnhanceHome = enhanceRegionProps(Home);
const EnhanceAbout = enhanceRegionProps(About);

class App extends PureComponent {
  render() {
    return (
      <div>
        App
        <EnhanceHome nickname="coderwhy" level={90}/>
        <EnhanceAbout nickname="kobe" level={99}/>
      </div>
    )
  }
}

export default App;

案例:增强props默认

import React, { PureComponent, createContext } from 'react';

// 创建Context
const UserContext = createContext({
  nickname: "默认",
  level: -1,
  区域: "中国"
});
class Home extends PureComponent {
  render() {
    return (
      <UserContext.Consumer>
        {
          user => {
            return <h2>Home: {`昵称: ${user.nickname} 等级: ${user.level} 区域: ${user.region}`}</h2>
          } 
        }
      </UserContext.Consumer>
    )
  }
}

class About extends PureComponent {
  render() {
    return (
      <UserContext.Consumer>
        {
          user => {
            return <h2>About: {`昵称: ${user.nickname} 等级: ${user.level} 区域: ${user.region}`}</h2>
          } 
        }
      </UserContext.Consumer>
    )
  }
}

class App extends PureComponent {
  render() {
    return (
      <div>
        App
        <UserContext.Provider value={{nickname: "why", level: 90, region: "中国"}}>
          <Home/>
          <About/>
        </UserContext.Provider>
      </div>
    )
  }
}

export default App;

案例:增强props-改进

import React, { PureComponent, createContext } from 'react';

// 定义一个高阶组件
function withUser(WrappedComponent) {
  return props => {
    return (
      <UserContext.Consumer>
        {
          user => {
            return <WrappedComponent {...props} {...user}/>
          } 
        }
      </UserContext.Consumer>
    )
  }
}

// 创建Context
const UserContext = createContext({
  nickname: "默认",
  level: -1,
  区域: "中国"
});

class Home extends PureComponent {
  render() {
    return <h2>Home: {`昵称: ${this.props.nickname} 等级: ${this.props.level} 区域: ${this.props.region}`}</h2>
  }
}


class About extends PureComponent {
  render() {
    return <h2>About: {`昵称: ${this.props.nickname} 等级: ${this.props.level} 区域: ${this.props.region}`}</h2>
  }
}

class Detail extends PureComponent {
  render() {
    return (
      <ul>
        <li>{this.props.nickname}</li>
        <li>{this.props.level}</li>
        <li>{this.props.region}</li>
      </ul>
    )
  }
}


const UserHome = withUser(Home);
const UserAbout = withUser(About);
const UserDetail = withUser(Detail);

class App extends PureComponent {
  render() {
    return (
      <div>
        App
        <UserContext.Provider value={{nickname: "why", level: 90, region: "中国"}}>
          <UserHome/>
          <UserAbout/>
          <UserDetail/>
        </UserContext.Provider>
      </div>
    )
  }
}

export default App;

应用二:渲染的判断

在开发中,某些页面是必须用户登录成功才能进入的,如果用户没有登录,那么跳转至登录页面

import React, { PureComponent } from 'react';

class LoginPage extends PureComponent {
  render() {
    return <h2>LoginPage</h2>
  }
}

function withAuth(WrappedComponent) {
  const NewCpn = props => {
    const {isLogin} = props;
    if (isLogin) {
      return <WrappedComponent {...props}/>
    } else {
      return <LoginPage/>
    }
  }

  NewCpn.displayName = "AuthCpn"

  return NewCpn;
}

// 购物车组件
class CartPage extends PureComponent {
  render() {
    return <h2>CartPage</h2>
  }
}

const AuthCartPage = withAuth(CartPage);

export default class App extends PureComponent {
  render() {
    return (
      <div>
        <AuthCartPage isLogin={true}/>
      </div>
    )
  }
}

应用三:生命周期劫持

案例:生命周期劫持-默认

import React, { PureComponent } from 'react';

class Home extends PureComponent {

  // 即将渲染获取一个时间 beginTime
  UNSAFE_componentWillMount() {
    this.beginTime = Date.now();
  }

  // 渲染完成再获取一个时间 endTime
  componentDidMount() {
    this.endTime = Date.now();
    const interval = this.endTime - this.beginTime;
    console.log(`Home渲染时间: ${interval}`)
  }

  render() {
    return <h2>Home</h2>
  }
}


class About extends PureComponent {
  // 即将渲染获取一个时间 beginTime
  UNSAFE_componentWillMount() {
    this.beginTime = Date.now();
  }

  // 渲染完成再获取一个时间 endTime
  componentDidMount() {
    this.endTime = Date.now();
    const interval = this.endTime - this.beginTime;
    console.log(`About渲染时间: ${interval}`)
  }

  render() {
    return <h2>About</h2>
  }
}

export default class App extends PureComponent {
  render() {
    return (
      <div>
        <Home />
        <About />
      </div>
    )
  }
}

案例:生命周期劫持-改进

import React, { PureComponent } from 'react';

function withRenderTime(WrappedComponent) {
  return class extends PureComponent {
    // 即将渲染获取一个时间 beginTime
    UNSAFE_componentWillMount() {
      this.beginTime = Date.now();
    }

    // 渲染完成再获取一个时间 endTime
    componentDidMount() {
      this.endTime = Date.now();
      const interval = this.endTime - this.beginTime;
      console.log(`${WrappedComponent.name}渲染时间: ${interval}`)
    }

    render() {
      return <WrappedComponent {...this.props}/>
    }
  }
}

class Home extends PureComponent {
  render() {
    return <h2>Home</h2>
  }
}


class About extends PureComponent {
  render() {
    return <h2>About</h2>
  }
}

const TimeHome = withRenderTime(Home);
const TimeAbout = withRenderTime(About);

export default class App extends PureComponent {
  render() {
    return (
      <div>
        <TimeHome />
        <TimeAbout />
      </div>
    )
  }
}

class Person {

}

console.log(Person.name);

高阶函数的意义

利用高阶函数,可以针对某些React代码进行更优雅的处理

早期的React中有提供组件之间的一种复用方式-- mixin,目前已经不建议使用

  • mixin之间可能会项目依赖,互相耦合,不利于代码维护
  • 不同的mixin中的方法可能会互相冲突
  • mixin非常多的时候,组件是可以感知的,甚至还要为其做相关的处理,这样会增加代码的复杂性

HOC的缺陷:

  • HOC需要在原组件上进行包裹或者嵌套,如果大量使用HOC,将会产生非常多的嵌套,让调试变的困难
  • HOC可以劫持props,在不遵守约定的情况下也可能造成冲突

Hooks的出现,解决了很多React之前存在的问题,比如this的指向问题,比如hoc的嵌套复杂度等

ref的转发

之前在学习ref时说,ref不能应用与函数式组件,因为函数式组件没有实例,所以不能获取到对应的组件对象

但是在开发过程中,我们可能想要去获取函数式组件中某个元素的DOM,这个时候

  • 直接传入ref属性(错误做法)
  • 通过forwardRef高阶函数
const Home = forwardRef(function(props,ref)) {
  return (
    <div>
      <h2 ref={ref}>Home</h2>
      <button>按钮</button>
    </div>
  )
}
import React, { PureComponent, createRef, forwardRef } from 'react';

class Home extends PureComponent {
  render() {
    return <h2>Home</h2>
  }
}

// 高阶组件forwardRef
const Profile = forwardRef(function(props, ref) {
  return <p ref={ref}>Profile</p>
})

export default class App extends PureComponent {
  constructor(props) {
    super(props);

    this.titleRef = createRef();
    this.homeRef = createRef();
    this.profileRef = createRef();
  }

  render() {
    return (
      <div>
        <h2 ref={this.titleRef}>Hello World</h2>
        <Home ref={this.homeRef}/>

        <Profile ref={this.profileRef} name={"why"}/>

        <button onClick={e => this.printRef()}>打印ref</button>
      </div>
    )
  }

  printRef() {
    console.log(this.titleRef.current);
    console.log(this.homeRef.current);
    console.log(this.profileRef.current);
  }
}

Protals的使用

在某些情况下,我们希望渲染的内容独立于父组件,甚至是独立于当前挂载到的DOM元素中(默认都是挂载到id为root的DOM元素上)

Protals提供了一种将子节点渲染到存在于父组件意外的DOM节点的方案

  • 第一个参数(child)是任何可渲染的React子元素,例如一个元素,字符串或者fragment
  • 第二个参数(container)是一个DOM元素
ReactDOM.createPortal(child,container)

通常来讲,当你从组件的render方法返回一个元素时,该元素将被挂载到DOM节点中离其最近的父节点

然而,有时候将子元素插入到DOM节点中的不同位置也有好处

render() {
  // React 挂载了一个新的div,并且把子元素渲染其中
  return(
  	<div>
    	{this.props.children}
    </div>
  )
}
render(){
  // React 并没有创建一个新的div,它只是把子元素渲染到domNode中
  return ReactDOM.createPortal(
    this.props.children,
    domNode
  )
}

案例

创建一个div,并且id是modal,将组件选在到modal上

<div id="root"></div>

<div id="modal"></div>
import React, { PureComponent } from 'react';
import ReactDOM from 'react-dom';

class Modal extends PureComponent {
  render() {
    return ReactDOM.createPortal(
      this.props.children,
      document.getElementById("modal")
    )
  }
}

class Home extends PureComponent {
  render() {
    return (
      <div>
        <h2>Home</h2>
        <Modal>
          <h2>Title</h2>
        </Modal>
      </div>
    )
  }
}

export default class App extends PureComponent {
  render() {
    return (
      <div>
        <Home/>
      </div>
    )
  }
}
#modal {
  position: fixed;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
}

fragment

在之前的开发中,我们总是在一个组件中返回内容时包裹一个div元素

我们希望可以不渲染这样一个div时,可以使用Fragment,Fragment允许你将子列表分组,而无需向DOM添加额外节点

React还提供了Fragment的段语法,他看起来像空标签<></>,但是,如果我们需要在Fragment中添加key,那么就不能使用短语法

import React, { PureComponent, Fragment } from 'react';

export default class App extends PureComponent {
  constructor(props) {
    super(props);

    this.state = {
      counter: 0,
      friends: [
        {name: "why", age: 18},
        {name: "lilei", age: 20},
        {name: "kobe", age: 25},
      ]
    }
  }

  render() {
    return (
      <>
        <h2>当前计数: {this.state.counter}</h2>
        <button onClick={e => this.increment()}>+1</button>
        <div>
          {
            this.state.friends.map((item, index) => {
              return (
                <Fragment key={item.name}>
                  <div>{item.name}</div>
                  <p>{item.age}</p>
                  <hr/>
                </Fragment>
              )
            })
          }
        </div>
      </>
    )
  }

  increment() {
    this.setState({
      counter: this.state.counter + 1
    })
  }
}

StrictMode

StrictMode是一个用来突出显示应用程序中潜在问题的工具

与Fragment一样,StrictMode不会渲染任何可见的UI,它为其后代元素触发额外的检查和警告,严格模式下检查仅在开发模式下运行,不会影响生产构建

可以为程序的任何部门启用严格模式

  • 不会对Header和Footer组件运行严格模式检查
  • 但是ComponentOne和ComponentTwo以及他们所有的后代元素都将进行检查
<Header/>
<React.StrictMode>
	<div>
    <ComponentOne/>
    <ComponentTwo/>
  </div>
</React.StrictMode>
<Footer/>

严格模式检查的内容:

  1. 识别不安全的生命周期
  2. 使用过时的ref API
  3. 使用废弃的findDOMNode方法
    • 在之前的React API中,可以通过findDOMNode来获取DOM,现在已经不推荐使用了
  4. 检查意外的副作用
    • 这个组件的constructor会被调用两次
    • 这是严格模式下的故意行为,让你来查看这里写的一些逻辑在多次调用时,是否会产生一些副作用
    • 在生产环境中是不会被调用两次的
  5. 检测过时的context API
    • 早期的Context是通过static属性声明Context对象属性的,通过getChildContext返回Context对象的方式来使用Context,目前已不推荐使用
import React, { PureComponent, StrictMode } from 'react';

class Home extends PureComponent {
  constructor(props) {
    super(props);

    console.log("home constrcutor");
  }

  // UNSAFE_componentWillMount() {
  //   console.log("home componentWillMount");
  // }

  render() {
    return (
      <div ref="title">
        Home
      </div>
    )
  }
}

class Profile extends PureComponent {
  constructor(props) {
    super(props);

    console.log("profile constructor");
  }

  // UNSAFE_componentWillMount() {
  //   console.log("profile componentWillMount");
  // }

  render() {
    return (
      <div ref="title">
        Profile
      </div>
    )
  }
}

export default class App extends PureComponent {
  render() {
    return (
      <div>
        <StrictMode>
          <Home/>
        </StrictMode>
        <Profile/>
      </div>
    )
  }
}