Skip to content

2 面向组件

约 1191 个字 387 行代码 预计阅读时间 9 分钟

  • 模块:向外提供特定功能的 JS 程序,一般是单个文件
  • 组件:实现局部功能的代码和资源集合,更具定义方式可以分为两类
    • 函数式组件(简单组件)
    • 类式组件(复杂组件)

1 函数式组件

function Demo() { // 函数名就是 tag 名,首字母大写
    // babel 开启了严格模式,this指向undefined
    return <h2>我是组件demo捏</h2>
}
// 记得首字母大写 + 自结束
ReactDOM.render(<Demo/>, document.getElementById('app')) 

用不了 state & refs ,但是可以用 props 捏!

function Wow(props) {
    const {name,age} = props // 没有 this 捏
    return (
        <h2>我叫{name}</h2>
        <h3>今年{age}</h3>
    )
}
// 限制只能写外面
Wow.propTypes = {}
Wow.defaultProps = {}
// 渲染
ReactDOM.render(<WOw name="Tt" age={13}/>, document.getElementById('app'))

2 类式组件

基本知识

// 创建一个 Person 类
class Person {
    // 构造函数
    constructor(name,age) {
        // this 指向当前实例对象
        this.name = name
        this.age = age
    }
    // 方法 - 在类的「原型对象」上
    show() { // 通过实例调用时,this 指向实例本身
        console.log(`我叫${this.name}, 今年${this.age}岁了`)
    }
}
// 创建一个实例对象(使用new关键字)
const p1 = new Person()

// Student 继承 Person, 继承所有方法(包括构造函数)
class Student extends Person {
    constructor(name, age, grade) {
        super(name, age) // 调用父类构造函数 - 必须放第一行
        this.grade = grade
    }
    // 重写父类方法(因为找本类原型就有了,所以调用的是自己的)
    speak() {
        console.log(`我叫${this.name}, 今年${this.age}岁了, 现在读${this.grade}`)
    }
}
// 调用 speak 过程:本身原型(无)-> 父类原型(有)

2.1 创建组件

// 创建类式组件 - 类名就是组件名
class Demo extends React.Component {
    render() { // 可以没有构造函数,但一定要有 render
        return <h2>This is demo</h2> // 返回渲染的结构
    }
}
// 渲染组件实例
ReactDOM.render(<Demo/>, document.getElementById('app'))
/*
1. React解析标签,找到 Demo 组件
2. 发现该组件是以类形式定义的,new 出该类实例,并调用原型上的 render 方法
3. 将 render 方法返回的虚拟 DOM 渲染到页面上
*/

2.2 State & 事件绑定

其实在 tag 里写 id,然后和以往那样 addEventListener 也行

class Weather extends React.Component {
    constuctor(props) {
        super(props) // 旧版本要传 props 作为参数,现在空着就行
        this.state = { // 类似于 Vue 里的 data,要求写成对象
            isHot: true
        }
        // 解决 undefined 问题:
        // 从原型上抓出后者,然后把 this 指针指向当前实例
        // 最后挂在前者声明的属性上
        this.changeWeather = this.changeWeather.bind(this)
    }
    render() {
        // 注意关键词是 onClick(本类函数要加 this,全局函数直接写名字)
        return <h1 onClick={this.changeWeather}>今天天气很{this.state.isHot?'炎热':'凉爽'}</h1>
    }
    changeWeather() { // 直接调用,默认开启strict,this指向 undefined
        // 直接更改不会触发监视,必须调用 setState
        // 这里直接取反好像会报错,用个中继变量过度一下就行
        var isHot = this.state.isHot
        this.setState({isHot:!isHot}) // 只 update 包含的属性(合并)
    }
}

ReactDOM.render(<Weather/>, document.getElementById('app'))
// 构造函数只调用一次,每次修改状态都再调用一次 render

function change() {
    console.log(this) // 全局函数的 this 指向 undefined
}

简写方式

class Weather extends React.Component {
    constructor(props) {
        super()
    }
    // 固定属性直接赋值
    type = "central"
    // state 也不用写在构造函数里
    state = {
        isHot: true
    }
    // 方法不在原型上,在自身
    changeWeather = () => { // 箭头函数会使用外部函数的this(属于本实例)
        this.setState({isHot:!this.state.isHot})
    }
}

2.3 Props - Read Only

修改 props 则整个页面寄掉

// 浅浅 intro - 在渲染的时候动一点手脚
ReactDOM.render(
    <Demo name="hahah" age={12}/>, // 双引号默认为 str,年龄是 Num,所以加花括号
    document.getElementById('app')
)
/* 此处的 name:"hahah" 将作为键值对塞到 props 中(和 Vue 一样捏)
   同样的,可以通过 {this.props.name} 在结构中进行访问 */

class Demo extends React.Component {
    render() 
        // 使用 const 接手 props 中的变量(类似于 Vue 中用 computed 去接)
        const {name} = this.props
        return <h2>my name is {name}</h2>
        // 下面同理
        return <h2>my name is {this.props.name}</h2>
    }
}

批量导入数据

下面展示ES6的语法糖(注意外部数据的key必须和const接的一致)

const info = {
    name: "TOM",
    age: 12,
    grade: "Three"
}
ReactDOM.render(<Person {...info}/>, document.getElementById('app'))

顺便复习一下展开运算符 ...

// 拼接一下数组捏
let arr1 = [1,3,5,7,9]
let arr2 = [2,4,6,8,10]
let arr3 = [...arr1, ...arr2]

// 函数穿参(计算n个数的和)
function sum(...nums) {
    // 此处的 nums 是一个数组(使用流式计算)
    return nums.reduce((pre,cur) => { return pre + cur})
}
console.log("计算结果是", sum(1,2,3,4,5)) // 直接穿参数(进去会被解析成数组)

// 不能直接展开 Obj
let p1 = {name:'Tom', age:12}
let p2 = p1 // 实际上是引用(指向了同一个地址空间)
// AND...
let p2 = ...p1 // 这是不行的捏
let p2 = {...p1} // 这样是可以的(而且是独立的拷贝)
// 加了花括号的语义是「拷贝」一个对象,而不是「展开」一个对象

// 在 JSX 里 {} 表示内部是 JS表达式,所以其实等价于裸奔的 ...obj
// 这在原生中显然是不被允许的 => 但我们用的是 JSX 捏!(并没有触发复制对象)
// 但只能用于「标签属性传递」,不能再其他地方胡乱泰勒Obj

// 合并对象(后面覆盖前面)
let p3 = {...p1, name:"Jack"}
// 此时 p3.name = "Jack" (Tom被盖掉了)

一些限制

和 Vue 很像捏,可以规定 prop 的类型和默认值

// 单页面文件中需要额外引入
<script src="https://cdnjs.cloudflare.com/ajax/libs/prop-types/15.6.0/prop-types.js"></script>
// 工程文件中使用 npm install --save prop-types 安装
// 随后进行引入 import PropTypes from 'prop-types';
class Person extends React.Conponent {}
// 在类外定义(注意大小写)
Person.propsTypes = {
    name: PropTypes.string,
    // 强制必填 name: PropTypes.string.isRequired
    // 此时不填 warning + 对应字段 undefined
    age: ProtoTypes.number,
    speak: ProtoTypes.func
}
// 默认值(为啥 type 和 default 要分开啊淦)
Person.defaultProps = {
    sex: "未知",
    age: -1
}
// 预期与实际不符时:页面正常显示,但是会有警告
function speak() {console.log("说话哩")}
// 可以通过 speak={speak} 传递

props 的简写

把 type & default 写在类外面多么痴呆啊

一些小小的知识复习:

class Car {
    demo = 100 // demo属性加载「实例」上
    static demo = 300 // 在「类」上 => Car.demo = 300
}
Car.demo = 200 // demo加载了「类」上

console.log(Car.demo) => 200
let car = new Car()
console.log(car.demo) => 100

简写一下 props

class Person extends React.Component {
    // 必须加 static -> 加载「类」上
    static propTypes = {
        name: PropTypes.string
    }
    static defaultProps = {
        sex: "Unknown"
    }
}

ft. 构造器

class Person extends React.Component {
    // 不接也不传,在「实例」中就不能通过 this.props 获取 props
    // (但是不写构造函数就没有问题)
    constructor(props) {
        super(props)
    }
}

2.4 Refs

可以认为类似于原生的 ID 选择器

字符串式(不推荐)

class Demo extends React.Component {
    render() {
        return (
            <div>
                // 使用 ref 表示当前标签
                <input ref="in1" type="text" placeholder="点击按钮显示数据"/>
                <button onClick={this.showData}>点我提示左侧数据</button>
            </div>)
    }
    showData = () => {
        // 使用 this.refs.name 拿到真实DOM
        alert(this.refs.in1.value)
    }
}

回调式

回调函数共调用「两次」,第一次传入NULL,第二次传入真实DOM

因为每次UPDATE的时候要「清空原来的ref」,再传送真正的节点

class Demo extends React.Component {
    render() {
        return (
            <div>
                // 回调接受的参数是真实DOM(函数题把实例挂在自己的属性 input1 上)
                <input ref={curnode=>this.input1 = curnode} type="text" placeholder="点击按钮显示数据"/>
                // 不使用箭头函数 -> 可以确保只调一次(没有NULL那次)
                <input ref={this.save} type="text"/>
                <button onClick={this.showData}>点我提示左侧数据</button>
            </div>)
    }
    save = (cur) => {
        this.input2 = cur
    }
    showData = () => {
        alert(this.input1.value)
    }
}

createRef

缺点:得手动创建很多个容器

class Demo extends React.Component {
    // React.createRef 可以返回一个容器,用于存储 ref 标识的节点
    myRef = React.createRef() // 专人专用(只能给一个人用,后面会覆盖)- 可以取其他名字
    render() {
        return (
            <div>
                <input ref={this.myRef} type="text" placeholder="点击按钮显示数据"/>
                <button onClick={this.showData}>点我提示左侧数据</button>
            </div>)
    }
    showData = () => {
        alert(this.myRef.current.value)
    }
}

3 事件处理

  1. React 中通过 onXxx 属性指定事件处理函数(注意大小写)
    • React 中使用的是自定义事件(不是原生DOM事件)
    • 通过事件委托方式处理(委托给「组件最外层」的元素)
  2. 我们可以通过 event.target 获得「触发」事件的对象
class Demo extends React.Component {
    render() {
        return (
            <div>
                <input onBlur={this.showData} type="text" placeholder="点击按钮显示数据"/>
                <button>点我提示左侧数据</button>
            </div>)
    }
    showData = (e) => {
        // 使用 event.target 获取触发事件的对象
        alert(e.target.value)
    }
}

4 表单数据

  • 非受控组件:只有在“提交表单”时获取元素的最终数据
  • 受控组件:类似于 v-model,随用户输入实时更新 State,使用时直接从 State 中获取

4.1 非受控组件

  • 所有「输入类」DOM 「现用现取」
  • 需要用很多 ref
class Login extends React.Component {
    render() {
        return (
            <form onSubmit={this.handle}>
                用户名<input ref={cur=>this.uname=cur} 
                            type="text" name="username"/>
                密码 <input ref={cur=>this.pwd=cur} 
                            type="password" name="password"/>
                // 点击后携带 query 参数
                <button>登录</button>
            </form>
        )
    }
    handle = (e) => {
        // 阻止默认事件(表单提交跳转)
        e.preventDefault()
        // 获取数据
        const {uname, pwd} = this
        alert(uname.value + "-" + pwd.value)
    }
}

4.2 受控组件

class Login extends React.Component {
    state = {
        username: "",
        password: ""
    }
    render() {
        return (
            <form>
                用户名<input onChange={this.getName} ref={cur=>this.uname=cur} 
                            type="text" name="username"/>
                密码 <input ref={cur=>this.pwd=cur} 
                            type="password" name="password"/>
                // 点击后携带 query 参数
                <button>登录</button>
            </form>
        )
    }
    // 类似于 v-model
    getName = (e) => {
        this.setState({username: e.target.value})
    }
}

5 高阶函数 & 纯函数

高阶函数

  • 接受参数为「函数」/ 返回值为「函数」的函数称为高阶函数
  • 函数柯里化:通过调用函数后返回函数,实现多次接收参数后统一处理

柯里化

相当于将 saveForm 的返回值作为回调(所以返回值也应该是函数)
<input onChange={this.saveForm('username')}/>
<input onChange={this.saveForm('password')}/>
// 批处理
saveForm = (data_item) => {
    return (e) => {
        // [] 表示使用下标访问属性
        this.setState({[data_item]: e.target.value})
    }
}

尝试不用柯里化

// 在内联函数中调用 saveForm,并传入 data_item & event 作为参数
<input onChange={(e)=>{this.saveForm('username', e)}}/>
saveForm = (data_item, e) {
    this.setState({[data_item]: e.target.value})
}

纯函数

同样的输入必定产生同样的输出

  • 不得改写参数数据
  • 不得产生副作用(发送网络请求 / 输入输出)
  • 不能调用 Date.now / Math.random 等方法

6 生命周期

6.1 旧版本

挂载流程

  1. constructor
  2. componentWillMount
  3. render
4. mount
componentDidMount() {
    // 挂载只调用一次(虽然会 update 多次)
    this.timer = setInterval(() => {
        let {opacity} = this.state
        opacity = -= 0.1
        if(opacity <= 0) opacity = 1
        this.setState({opacity})
    }, 200)
}
5. unmount
ReactDOM.unmountComponentAtNode('app') // 手动卸载组件

componentWillUnmount() { // 将要卸载时调用
    clearInterval(this.timer)
}

更新流程

  1. 父组件 render

    在 render 中包含另一组件的 tag 即可称为父组件

  2. componentWillReceiveProps (父子组件数据传递)

    事实上,在「首次接受参数」时不调用(UPDATE props 的时候才开始调用)

  3. shouldComponentUpdate - setState 由此开始

    只能返回 true / false(是否继续向下执行),默认返回 true(继续更新)

  4. componentWillUpdate - forceUpdate 由此开始

    不更改数据的情况下强制更新(不受 should 钩子的限制)

  5. render 自身

  6. componentDidUpdate
  7. componentWillUnmount

小结

旧版本的生命周期大致可以分为三个阶段:

  1. 初始化阶段:初次渲染,由 ReactDOM.render 触发

    • constructor
    • componentWillMount
    • render
    • componentDidMount 一般开启定时器、发送网络请求、订阅消息
  2. 更新阶段:由 this.setState 或 父组件重新 render 触发

    • shouldComponentUpdate
    • componentWillUpdate
    • render
    • componentDidUpdate(prev_props, prev_state, snapshot)
  3. 卸载组件:由 ReactDOM.unmounComponentAtNode 触发
    • componentWillUnmount 一般:关闭定时器、取消订阅操作

6.2 新版本

废弃了 componentWillMount & conponentWillUpdate & componentWillReceiveProps,新增了两个钩子:

  • static getDerivedStateFromProps(props, state) state 取决于 props 时使用(其实在构造函数里也能写)
    • 属于类的静态方法,需要返回 state 对象 / NULL
    • 从 props 返回 computed,不允许修改
    • 若返回的 state 和预定义冲突,以该返回值为准
  • getSanpshotBeforeUpdate 获取快照(在实际渲染前调用)
    • 返回 snapshot / NULL(任何值都可以作为 snapshot),传递给 DidUpate
    • 可以从 DOM 中捕获先前的滚动位置 scrollTop,丢给 DidUpdate 进行复位

小结

  1. 初始化阶段
    • consturctor
    • getDerivedStateFromProps
    • render 初始化 / 更新渲染
    • componentDidMount 开启监听,发送 ajax 请求
  2. 更新阶段
    • getDerivedStateBeforeUpdate
    • shouldComponentUpdate
    • render
    • getSnapshotBeforeUpdate
    • componentDIdUpdate
  3. 卸载阶段
    • componentWillUnmount 收尾,清理定时器

7 PureComponent

  • 「问题」
    • 只要执行 setState 即便是 setState({}) 都会引起当前组件重新 `render
    • 只要父组件重新 render,所有子组件都会重新 render
  • 「解决」
    1. 重写 shouldComponentUpdate,只有 state/props 更新才返回 true js shouldComponentUpdate(nextProps,nextState) { return !this.state.xxx === nextState.xxx return !this.props.xxx === nextPtops. // 只能一个个分量对比 }
    2. 使用 PureComponent 只进行浅比较(比最外层addr),内层数据改变也返回 false jsx import {PureComponent} from 'react' export default class Demo extends PureComponent { this.setState({name:'Tom'}) // 正常检测 // 下面仅修改分量,无法正常检测 const obj = this.state obj.name = 'Tom' this.setState(obj) // 对象地址没有改变(push/unshift也) }

8 Render Props(插槽)

类似于 slot,组件的标签体内容可以通过 this.props.children 获得

Children Props

  1. 传递普通的字符串
    render() {
        return (
            <A>Ahhhhhhh</A>
        )
    }
    // @ A.jsx
    render() {
        return (
            <div>In CompA Receive:{this.props.children}</div>
        )
    }
    
  2. 传递一个组件 js render() { return ( <A> <B/> </A> ) } // 组件B可以通过 children 获取,从而构成父子关系;下面是等价操作

Render Props

<A render={()=><B/>}/>
// 在 A 中可以通过以下方式渲染
{this.props.render()}

// 需要穿参的情景
<A render={(name)=><B name={name}/>}/>
render() {
    const {name} = this.state
    return (
        {this.props.render(name)} // 类插槽,但是指定了 data
    )
}

9 Error Boudary

用于捕获后代组件的错误,渲染备用页面; 不能捕获自身 / 合成事件 / 定时器中产生的错误

// 是一个生命周期函数,后代组件在生命周期中报错时触发(比如render)
static getDerivedStateFormError(err) {
    console.log(err)
    // 在render前触发,返回的是 newState
    return {
        hasError: true // => 可以用 if 判断渲染正常/报错页面
        // be like {this.state,hasError ? 'Sorry' : <Child/>}
    }
}
// 寄了就会调这个函数:发送给后台,统计页面错误
componentDidCatch(err, info) {
    console.log(err, info)
}