React 面向组件编程 | 青训营笔记

76 阅读12分钟

这是我参与【第四届青训营】笔记创作活动的第9天。

1、函数式组件

// 1. 创建函数式组件 
function MyComponent(){ 
    console.log(this); //此处的this是undefined,因为babel编译后开启了严格模式 
    return <h2>我是用函数定义的组件(适用于简单组件的定义)</h2> 
} 
// 2. 渲染组件到页面 
ReactDOM.render(<MyComponent/>, document.getElementById('test'))

实现效果

image.png

问题:执行了ReactDOM.render(.......之后,发生了什么?

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

【补充】:严格模式中的 this。

function sayThis() { 
    console.log(this) 
} 
sayThis() // Window {...} 

function sayThis2() { 
    'use strict' 
    console.log(this) 
}
sayThis2() // undefined
  • 如果不开启严格模式,直接调用函数,函数中的 this 指向 window
  • 如果开启了严格模式,直接调用函数,函数中的 this 是 undefined

2、类式组件

//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'))

实现效果

image.png

image.png

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

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

【补充】关于ES6中类的注意事项

  1. 类中的构造器不是必须要写的,要对实例进行一些初始化的操作,如添加指定属性时才写。
  2. 如果 A 类继承了 B 类,且 A 类中写了构造器,那么 A 类构造器中的 super 是必须要调用的。
  3. 类中所定义的方法,都放在了类的原型对象上,供实例去使用。

注意

  1. 组件名必须首字母大写
  2. 虚拟 DOM 元素只能有一个根元素
  3. 虚拟 DOM 元素必须有结束标签

重点关注下渲染类组件标签的基本流程

  1. React 内部会创建组件实例对象
  2. 调用 render() 得到虚拟 DOM, 并解析为真实 DOM
  3. 插入到指定的页面元素内部

3、组件实例的三大核心属性

3.1、state

3.1.1、理解

  1. state 是组件对象最重要的属性, 值是对象(可以包含多个 key-value 的组合)
  2. 组件被称为“状态机”, 通过更新组件的 state 来更新对应的页面显示(重新渲染组件)

3.1.2、应用

需求: 定义一个展示天气信息的组件

  • 默认展示天气炎热或凉爽
  • 点击文字切换天气

3.1.3、手动切换版

类式组件,在构造器中初始化状态,在 render 中通过 this.state 读取状态。

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

实现效果

image.png

【补充】原生 JavaScript 绑定事件监听的三种方式

<button id="btn1">按钮1</button> 
<button id="btn2">按钮2</button> 
<button onclick="demo()">按钮3</button> 
<script type="text/javascript" > 
    const btn1 = document.getElementById('btn1') 
    btn1.addEventListener('click',()=>{ alert('按钮1被点击了') }) 
    const btn2 = document.getElementById('btn2') 
    btn2.onclick = ()=>{ alert('按钮2被点击了') } 
    function demo(){ alert('按钮3被点击了') } 
</script>

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

class Person { 
    constructor(name,age){ 
        this.name = name 
        this.age = age 
    } 
    study(){ 
        //study方法放在了哪里?——类的原型对象上,供实例使用 
        //通过Person实例调用study时,study中的this就是Person实例 
        console.log(this); 
    }
}
const p1 = new Person('tom',18) 
p1.study() 
//通过实例调用study方法 
Person const x = p1.study 
x() // 直接调用 undefined

3.1.4、点击切换版

// 1.创建组件
class Weather extends React.Component { 
    // 构造器调用几次? ———— 1次 
    constructor(props){ 
        console.log('constructor'); 
        super(props) // 初始化状态 
        this.state = {isHot:false,wind:'微风'} 
        // 解决 changeWeather 中 this 指向问题 
        this.changeWeather = this.changeWeather.bind(this) 
    } 
    
    // render调用几次? ———— 1+n次 1是初始化的那次 n是状态更新的次数
    render(){
        console.log('render'); 
        //读取状态 
        const {isHot,wind} = this.state 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'))

实现效果

image.png

3.1.5、精简代码(实际开发写法)

  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>
    } 
    // 自定义方法————要用赋值语句的形式+箭头函数 
    // 没有放在原型上,而是放在实例上 
    changeWeather = () => { 
        const isHot = this.state.isHot 
        this.setState({isHot:!isHot}) 
    }
} 
// 2.渲染组件到页面 
ReactDOM.render(<Weather/>,document.getElementById('test'))

【补充】类中直接写赋值语句:在类中直接写赋值语句,等于是给类的实例对象添加属性且赋值。

注意

  1. 组件中 render 方法中的 this 为组件实例对象。
  2. 组件自定义的方法中 this 为 undefined,如何解决?
    1. 强制绑定 this:通过函数对象的 bind() 方法
    2. 箭头函数+赋值语句(推荐)
  3. 状态数据 state,不能直接修改或更新。
  4. 状态必须通过 setState() 进行更新,且更新是一种合并,不是替换。

3.2、props

3.2.1、理解

  1. 每个组件对象都会有 props 属性。
  2. 组件标签的所有属性都保存在 props 中。

3.2.2、作用

  1. 通过标签属性从组件外向组件内传递变化的数据。
  2. 注意:组件内部不可修改 props 数据,是只读的。

3.2.3、浅试一下

// 创建组件
class Person extends React.Component{
    render() { 
        return (
            <ul> 
                <li>姓名:{this.props.name}</li> 
                <li>性别:{this.props.sex}</li> 
                <li>年龄:{this.props.age}</li> 
            </ul> 
        )
    } 
} 
// 渲染组件到页面上 
ReactDOM.render(<Person name="yk" age="18" sex="男"/>, document.getElementById('test'))

实现效果image.png

3.2.4、使用指南

1、内部读取某个属性值

this.props.name

2、扩展属性:将对象的所有属性通过props传递(批量传递标签属性)

ReactDOM.render(<Person name="yk" age="18" sex="男"/>, document.getElementById('test')) 

const person = {name: 'yk', age: 18, sex: '男'}
//...person在原生JS中会报错,但是在Babel和react的加持下是成立的 
//但是仅仅限于标签属性的传递 
ReactDOM.render(<Person { ...person }/>, document.getElementById('test'))

【补充】展开运算符

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、对props中的属性值进行类型限制和必要性限制

  1. 第一种方式(React v15.5 开始已弃用)
Person.propTypes = {
    name: React.PropTypes.string.isRequired, 
    age: React.PropTypes.number 
}
  1. 第二种方式(新):使用prop-types库进限制(需要引入 prop-types 库)
<!-- 引入prop-types,用于对组件标签属性进行限制 --> 
<script type="text/javascript" src="../js/prop-types.js"></script>
//对标签属性进行类型、必要性的限制 
Person.propTypes = { 
    name:PropTypes.string.isRequired, // 限制name必传,且为字符串
    sex:PropTypes.string, // 限制sex为字符串 
    age:PropTypes.number, // 限制age为数值 
    speak:PropTypes.func, // 限制speak为函数
}

简写方式:可以写在类的里面,前面加 static 关键字。

4、默认属性值

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

简写方式:可以写在类的里面,前面加 static 关键字。

注意:props 不能修改值,他是只读的。

5、组件类的构造函数

constructor(props){
    super(props) 
    console.log(this.props)//打印所有属性
}

image.png

构造器是否接收 props,是否传递给 super。

取决于:是否希望在构造器中通过 this 访问 props。

3.2.5、应用

求需: 自定义用来显示一个人员信息的组件

  1. 姓名必须指定,且为字符串类型;
  2. 性别为字符串类型,如果性别没有指定,默认为男
  3. 年龄为字符串类型,且为数字类型,默认值为 18
//创建组件
class Person extends React.Component{
    //对标签属性进行类型、必要性的限制 
    static propTypes = { 
        name:PropTypes.string.isRequired, //限制name必传,且为字符串 
        sex:PropTypes.string,//限制sex为字符串
        age:PropTypes.number,//限制age为数值 
    } 
    //指定默认标签属性值 
    static defaultProps = {
        sex:'男',//sex默认值为男
        age:18 //age默认值为18 
    } 
    
    render(){
        // console.log(this);
        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> 
        )
    } 
}
//渲染组件到页面 
ReactDOM.render(<Person name="jerry"/>,document.getElementById('test1'))

实现效果

image.png

3.2.6、函数式组件使用 props

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

注意:函数组件中,可以接收并使用 props 参数,但是 state 和 refs 只能在类式组件中使用!!!

3.3、refs 与事件处理

3.3.1、理解

组件内的标签可以定义 ref 属性来标识自己。

3.3.2、应用

需求: 自定义组件, 功能说明如下

  1. 点击按钮, 提示第一个输入框中的值
  2. 当第 2 个输入框失去焦点时, 提示这个输入框中的值

image.png

3.3.3、编码

1、字符串形式的 ref(新版本不推荐使用了)

  1. 定义
<input ref="input1"/>
  1. 使用
this.refs.input1

image.png

注意:String 类型的 Refs 效率不高

  1. 示例
//创建组件
class Demo extends React.Component{ 
    //展示左侧输入框的数据 
    showData = ()=>{
        const {input1} = this.refs 
        alert(input1.value)
    } 
    //展示右侧输入框的数据 
    showData2 = ()=>{ 
        const {input2} = this.refs 
        alert(input2.value) 
    } 
    
    render(){
        return( 
            <div>
                <input ref="input1" type="text" placeholder="点击按钮提示数据"/> 
                <button onClick={this.showData}>点我提示左侧的数据</button> 
                <input ref="input2" onBlur={this.showData2} type="text" placeholder="失去焦点提示数据"/>
            </div> 
        )
    }
} 
//渲染组件到页面 
ReactDOM.render(<Demo />,document.getElementById('test'))

2、回调形式的 ref

  1. 定义
<input ref={(currentNode)=>{this.input1 = currentNode}} />

简写

<input ref={ c => this.input1 = c } />
  1. 使用
this.input1
  1. 示例
//创建组件
class Demo extends React.Component{ 
    //展示左侧输入框的数据 
    showData = ()=>{ 
        const {input1} = this alert(input1.value)
    }
    //展示右侧输入框的数据
    showData2 = ()=>{
        const {input2} = this alert(input2.value)
    }
    saveInput = (c)=>{
        this.input1 = c; 
    }
    render(){
        return(
            <div>
                // 内联式回调 
                <input ref={ c => this.input1 = c; } type="text" placeholder="点击按钮提示数据"/>  
                // 类绑定式回调 
                <input ref={ this.saveInput } type="text" placeholder="点击按钮提示数据"/>  
                <button onClick={this.showData}>点我提示左侧的数据</button>  
                <input onBlur={this.showData2} ref={c => this.input2 = c } type="text" placeholder="失去焦点提示数据"/> 
            </div>
        )
    } 
} 
//渲染组件到页面 
ReactDOM.render(<Demo />,document.getElementById('test'))
  1. 回调执行次数

内联的回调:渲染时调用一次,每次更新都会执行两次。

类绑定的回调:就在初始渲染时调用一次。

影响不大,日常开发基本都用内联,方便一点。

image.png

补充:JSX 中的注释:{/* xxxxxx */}

3、createRef 创建 ref 容器

  1. 定义
// React.createRef调用后可以返回一个容器 
// 该容器可以存储被ref所标识的节点,该容器是“专人专用”的
myRef = React.createRef()
<input ref={this.myRef}/>
  1. 使用
this.myRef.current
  1. 示例
//创建组件
class Demo extends React.Component{ 
    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" placeholder="点击按钮提示数据"/> 
                <button onClick={this.showData}>点我提示左侧的数据</button> 
                <input onBlur={this.showData2} ref={this.myRef2} type="text" placeholder="失去焦点提示数据"/>
            </div>
        )
    }
}
//渲染组件到页面 
ReactDOM.render(<Demo />,document.getElementById('test'))

3.3.4、React 中的事件处理

  1. 通过 onXxx 属性指定事件处理函数(注意大小写)。
    1. React 使用的是自定义(合成)事件, 而不是使用的原生 DOM 事件。
      目的:为了更好的兼容性。
    2. React 中的事件是通过事件委托方式处理的(委托给组件最外层的元素)。
      目的:为了高效。
  2. 通过 event.target 得到发生事件的 DOM 元素对象

注意:不要过度使用 ref。

image.png

发生事件的元素是需要操作的元素时,可以避免使用 ref。

//创建组件
class Demo extends React.Component{ 
    //创建ref容器 
    myRef = React.createRef()
    // myRef2 = React.createRef() 
    //展示左侧输入框的数据 
    showData = (event)=>{
        console.log(event.target); // <button>点我提示左侧的数据</button> 
        alert(this.myRef.current.value); 
    } 
    //展示右侧输入框的数据 
    showData2 = (event)=>{ 
        alert(event.target.value);
    }
    render(){ 
        return( 
            <div> 
                <input ref={this.myRef} type="text" placeholder="点击按钮提示数据"/>  
                <button onClick={this.showData}>点我提示左侧的数据</button>  
                <input onBlur={this.showData2} type="text" placeholder="失去焦点提示数据"/>  
            </div> 
        )
    }
}
//渲染组件到页面
ReactDOM.render(<Demo />,document.getElementById('test'))

3.3.5、收集表单数据

1、理解

包含表单的组件分类。

  1. 受控组件
  2. 非受控组件

2、应用

需求:定义一个包含表单的组件,输入用户名密码后,点击登录提示输入信息。

3、非受控组件

页面中所有输入类DOM的值,都是现用现取的。

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

实现效果

image.png

4、受控组件

页面中输入类的 DOM,随着输入的过程,将数据存储在状态 state 中,需要用的时候在从状态 state 中取(有点类似 Vue 中的双向数据绑定)

// 创建组件
class Login extends React.Component { 
    // 初始化状态 
    state = { username: '', password: '' }
    // 保存用户名到状态中
    saveUsername = (event) => { 
        this.setState({username: event.target.value}) 
    } 
    // 保存密码到状态中 
    savePassword = (event) => { 
        this.setState({password: event.target.value}) 
    } 
    // 表单提交的回调
    handleSubmit = (event) => {
        event.preventDefault() const {username, password} = this.state alert(`您输入的用户名是 ${username},您输入的密码是:${password}`) 
    } 
    render() {
        return ( 
            <form action="https://www.baidu.com/" onSubmit={this.handleSubmit}> 
                用户名:<input onChange={this.saveUsername} type="text" name="username" /> 
                密码:<input onChange={this.savePassword} type="password" name="password" /> 
                <button>登录</button>
            </form>
        )
    }
} 
// 渲染组件 
ReactDOM.render(<Login />, document.getElementById('test'))

实现效果:

image.png

3.3.6、高阶函数与函数的柯里化

  1. 高阶函数

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

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

常见的高阶函数有:Promise、setTimeout、arr.map()等等。

  1. 函数的柯里化

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

function sum1(a, b, c){ 
    return a + b + c; 
} 
sum1(1, 2, 3) 
// 柯里化后
function sum(a){ 
    return(b)=>{
        return (c)=>{ 
            return a+b+c 
        }
    } 
}
sum(1)(2)(3)
  1. 利用高阶函数与函数柯里化简写 5.4 代码
//创建组件
class Login extends React.Component{ 
    //初始化状态 
    state = { 
        username:'', //用户名 
        password:'' //密码 
    } 
    //保存表单数据到状态中 (高阶函数+函数柯里化) 
    saveFormData = (dataType)=>{ 
        return (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={this.saveFormData('username')} type="text" name="username"/> 
                密码:<input onChange={this.saveFormData('password')} type="password" name="password"/> 
                <button>登录</button>
            </form> 
        ) 
    } 
} 
//渲染组件
ReactDOM.render(<Login/>,document.getElementById('test'))
  1. 不用柯里化实现 6.3
//保存表单数据到状态中 
saveFormData = (dataType,event)=>{
    this.setState({[dataType]:event.target.value}) 
} 
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>
    ) 
}