React基础


一、React介绍

  1. 英文官网: https://reactjs.org/

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

1、简介

用于动态构建用户界面的 JavaScript 库(只关注于视图)

  1. 发送请求获取数据
  2. 处理数据(过滤,整理格式等)
  3. 操作DOM呈现页面(React做的事情)

React是一个将数据渲染为HTML视图的开源JavaScript库

原生JavaScript的缺点

原生JavaScript操作DOM繁琐,效率低(DOM-API操作UI)
使用JavaScript直接操作DOM,浏览器会进行大量的重绘重排
原生JavaScript没有组件化编码方案,代码复用率很低

【补充】浏览器重绘重排

浏览器重绘(repaint)重排(reflow)与优化[浏览器机制]

重绘(repaint):当一个元素的外观发生改变,但没有改变布局,重新把元素外观绘制出来的过程,叫做重绘

重排(reflow):当DOM的变化影响了元素的几何信息(DOM对象的位置和尺寸大小),浏览器需要重新计算元素的几何属性,将其安放在界面中的正确位置,这个过程叫做重排

React的特点

采用组件化模式、声明式编码,提高开发效率及组件复用率
在 React Native中可以使用React语法进行移动端开发
使用虚拟DOM+Diff算法,尽量减少与真实DOM的交互

React高效的原因

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

2、相关库

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

浏览器不能直接解析JSX代码, 需要babel转译为纯JS的代码才能运行

只要用了JSX,都要加上type="text/babel", 声明需要babel来处理

二、虚拟DOM

1、渲染虚拟DOM

  1. 语法: ReactDOM.render(virtualDOM, containerDOM)
  2. 作用: 将虚拟DOM元素渲染到页面中的真实容器DOM中显示
  3. 参数说明
  • 参数一: 纯js或jsx创建的虚拟dom对象

  • 参数二: 用来包含虚拟DOM元素的真实dom元素对象(一般是一个div)

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <title>hello_react</title>
</head>

<body>
  <!-- 准备好一个“容器” -->
  <div id="test"></div>

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

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

</html>

渲染结果

<div id="test">
    <h1>Hello,React</h1>
</div>

2、创建虚拟DOM

(1)JSX方式

JSX方式就是js创建虚拟DOM的语法糖

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

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

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

(2)纯 js 方式

不推荐用该方式创建虚拟DOM

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

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

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

3、虚拟与真实DOM

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

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

通过控制台打印可观察到虚拟DOM是JS的Object类型,而真实DOM打印的结果则是html标签

通过debugger可观察到真实DOM的属性参数比虚拟DOM多了好多。

结论

  1. 虚拟DOM本质是Object类型的对象(一般对象)
  2. 虚拟DOM比较“轻”,真实DOM比较“重”,因为虚拟DOM是React内部在用,无需真实DOM上那么多的属性
  3. 虚拟DOM最终会被React转化为真实DOM,呈现在页面上

三、JSX

1、概述

  1. 全称: JavaScript XML

  2. React定义的一种类似于XML的JS扩展语法: JS + XML本质是React.createElement(component, props, …children)方法的语法糖

  3. 作用: 用来简化创建虚拟DOM

    1. 写法:var ele = <h1>Hello JSX!</h1>
    2. 注意1:它不是字符串, 也不是HTML/XML标签
    3. 注意2:它最终产生的就是一个JS对象
  4. 标签名任意: HTML标签或其它标签

  5. 标签属性任意: HTML标签属性或其它

2、基本语法规则

  • 定义虚拟DOM时,不要写引号。
  • 标签中混入JS表达式时要用 { }。只能是表达式,不能是语句
  • 样式的类名指定不要用 class,要用 className。(因为class是ES6中类的关键字,所以不让用)
    内联样式,要用 style={{ key:value }} 的形式去写。
  • 只有一个根标签,如果需要多个,可用一个div包裹
  • 标签必须闭合
  • 标签首字母
    • (1). 若小写字母开头,则将该标签转为html中同名元素,若html中无该标签对应的同名元素,则报错。
    • (2). 若大写字母开头,React就去渲染对应的组件,若组件没有定义,则报错。

【补充】 区分js表达式与js语句

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

下面这些都是表达式:

(1) a
(2) a+b
(3) demo(1) // 函数调用表达式
(4) arr.map()
(5) function test () {}

下面这些都是语句(代码):

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

演示代码

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>jsx语法规则</title>
    <style>
        .title{
            background-color: orange;
            width: 200px;
        }
    </style>
</head>
<body>
    <!-- 准备好一个“容器” -->
    <div id="test"></div>

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

    <script type="text/babel" >
        const myId = 'aTgUiGu'
        const myData = 'HeLlo,rEaCt'

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

3、html转jsx

  • 需要把 class 替换为 className
  • 修改style:style={{color:'white',fontSize:'29px'}}

四、组件

1、定义组件方式

(1)函数式组件

// 1. 创建函数式组件
function Welcome(props) {
  console.log(this); //此处的this是undefined,因为babel编译后开启了严格模式
  return <h1>Hello, {props.name}</h1>;
}

const element = <Welcome name="Sara" />;
// 2. 渲染组件到页面
ReactDOM.render(
  element,
  document.getElementById('root')
);

这个例子中发生了什么:

  1. 我们调用 ReactDOM.render() 函数,并传入 <Welcome name="Sara" /> 作为参数。
  2. React 调用 Welcome 组件,并将 {name: 'Sara'} 作为 props 传入。
  3. Welcome 组件将 <h1>Hello, Sara</h1> 元素作为返回值。
  4. React DOM 将 DOM 高效地更新为 <h1>Hello, Sara</h1>

(2)类式组件

通过以下五步将函数组件转成 class 组件:

  1. 创建一个的 ES6 class,并且继承于 React.Component
  2. 添加一个空的 render() 方法。
  3. 将函数体移动到 render() 方法之中。
  4. render() 方法中使用 this.props 替换 props
  5. 删除剩余的空函数声明。
//1.创建类式组件
class MyComponent extends React.Component {
    render(){
        //render是放在哪里的?—— MyComponent的原型对象上,供实例使用。
        //render中的this是谁?—— MyComponent的实例对象 <=> MyComponent组件实例对象。
        console.log('render中的this:',this);
        return <h2>我是用类定义的组件(适用于【复杂组件】的定义)</h2>
    }
}
//2.渲染组件到页面
ReactDOM.render(<MyComponent/>,document.getElementById('test'))

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

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

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

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

每次组件更新时 render 方法都会被调用,但只要在相同的 DOM 节点中渲染 <MyComponent/> ,就仅有一个 MyComponent 组件的 class 实例被创建使用。这就使得我们可以使用如 state 或生命周期方法等很多其他特性。

2、类式组件三大属性

组件实例指的是以类的方式创建的组件

(1)state

  1. state是组件对象最重要的属性, 值是对象(可以包含多个key-value的组合)
  2. 组件被称为”状态机”, 通过更新组件的state来更新对应的页面显示(重新渲染组件)
  3. 状态(state)不可直接更改,必须通过setState进行更新,且更新是一种合并,不是替换。
  4. 通过setState进行更新时,是默认调用rander
/* 
    需求: 定义一个展示天气信息的组件

    根据isHost属性的值展示炎热或凉爽
*/

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

类中方法this指向

类中定义的方法,在内部默认开启了局部的严格模式
开启严格模式,函数如果直接调用,this不会指向window,而是undefined

/* 
    需求: 定义一个展示天气信息的组件
    点击可切换炎热和凉爽
*/

//1.创建组件
class Weather extends React.Component{

    //构造器调用几次? ———— 1次
    constructor(props){
        console.log('constructor');
        super(props)
        //初始化状态
        this.state = {isHot:false,wind:'微风'}
        //解决changeWeather中this指向为undefined问题
        //第一个this.changeWeather对应render中调用的this.changeWeather
        //第二个this.changeWeather对应Weather类中定义的changeWeather方法
        this.changeWeather = this.changeWeather.bind(this)
    }

    //render调用几次? ———— 1+n次 1是初始化的那次 n是状态更新的次数(this.setState次数)
    render(){
        console.log('render');
        //读取状态
        const {isHot,wind} = this.state
        //注意:此处this.changeWeather方法不可加(),
        // 加了小括号的含义是获得立即执行该方法,获取返回值,会在组件初始化就调用
        // 不加小括号的含义是获得该方法,作为事件的回调函数,不会自动调用
        return <h1 onClick={this.changeWeather}>今天天气很{isHot ? '炎热' : '凉爽'}{wind}</h1>
    }

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

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

        //严重注意:状态(state)不可直接更改,下面这行就是直接更改!!!
        //this.state.isHot = !isHot //这是错误的写法
    }
}
//2.渲染组件到页面
ReactDOM.render(<Weather/>,document.getElementById('test'))

简写模式(重点)

  1. 可以不写构造器,类中直接写赋值语句来初始化状态
  2. 不用bind来绑定this(赋值语句的形式+箭头函数)
//1.创建组件
class Weather extends React.Component{
    //初始化状态
    state = {isHot:false,wind:'微风'}

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

//自定义方法————要用赋值语句的形式+箭头函数
//箭头函数的this是外层的this
changeWeather = ()=>{
    const isHot = this.state.isHot
    this.setState({isHot:!isHot})
}
}
//2.渲染组件到页面
ReactDOM.render(<Weather/>,document.getElementById('test'))

注意点

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

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

    ​ a) 强制绑定this: 通过函数对象的bind()

    ​ b) 箭头函数 + 赋值语句

  • 状态数据state,不能直接修改或更新

  • 状态必须通过setState()进行更新, 且更新是一种合并,不是替换。

(2)props

概念

理解:

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

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

作用:

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

  2. 注意: 组件内部不要修改props数据

基本使用

//创建组件
class Person extends React.Component{
    render(){
        // 结构表达式,将this.props中的同名属性赋值到name,age,sex三个属性中,便于调用
        const {name,age,sex} = this.props
        return (
            <ul>
                <li>姓名:{name}</li>
                <li>性别:{sex}</li>
                <li>年龄:{age+1}</li>
            </ul>
        )
    }
}
//渲染组件到页面
ReactDOM.render(
    <Person name="jerry" age={19} sex=""/>
    ,document.getElementById('test1')
)
ReactDOM.render(
    <Person name="tom" age={18} sex=""/>
    ,document.getElementById('test2')
)

const p = {name:'老刘',age:18,sex:'女'}

//ReactDOM.render(<Person name={p.name} age={p.age} sex={p.sex}/>,document.getElementById('test3'))
//批量传递数据
ReactDOM.render(<Person {...p}/>,document.getElementById('test3'))

限制属性类型

react15.5之后需要先引入以下js

    <!-- 引入prop-types,用于对组件标签属性进行限制 -->
    <script type="text/javascript" src="../js/prop-types.js"></script>

在使用脚手架的情况下

// 通过 npm install --save prop-types 安装,用于限制属性类型
import PropTypes from "prop-types";

使用方法(类型错误在控制台会有警告)

//创建组件
class Person extends React.Component{
    //对标签属性进行类型、必要性的限制
    static propTypes = {
        name:PropTypes.string.isRequired, //限制name必传,且为字符串
        sex:PropTypes.string,//限制sex为字符串
        age:PropTypes.number,//限制age为数值
        speak:PropTypes.func,//限制speak为函数
    }

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

    render(){
        const {name,age,sex} = this.props
        //props是只读的
        //this.props.name = 'jack' //此行代码会报错,因为props是只读的
        return (
            <ul>
                <li>姓名:{name}</li>
                <li>性别:{sex}</li>
                <li>年龄:{age+1}</li>
            </ul>
        )
    }
}
/*
//对标签属性进行类型、必要性的限制
Person.propTypes = {
    name:PropTypes.string.isRequired, //限制name必传,且为字符串
    sex:PropTypes.string,//限制sex为字符串
    age:PropTypes.number,//限制age为数值
    speak:PropTypes.func,//限制speak为函数
}
//指定默认标签属性值
Person.defaultProps = {
    sex:'男',//sex默认值为男
    age:18 //age默认值为18
}

//react15.5之前的写法,无需引入上述的js依赖
Person.propTypes = {
 name: React.PropTypes.string.isRequired,
 age: React.PropTypes.number
}
*/

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

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

function speak(){
    console.log('我说话了');
}

函数组件使用props

函数式组件只能使用props,不能使用state和ref

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

展开运算符复习

let arr1 = [1, 3, 5, 7, 9]
let arr2 = [2, 4, 6, 8, 10]
// 1. 展开一个数组
console.log(...arr1); // 1 3 5 7 9
// 2. 连接数组
let arr3 = [...arr1, ...arr2]

// 3. 在函数中使用
function sum(...numbers) {
  return numbers.reduce((preValue, currentValue) => {
    return preValue + currentValue
  })
}
console.log(sum(1, 2, 3, 4)); // 10

// 4. 构造字面量对象时使用展开语法
let person = {
  name: 'tom',
  age: 18
}

// console.log(...person); // 报错,展开运算符不能展开对象
console.log({...person}) // {name: "tom", age: 18}

let person2 = { ...person } // 可以拷贝一个对象
person.name = 'jerry'
console.log(person2); // {name: "tom", age: 18}
console.log(person); // {name: "jerry", age: 18}

// 5. 合并对象
let person3 = {
  ...person,
  name: 'jack',
  address: "地球"
}
console.log(person3); // {name: "jack", age: 18, address: "地球"}

(3)refs

组件内的标签可以定义ref属性来标识自己(相当于html的id属性)

以下为使用refs获取DOM节点的方式

字符串形式

官方已不推荐使用,使用多了有效率问题

使用方法

1、DOM绑定ref字符串:<input ref="input1" type="text" placeholder="点击按钮提示数据"/>

2、获取指定的DOM节点:const {input1} = this.refs

3、获取DOM节点数据:input1.value

//创建组件
class Demo extends React.Component{
    //展示左侧输入框的数据
    showData = ()=>{
        //refs内有该组件实例所有的DOM节点
        const {input1} = this.refs
        alert(input1.value)
    }
    //展示右侧输入框的数据
    showData2 = ()=>{
        //结构表达式,将refs中的input2节点赋值到input2变量
        const {input2} = this.refs
        alert(input2.value)
    }
    render(){
        return(
            <div>
                <input ref="input1" type="text" placeholder="点击按钮提示数据"/>&nbsp;
                <button onClick={this.showData}>点我提示左侧的数据</button>&nbsp;
                <input ref="input2" onBlur={this.showData2} type="text" placeholder="失去焦点提示数据"/>
            </div>
        )
    }
}
//渲染组件到页面
ReactDOM.render(<Demo a="1" b="2"/>,document.getElementById('test'))

回调(内联)形式

将DOM节点直接复制到实例对象this上

使用方式

1、将节点赋值到this中:<input ref={c => this.input1 = c } type="text"/>

2、获取节点:const {input1} = this

3、获取节点数据:input1.value

//创建组件
class Demo extends React.Component{

    //回调函数方式,取出数据
    showData = ()=>{
        const {input1} = this
        alert(input1.value)
    }

    //class 的绑定函数的方式
    showEvent = (c)=>{
        this.input2 = c;
    }

    render(){
        return(
            <div>
                {/* 回调函数的参数c是当前节点, JSX的注释方式 */}
                <input ref={c => this.input1 = c } type="text"/>&nbsp;
                <button onClick={this.showData}>点我提示左侧的数据</button>&nbsp;

                {/* class 的绑定函数的方式 */}
                <input ref={this.showEvent}  type="text"/>

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

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

因为该问题是无关紧要的,并且回调方式相对于class绑定函数的方式较为简单,所有回调方式使用的比较多

class绑定函数形式

见回调形式案例

使用方式

1、class绑定函数:<input ref={this.showEvent} type="text"/>

2、将DOM节点赋值到this

//class 的绑定函数的方式
showEvent = (c)=>{
    this.input2 = c;
}

3、获取节点:const {input2} = this

4、获取节点数据:input2.value

createRef形式

官方推荐

使用方式

1、创建容器:myRef = React.createRef()

2、将DOM节点放入容器:<input ref={this.myRef} type="text"/>

3、取出节点并获取数据:this.myRef.current.value

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

3、函数式组件属性

(1)state

函数式组件中没有state

可以借助react提供的hooks在函数式组建中做状态和方法

react对象有个useState函数,可以通过该函数创建状态,并且该函数是有返回值的:

4、事件处理

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

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

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

  2. 通过event.target得到发生事件的DOM元素对象——————————不要过度使用ref

使用方式

1、绑定事件:<input onBlur={this.showData2} type="text" placeholder="失去焦点事件"/>

2、定义事件

//展示右侧输入框的数据
showData2 = (event)=>{
    //event.target 即为当前DOM节点
    alert(event.target.value);
}

5、收集表单数据

(1)非受控组件

现用现取

缺点:过度使用ref,影响性能

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

(2)受控组件

随着输入维护状态(通过onChange事件),就是受控组件

类似于vue的双向绑定

推荐使用受控组件,可以避免过度使用ref,影响性能

//创建组件
class Login extends React.Component{

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

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

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

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

    render(){
        return(
            <form onSubmit={this.handleSubmit}>
                用户名:<input onChange={this.saveUsername} 
                        type="text" name="username"/>
                密码:<input onChange={this.savePassword} 
                       type="password" name="password"/>
                <button>登录</button>
            </form>
        )
    }
}
//渲染组件
ReactDOM.render(<Login/>,document.getElementById('test'))

(3)函数柯里化

用于解决受控组件收集表单数据时,需要多个收集数据的函数

首先了解以下2行代码的区别

<!-- 此行是将this.saveUsername这个方法作为onChange事件的回调
        只有onChange事件触发时,才会执行该方法-->
<input onChange={this.saveUsername} type="text" name="username"/>

<!-- 此行是将this.saveUsername这个方法的 ‘返回值’ 作为onChange事件的回调
        在该DOM渲染时便会执行该方法,并且在onChange事件触发时,也不会再次执行该方法
        所以该写法无法实现收集表单数据-->
<input onChange={this.saveUsername(name)} type="text" name="username"/>

当事件函数需要参数时

利用onChange={this.saveUsername(name)}事件回调函数是this.saveUsername(name)方法返回值的特点,将this.saveUsername(name)的返回值定义为一个箭头函数,每次事件触发时,都会执行该函数。

//保存表单数据到状态中
saveFormData = (dataType)=>{
    return (event)=>{
        this.setState({[dataType]:event.target.value})
    }
}
<form onSubmit={this.handleSubmit}>
    用户名:<input onChange={this.saveFormData('username')} 
               type="text" name="username"/>
    密码:<input onChange={this.saveFormData('password')} 
              type="password" name="password"/>
    <button>登录</button>
</form>

高阶函数

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

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

(4)不使用柯里化实现

//创建组件
class Login extends React.Component{
    //初始化状态
    state = {
        username:'', //用户名
        password:'' //密码
    }

//保存表单数据到状态中
saveFormData = (dataType,event)=>{
    this.setState({[dataType]:event.target.value})
}

//表单提交的回调
handleSubmit = (event)=>{
    event.preventDefault() //阻止表单提交
    const {username,password} = this.state
    alert(`你输入的用户名是:${username},你输入的密码是:${password}`)
}
render(){
    return(
        <form onSubmit={this.handleSubmit}>
            用户名:<input onChange={event => this.saveFormData('username',event) } 
                    type="text" name="username"/>
            密码:<input onChange={event => this.saveFormData('password',event) } 
                   type="password" name="password"/>
            <button>登录</button>
        </form>
    )
}
}
//渲染组件
ReactDOM.render(<Login/>,document.getElementById('test'))

五、组件生命周期(类)

1、旧版

2_react生命周期(旧)

1.3.1 初始化阶段

ReactDOM.render()触发—初次渲染

  • constructor() —— 类组件中的构造函数
  • componentWillMount() —— 组件将要挂载 【即将废弃】
  • render() —— 挂载组件
  • componentDidMount() —— 组件挂载完成 比较常用
    一般在这个钩子中做一些初始化的事,例如:开启定时器、发送网络请求、订阅消息

1.3.2 更新阶段

【第一种情况】父组件重新render触发

componentWillReceiveProps() —— 接收属性参数(非首次)【即将废弃】
然后调用下面的钩子函数

【第二种情况】由组件内部this.setSate()

shouldComponentUpdate() —— 组件是否应该被更新(默认返回true)
然后调用下面的钩子函数

【第三种情况】强制更新 forceUpdate()

componentWillUpdate() ——组件将要更新 【即将废弃】

render() —— 组件更新

componentDidUpdate() —— 组件完成更新

1.3.3 卸载组件

由ReactDOM.unmountComponentAtNode()触发

componentWillUnmount() —— 组件即将卸载

一般在这个钩子中做一些收尾的事,例如:关闭定时器、取消订阅消息

//创建组件
class Count extends React.Component{

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

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

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

    //强制更新按钮的回调
    force = ()=>{
        //强制更新数据,不受组件更新阀门控制
        this.forceUpdate()
    }

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

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

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

    //控制组件更新的“阀门”,返回值控制页面是否可更新
    //返回值为true:允许更新,当不写该钩子时,默认返回true
    //返回false,拒绝更新,数据不会变化
    shouldComponentUpdate(){
        console.log('Count---shouldComponentUpdate');
        return true
    }

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

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

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

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

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

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

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

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

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

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

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

2、新版

3_react生命周期(新)

初始化阶段

ReactDOM.render()触发 —— 初次渲染

  • constructor() —— 类组件中的构造函数

  • static getDerivedStateFromProps(props, state) 从props得到一个派生的状态【新增】

  • render() —— 挂载组件

  • componentDidMount() —— 组件挂载完成 比较常用

更新阶段

由组件内部this.setSate()或父组件重新render触发或强制更新forceUpdate()

  • getDerivedStateFromProps() —— 从props得到一个派生的状态 【新增】
  • shouldComponentUpdate() —— 组件是否应该被更新(默认返回true)
  • render() —— 挂载组件
  • getSnapshotBeforeUpdate() —— 在更新之前获取快照【新增】
  • componentDidUpdate(prevProps, prevState, snapshotValue) —— 组件完成更新

卸载组件

由ReactDOM.unmountComponentAtNode()触发

  • componentWillUnmount() —— 组件即将卸载

重要的勾子

  • render:初始化渲染或更新渲染调用

  • componentDidMount:开启监听, 发送ajax请求

  • componentWillUnmount:做一些收尾工作, 如: 清理定时器

即将废弃的勾子

  • componentWillMount
  • componentWillReceiveProps
  • componentWillUpdate

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

//创建组件
class Count extends React.Component{
    //构造器
    constructor(props){
        console.log('Count---constructor');
        super(props)
        //初始化状态
        this.state = {count:0}
    }

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

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

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

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

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

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

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

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

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

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

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

getSnapshotBeforeUpdate使用场景

在滚动区域中,如果持续有内容加入,可通过getSnapshotBeforeUpdate计算前后高度差,来保证页面不会一直向上滚动

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>4_getSnapShotBeforeUpdate的使用场景</title>
    <style>
        .list{
            width: 200px;
            height: 150px;
            background-color: skyblue;
            overflow: auto;
        }
        .news{
            height: 30px;
        }
    </style>
</head>
<body>
    <!-- 准备好一个“容器” -->
    <div id="test"></div>

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

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

            state = {newsArr:[]}

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

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

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

            render(){
                return(
                    <div className="list" ref="list">
                        {
                            this.state.newsArr.map((n,index)=>{
                                return <div key={index} className="news">{n}</div>
                            })
                        }
                    </div>
                )
            }
        }
        ReactDOM.render(<NewsList/>,document.getElementById('test'))
    </script>
</body>
</html>

六、Hooks(函数)

1、函数式组件怎么执行的

1.首先初始化,会按顺序执行组件中的所有函数,然后组件被挂载到根节点上。

  • 遇到useState执行会生成一个state和setState存在当前组件作用域中

  • 遇到useEffect之后回去订阅其中的消息,然后再组件挂载后去发布消息

  • 遇到useMemo或者useCallback就会根据他们依赖的状态去缓存,属性(函数返回结果值)或方法

2.当setState状态改变后,又会去执行对应的组件函数。

  • 遇到useState也会去执行,返回修改后的状态(指向新的地址,所以可以用const)
  • 遇到useEffect、useMemo、useCallback就会根据第二个参数中的状态依赖,来判断怎样执行。
import React , {useState,useEffect,useMemo,useCallback} from 'react'

const Com= () => {
    //执行返回 const的state和setState
    const [state,setState] = useState(false)

    //根据返回的state去判断,执行
    useEffect(()=>{
    }, [state])

    //根据返回的state去判断缓存属性(函数返回结果值)
    useMemo(()=>{
        return 1
    },[state])

    //根据返回的state去判断缓存方法
    useCallback(()=>{
    },[state])


    function func(){
        console.log('执行func')
    }
    //每次setState去触发视图更新之后,都会被执行
    func()


    return (
        <div onClick={()=>{setState(!state)}}>{state}</div>
    )
}

export default Com

2、对比类式的生命周期

函数组件 的本质是函数,没有 state 的概念的,因此不存在生命周期一说,仅仅是一个 render 函数而已。

但是引入 Hooks 之后就变得不同了,它能让组件在不使用 class 的情况下拥有 state,所以就有了生命周期的概念,所谓的生命周期其实就是 useState、 useEffect() 和 useLayoutEffect() 。

即:Hooks 组件(使用了Hooks的函数组件)有生命周期,而函数组件(未使用Hooks的函数组件)是没有生命周期的。

下面,是具体的 class 与 Hooks 的生命周期对应关系:

说明 class 组件 Hooks 组件
构造函数 constructor useState
从props得到一个派生的状态 getDerivedStateFromProps useState 里面 update 函数
组件是否应该被更新(默认返回true) shouldComponentUpdate useMemo
render 函数本身
组件挂载完成触发 componentDidMount useEffect
组件完成更新触发 componentDidUpdate useEffect
组件卸载前触发 componentWillUnmount useEffect 里面返回的函数
componentDidCatch
getDerivedStateFromError

3、Hook 的含义

Hook 这个单词的意思是”钩子”。

React Hooks 的意思是,组件尽量写成纯函数,如果需要外部功能和副作用,就用钩子把外部代码”钩”进来。 React Hooks 就是那些钩子。

你需要什么功能,就使用什么钩子。React 默认提供了一些常用钩子,你也可以封装自己的钩子。

所有的钩子都是为函数引入外部功能,所以 React 约定,钩子一律使用use前缀命名,便于识别。你要使用 xxx 功能,钩子就命名为 usexxx。

下面介绍 React 默认提供的四个最常用的钩子。

  • useState()
  • useContext()
  • useReducer()
  • useEffect()

4、useState():状态钩子

useState()用于为函数组件引入状态(state)。纯函数不能有状态,所以把状态放在钩子里面。

本文前面那个组件类,用户点击按钮,会导致按钮的文字改变,文字取决于用户是否点击,这就是状态。使用useState()重写如下。

import React, { useState } from "react";

export default function  Button()  {
  const  [buttonText, setButtonText] =  useState("Click me,   please");

  function handleClick()  {
    return setButtonText("Thanks, been clicked!");
  }

  return  <button  onClick={handleClick}>{buttonText}</button>;
}

上面代码中,Button 组件是一个函数,内部使用useState()钩子引入状态。

useState()这个函数接受状态的初始值,作为参数,上例的初始值为按钮的文字。该函数返回一个数组,数组的第一个成员是一个变量(上例是buttonText),指向状态的当前值。第二个成员是一个函数,用来更新状态,约定是set前缀加上状态的变量名(上例是setButtonText)。

5、useContext():共享状态钩子

如果需要在组件之间共享状态,可以使用useContext()

现在有两个组件 Navbar 和 Messages,我们希望它们之间共享状态。

<div className="App">
  <Navbar/>
  <Messages/>
</div>

第一步就是使用 React Context API,在组件外部建立一个 Context。

const AppContext = React.createContext({});

参数为默认值,此处默认值为空对象 {}

组件封装代码如下。

<AppContext.Provider value={{
  username: 'superawesome'
}}>
  <div className="App">
    <Navbar/>
    <Messages/>
  </div>
</AppContext.Provider>

上面代码中,AppContext.Provider提供了一个 Context 对象,这个对象可以被子组件共享。

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

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

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

Navbar 组件的代码如下。

const Navbar = () => {
  const { username } = useContext(AppContext);
  return (
    <div className="navbar">
      <p>AwesomeSite</p>
      <p>{username}</p>
    </div>
  );
}

上面代码中,useContext()钩子函数用来引入 Context 对象,从中获取username属性。

代码整合

import React, { useContext } from "react";
import ReactDOM from "react-dom";
import "./styles.css";

const AppContext = React.createContext({});

const Navbar = () => {
  const { username } = useContext(AppContext)

  return (
    <div className="navbar">
      <p>AwesomeSite</p>
      <p>{username}</p>
    </div>
  )
}

const Messages = () => {
  const { username } = useContext(AppContext)

  return (
    <div className="messages">
      <h1>Messages</h1>
      <p>1 message for {username}</p>
      <p className="message">useContext is awesome!</p>
    </div>
  )
}

function App() {
  return (
    <AppContext.Provider value={{
      username: 'superawesome'
    }}>
      <div className="App">
        <Navbar />
        <Messages />
      </div>
    </AppContext.Provider>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

6、useReducer():action 钩子

7、useEffect():副作用钩子

(1)基本使用

useEffect()用来引入具有副作用的操作,最常见的就是向服务器请求数据。以前,放在componentDidMount里面的代码,现在可以放在useEffect()

useEffect()的用法如下。

useEffect(()  =>  {
  // Async Action
}, [dependencies])

上面用法中,useEffect()接受两个参数。第一个参数是一个函数,异步操作的代码放在里面。第二个参数是一个数组,用于给出 Effect 的依赖项,只要这个数组发生变化,useEffect()就会执行。第二个参数可以省略,这时每次组件渲染时,就会执行useEffect()

下面看一个例子。

const Person = ({ personId }) => {
  const [loading, setLoading] = useState(true);
  const [person, setPerson] = useState({});

  useEffect(() => {
    setLoading(true); 
    fetch(`https://swapi.co/api/people/${personId}/`)
      .then(response => response.json())
      .then(data => {
        setPerson(data);
        setLoading(false);
      });
  }, [personId])

  if (loading === true) {
    return <p>Loading ...</p>
  }

  return <div>
    <p>You're viewing: {person.name}</p>
    <p>Height: {person.height}</p>
    <p>Mass: {person.mass}</p>
  </div>
}

上面代码中,每当组件参数personId发生变化,useEffect()就会执行。组件第一次渲染时,useEffect()也会执行。

没有依赖数组作为第二个参数,与依赖数组位空数组 [] 的行为是不一致的:

useEffect(() => {
  // 这里的代码会在每次渲染后执行
});

useEffect(() => {
  // 这里的代码只会在组件挂载后执行
}, []);

useEffect(() => {
  //这里的代码只会在每次渲染后,并且 a 或 b 的值与上次渲染不一致时执行
}, [a, b]);

(2)返回值

副效应是随着组件加载而发生的,那么组件卸载时,可能需要清理这些副效应。

useEffect()允许返回一个函数,在组件卸载时,执行该函数,清理副效应。如果不需要清理副效应,useEffect()就不用返回任何值。

useEffect(() => {
  const subscription = props.source.subscribe();
  return () => {
    subscription.unsubscribe();
  };
}, [props.source]);

上面例子中,useEffect()在组件加载时订阅了一个事件,并且返回一个清理函数,在组件卸载时取消订阅。

实际使用中,由于副效应函数默认是每次渲染都会执行,所以清理函数不仅会在组件卸载时执行一次,每次副效应函数重新执行之前,也会执行一次,用来清理上一次渲染的副效应。

(3)注意点

使用useEffect()时,有一点需要注意。如果有多个副效应,应该调用多个useEffect(),而不应该合并写在一起。

function App() {
  const [varA, setVarA] = useState(0);
  const [varB, setVarB] = useState(0);
  useEffect(() => {
    const timeoutA = setTimeout(() => setVarA(varA + 1), 1000);
    const timeoutB = setTimeout(() => setVarB(varB + 2), 2000);

    return () => {
      clearTimeout(timeoutA);
      clearTimeout(timeoutB);
    };
  }, [varA, varB]);

  return <span>{varA}, {varB}</span>;
}

上面的例子是错误的写法,副效应函数里面有两个定时器,它们之间并没有关系,其实是两个不相关的副效应,不应该写在一起。正确的写法是将它们分开写成两个useEffect()

function App() {
  const [varA, setVarA] = useState(0);
  const [varB, setVarB] = useState(0);

  useEffect(() => {
    const timeout = setTimeout(() => setVarA(varA + 1), 1000);
    return () => clearTimeout(timeout);
  }, [varA]);

  useEffect(() => {
    const timeout = setTimeout(() => setVarB(varB + 2), 2000);

    return () => clearTimeout(timeout);
  }, [varB]);

  return <span>{varA}, {varB}</span>;
}

(4)用途

只要是副效应,都可以使用useEffect()引入。它的常见用途有下面几种。

  • 获取数据(data fetching)
  • 事件监听或订阅(setting up a subscription)
  • 改变 DOM(changing the DOM)
  • 输出日志(logging)

下面是从远程服务器获取数据的例子。

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

function App() {
  const [data, setData] = useState({ hits: [] });

  useEffect(() => {
    const fetchData = async () => {
      const result = await axios(
        'https://hn.algolia.com/api/v1/search?query=redux',
      );

      setData(result.data);
    };

    fetchData();
  }, []);

  return (
    <ul>
      {data.hits.map(item => (
        <li key={item.objectID}>
          <a href={item.url}>{item.title}</a>
        </li>
      ))}
    </ul>
  );
}

export default App;

上面例子中,useState()用来生成一个状态变量(data),保存获取的数据;useEffect()的副效应函数内部有一个 async 函数,用来从服务器异步获取数据。拿到数据以后,再用setData()触发组件的重新渲染。

由于获取数据只需要执行一次,所以上例的useEffect()的第二个参数为一个空数组。

8、useRef():引用对象

React 中包含的各种 hook 之一是 useRef hook,它用于在函数组件中引用对象,并在重新呈现之间保留被引用对象的状态。useRef 有一个名为 current 的属性,用于在任何时候检索被引用对象的值,同时也接受一个初始值作为参数。你可以通过更新当前值来更改引用对象的值。

下面演示了如何创建一个引用对象:

import { useRef } from ‘react’

const myComponent = () => {
  const refObj = useRef(initialValue)

  return (
  //…
  )
}

在上面的代码片段中,我们有一个对象 refObj,我们想在应用程序中引用它,为了访问这个值或更新它的值,我们可以像这样调用 current 属性:

// 在函数内部
const handleRefUpdate = () => {
    // 访问被引用对象的值
    const value = refObj.current

    // 更新被引用对象的值
   refObj.current = newValue
}

下面两点是你应该需要注意的:

  • 被引用对象的值在重新渲染之前保持不变。
  • 更新被引用对象的值不会触发重新渲染。

通过 ref 操作 DOM

使用 ref 操作 DOM 是非常常见的。React 内置了对它的支持。

首先,声明一个 initial value 为 null 的 ref 对象

import { useRef } from 'react';

function MyComponent() {
  const inputRef = useRef(null);
  // ...
}

然后将你的 ref 对象作为 ref 属性传递给你想要操作的 DOM 节点的 JSX:

  // ...
  return <input ref={inputRef} />;

当 React 创建 DOM 节点并将其渲染到屏幕时,React 将会把 DOM 节点设置为你的 ref 对象的 current 属性。现在你可以访问 <input> 的 DOM 节点,并且可以调用类似于 focus() 的方法:

  function handleClick() {
    inputRef.current.focus();
  }

当节点从屏幕上移除时,React 将把 current 属性设回 null

9、自定义 Hook

上例的 Hooks 代码还可以封装起来,变成一个自定义的 Hook,便于共享。

const usePerson = (personId) => {
  const [loading, setLoading] = useState(true);
  const [person, setPerson] = useState({});
  useEffect(() => {
    setLoading(true);
    fetch(`https://swapi.co/api/people/${personId}/`)
      .then(response => response.json())
      .then(data => {
        setPerson(data);
        setLoading(false);
      });
  }, [personId]);  
  return [loading, person];
};

上面代码中,usePerson()就是一个自定义的 Hook。

Person 组件就改用这个新的钩子,引入封装的逻辑。

const Person = ({ personId }) => {
  const [loading, person] = usePerson(personId);

  if (loading === true) {
    return <p>Loading ...</p>;
  }

  return (
    <div>
      <p>You're viewing: {person.name}</p>
      <p>Height: {person.height}</p>
      <p>Mass: {person.mass}</p>
    </div>
  );
};

六、响应式事件


  目录