初识React

React简介

React是用于构建用户界面的JavaScript库,起源于FaceBook的内部项目,于 2013 年 5 月开源。

React主要用于构建UI。你可以在React里传递多种类型的参数,如声明代码,帮助你渲染出UI、也可以是静态的HTML DOM元素、也可以传递动态变量、甚至是可交互的应用组件。

React的特点

  • 1.声明式设计 −React采用声明范式,可以轻松描述应用。
  • 2.高效 −React通过对DOM的模拟,最大限度地减少DOM操作。
  • 3.灵活 −React可以与已知的库或框架很好地配合,可以在无需重构代码的情况下使用React实现新功能。
  • 4.JSX − JSX 是 JavaScript 语法的扩展。React 开发不一定使用 JSX ,但我们建议使用它。
  • 5.组件 − 通过 React 构建组件,使得代码更加容易得到复用,能够很好的应用在大项目的开发中。
  • 6.单向响应的数据流 − React 实现了单向响应的数据流,从而减少了重复代码,这也是它为什么比传统数据绑定更简单。

安装

在不使用脚手架的情况下,我们可以下载React相关的包进行使用,也可以使用CDN

这里我使用的是v16.4.0版本

React相关的文件:

  • react.development.js —— React核心文件
  • react-dom.development.js —— React用来操作DOM的文件
  • babel.min.js —— 在React中会使用到jsx,但是浏览器不支持jsx,所以使用babel将jsx编译为js
<!--引入react核心库 核心库必须最先引入-->
<script type="text/javascript" src="https://cdn.staticfile.org/react/16.4.0/umd/react.development.js"></script>
<!--引入react拓展库,react-dom,支持react操作dom-->
<script type="text/javascript" src="https://cdn.staticfile.org/react-dom/16.4.0/umd/react-dom.development.js"></script>
<!--引入babel,用于将jsx转换为js-->
<script type="text/javascript" src="https://cdn.staticfile.org/babel-standalone/6.26.0/babel.min.js"></script>

Hello React

新建一个html文件,编写如下代码

<!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="../reactjs/react.development.js"></script>
<!--引入react拓展库,react-dom,支持react操作dom-->
<script type="text/javascript" src="../reactjs/react-dom.development.js"></script>
<!--引入babel,用于将jsx转换为js-->
<script type="text/javascript" src="../reactjs/babel.min.js"></script>

<!-- 此处一定要写text/babel,使用babel翻译jsx,默认js-->
<script type="text/babel">
    // 创建虚拟dom
    const VDOM = <h1>hello,react</h1> // jsx语法
    // 渲染虚拟dom
    ReactDOM.render(VDOM,document.getElementById('test'))
</script>
</body>
</html>

这里我是下载了React相关js文件到本地进行使用

需要注意的是,用来编写jsx语法的script标签,需要把type设置为babel,否则就不能使用jsx语法。

注:接下来的所有代码都写在类型为“babel”的script标签中(type=“text/babel”)

在这个例子中,先准备了一个id为test的一个容器,然后使用jsx语法创建了一个虚拟DOM,也就是h1,最后使用ReactDOM的render方法将虚拟DOM进行渲染。

最后在浏览器打开
请添加图片描述

看到如下内容,第一个React程序就写完了。

创建虚拟DOM的方式

使用jsx进行创建

jsx支持在js代码中嵌入标签,可以让编码人员更加简单的创建虚拟DOM

// 创建虚拟dom
const VDOM = <h1 id="title">hello,react</h1> // jsx语法
// 渲染虚拟dom   虚拟DOM, 获取节点进行挂载
ReactDOM.render(VDOM,document.getElementById('test'))

使用js进行创建

使用React.createElement(标签名,标签属性,标签内容)创建一个虚拟DOM,

// 创建虚拟dom
const VDOM = React.createElement('h1',{id:'title'},'hello react!')
// 渲染虚拟dom
ReactDOM.render(VDOM,document.getElementById('test'))

如果需要创建子节点,则需要将标签内容修改为使用React.createElement方法创建的虚拟DOM

// 多层标签嵌套
const VDOM = React.createElement('div',{id:'title'},
                                React.createElement('h1',{id:'title'},'hello react!'))
// 渲染虚拟dom
ReactDOM.render(VDOM,document.getElementById('test'))

请添加图片描述

其实,jsx创建的虚拟DOM最终编译后得到代码和js方式创建的是一样的,但是jsx更加简单,而且当面多多重标签深度嵌套的时候,代码会显得很冗余,所以一般情况下建议使用jsx

虚拟DOM与真实DOM的区别

先编辑以下代码,在浏览器打开控制台,查看打印的结果

// 创建虚拟dom
const VDOM = <h1 id="title">hello,react</h1> // jsx语法
// 渲染虚拟dom
ReactDOM.render(VDOM,document.getElementById('test'))
console.log("虚拟dom",VDOM)

//真实dom
const TDOM = document.getElementById('demo')
console.log("真实dom")
console.dir(TDOM)

请添加图片描述

总结一下

  • 虚拟dom本质就是Object类型的js对象(一般对象)

  • 真实dom属性多,虚拟dom属性少,比较轻

  • 虚拟dom最终会被react转换为真实dom,呈现在页面上

jsx语法规则

jsx 语法规则:

  • 1.定义虚拟dom时,不要写引号\

  • 2.标签中混入js 表达式 时,使用 {} ,注意区分js语句表达式

    • 表达式:一个表达式会产生一个值,
      • 例:a, a+b, demo(1), arr.map(), function test() {}
    • 语句
      • 例:if(){}, for(){}, switch(){case:xxx}
  • 3.样式类名指定不用class,用className(class 是 ES6里类定义关键字,为了避免冲突)

  • 4.内联样式要加双花括号 style={{key:value}}

  • 5.虚拟dom只能有一个根标签

  • 6.标签必须闭合

  • 7.标签首字母

    • (1)若小写字母开头,则将标签改为html中同名元素,若html中没有该标签同名元素,则报错

    • (2)若大写字母标签,react就去渲染对应的组件,若组件没有定义,则报错。

const myId = "title"
const myData = "hello jsx"
const VDOM = (
    <div>
        <h2 id={myId} className="title">
            <span style={{color: "green",fontSize:'20px'}}>{myId}</span>
        </h2>
    </div>
)
ReactDOM.render(VDOM,document.getElementById("jsx"))

注:使用jsx语法写标签时,不能在标签中写注释,只能写在{}包含的语句块中例:{/*这是一段注释*/}

小练习——列表渲染

先定义几个模拟数据,放在一个列表中,然后遍历列表,取出列表中的元素,渲染在页面上

//模拟一些数据
const data = ["AngularJS", "ReactJS", "VueJS"]

// 创建虚拟dom
const VDOM = (
    <div>
        <h1>前端框架列表</h1>
        <ul>
            {
                data.map((item,index)=>{
                    return <li key={index}>{item}</li>
                })
            }
        </ul>
    </div>
)
ReactDOM.render(VDOM,document.getElementById("jsx"))

为什么在{}中没有写for循环?因为在{}中只能写表达式,不能写语句,for循环就是一个语句。

使用map取出数组中的元素,返回一个被li标签包含的元素集合。这里的key则是该DOM元素的唯一标识,不应该重复,在react渲染DOM时,会将新的虚拟DOMkey旧的虚拟DOMkey进行对比,保留已经存在的节点,并添加新的节点。也就是说,如果在上面的列表中再添加一个元素,react只会渲染新添加的这个元素,其他的保持不变,通过复用节点的方式提高效率。如果没有这个key,则无法判断两个节点是否相同,只能全部重新渲染,如果实在节点较多的情况下,容易使得浏览器卡顿。

最后看一下效果

请添加图片描述

组件的定义

函数式组件

创建函数式组件 函数名就是类名,例如:

function MyComponent(){
    return <h2>函数定义组件(适用于 "简单组件" 定义)</h2>
}
// 渲染组件到页面
ReactDOM.render(<MyComponent/> ,document.getElementById("test"))

函数式组件:

  • 函数和组件标签首字母必须大写
  • 函数必须有返回值
  • render必须写闭合标签,<MyComponent/><MyComponent></MyComponent>

ReactDom.render 做了什么

    1. react解析组件标签,找到 MyComponent 组件。
    1. 发现组件是函数定义的,随后调用该函数,将返回的虚拟dom转换为真实dom,呈现在页面中。

类式组件

类式组件,通过类来定义一个组件,类名就是组件名,例如:

class MyComponent extends React.Component {
    render(){
        return <h2>类定义组件(适用于复杂组件的定义)</h2>
    }
}
ReactDOM.render(<MyComponent />, document.getElementById("test"))

在这个示例中,定义了一个MyComponent类继承React.Component(这个继承是创建类式组件所必须的)

在类式组件中,必须写render方法并返回虚拟DOM。

注:自定义组件首字母大写(类名大写)

render是放在哪里的? —— 类的原型对象上,供实例使用。 MyComponent.prototype

render中的this指向哪里 —— MyComponent的(组件)实例对象。

ReactDom.render 做了什么

    1. react解析组件标签,找到 MyComponent 组件。
    1. 发现组件是使用类定义的,随后new出来该类的实例,
    1. 并通过该实例调用原型上的render方法,将render返回的虚拟dom转换为真实dom,呈现在页面中

复合组件

定义一个组件作为父组件,在父组件中使用子组件,示例:

class PersonComponent  extends React.Component {
    render() {
        return (
            <div>
                <div>父组件</div>
                <SonComponent />
            </div>
        )
    }
}
class SonComponent extends React.Component {
    render() {
        return (
            <div>子组件</div>
        )
    }
}

React三大属性

接下来的内容基本都是在针对类式组件进行展开,座椅学习接下来的内容之前,需要对类的相关知识有一定的了解

state

在前面的例子中,我们一直在使用ReactDOM.render()来渲染我们定义的组件,但是第一次挂载完成后,我们很难再修改组件的样式或状态。

React 把组件看成是一个状态机(State Machines)。通过与用户的交互,实现不同状态,然后渲染 UI,让用户界面和数据保持一致。

React 里,只需更新组件的 state,React则会根据新的 state 重新渲染用户界面(不要操作 DOM),也就是状态驱动着组件的更新。

创建一个类定义的组件(为何是类定义?因为在旧版本的React中,函数定义组件不支持state和refs)

// 创建类定义组件
class Weather extends React.Component {
    // 构造器调用几次? --- 1次
    constructor(props) {
        super(props);
        // console.log(this) //这里的this是组件实例
        // state必须是一个对象
        this.state = {isHot: false,wind:'微风'}
        // 解决changeWeather中this指向问题
        this.changeWeather = this.changeWeather.bind(this) 
    }
    // 注意:通过类调用方法和通过实例调用不同
    changeWeather() {
        // changeWeather是作为onClick的回调,所以不是通过实例调用,是直接调用
        // 类中的方法默认开启了局部的严格模式,所以changeWeather中的this为undefind
        // 获取isHot原始值
        const isHot = this.state.isHot
        // 原始值取反,使用setState进行更新,(合并,非替换)
        this.setState({isHot: !isHot})
    }
    render(){
        // 读取状态
        const { isHot, wind } = this.state
        return (
            {/*这里是通过类直接调用方法,this指向不为实例,为undefind*/}
            <h1 onClick={ this.changeWeather }>
                今天天气{ isHot ? "炎热" : "凉爽" },{wind}
            </h1>
        )
    }
}
// 渲染组件到页面
ReactDOM.render(<Weather/>,document.getElementById("test"))

这个示例实现的功能是,通过点击组件,修改组件的state,实现组件内容的改变

注!!:react不认可直接修改state借助内置api去更改,通过setState方法进行修改,该方法接受一个对象,在对象中添加需要修改的属性值。

首先在构造器中初始化state,添加相关的状态,例如isHotwind,在render中使用了一个三元表达式,根据isHot的真假显示”炎热“或者”凉爽“。

h1标签添加了一个点击事件,定义了changeWeather方法作为点击事件的回调。

onClick={ this.changeWeather }语句标识将类方法changeWeather作为点击事件的回调,this指向的是Weather这个类,不能使用this.changeWeather(),加()表示直接调用该方法,会使得点击事件的回调变成changeWeather的返回值,但是该方法没有返回值,也就是undefind,使得点击事件不生效。

关于changeWeather中的this指向问题,在这里需要先了解类方法和实例方法的区别,类方法则是直接通过该类进行调用,例如ClassName.function(),就是类名.方法名。实例方法需要先通过new关键字创建一个实例(类的实例)。通过实例名.实例方法的方式进行调用,两种方法中this的作用域是不同的。

在点击事件中的this.changeWeather,this指向当前类,是直接调用类方法,类中的方法默认开启了局部的严格模式,所以changeWeather中的this为undefind(如果不开启严格模式,则指向window)所以使用this.statethis.setState时,会提示undefind。所以我们需要修改this的指向,可以使用bind方法,bind方法会返回一个新的函数,并将修改该函数的作用域。

在构造器中写入this.changeWeather = this.changeWeather.bind(this) ,构造器中的this指向的是当前实例,通过传入当前实例,调用bind方法返回一个新的changeWeather并挂载在实例上,该方法的this指向当前实例,然后我们就能在changeWeather这个方法中修改state。

可以在构造器中打印一下this,结合上面内容进行分析

请添加图片描述

简写一下

可以弃用构造函数,直接将state添加到类属性中

然后类方法使用箭头函数:

  • 箭头函数没有自己的this。
  • 如果在箭头函数中使用this,则使用外层函数的this作为自己的this。
class Weather extends React.Component {
    // 初始化状态
    state = {isHot: false,wind:'微风'}
    // 自定义方法 赋值语句 + 箭头函数
    changeWeather = () => {
        const isHot = this.state.isHot
        this.setState({isHot: !isHot})
    }
    render(){
        // 读取状态
        const { isHot, wind } = this.state
        return (
            <h1 onClick={ this.changeWeather}>
                今天天气{ isHot ? "炎热" : "凉爽" },{wind}
            </h1>
        )
    }
}

props

props用于向组件传递数据

state 和 props 主要的区别在于 props是只读,不可变,而 state 可以根据与用户交互来改变。这就是为什么有些容器组件需要定义 state 来更新和修改数据。 而子组件只能通过 props 来传递数据。

props传值
// props传值
class Person extends React.Component {
    render() {
        const {name, age, sex} = this.props;
        console.log(this)
        return (
            <ol>
                <li>{name}</li>
                <li>{parseInt(age) + 1}</li>
                <li>{sex}</li>
            </ol>
        )
    }
}
// ReactDOM.render(<Person name="四个火" age="21" sex="男"/>, document.getElementById("test"));
const person = {name:"四个火",age:"21",sex:"男"}
ReactDOM.render(<Person {...person}/>, document.getElementById("test"));

可以直接在标签内写自定义属性,写在组件标签内的属性会被添加到实例的props属性中,也可以使用展开运算符{...person}

这里的 展开语法与ES6有些不同,ES6中的展开运算符不允许展开对象,React组件标签内允许,其他地方不允许

打印一下当前组件实例

请添加图片描述

可以看到我们添加的属性都被添加到props中了

然后就可以在组件中使用props里面的数据

props限制

有时候我们希望对props的数据进行限制,例如是否必传,默认值、数据类型等

可以使用React.PropTypes完成该需求,在15.5版本后的React中,为了减轻React的重量,所以该模块被移到了 prop-types 库,用户根据需要自行引入该模块

下载prop-types.js文件,或CDN引入文件。

<script src="https://cdn.bootcss.com/prop-types/15.6.1/prop-types.js"></script>

引入之后,全局多了一个对象 PropTypes,假如现在创建了一个名为Person的组件,为组件添加一个propTypes属性,用于属性限制

Person.propTypes = {
    // 类型和必要性限制
    name: PropTypes.string.isRequired,
    age: PropTypes.number,
    sex: PropTypes.string,
    spack: PropTypes.func
}
Person.defaultProps = {
    // 默认值限制,如果没有进行传值,则使用默认值
    age: 21,
    sex: "男"
}

需要注意的是这里的propTypes首字符小写,表示用于类型限制的属性,PropTypes首字母大写,是一个对象,用来限制的属性

几种常见限制:

  • isRequired 限制必填
  • string 限制字符串
  • number 限制数值
  • object 对象
  • array 数组
  • func 限制方法 ------ function 由于function是js里面的关键字,所以使用func避免冲突

然后向组件传参数

const person = {name:"四个火",age:21}
ReactDOM.render(<Person {...person} spack={spack}/>, document.getElementById("test"));
function spack() {
    alert("spack")
}

虽然没有向组件传递sex,但是由于写了默认参数,所以组件就会默认sex=“男”。

props简写

直接给类添加propTypesdefaultProps属性,注意:是类属性,不是实例属性,所以加上static关键字

class Person extends React.Component {
    constructor(props) {
        /*
        * 拓展
        * 构造器是否接收props,并传递给super,取决于:是否希望在构造器中使用this访问props
        * */
        super(props);
        console.log(this.props) // 通过实例使用props ,这里如果不传递props给super,this.props为undefind,可直接使用props,如下
        console.log(props) //直接使用props
    }
    static propTypes = {  //这里是类属性,不是实例属性,所以加上static关键字,需要注意区分
        name: PropTypes.string.isRequired,
        age: PropTypes.number,
        sex: PropTypes.string,
        spack: PropTypes.func
    }
    static defaultProps = {
        age: 21,
        sex: "男"
    }
    render() {
        const {name, age, sex} = this.props;
        console.log(this)
        return (
            <ul>
                <li>{name}</li>
                <li>{age + 1 /*props是只读的,这个地方不是修改age,只是对展示的值进行计算*/ }</li>
                <li>{sex}</li>
            </ul>
        )
    }
}

const person = {name:"四个火",age:21}
ReactDOM.render(<Person {...person} spack={spack}/>, document.getElementById("test"));
function spack() {
    alert("spack")
}

这里的构造器一般可以不用添加,只是作为一个拓展,如果希望在构造器中使用this访问props时,需要将props传递给super()。

函数式组件使用props

前面提到,函数式组件不支持state和refs,但是也能使用props以及限制props限制

参考下面的示例

function Person(props) {
    const {name,age,sex} = props
    return (
        <ul>
            <li>{name}</li>
            <li>{age}</li>
            <li>{sex}</li>
        </ul>
    )
}
Person.propTypes = { //组件属性,不是方法
    /**
     * 类型限制,必要性限制
     */
    name: PropTypes.string.isRequired,
    age: PropTypes.number,
    sex: PropTypes.string,
}
Person.defaultProps = {
    // 默认值限制,如果没有进行传值,则使用默认值
    age: 21,
    sex: "男"
}
const person = {name:"四个火"}
ReactDOM.render(<Person {...person} />, document.getElementById("test"));

注意:props限制不能写到方法体里面

refs

在原生js中,通常使用document.getElementByIddocument.querySelector等这种类似的操作来获取我们需要的DOM元素

React为我们提供了一种非常特殊的属性 ref,你可以用来绑定到 render() 输出的任何组件上

下面是几个适合使用 refs 的情况:

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

不能在函数组件上使用 ref 属性,因为他们没有实例

字符串形式的ref

给组件添加ref属性 ref="refName",然后就能通过在示例方法中通过this.refs.refName获得DOM。

class Demo extends React.Component {
    // 点击按钮时,显示左边输入框的数据
    showInputData1 = () =>{
        // 通过获取id的方式
        // const input = document.getElementById('input1')
        // alert(input.value)
        // 通过ref
        const { input1 } = this.refs
        alert(input1.value)
    }
    // 右边输入框失去焦点时,显示右边输入框的数据
    showInputData2 = () => {
        const { input2 } = this.refs
        alert(input2.value)
    }
    render() {
        return (
            <div>
                <input ref="input1" id="input1" type="text" placeholder="点击按钮提示数据"/>&nbsp;
                <button onClick={this.showInputData1}>点击提示左边的数据</button>&nbsp;
                <input onBlur={this.showInputData2} ref="input2" type="text" placeholder="失去焦点提示数据"/>
            </div>
        )
    }
}

请添加图片描述

字符串形式的ref虽然方便,但是效率并不高,所以官方不建议使用,并可能会在未来的版本被移除

回调函数形式的ref

React 还支持另一种设置 refs 的方式,称为“回调 refs”。它能助你更精细地控制何时 refs 被设置和解除。

内联函数语法:ref={ currentNode => this.input1 = currentNode } —— 把ref当前所处的节点挂载在实例自身上,并取名为input1

回调函数语法:ref={ this.input2 } —— 把当前节点传递给回调函数input2

下面是一个完整的示例

class Demo extends React.Component {
    // 点击按钮时,显示左边输入框的数据
    showInputData1 = () =>{
        // 通过ref
        const { input1 } = this
        alert(input1.value)

    }
    // 右边输入框失去焦点时,显示右边输入框的数据
    showInputData2 = () => {
        alert(this.input2.value)
    }
    saveInput = (currentNode) => {
        this.input2 = currentNode
    }
    /*
    *  ref={ currentNode => this.input1 = currentNode }
    *   把ref当前所处的节点挂载在实例自身上,并取名为input1
    * */
    render() {
        return (
            <div>
                <input ref={ currentNode => this.input1 = currentNode }/*内联函数*/ id="input1" type="text" placeholder="点击按钮提示数据"/>&nbsp;
                <button onClick={this.showInputData1}>点击提示左边的数据</button>&nbsp;
                <input onBlur={this.showInputData2} ref={ this.saveInput /*使用类绑定的函数作为回调函数*/} type="text" placeholder="失去焦点提示数据"/>
            </div>
        )
    }
}

关于refs回调中,回调函数是内联函数与回调函数是类绑定函数的区别:

  • 内联函数: 当组件更新时,会触发两次回调,第一次为null,第二次为当前节点,因为react会清空旧的ref并设置新的
  • 类绑定函数: 当组件更新时,react会比较两次函数名相同,只会触发一次回调
createRef

使用 React.createRef() 创建ref,并通过 ref 属性附加到 React 元素

React.createRef调用后会返回一个容器,该容器可以存储被ref所标识的节点

render执行的时候,发现有一个ref,而且是用createRef创建的容器,就会将当前ref所在的节点存储到这个容器里面

class Demo extends React.Component {
    myRef = React.createRef(); // 一个节点单独使用一个
    showInputData = () =>{
        const { current } = this.myRef;
        alert(current.value)
    }
    render() {
        return (
            <div>
                <input ref={ this.myRef } id="input1" type="text" placeholder="点击按钮提示数据"/>&nbsp;
                <button onClick={this.showInputData}>点击提示左边的数据</button>
            </div>
        )
    }
}

关于refs回调中,回调函数是内联函数与回调函数是类绑定函数的区别:

  • 内联函数: 当组件更新时,会触发两次回调,第一次为null,第二次为当前节点,因为react会清空旧的ref并设置新的
  • 类绑定函数: 当组件更新时,react会比较两次函数名相同,只会触发一次回调

React中的事件处理

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

    • React使用的是自定义(合成)事件,而不是原生的DOM事件 — 为了更好的兼容性
    • React中的事件是通过事件委托方式处理的(通过冒泡的形式委托给最外层的元素) — 为了高效
  2. 通过event.target得到发生事件的DOM元素对象 ,如下例,不要过渡使用ref

class Demo extends React.Component {
    render() {
        return <button onClick={ this.logText }>按钮</button>
    }
    logText= (event) => {
        console.log(event.target)
    }
}
ReactDOM.render(<Demo />, document.getElementById("test"))

收集表单数据

HTML 中,表单元素如 <input><select>等 表单元素通常保持自己的状态,并根据用户输入进行更新

而在 React 中,可变状态一般保存在组件的 state(状态) 属性中,并且只能通过setState方法更新。

我们可以通过使 Reactstate 成为 “单一数据源原则” 来结合这两个形式。然后渲染表单的 React 组件也可以控制在用户输入之后的行为。

这种形式,其值由 React 控制的输入表单元素称为“受控组件”。

那么相反的,值并不由 React 进行控制,该组件自己输入,减少等等,该元素成为”非受控组件“。

非受控组件

现用现取,非受控组件

如下例,只有在触发submit提交的时候,触发回调函数,才会从输入框中取值。

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

受控组件

在下面这个实例中,通过绑定输入框的change事件,获取value,将value添加到组件状态(state)中,

然后将state添加到输入框的value,实现数据的双向绑定

class Login extends React.Component {
     state = {
        username: '19812345678', // 添加一个默认值查看效果
         password: ''
     }
     // input框change时的回调,将value添加到state中
     usernameInput = (event) => {
         const { value } = event.target
         this.setState({username: value})
     }
     passwordInput = (event) => {
         const { value } = event.target
         this.setState({password: value})
     }
     handleSubmit = (event) => {
         event.preventDefault() //阻止表单提交
         alert(`用户名:${this.state.username},密码:${this.state.password}`)
     }
     render(){
         return (
             <form action="http://www.zhaojiuyi.top" onSubmit={ this.handleSubmit }>
                 用户名:<input onChange={ this.usernameInput } value={this.state.username} type="text" name="username"/>
                 密码:<input onChange={ this.passwordInput } value={this.state.password} type="password" name="password"/>
                 <button>登录</button>
             </form>
         )
     }
 }

高阶函数与函数柯里化

高阶函数:如果一个函数满足以下2个规范中的任何一个,那么该函数就是高阶函数

  • 若A函数,接受的参数是一个函数,那么A函数就可以称为高阶函数
  • 若A函数,调用A函数得到的返回值依然是一个函数,那么A就可以成为高阶函数
    • 常见的高阶函数:Promise,setTimeOut,forEach.map等

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

class Login extends React.Component {
    state = {
        username: '',
        password: ''
    }
    saveFromData = (dataType) => {
        return (event) => {
            this.setState({[dataType]:event.target.value})
        }
    }
    handleSubmit = (event) => {
        event.preventDefault() //阻止表单提交
        alert(`用户名:${this.state.username},密码:${this.state.password}`)
    }
    render(){
        return (
            <form action="http://www.zhaojiuyi.top" onSubmit={ this.handleSubmit }>
                用户名:<input onChange={ this.saveFromData("username") } value={this.state.username} type="text" name="username"/>
                密码:<input onChange={ this.saveFromData("password") } value={this.state.password} type="password" name="password"/>
                <button>登录</button>
            </form>
        )
    }
}

值得注意的是,此例子使用了saveFromData传入一个数据类型参数,然后返回一个箭头函数作为onChange的回调函数,并接收event参数

也就是说,onChange的回调不再是saveFromData函数,而是它返回的一个箭头函数

我们称saveFromData为高阶函数

而在调用saveFromData方法的同时,我们传入了一个参数,然后使用返回的函数接收了另一个参数,并在返回的函数中使用了统一处理了所有参数,这就是柯里化,看下面这个示例

addNumber = (a) => {
    return (b) => {
        return b + a
    }
}

组件的生命周期

所谓的生命周期,就是指组件从被创建出来,到被使用,更新,最后被销毁的这么一个过程

在这个过程中,React提供了我们会自动执行的不同的钩子函数,我们称之为生命周期函数,或生命周期钩子

先通过一个简单的案例认识一下生命周期

class Life extends React.Component {
    state = {
        opacity:1
    }
    death = () => {
        // 卸载组件
        ReactDOM.unmountComponentAtNode(document.getElementById("test"))
    }
    // 生命周期回调函数 <==>生命周期钩子:组件挂载完成
    componentDidMount() {
        // 每隔200毫秒将透明度减少0.1
        this.interval = setInterval(()=>{
            let {opacity} = this.state
            opacity -= 0.1
            if (opacity <= 0){
                opacity = 1
            }
            this.setState({opacity})
        },100)
    }
    // 生命周期回调函数:组件即将卸载
    componentWillUnmount() {
        //清空定时器
        clearInterval(this.interval)
    }
    render() {
        return (
            <div>
                {/*绑定state状态,修改样式*/}
                <h2 style={{opacity: this.state.opacity}}>React好难啊,不想学了</h2>
                <button onClick={this.death}>不学了</button>
            </div>
        )
    }
}
ReactDOM.render(<Life/>,document.getElementById("test")) //挂载组件

在示例中,定义了一个state,保存组件的透明度状态,我们需要创建一个计时器,每隔一定时间,修改state,将透明度降低

但是我们不能在render中写入我们的定时器,应为定时器会不断更新state,然而state的修改会驱动render方法的调用,这样我们就会不停创建新的计时器,对cpu造成很大的负担

这时候我们希望只在组件挂载时创建一个定时器,这时候就会用到我们的生命周期钩子

componentDidMount表示组件挂载完成时,也就是说组件挂载完成后会调用这个函数
componentWillUnmount表示组件即将销毁,也就是说组件销毁之前会触发这个函数

用一张图片表示组件的生命周期

请添加图片描述

用一个示例来打印表示组件的生命周期,可以打开控制台查看

class Count extends React.Component {

    //构造器
    constructor(props) {
        super(props);
        this.state = {num: 0}
        console.log("Count---constructor")
    }

    //组件即将挂载钩子
    componentWillMount() {
        console.log("Count---componentWillMount")
    }

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

    //即将卸载组件钩子
    componentWillUnmount() {
        console.log("Count---componentWillUnmount")
    }

    /**
     * 控制组件是否更新的钩子(阀门),返回true则允许更新,否则不允许
     * 如果不写该钩子,则默认为true
     */
    shouldComponentUpdate() {
        console.log("Count---shouldComponentUpdate")
        return true
    }

    // 组件将要更新的钩子,接下来调用render
    componentWillUpdate() {
        console.log("Count---componentWillUpdate")
    }

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

    render() {
        console.log("Count---render")
        return (
            <div>
                <h3>{this.state.num}</h3>
                <button onClick={this.addCount}>点击+1</button>
                <button onClick={this.death}>卸载组件</button>
                <button onClick={this.force}>强制更新组件</button>
            </div>
        )
    }

    addCount = () => {
        let {num} = this.state
        this.setState({num: num + 1})
    }
    death = () => {
        // 卸载组件
        ReactDOM.unmountComponentAtNode(document.getElementById("test"))
    }
    force = () => {
        //强制更新组件,不需要通过shouldcomponentUpdate钩子
        this.forceUpdate()
    }
    /*
    * setState后就是shouldComponentUpdate钩子,在到达componentWillUpdate钩子
    * forceUpdate跳过了shouldComponentUpdate,直接到达componentWillUpdate
    * */
}

// 父组件
class A extends React.Component {
    state = {name: "鄙人四个火"}
    changeProps = () => {
        this.setState({name:"太阳当空丶赵"})
    }

    render() {
        return (
            <div>
                <div>组件A</div>
                <button onClick={this.changeProps}>组件传参</button>
                <B name={this.state.name}></B>
            </div>
        )
    }
}

//子组件
class B extends React.Component {
    componentWillReceiveProps(){
        // 当父组件render时,触发此钩子
        console.log("B---componentWillReceiveProps")
    }
    render() {
        console.log("B---render")
        return (
            <div>
                <div>组件B</div>
                <div>name:{this.props.name}</div>
            </div>
        )
    }
}

ReactDOM.render(<Count/>, document.getElementById("test"))
ReactDOM.render(<A/>, document.getElementById("test1"))

打开控制台,点击按钮,查看对应打印的生命周期
请添加图片描述

总结一下:

组件挂载时,会首先触发constructor构造器,然后触发componentWillMount钩子(组件即将挂载,也就是挂载前,此时还未进行挂载),然后执行render方法返回虚拟dom,将虚拟dom转换为真实dom后完成挂载,进入componentDidMount钩子(完成挂载);

组件更新时,如果是使用setState方法进行状态更新,则进入shouldComponentUpdate钩子,判断是否更新状态,就相当于一个开关,如果为false,则不会触发接下来的钩子,用户没有自定义shouldComponentUpdate钩子时,默认为true,否则更具自定义规则进行,随后进入componentWillUpdate钩子(组件即将更新,也就是更新之前),再然后调用render方法,更新组件状态,然后进入componentDidUpdate钩子(更新完成);如果使用forceUpdate方法强制更新组件,则会跳过shouldComponentUpdate钩子(不判断是否更新),直接进入componentWillUpdate,然后调用render,最后componentDidUpdate(更新完成);

如果是父组件触发render方法,则子组件将触发componentWillReceiveProps钩子(即将向子组件传递props,也就是即将向子组件传递参数),然后就和正常更新组件状态的钩子一样,到达shouldComponentUpdate => componentWillUpdate => render => componentDidUpdate;

销毁组件时,进入componentWillUnmount钩子(即将销毁组件,销毁组件可以调用ReactDOM.unmountComponentAtNode(这里传入一个dom节点)),最后完成组件的销毁

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

太阳当空丶赵

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值