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 事件处理
- React 中通过
onXxx
属性指定事件处理函数(注意大小写)- React 中使用的是自定义事件(不是原生DOM事件)
- 通过事件委托方式处理(委托给「组件最外层」的元素)
- 我们可以通过
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)}}/>
纯函数
同样的输入必定产生同样的输出
- 不得改写参数数据
- 不得产生副作用(发送网络请求 / 输入输出)
- 不能调用 Date.now / Math.random 等方法
6 生命周期
6.1 旧版本
挂载流程
constructor
componentWillMount
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)
}
更新流程
-
父组件
render
在 render 中包含另一组件的 tag 即可称为父组件
-
componentWillReceiveProps
(父子组件数据传递)事实上,在「首次接受参数」时不调用(UPDATE props 的时候才开始调用)
-
shouldComponentUpdate
- setState 由此开始只能返回 true / false(是否继续向下执行),默认返回 true(继续更新)
-
componentWillUpdate
- forceUpdate 由此开始不更改数据的情况下强制更新(不受 should 钩子的限制)
-
render
自身 componentDidUpdate
componentWillUnmount
小结
旧版本的生命周期大致可以分为三个阶段:
-
初始化阶段:初次渲染,由
ReactDOM.render
触发constructor
componentWillMount
render
componentDidMount
一般开启定时器、发送网络请求、订阅消息
-
更新阶段:由
this.setState
或 父组件重新render
触发shouldComponentUpdate
componentWillUpdate
render
componentDidUpdate(prev_props, prev_state, snapshot)
- 卸载组件:由
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 进行复位
小结
- 初始化阶段
consturctor
getDerivedStateFromProps
render
初始化 / 更新渲染componentDidMount
开启监听,发送 ajax 请求
- 更新阶段
getDerivedStateBeforeUpdate
shouldComponentUpdate
render
getSnapshotBeforeUpdate
componentDIdUpdate
- 卸载阶段
componentWillUnmount
收尾,清理定时器
7 PureComponent
- 「问题」
- 只要执行
setState
即便是setState({})
都会引起当前组件重新 `render - 只要父组件重新
render
,所有子组件都会重新render
- 只要执行
- 「解决」
- 重写
shouldComponentUpdate
,只有 state/props 更新才返回true
js shouldComponentUpdate(nextProps,nextState) { return !this.state.xxx === nextState.xxx return !this.props.xxx === nextPtops. // 只能一个个分量对比 }
- 使用
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
- 传递普通的字符串
- 传递一个组件
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)
}