思考一个问题:
在DOM结构中的某一个div元素,是一个对象吗?
如果第一时间无法确定的同学可以在浏览器的Console中创建并查看一个div元素,如下图。
很显然,所有的DOM元素其实都是一个对象,而每当我们在html中写入一个div元素时,其实就是相当于new了一个div的实例。
<div></div>
// like
new div()
我想很多人其实没有意识到这一点,在React中,也运用了类似的理念。当我们学习React时,理解到这个知识点非常重要,因为在html中,我们不需要创建DOM元素,而在React中则不同,我们通常情况下会创建自己的组件,然后在jsx中实例化,这简直像极了DOM元素的使用。
那么理解了DOM元素的这个点,并且知道其实React元素其实是非常类似的,那么现在我们思考一下我们会如何创建React组件呢?很显然,React组件实际上就是一个对象,在ES6的语法中,我们使用class
语法来创建对象。因此首先大概会如下写:
class ReactDemo {
}
每一个React组件都内含了许多共同的逻辑,例如生命周期,状态等,因此必然会有基类来实现这些共通的逻辑,所以我们会继承他 React.Component
。
class ReactDemo extends React.Component {
}
我们需要进一步学习的,则就是这些组件共通的特性,他们分别是:
- 生命周期
- state
- props
- ref
学完了这些知识点,你就可以大胆的宣告React你已经学会啦 ~
生命周期函数 render
生命周期中,最常见的就是render。顾名思义,它就是用来渲染模板语言jsx的。状态<state>与属性<props>每次改动,它都会执行一次,因此它是执行次数最多的生命周期方法,同时通过优化代码逻辑,减少它的执行次数,也是优化React程序性能的最常用手段。因为在无意识中,你的逻辑可能会让render无意义的执行更多次。
因此创建组件的下一步,就是渲染模板语言
class ReactDemo extends React.Component {
render() {
return (
<div>hello world!</div>
)
}
}
就这样,我们已经创建了一个最简单的组件。我们已经可以将组件ReactDemo
运用起来了。
import { render } from 'react-dom';
import ReactDemo from './ReactDemo';
render(<ReactDemo />, document.querySelector('#root'));
生命周期函数 componentWillMount
当第一次render执行完,则表示组件已经完全创立。
因此如果我们想要在组件创建完成之前对组件做一些事情,那么我们可以使用componentWillMount
,该组件的应用场景相对比较少,我通常用它来初始化一些无副作用的数据。
无副作用:不会引发组件render函数的执行
class ReactDemo extends React.Component {
currentKey = ''
componentWillMount() {
this.currentKey = this.props.key || 'tony'
}
render() {
return (
<div>hello {this.currentKey}!</div>
)
}
}
生命周期函数 componentDidMount
当组件第一次render执行完,会通过componentDidMount
告诉我们。意味着此时组件处于一个相对稳定的状态,我们可以处理更多的逻辑。例如获取网络请求的数据等。
class ReactDemo extends React.Component {
componentDidMount() {
axios.get('xxxxx').then(res => {
// todo
})
}
render() {
return (
<div>hello wrold!</div>
)
}
}
生命周期函数 componentWillUnmount
当我们切换页面或者需要隐藏掉一些组件时,组件会被销毁,在销毁之前,componentWillUnmount
会告诉我们。在这个钩子函数中,我们常常会手动处理一些扫尾工作,例如取消某些事件监听,清除定时器,重置存储在闭包中的引用等。
class ReactDemo extends React.Component {
componentDidMount() {
document.addEventListener(...)
}
componentWillUnmount() {
document.removeEventListener(...)
}
render() {
return (
<div>hello wrold!</div>
)
}
}
需要注意的是,willMount, didMount, WillUnmount三个生命周期函数在组件的整个生命周期中仅执行一次。
理解了这三个函数的使用,基本已经可以应对大多数场景。因此生命周期函数暂时先介绍到这里,后续再介绍其他的生命周期函数。
我们想要让一个组件呈现出来的样子与逻辑是可控的,那么就必然会引入状态管理的机制。一个组件通常受控于内部状态state与外部状态props
每一个状态的改动,都会引起render函数执行,这一点非常重要。
内部状态state
每个组件实例的内部状态仅仅只需要管理当前实例,因此state必然是挂载在构造函数中,因此我们通常使用如下的方式初始化state
不懂这句的朋友就必须回到<前端基础进阶>中去补充基础知识了,这个点非常重要
class Demo extends React.Component {
constructor(props) {
super(props);
this.state = {
bar: true
}
}
render() {
return (
<div>{this.state.bar ? 'true' : 'false'}</div>
)
}
}
正因为state挂载中构造函数中,知道babel编译结果的同学应该会明白,state的初始化还可以简写成如下方式:
class Demo extends React.Component {
state = {
bar: true
}
render() {
return (
<div>{this.state.bar ? 'true' : 'false'}</div>
)
}
}
如上所示,在使用时,我们通过this.state
来访问state的值。
除此之外,我们还可以通过this.setState
的方式来改变state的值。
class Demo extends React.Component {
state = {
data: []
}
componentDidMount() {
axios.get('/group/users').then(res => {
this.setState({
data: res.data
})
})
}
render() {
const { data } = this.state;
return (
<div>
{data.map((item, key) => (
<div key={key}>{item.name}</div>
))}
</div>
)
}
}
在这个例子中,render函数会执行两次,第一次为组件初始化时,data没有数据。第二次是接口请求成功后,我们手动修改了state的值,引发一次render的修改。
在这里我们可以总结几个非常有意思的点:
- 当我们想要改变页面时,不再直接修改DOM元素,而仅仅只是修改了state中的数据。这个思维的转变也是React的核心所在,它直接改变了我们前端的开发思维与开发模式
- render会因为状态的变化而多次执行,而状态的变化不可预测,因此计算量大的逻辑通常不会放在render中来执行,否则会带来更高的性能消耗
- 当组件状态首次稳定了之后我们才会尝试去修改它,因此通常我们会将数据请求放在
componentDidMount
中执行 - setState是一个异步操作。明白事件循环的同学知道了这个点之后坑定能够瞬间明白它可能会带来的麻烦与小坑。所以,如果还不知道事件循环的同学得回过头去补补知识了
大家还可以深入思考一下,为什么setState需要异步的方式,如果就弄成同步的方式会引发什么麻烦?<面试必备>
外部状态props
<div className="foo" index="1" dataset="xxx"></div>
熟练使用html的同学对上面例子中的class, index, dataset
肯定不会陌生。当我们用这种方式尝试从外部控制组件的呈现时,就需要用到props。在div组件的内部,我们可以使用如下的方式访问外部传进来的参数。
this.props.className
this.props.index
this.props.dataset
我们可以使用defaultProps
来设置props的默认值
例如我们想要创建一个方块,在创建时我们可以随意定制它的颜色,长宽。那么我们可以简单如下实现。
class Rect extends React.Component {
static defaultProps = {
color: 'red',
width: '10px',
height: '10px'
}
render() {
const { color, width, height } = this.props;
const _style = {
color, width, height
}
return (
<div style={_style} />
)
}
}
<Rect />
<Rect color="orange" width="20px" height="40px" />
我们不能直接在内部修改props的状态
传入到组件中的props发生变化时,render函数仍然会执行一次
生命周期函数 componentWillReceiveProps
每次接收到新的props之前<组件初始化除外>,componentWillReceiveProps
都会执行一次,因此我们可以利用它来判断某些关键的props是否发生了变化。
在该函数中,旧的props使用this.props
访问,而新的props会通过参数传入
componentWillReceiveProps(nextProps) {
if (this.props.bar === nextProps.bar) {
return;
}
}
需要警惕的是,许多人常常在想不到其他办法的情况下会在该函数中根据props来修改内部状态state。这是一个非常危险的行为。特别是当前组件拥有大量子元素时,这种行为会给你的代码性能带来严峻的考验,甚至可能是异常灾难。
// 例如随便摘抄一个例子
componentWillReceiveProps: function(nextProps) {
this.setState({
likesIncreasing: nextProps.likeCount > this.props.likeCount
});
}
我通常只在没有什么子元素的组件中使用这个生命周期函数,其他场景会想办法避开这个雷区。
另外update相关的几个生命周期如果使用不善也会存在性能问题,因此也不建议大家在实践中使用,非要使用时请千万要谨慎