react 复习

一.jsx

 1.定义虚拟DOM时,不要写引号
           2.标签中混入JS表达式时要用{}
           3.样式的类名指定不要用class,要用className
           4.内联样式,要用style={{key:value}}的形式去写
           5.标签必须只有一个跟标签
           6.标签必须闭合
           7.标签首字母
             (1).若小写字母开头,则将改标签转为html中同名元素,若html中无该标签对应得同名元素,则报错
             (2).若大写字母开头,react就去渲染对应得组件,若组件没有定义,则报错  

二.函数式组件

      //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,随后呈现在页面中 
       */

三.类式组件

      //1.创建类式组件
       class MyComponent extends React.Component{
           //render是放在哪里的?--MyComponent的原型对象上,供实例使用
           //render中的this是谁?--MyComponent的实例对象 <=>MyComponent组件实例对象
           render(){
               console.log('render中的this',this)
               return <div>我是用类定义的组件(使用于【复杂组件的定义】)</div>
           }
       }
       //2.渲染组件到页面
       ReactDOM.render(<MyComponent/>,document.getElementById('test'))
       /*
         执行了ReactDOM.render(<MyComponent/>......之后,发生了什么?)
           1.React解析组件标签,找到了MyComponent组件
           2.发现组件是使用类定义的,随后new出来该类的实例,并通过该实例调用到原型上的render方法
           3.将render返回的虚拟DOM转为真实DOM,随后呈现在页面中 
       */

四.组件实例三大属性state

       //1.创建类式组件
       class Weather extends React.Component{
           //初始化状态
           state = {isHot:false,wind:'微风'}

           render(){
               const {isHot,wind} = this.state
               return <div onClick={this.changeWeather}>今天天气很{this.state.isHot?'炎热':'凉爽'},{wind}</div>
           }
           
           //自定义方法----要用赋值语句的形式+箭头函数
           changeWeather = ()=>{
               const isHot = this.state.isHot
               this.setState({isHot:!isHot})
           }
       }
       //2.渲染组件到页面
       ReactDOM.render(<Weather/>,document.getElementById('test'))

五.组件实例三大属性props

1.类式组件

       //1.创建类式组件
       class Person extends React.Component{

        constructor(props){
            //构造器是否接收props,是否传递给super,取决于:是否希望在构造器中通过this访问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:'不男不女',
            age:18
        }
           render(){
               const {name,sex,age} = this.props
               //props是只读的
               this.props.name = 'jack' //此行代码会报错,因为props是只读的
               return (
                   <ul>
                    <li>姓名:{name}</li>
                    <li>性别:{sex}</li>
                    <li>年龄:{age}</li>
                   </ul>
               )
           }
        }
        
        //2.渲染组件到页面
       ReactDOM.render(<Person name="tom"/>,document.getElementById('test1'))

2.函数组件

        //创建组件
        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'))

六.组件实例三大属性refs

回调形式ref

        //创建组件
        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'))
/* React.createRef调用后可以返回一个容器,该容器可以存储被ref所标识的节点,该容器是“专人专用”的 */
            myRef = React.createRef()
            myRef2 = React.createRef()

七.事件处理

        //创建组件
        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,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>
                )
            }
        }

八.非受控组件和受控组件

在大多数情况下,我们推荐使用 受控组件 来处理表单数据。在一个受控组件中,表单数据是由 React 组件来管理的。另一种替代方案是使用非受控组件,这时表单数据将交由 DOM 节点来处理。

要编写一个非受控组件,而不是为每个状态更新都编写数据处理函数,你可以 使用 ref 来从 DOM 节点中获取表单数据。

1.非受控组件

        class Login extends React.Component{
            handleSubmit = (event)=>{
                event.preventDefault() //阻止表单提交
                const {username,password} = this
                alert(`你输入的用户名是:${username.value},你输入的密码是:${password.value}`)
            }
            render(){
                return(
                    <form onSubmit={this.handleSubmit}>
                        用户名:<input ref={c => this.username = c} type="text" name="username"/>
                        密码:<input ref={c => this.password = c} type="password" name="password"/>
                        <button>登录</button>
                    </form>
                )
            }
        }

2.受控组件

在 HTML 中,表单元素(如<input><textarea><select>)之类的表单元素通常自己维护 state,并根据用户输入进行更新。而在 React 中,可变状态(mutable state)通常保存在组件的 state 属性中,并且只能通过使用 setState()来更新。

我们可以把两者结合起来,使 React 的 state 成为“唯一数据源”。渲染表单的 React 组件还控制着用户输入过程中表单发生的操作。被 React 以这种方式控制取值的表单输入元素就叫做“受控组件”。

        class Login extends React.Component{

            //初始化状态
            state = {
                username:'', //用户名
                password:'' //密码
            }

            //保存用户名到状态中
            saveUsername = (event)=>{
                this.setState({username:event.target.value})
            }

            //保存密码到状态中
            savePassword = (event)=>{
                this.setState({password:event.target.value})
            }

            //表单提交的回调
            handleSubmit = (event)=>{
                event.preventDefault() //阻止表单提交
                const {username,password} = this.state
                alert(`你输入的用户名是:${username},你输入的密码是:${password}`)
            }

            render(){
                return(
                    <form onSubmit={this.handleSubmit}>
                        用户名:<input onChange={this.saveUsername} type="text" name="username"/>
                        密码:<input onChange={this.savePassword} type="password" name="password"/>
                        <button>登录</button>
                    </form>
                )
            }
        }

九.高阶函数和函数柯里化

高阶函数:如果一个函数符合下面2个规范中的任何一个,那该函数就是高阶函数。
                1.若A函数,接收的参数是一个函数,那么A就可以称之为高阶函数。
                2.若A函数,调用的返回值依然是一个函数,那么A就可以称之为高阶函数。
                常见的高阶函数有:Promise、setTimeout、arr.map()等等

函数的柯里化:通过函数调用继续返回函数的方式,实现多次接收参数最后统一处理的函数编码形式。 
    function sum(a){
        return(b)=>{
            return (c)=>{
                return a+b+c
            }
        }
    }

十.生命周期

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()  =====> 常用
        一般在这个钩子中做一些收尾的事,例如:关闭定时器、取消订阅消息
image.png

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()  =====> 常用
        一般在这个钩子中做一些收尾的事,例如:关闭定时器、取消订阅消息
image.png

十一.diff算法

经典面试题:
    1). react/vue中的key有什么作用?(key的内部原理是什么?)
    2). 为什么遍历列表时,key最好不要用index?
      
1. 虚拟DOM中key的作用:
    1). 简单的说: key是虚拟DOM对象的标识, 在更新显示时key起着极其重要的作用。

    2). 详细的说: 当状态中的数据发生变化时,react会根据【新数据】生成【新的虚拟DOM】, 
        随后React进行【新虚拟DOM】与【旧虚拟DOM】的diff比较,比较规则如下:

        a. 旧虚拟DOM中找到了与新虚拟DOM相同的key:
            (1).若虚拟DOM中内容没变, 直接使用之前的真实DOM
            (2).若虚拟DOM中内容变了, 则生成新的真实DOM,随后替换掉页面中之前的真实DOM

        b. 旧虚拟DOM中未找到与新虚拟DOM相同的key
            根据数据创建新的真实DOM,随后渲染到到页面
                        
2. 用index作为key可能会引发的问题:
    1. 若对数据进行:逆序添加、逆序删除等破坏顺序操作:
        会产生没有必要的真实DOM更新 ==> 界面效果没问题, 但效率低。

    2. 如果结构中还包含输入类的DOM:
        会产生错误DOM更新 ==> 界面有问题。
                    
    3. 注意!如果不存在对数据的逆序添加、逆序删除等破坏顺序操作,
        仅用于渲染列表用于展示,使用index作为key是没有问题的。
        
3. 开发中如何选择key?:
    1.最好使用每条数据的唯一标识作为key, 比如id、手机号、身份证号、学号等唯一值。
    2.如果确定只是简单的展示数据,用index也是可以的。

十二.配置代理

方法1

在package.json中追加如下配置

"proxy":"http://localhost:5000"

说明:

  1. 优点:配置简单,前端请求资源时可以不加任何前缀。
  2. 缺点:不能配置多个代理。
  3. 工作方式:上述方式配置代理,当请求了3000不存在的资源时,那么该请求会转发给5000 (优先匹配前端资源)

方法2

  1. 第一步:创建代理配置文件

    在src下创建配置文件:src/setupProxy.js
    
  2. 编写setupProxy.js配置具体代理规则:

    const proxy = require('http-proxy-middleware')
    
    module.exports = function(app) {
      app.use(
        proxy('/api1', {  //api1是需要转发的请求(所有带有/api1前缀的请求都会转发给5000)
          target: 'http://localhost:5000', //配置转发目标地址(能返回数据的服务器地址)
          changeOrigin: true, //控制服务器接收到的请求头中host字段的值
          /*
             changeOrigin设置为true时,服务器收到的请求头中的host为:localhost:5000
             changeOrigin设置为false时,服务器收到的请求头中的host为:localhost:3000
             changeOrigin默认值为false,但我们一般将changeOrigin值设为true
          */
          pathRewrite: {'^/api1': ''} //去除请求前缀,保证交给后台服务器的是正常请求地址(必须配置)
        }),
        proxy('/api2', { 
          target: 'http://localhost:5001',
          changeOrigin: true,
          pathRewrite: {'^/api2': ''}
        })
      )
    }
    

说明:

  1. 优点:可以配置多个代理,可以灵活的控制请求是否走代理。
  2. 缺点:配置繁琐,前端请求资源时必须加前缀。

十三.PubSub发布订阅者模式

import React, { Component } from 'react'
import PubSub from 'pubsub-js'
import './index.css'

export default class List extends Component {

    state = { //初始化状态
        users:[], //users初始值为数组
        isFirst:true, //是否为第一次打开页面
        isLoading:false,//标识是否处于加载中
        err:'',//存储请求相关的错误信息
    } 

    componentDidMount(){
        this.token = PubSub.subscribe('atguigu',(_,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 style={{color:'red'}}>{err}</h2> :
                    users.map((userObj)=>{
                        return (
                            <div key={userObj.id} className="card">
                                <a rel="noreferrer" href={userObj.html_url} target="_blank">
                                    <img alt="head_portrait" src={userObj.avatar_url} style={{width:'100px'}}/>
                                </a>
                                <p className="card-text">{userObj.login}</p>
                            </div>
                        )
                    })
                }
            </div>
        )
    }
}

十四.Fragments标签

React 中的一个模式是,一个组件返回多个元素。而Fragments允许你将子列表分组,而不必向DOM中新加一个节点。

为何使用Fragments

React 15 以前,render()的返回必须要有一个根节点,否则就会报错。
但是这样的后果就是多了一个毫无用处的标签,使代码难看。

class demo extends React.Component{
  render() {
    return(
      <div>Hello</div>
      <div>world</div>
       );
  }
}
//会报错,没有根节点
//要写成:
class demo extends React.Component{
  render() {
    return(
         <div>
         <div>Hello<div>
         <div>world<div>
      </div>
       );
  }
}
React 16 开始,render() 允许返回数组

然后使用Fragments的原因就在于这,

class Table extends React.Component {
  render() {
    return (
      <table>
        <tr>
          <Columns />//包含多个<td>标签,所以写这个组件时,我们需要一个
                  //根节点<div>
        </tr>
      </table>
    );
  }
}

class Columns extends React.Component {
  render() {
    return (
      <div>//当我们用一个根节点去嵌套时,就会生不成列表
        <td>Hello</td>
        <td>World</td>
      </div>
    );
  }
}

所以我们应该将 根节点的 节点换成<Fragments>:

class Columns extends React.Component {
  render() {
    return (
      <Fragment>
        <td>Hello</td>
        <td>World</td>
      </Fragment>
    );
  }
}
Fragments补充

1.短语法

class Columns extends React.Component {
  render() {
    return (
      <>//省略中间的内容
        <td>Hello</td>
        <td>World</td>
      </>
    );
  }
}

2.Key属性
key是目前版本唯一一个可以传递给Fragments的属性将一个集合映射到Fragments数组:

function Glossary(props) {
  return (
    <dl>
      {props.items.map(item => (
        // 没有`key`,React 会发出一个关键警告
        <Fragment key={item.id}>
          <dt>{item.term}</dt>
          <dd>{item.description}</dd>
        </Fragment>
      ))}
    </dl>
  );
}

十五.代码分割 React.lazy,Suspense,fallback

React.lazy 函数能让你像渲染常规组件一样处理动态引入(的组件)

const OtherComponent = React.lazy(() => import('./OtherComponent'));

此代码将会在组件首次渲染时,自动导入包含 OtherComponent 组件的包。

React.lazy 接受一个函数,这个函数需要动态调用 import()。它必须返回一个 Promise,该 Promise 需要 resolve 一个 defalut export 的 React 组件。

然后应在 Suspense 组件中渲染 lazy 组件,如此使得我们可以使用在等待加载 lazy 组件时做优雅降级(如 loading 指示器等)。

import React, { Suspense } from 'react';

const OtherComponent = React.lazy(() => import('./OtherComponent'));

function MyComponent() {
  return (
    <div>
      <Suspense fallback={<div>Loading...</div>}>
        <OtherComponent />
      </Suspense>
    </div>
  );
}

fallback 属性接受任何在组件加载过程中你想展示的 React 元素。你可以将 Suspense 组件置于懒加载组件之上的任何位置。你甚至可以用一个 Suspense 组件包裹多个懒加载组件。

import React, { Suspense } from 'react';

const OtherComponent = React.lazy(() => import('./OtherComponent'));
const AnotherComponent = React.lazy(() => import('./AnotherComponent'));

function MyComponent() {
  return (
    <div>
      <Suspense fallback={<div>Loading...</div>}>
        <section>
          <OtherComponent />
          <AnotherComponent />
        </section>
      </Suspense>
    </div>
  );
}
基于路由的代码分割

这里是一个例子,展示如何在你的应用中使用 React.lazyReact Router 这类的第三方库,来配置基于路由的代码分割。

import React, { Suspense, lazy } from 'react';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';

const Home = lazy(() => import('./routes/Home'));
const About = lazy(() => import('./routes/About'));

const App = () => (
  <Router>
    <Suspense fallback={<div>Loading...</div>}>
      <Switch>
        <Route exact path="/" component={Home}/>
        <Route path="/about" component={About}/>
      </Switch>
    </Suspense>
  </Router>
);
命名导出(Named Exports)

React.lazy 目前只支持默认导出(default exports)。如果你想被引入的模块使用命名导出(named exports),你可以创建一个中间模块,来重新导出为默认模块。这能保证 tree shaking 不会出错,并且不必引入不需要的组件。

// ManyComponents.js
export const MyComponent = /* ... */;
export const MyUnusedComponent = /* ... */;
// MyComponent.js
export { MyComponent as default } from "./ManyComponents.js";
// MyApp.js
import React, { lazy } from 'react';
const MyComponent = lazy(() => import("./MyComponent.js"));

十六 Context

使用步骤

1.创建Context(ValueContext可任意命名)

const ValueContext = React.createContext('') //默认值为''

2.用ValueContext.Provider包裹组件树中的根节点,并传递value值

<ValueContext.Provider value="hello hello">

3.此时,该组件树中的节点均能访问到步骤2中根节点所传递的value值
3.1 指定contextType读取当前的ValueContext

static contextType = ValueContext

3.2 读取value值

this.context

Context 提供了一个无需为每层组件手动添加 props,就能在组件树间进行数据传递的方法。解决多个组件都需要使用公共属性而需要props一层层向下传递数据的问题

// Context 可以让我们无须明确地传遍每一个组件,就能将值深入传递进组件树。
// 为当前的 theme 创建一个 context(“light”为默认值)。
const ThemeContext = React.createContext('light');
class App extends React.Component {
  render() {
    // 使用一个 Provider 来将当前的 theme 传递给以下的组件树。
    // 无论多深,任何组件都能读取这个值。
    // 在这个例子中,我们将 “dark” 作为当前的值传递下去。
    return (
      <ThemeContext.Provider value="dark">
        <Toolbar />
      </ThemeContext.Provider>
    );
  }
}

// 中间的组件再也不必指明往下传递 theme 了。
function Toolbar() {
  return (
    <div>
      <ThemedButton />
    </div>
  );
}

class ThemedButton extends React.Component {
  // 指定 contextType 读取当前的 theme context。
  // React 会往上找到最近的 theme Provider,然后使用它的值。
  // 在这个例子中,当前的 theme 值为 “dark”。
  static contextType = ThemeContext;
  render() {
    return <Button theme={this.context} />;
  }
}

如果你只是想避免层层传递一些属性,组件组合(component composition)有时候是一个比 context 更好的解决方案。

比如,考虑这样一个 Page 组件,它层层向下传递 user 和 avatarSize 属性,从而深度嵌套的 Link 和 Avatar 组件可以读取到这些属性:

一种无需 context 的解决方案是Avatar 组件自身传递下去,因而中间组件无需知道 user 或者 avatarSize 等 props:

function Page(props) {
  const user = props.user;
  const userLink = (
    <Link href={user.permalink}>
      <Avatar user={user} size={props.avatarSize} />
    </Link>
  );
  return <PageLayout userLink={userLink} />;
}

// 现在,我们有这样的组件:
<Page user={user} avatarSize={avatarSize} />
// ... 渲染出 ...
<PageLayout userLink={...} />
// ... 渲染出 ...
<NavigationBar userLink={...} />
// ... 渲染出 ...
{props.userLink}

这种变化下,只有最顶部的 Page 组件需要知道 Link 和 Avatar 组件是如何使用 user 和 avatarSize 的。

这种对组件的控制反转减少了在你的应用中要传递的 props 数量,这在很多场景下会使得你的代码更加干净,使你对根组件有更多的把控。但是,这并不适用于每一个场景:这种将逻辑提升到组件树的更高层次来处理,会使得这些高层组件变得更复杂,并且会强行将低层组件适应这样的形式,这可能不会是你想要的。

React.createContext
const MyContext = React.createContext(defaultValue);

创建一个 Context 对象。当 React 渲染一个订阅了这个 Context 对象的组件,这个组件会从组件树中离自身最近的那个匹配的 Provider 中读取到当前的 context 值。

只有当组件所处的树中没有匹配到 Provider 时,其 defaultValue 参数才会生效。这有助于在不使用 Provider 包装组件的情况下对组件进行测试。注意:将 undefined 传递给 Provider 的 value 时,消费组件的 defaultValue 不会生效。

Context.Provider
<MyContext.Provider value={/* 某个值 */}>

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

Provider 接收一个 value 属性,传递给消费组件。一个 Provider 可以和多个消费组件有对应关系。多个 Provider 也可以嵌套使用,里层的会覆盖外层的数据。

当 Provider 的 value 值发生变化时,它内部的所有消费组件都会重新渲染。Provider 及其内部 consumer 组件都不受制于 shouldComponentUpdate 函数,因此当 consumer 组件在其祖先组件退出更新的情况下也能更新。

通过新旧值检测来确定变化,使用了与 Object.is 相同的算法。

Class.contextType

挂载在 class 上的 contextType 属性会被重赋值为一个由 React.createContext() 创建的 Context 对象。这能让你使用 this.context 来消费最近 Context 上的那个值。你可以在任何生命周期中访问到它,包括 render 函数中。

class MyClass extends React.Component {
  static contextType = MyContext;
  render() {
    let value = this.context;
    /* 基于这个值进行渲染工作 */
  }
}
Context.Consumer

能让你在[函数式组件]中完成订阅 context

<MyContext.Consumer>
  {value => /* 基于 context 值进行渲染*/}
</MyContext.Consumer>
Context.displayName

context 对象接受一个名为 displayName 的 property,类型为字符串。React DevTools 使用该字符串来确定 context 要显示的内容。

示例,下述组件在 DevTools 中将显示为 MyDisplayName:

const MyContext = React.createContext(/* some value */);
MyContext.displayName = 'MyDisplayName';

<MyContext.Provider> // "MyDisplayName.Provider" 在 DevTools 中
<MyContext.Consumer> // "MyDisplayName.Consumer" 在 DevTools 中

十七 错误边界

错误边界是一种 React 组件,这种组件可以捕获并打印发生在其子组件树任何位置的 JavaScript 错误,并且,它会渲染出备用 UI,而不是渲染那些崩溃了的子组件树。解决了部分 UI 的 JavaScript 错误不应该导致整个应用崩溃

注意
错误边界无法捕获以下场景中产生的错误:
1.事件处理(了解更多
2.异步代码(例如 setTimeoutrequestAnimationFrame 回调函数)
3.服务端渲染
4.它自身抛出来的错误(并非它的子组件)

如果一个 class 组件中定义了 static getDerivedStateFromError()componentDidCatch() 这两个生命周期方法中的任意一个(或两个)时,那么它就变成一个错误边界。当抛出错误后,请使用 static getDerivedStateFromError() 渲染备用 UI ,使用 componentDidCatch() 打印错误信息。

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error) {
    // 更新 state 使下一次渲染能够显示降级后的 UI
    return { hasError: true };
  }

  componentDidCatch(error, errorInfo) {
    // 你同样可以将错误日志上报给服务器
    logErrorToMyService(error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      // 你可以自定义降级后的 UI 并渲染
      return <h1>Something went wrong.</h1>;
    }

    return this.props.children; 
  }
}

然后你可以将它作为一个常规组件去使用:

<ErrorBoundary>
  <MyWidget />
</ErrorBoundary>

只有 class 组件才可以成为错误边界组件。大多数情况下, 你只需要声明一次错误边界组件, 并在整个应用中使用它。

十八 Refs 转发

Ref 转发是一项将 ref 自动地通过组件传递到其一子组件的技巧。对于大多数应用中的组件来说,这通常不是必需的。但其对某些组件,尤其是可重用的组件库是很有用的

Ref 转发是一个可选特性,其允许某些组件接收 ref,并将其向下传递(换句话说,“转发”它)给子组件。

在下面的示例中,FancyButton 使用 React.forwardRef 来获取传递给它的 ref,然后转发到它渲染的 DOM button:

const FancyButton = React.forwardRef((props, ref) => (
  <button ref={ref} className="FancyButton">
    {props.children}
  </button>
));

// 你可以直接获取 DOM button 的 ref:
const ref = React.createRef();
<FancyButton ref={ref}>Click me!</FancyButton>;

这样,使用 FancyButton 的组件可以获取底层 DOM 节点 button 的 ref ,并在必要时访问,就像其直接使用 DOM button 一样。

注意
1.第二个参数 ref 只在使用 React.forwardRef 定义组件时存在。常规函数和 class 组件不接收 ref 参数,且 props 中也不存在 ref。
2.Ref 转发不仅限于 DOM 组件,你也可以转发 refs 到 class 组件实例中。

十九 高阶组件

高阶组件(HOC)是 React 中用于复用组件逻辑的一种高级技巧。高阶组件是参数为组件,返回值为新组件的函数

首先, 高阶组件本身不是一个组件,而是一个函数;
其次,这个函数的参数是一个组件,返回值也是一个组件;

// 定义一个高阶组件
// 1.高阶组件会接收一个组件作为参数
function hoc(Cpn) {
  class NewCpn extends PureComponent {
    render() {
      return <Cpn/>
    }
  }

  // 2.并且返回一个新的组件
  return NewCpn
}

// 创建一个组件作为参数
class HelloWorld extends PureComponent {
  render() {
    return <h2>Hello World</h2>
  }
}
// 调用高阶组件会返回一个新的组件
const HelloWorldHOC = hoc(HelloWorld)

export class App extends PureComponent {
  render() {
    return (
      <div>
        {/* 返回的新组件展示到App组件中 */}
        <HelloWorldHOC/>
      </div>
    )
  }
}

高阶组件并不是React API的一部分,它是基于React的 组合特性而形成的设计模式;

高阶组件应用场景

应用一: props增强

例如我们可以定义一个高阶组件, 对传入的组件进行props增强, 也就是为传入的组件添加一些参数, 需要注意的是, 如果传入的组件本身也有传递参数的话, 我们在为组件注入要增强的参数的同时, 还需要将本身传递的参数也注入进来, 实例代码如下:

// 定义一个高阶组件, 对传入的组件进行props增强
function enhancedProps(WrapperCpn) {
  class NewComponent extends PureComponent {
    constructor() {
      super()

      this.state = {
        userInfo: {
          name: "chenyq",
          age: 18
        }
      }
    }
    
    render() {
      // 如果组件本身也有传递参数, 也需要将参数添加进来
      return <WrapperCpn {...this.props} {...this.state.userInfo}/>
    }
  }

  return NewComponent
}

// 调用高阶组件, 对组件进行props增强
const Home = enhancedProps(function(props) {
  return <h2>{props.name}-{props.age}</h2>
})
const About = enhancedProps(function(props) {
  return <h2>{props.name}-{props.age}-{props.names}</h2>
})

export class App extends PureComponent {
  render() {
    return (
      <div>
        {/* 展示增强后的组件 */}
        <Home/>
        {/* 本身也有传递参数 */}
        <About names={["aaa", "bbb", "ccc"]}/>
      </div>
    )
  }
}

也可以利用高阶组件来共享Context

function withTheme(OriginComponment) {
  return (props) => {
    return (
      <ThemeContext.Consumer>
        {
          value => {
            return <OriginComponment {...value} {...props}/>
          }
        }
      </ThemeContext.Consumer>
    )
  }
}

应用二: 渲染判定鉴权
在开发中,我们可能遇到这样的场景:

某些页面是必须用户登录成功才能进行进入;
如果用户没有登录成功,那么直接跳转到登录页面;

这个时候,我们就可以使用高阶组件来完成鉴权操作:

// 定义一个高阶组件, 用于鉴权的操作
function loginAuth(WrapperCpn) {
  return props => {
    // 从本地存储中获取token, token有值表示已登录, 没有值表示未登录
    const token = localStorage.getItem("token")
    if(token) {
      return <WrapperCpn {...props}/>
    } else {
      return <h2>请先登录, 再跳转到对应的页面中</h2>
    }
  }
}

const Cart = loginAuth(function() {
  return <h2>购物车页面</h2>
})

export class App extends PureComponent {
  render() {
    return (
      <div>
        <Cart/>
      </div>
    )
  }
}

高阶组件的意义
我们会发现利用高阶组件可以针对某些React代码进行更加优雅的处理。

其实早期的React有提供组件之间的一种复用方式是mixin,目前已经不再建议使用:

Mixin 可能会相互依赖,相互耦合,不利于代码维护;
不同的Mixin中的方法可能会相互冲突;
Mixin非常多时,组件处理起来会比较麻烦,甚至还要为其做相关处理,这样会给代码造成滚雪球式的复杂性;

当然,HOC也有自己的一些缺陷:

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

Hooks的出现,是开创性的,它解决了很多React之前的存在的问题

比如this指向问题、比如hoc的嵌套复杂度问题等等;

二十 深入 JSX

实际上,JSX 仅仅只是 React.createElement(component, props, ...children) 函数的语法糖。如下 JSX 代码:

<MyButton color="blue" shadowSize={2}>
  Click Me
</MyButton>

会编译为:

React.createElement(
  MyButton,
  {color: 'blue', shadowSize: 2},
  'Click Me'
)

如果没有子节点,你还可以使用自闭合的标签形式,如:

<div className="sidebar" />

会编译为:

React.createElement(
  'div',
  {className: 'sidebar'}
)
在 JSX 类型中使用点语法

在 JSX 中,你也可以使用点语法来引用一个 React 组件。当你在一个模块中导出许多 React 组件时,这会非常方便。例如,如果 MyComponents.DatePicker 是一个组件,你可以在 JSX 中直接使用:

import React from 'react';

const MyComponents = {
  DatePicker: function DatePicker(props) {
    return <div>Imagine a {props.color} datepicker here.</div>;
  }
}

function BlueDatePicker() {
  return <MyComponents.DatePicker color="blue" />;
}
用户定义的组件必须以大写字母开头

例如,以下的代码将无法按照预期运行:

import React from 'react';

// 错误!组件应该以大写字母开头:
function hello(props) {
  // 正确!这种 <div> 的使用是合法的,因为 div 是一个有效的 HTML 标签
  return <div>Hello {props.toWhat}</div>;
}

function HelloWorld() {
  // 错误!React 会认为 <hello /> 是一个 HTML 标签,因为它没有以大写字母开头:
  return <hello toWhat="World" />;
}

要解决这个问题,我们需要重命名 hello 为 Hello,同时在 JSX 中使用 <Hello /> :

import React from 'react';

// 正确!组件需要以大写字母开头:
function Hello(props) {
  // 正确! 这种 <div> 的使用是合法的,因为 div 是一个有效的 HTML 标签:
  return <div>Hello {props.toWhat}</div>;
}

function HelloWorld() {
  // 正确!React 知道 <Hello /> 是一个组件,因为它是大写字母开头的:
  return <Hello toWhat="World" />;
}

JSX 中的 Props

你可以把包裹在 {} 中的 JavaScript 表达式作为一个 prop 传递给 JSX 元素。例如,如下的 JSX:

<MyComponent foo={1 + 2 + 3 + 4} />

二十一 性能优化

UI 更新需要昂贵的 DOM 操作,而 React 内部使用几种巧妙的技术以便最小化 DOM 操作次数。对于大部分应用而言,使用 React 时无需专门优化就已拥有高性能的用户界面。尽管如此,你仍然有办法来加速你的 React 应用。

单文件构建
<script src="https://unpkg.com/react@16/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script>
虚拟化长列表

如果你的应用渲染了长列表(上百甚至上千的数据),我们推荐使用“虚拟滚动”技术。这项技术会在有限的时间内仅渲染有限的内容,并奇迹般地降低重新渲染组件消耗的时间,以及创建 DOM 节点的数量。

react-windowreact-virtualized 是热门的虚拟滚动库。 它们提供了多种可复用的组件,用于展示列表、网格和表格数据。 如果你想要一些针对你的应用做定制优化,你也可以创建你自己的虚拟滚动组件,就像 Twitter 所做的

避免调停

React 构建并维护了一套内部的 UI 渲染描述。它包含了来自你的组件返回的 React 元素。该描述使得 React 避免创建 DOM 节点以及没有必要的节点访问,因为 DOM 操作相对于 JavaScript 对象操作更慢。虽然有时候它被称为“虚拟 DOM”,但是它在 React Native 中拥有相同的工作原理。

当一个组件的 props 或 state 变更,React 会将最新返回的元素与之前渲染的元素进行对比,以此决定是否有必要更新真实的 DOM。当它们不相同时,React 会更新该 DOM。

即使 React 只更新改变了的 DOM 节点,重新渲染仍然花费了一些时间。在大部分情况下它并不是问题,不过如果它已经慢到让人注意了,你可以通过覆盖生命周期方法 shouldComponentUpdate 来进行提速。该方法会在重新渲染前被触发。其默认实现总是返回 true,让 React 执行更新:

shouldComponentUpdate(nextProps, nextState) {
  return true;
}

如果你知道在什么情况下你的组件不需要更新,你可以在 shouldComponentUpdate 中返回 false 来跳过整个渲染过程。其包括该组件的 render 调用以及之后的操作。

在大部分情况下,你可以继承 React.PureComponent 以代替手写 shouldComponentUpdate()。它用当前与之前 props 和 state 的浅比较覆写了 shouldComponentUpdate() 的实现。

shouldComponentUpdate 的作用

shouldComponentUpdate(nextProps,nextState)

nextProps:表示下一个props
nextState:表示下一个state的值
shoudldComponentUpdate()的返回值,判断React组件的输出是否受到当前的state或props更改的影响,默认行为使state每次发生变化组件都会重新渲染

shouldComponentUpdate(nextProps, nextState){
    //组件是否需要更新,需要返回一个布尔值,返回true则更新,返回flase不更新,这是一个关键点
    console.log('shouldComponentUpdate组件是否应该更新,需要返回布尔值',nextProps, nextState)
    return true
}

示例
如果你的组件只有当 props.color 或者 state.count 的值改变才需要更新时,你可以使用 shouldComponentUpdate 来进行检查:

class CounterButton extends React.Component {
  constructor(props) {
    super(props);
    this.state = {count: 1};
  }

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

  render() {
    return (
      <button
        color={this.props.color}
        onClick={() => this.setState(state => ({count: state.count + 1}))}>
        Count: {this.state.count}
      </button>
    );
  }
}

在这段代码中,shouldComponentUpdate 仅检查了 props.color 或 state.count 是否改变。如果这些值没有改变,那么这个组件不会更新。如果你的组件更复杂一些,你可以使用类似“浅比较”的模式来检查 props 和 state 中所有的字段,以此来决定是否组件需要更新。React 已经提供了一位好帮手来帮你实现这种常见的模式 - 你只要继承 React.PureComponent 就行了。所以这段代码可以改成以下这种更简洁的形式:

class CounterButton extends React.PureComponent {
  constructor(props) {
    super(props);
    this.state = {count: 1};
  }

  render() {
    return (
      <button
        color={this.props.color}
        onClick={() => this.setState(state => ({count: state.count + 1}))}>
        Count: {this.state.count}
      </button>
    );
  }
}

大部分情况下,你可以使用 React.PureComponent 来代替手写 shouldComponentUpdate。但它只进行浅比较,所以当 props 或者 state 某种程度是可变的话,浅比较会有遗漏,那你就不能使用它了。当数据结构很复杂时,情况会变得麻烦。例如,你想要一个 ListOfWords 组件来渲染一组用逗号分开的单词。它有一个叫做 WordAdder 的父组件,该组件允许你点击一个按钮来添加一个单词到列表中。以下代码并不正确:

class ListOfWords extends React.PureComponent {
  render() {
    return <div>{this.props.words.join(',')}</div>;
  }
}

class WordAdder extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      words: ['marklar']
    };
    this.handleClick = this.handleClick.bind(this);
  }

  handleClick() {
    // 这部分代码很糟,而且还有 bug
    const words = this.state.words;
    words.push('marklar');
    this.setState({words: words});
  }

  render() {
    return (
      <div>
        <button onClick={this.handleClick} />
        <ListOfWords words={this.state.words} />
      </div>
    );
  }
}

问题在于 PureComponent 仅仅会对新老 this.props.words 的值进行简单的对比。由于代码中 WordAdder 的 handleClick 方法改变了同一个 words 数组,使得新老 this.props.words 比较的其实还是同一个数组。即便实际上数组中的单词已经变了,但是比较结果是相同的。可以看到,即便多了新的单词需要被渲染, ListOfWords 却并没有被更新。

二十二 PureComponent 纯组件

PureComponent 纯组件和Component组件的区别

PureComponent 底层实现了shouldComponentUpdate,不需要我们手写shouldComponentUpdate做性能优化。
PureComponent自带通过props和state的浅对比来实现 shouldComponentUpate(),而Component没有

PureComponent组件的缺点是:

可能会因深层的数据不一致而产生错误的否定判断,从而shouldComponentUpdate结果返回false,界面得不到更新。要想避免这个坑,就需要结合immutable来管理数据。

// extends  PureComponent 
class Home extends PureComponent {
    render(){}
}

二十三 Portals

Portal 提供了一种将子节点渲染到存在于父组件以外的 DOM 节点的优秀的方案、

ReactDOM.createPortal(child, container)

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

用法

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

render() {
  // React 挂载了一个新的 div,并且把子元素渲染其中
  return (
    <div>
      {this.props.children}
    </div>
  );
}

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

render() {
  // React 并*没有*创建一个新的 div。它只是把子元素渲染到 `domNode` 中。
  // `domNode` 是一个可以在任何位置的有效 DOM 节点。
  return ReactDOM.createPortal(
    this.props.children,
    domNode
  );
}

一个 portal 的典型用例是当父组件有 overflow: hidden 或 z-index 样式时,但你需要子组件能够在视觉上“跳出”其容器。例如,对话框、悬浮卡以及提示框:

const modalRoot = document.getElementById('modal-root');

class Modal extends React.Component {
  constructor(props) {
    super(props);
    this.el = document.createElement('div');
  }

  componentDidMount() {
    modalRoot.appendChild(this.el);
  }

  componentWillUnmount() {
    modalRoot.removeChild(this.el);
  }

  render() {
    return ReactDOM.createPortal(
      this.props.children,
      this.el,
    );
  }
}

二十四 Profiler API

Profiler 测量渲染一个 React 应用多久渲染一次以及渲染一次的“代价”。 它的目的是识别出应用中渲染较慢的部分,或是可以使用类似 memoization 优化的部分,并从相关优化中获益。

用法

Profiler 能添加在 React 树中的任何地方来测量树中这部分渲染所带来的开销。 它需要两个 prop :一个是 id(string),一个是当组件树中的组件“提交”更新的时候被React调用的回调函数 onRender(function)。

例如,为了分析 Navigation 组件和它的子代

render(
  <App>
    <Profiler id="Navigation" onRender={callback}>
      <Navigation {...props} />
    </Profiler>
    <Main {...props} />
  </App>
);

嵌套使用 Profiler 组件来测量相同一个子树下的不同组件。

render(
  <App>
    <Profiler id="Panel" onRender={callback}>
      <Panel {...props}>
        <Profiler id="Content" onRender={callback}>
          <Content {...props} />
        </Profiler>
        <Profiler id="PreviewPane" onRender={callback}>
          <PreviewPane {...props} />
        </Profiler>
      </Panel>
    </Profiler>
  </App>
);

注意
尽管 Profiler 是一个轻量级组件,我们依然应该在需要时才去使用它。对一个应用来说,每添加一些都会给 CPU 和内存带来一些负担。

onRender 回调

Profiler 需要一个 onRender 函数作为参数。 React 会在 profile 包含的组件树中任何组件 “提交” 一个更新的时候调用这个函数。 它的参数描述了渲染了什么和花费了多久。

function onRenderCallback(
  id, // 发生提交的 Profiler 树的 “id”
  phase, // "mount" (如果组件树刚加载) 或者 "update" (如果它重渲染了)之一
  actualDuration, // 本次更新 committed 花费的渲染时间
  baseDuration, // 估计不使用 memoization 的情况下渲染整颗子树需要的时间
  startTime, // 本次更新中 React 开始渲染的时间
  commitTime, // 本次更新中 React committed 的时间
  interactions // 属于本次更新的 interactions 的集合
) {
  // 合计或记录渲染时间。。。
}

二十五 使用 PropTypes 进行类型检查

随着你的应用程序不断增长,你可以通过类型检查捕获大量错误。对于某些应用程序来说,你可以使用 FlowTypeScript 等 JavaScript 扩展来对整个应用程序做类型检查。但即使你不使用这些扩展,React 也内置了一些类型检查的功能。要在组件的 props 上进行类型检查,你只需配置特定的 propTypes 属性:

import PropTypes from 'prop-types';

class Greeting extends React.Component {
  render() {
    return (
      <h1>Hello, {this.props.name}</h1>
    );
  }
}

Greeting.propTypes = {
  name: PropTypes.string
};

PropTypes 提供一系列验证器,可用于确保组件接收到的数据类型是有效的。在本例中, 我们使用了 PropTypes.string。当传入的 prop 值类型不正确时,JavaScript 控制台将会显示警告。出于性能方面的考虑,propTypes 仅在开发模式下进行检查。

PropTypes
import PropTypes from 'prop-types';

MyComponent.propTypes = {
  // 你可以将属性声明为 JS 原生类型,默认情况下
  // 这些属性都是可选的。
  optionalArray: PropTypes.array,
  optionalBool: PropTypes.bool,
  optionalFunc: PropTypes.func,
  optionalNumber: PropTypes.number,
  optionalObject: PropTypes.object,
  optionalString: PropTypes.string,
  optionalSymbol: PropTypes.symbol,

  // 任何可被渲染的元素(包括数字、字符串、元素或数组)
  // (或 Fragment) 也包含这些类型。
  optionalNode: PropTypes.node,

  // 一个 React 元素。
  optionalElement: PropTypes.element,

  // 一个 React 元素类型(即,MyComponent)。
  optionalElementType: PropTypes.elementType,

  // 你也可以声明 prop 为类的实例,这里使用
  // JS 的 instanceof 操作符。
  optionalMessage: PropTypes.instanceOf(Message),

  // 你可以让你的 prop 只能是特定的值,指定它为
  // 枚举类型。
  optionalEnum: PropTypes.oneOf(['News', 'Photos']),

  // 一个对象可以是几种类型中的任意一个类型
  optionalUnion: PropTypes.oneOfType([
    PropTypes.string,
    PropTypes.number,
    PropTypes.instanceOf(Message)
  ]),

  // 可以指定一个数组由某一类型的元素组成
  optionalArrayOf: PropTypes.arrayOf(PropTypes.number),

  // 可以指定一个对象由某一类型的值组成
  optionalObjectOf: PropTypes.objectOf(PropTypes.number),

  // 可以指定一个对象由特定的类型值组成
  optionalObjectWithShape: PropTypes.shape({
    color: PropTypes.string,
    fontSize: PropTypes.number
  }),
  
  // An object with warnings on extra properties
  optionalObjectWithStrictShape: PropTypes.exact({
    name: PropTypes.string,
    quantity: PropTypes.number
  }),   

  // 你可以在任何 PropTypes 属性后面加上 `isRequired` ,确保
  // 这个 prop 没有被提供时,会打印警告信息。
  requiredFunc: PropTypes.func.isRequired,

  // 任意类型的数据
  requiredAny: PropTypes.any.isRequired,

  // 你可以指定一个自定义验证器。它在验证失败时应返回一个 Error 对象。
  // 请不要使用 `console.warn` 或抛出异常,因为这在 `onOfType` 中不会起作用。
  customProp: function(props, propName, componentName) {
    if (!/matchme/.test(props[propName])) {
      return new Error(
        'Invalid prop `' + propName + '` supplied to' +
        ' `' + componentName + '`. Validation failed.'
      );
    }
  },

  // 你也可以提供一个自定义的 `arrayOf` 或 `objectOf` 验证器。
  // 它应该在验证失败时返回一个 Error 对象。
  // 验证器将验证数组或对象中的每个值。验证器的前两个参数
  // 第一个是数组或对象本身
  // 第二个是他们当前的键。
  customArrayProp: PropTypes.arrayOf(function(propValue, key, componentName, location, propFullName) {
    if (!/matchme/.test(propValue[key])) {
      return new Error(
        'Invalid prop `' + propFullName + '` supplied to' +
        ' `' + componentName + '`. Validation failed.'
      );
    }
  })
};
默认 Prop 值
class Greeting extends React.Component {
  render() {
    return (
      <h1>Hello, {this.props.name}</h1>
    );
  }
}

// 指定 props 的默认值:
Greeting.defaultProps = {
  name: 'Stranger'
};

// 渲染出 "Hello, Stranger":
ReactDOM.render(
  <Greeting />,
  document.getElementById('example')
);

defaultProps 用于确保 this.props.name 在父组件没有指定其值时,有一个默认值。propTypes 类型检查发生在 defaultProps 赋值后,所以类型检查也适用于 defaultProps。

class Greeting extends React.Component {
  static defaultProps = {
    name: 'stranger'
  }

  render() {
    return (
      <div>Hello, {this.props.name}</div>
    )
  }
}

二十六 Hook 概述

Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。

Effect Hook 副作用函数

useEffect 就是一个 Effect Hook,给函数组件增加了操作副作用的能力。它跟 class 组件中的 componentDidMountcomponentDidUpdatecomponentWillUnmount 具有相同的用途,只不过被合并成了一个 API。(我们会在使用 Effect Hook 里展示对比 useEffect 和这些方法的例子。)

例如,下面这个组件在 React 更新 DOM 后会设置一个页面标题:

import React, { useState, useEffect } from 'react';

function Example() {
  const [count, setCount] = useState(0);

  // 相当于 componentDidMount 和 componentDidUpdate:
  useEffect(() => {
    // 使用浏览器的 API 更新页面标题
    document.title = `You clicked ${count} times`;
  });

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

当你调用 useEffect 时,就是在告诉 React 在完成对 DOM 的更改后运行你的“副作用”函数。由于副作用函数是在组件内声明的,所以它们可以访问到组件的 props 和 state。默认情况下,React 会在每次渲染后调用副作用函数 —— 包括第一次渲染的时候。(我们会在使用 Effect Hook 中跟 class 组件的生命周期方法做更详细的对比。)

副作用函数还可以通过返回一个函数来指定如何“清除”副作用。例如,在下面的组件中使用副作用函数来订阅好友的在线状态,并通过取消订阅来进行清除操作:

跟 useState 一样,你可以在组件中多次使用 useEffect :

function FriendStatusWithCounter(props) {
  const [count, setCount] = useState(0);
  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });

  const [isOnline, setIsOnline] = useState(null);
  useEffect(() => {
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });

  function handleStatusChange(status) {
    setIsOnline(status.isOnline);
  }
  // ...

通过使用 Hook,你可以把组件内相关的副作用组织在一起(例如创建订阅及取消订阅),而不要把它们拆分到不同的生命周期函数里。

Hook 使用规则

Hook 就是 JavaScript 函数,但是使用它们会有两个额外的规则:

  • 只能在函数最外层调用 Hook。不要在循环、条件判断或者子函数中调用。
  • 只能在 React 的函数组件中调用 Hook。不要在其他 JavaScript 函数中调用。(还有一个地方可以调用 Hook —— 就是自定义的 Hook 中,我们稍后会学习到。)

同时,我们提供了 linter 插件来自动执行这些规则。这些规则乍看起来会有一些限制和令人困惑,但是要让 Hook 正常工作,它们至关重要。

自定义 Hook

有时候我们会想要在组件之间重用一些状态逻辑。目前为止,有两种主流方案来解决这个问题:高阶组件render props。自定义 Hook 可以让你在不增加组件的情况下达到同样的目的。

前面,我们介绍了一个叫 FriendStatus 的组件,它通过调用 useStateuseEffect 的 Hook 来订阅一个好友的在线状态。假设我们想在另一个组件里重用这个订阅逻辑。

首先,我们把这个逻辑抽取到一个叫做 useFriendStatus 的自定义 Hook 里:

import React, { useState, useEffect } from 'react';

function useFriendStatus(friendID) {
  const [isOnline, setIsOnline] = useState(null);

  function handleStatusChange(status) {
    setIsOnline(status.isOnline);
  }

  useEffect(() => {
    ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
    };
  });

  return isOnline;
}

它将 friendID 作为参数,并返回该好友是否在线:
现在我们可以在两个组件中使用它:

function FriendStatus(props) {
  const isOnline = useFriendStatus(props.friend.id);

  if (isOnline === null) {
    return 'Loading...';
  }
  return isOnline ? 'Online' : 'Offline';
}
function FriendListItem(props) {
  const isOnline = useFriendStatus(props.friend.id);

  return (
    <li style={{ color: isOnline ? 'green' : 'black' }}>
      {props.friend.name}
    </li>
  );
}

这两个组件的 state 是完全独立的。Hook 是一种复用状态逻辑的方式,它不复用 state 本身。事实上 Hook 的每次调用都有一个完全独立的 state —— 因此你可以在单个组件中多次调用同一个自定义 Hook。

自定义 Hook 更像是一种约定而不是功能。如果函数的名字以 “use” 开头并调用其他 Hook,我们就说这是一个自定义 Hook。 useSomething 的命名约定可以让我们的 linter 插件在使用 Hook 的代码中找到 bug。

其他 Hook

除此之外,还有一些使用频率较低的但是很有用的 Hook。比如,useContext 让你不使用组件嵌套就可以订阅 React 的 Context。

function Example() {
  const locale = useContext(LocaleContext);
  const theme = useContext(ThemeContext);
  // ...
}

另外 useReducer 可以让你通过 reducer 来管理组件本地的复杂 state。

function Todos() {
  const [todos, dispatch] = useReducer(todosReducer);
  // ...

二十七 使用 State Hook

import React, { useState } from 'react';

function Example() {
  // 声明一个叫 "count" 的 state 变量
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

我们将通过将这段代码与一个等价的 class 示例进行比较来开始学习 Hook。

等价的 class 示例
class Example extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }

  render() {
    return (
      <div>
        <p>You clicked {this.state.count} times</p>
        <button onClick={() => this.setState({ count: this.state.count + 1 })}>
          Click me
        </button>
      </div>
    );
  }
}

state 初始值为 { count: 0 } ,当用户点击按钮后,我们通过调用 this.setState() 来增加 state.count。整个章节中都将使用该 class 的代码片段做示例。

二十八 使用 Effect Hook

Effect Hook 可以让你在函数组件中执行副作用操作

import React, { useState, useEffect } from 'react';

function Example() {
  const [count, setCount] = useState(0);

  // Similar to componentDidMount and componentDidUpdate:
  useEffect(() => {
    // Update the document title using the browser API
    document.title = `You clicked ${count} times`;
  });

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

提示
如果你熟悉 React class 的生命周期函数,你可以把 useEffect Hook 看做 componentDidMount,componentDidUpdate 和 componentWillUnmount 这三个函数的组合。

需要清除的 effect 使用 Hook 的示例

你可能认为需要单独的 effect 来执行清除操作。但由于添加和删除订阅的代码的紧密性,所以 useEffect 的设计是在同一个地方执行。如果你的 effect 返回一个函数,React 将会在执行清除操作时调用它:

 useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }

    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });

为什么要在 effect 中返回一个函数? 这是 effect 可选的清除机制。每个 effect 都可以返回一个清除函数。如此可以将添加和移除订阅的逻辑放在一起。它们都属于 effect 的一部分。

React 何时清除 effect? React 会在组件卸载的时候执行清除操作。正如之前学到的,effect 在每次渲染的时候都会执行。这就是为什么 React 在执行当前 effect 之前对上一个 effect 进行清除。我们稍后将讨论为什么这将助于避免 bug以及如何在遇到性能问题时跳过此行为

二十九 useReducer

const [state, dispatch] = useReducer(reducer, initialArg, init);

useState 的替代方案。它接收一个形如 (state, action) => newState 的 reducer,并返回当前的 state 以及与其配套的 dispatch 方法。(如果你熟悉 Redux 的话,就已经知道它如何工作了。)

在某些场景下,useReducer 会比 useState 更适用,例如 state 逻辑较复杂且包含多个子值,或者下一个 state 依赖于之前的 state 等。并且,使用 useReducer 还能给那些会触发深更新的组件做性能优化,因为你可以向子组件传递 dispatch 而不是回调函数

以下是用 reducer 重写 useState 一节的计数器示例:

const initialState = {count: 0};

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return {count: state.count + 1};
    case 'decrement':
      return {count: state.count - 1};
    default:
      throw new Error();
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({type: 'decrement'})}>-</button>
      <button onClick={() => dispatch({type: 'increment'})}>+</button>
    </>
  );
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容