一、创建一个类组件
import React from 'react';
class B extends React.Component {
constructor(props) {
super(props);
}
render() {
return (
<div>hi</div>
)
}
}
export default B;
//extends, constructor, super强行记忆,别问原因
不需要额外代码时,
constructor(props) {
super(props);
}
可以删掉
浏览器不支持ES6的这种写法怎么办:用webpack+babel将ES6翻译成ES5即可。
二、Props-外部数据/属性
定义
props
是组件对外的数据接口
作用
-
接受外部数据
- 只能读取不能修改写入
- 外部数据由父组件传递
-
接受外部函数
- 在子组件调用外部函数
- 该函数一般为父组件的函数
代码实例
- 初始化
props
// construtor和super可以被省略不写,或者必须写全套
class B extends React.Component {
constructor(props) {
super(props);
}
render(){}
}
通过初始化props
, this.props
就是外部数据对象的地址
注意:constructor
初始化函数里边,如果还需要写其他东西,比如 this.state
等,那就必须把这三行写全,必须把 props
参数写在那。
- 传入
props
给B组件
class Parent extends React.Component {
constructor(props){
super(props)
this.state = {name:'frank'}//外部数据一般都是来自于父元素的state
}
onClick = ()=>{}
render(){
return <B name={this.state.name} onClick={this.onClick}>hi</B>
//里面的name(来源:state)和onClick(来源:onClick = ()=>{})就是props
//此处的onClick是一个回调
}
// parent组件传入`props`给`B`组件, 外部数据被包装成为一个对象
传进去的 props
会被包装成一个对象,{name:'frank',onClick:...,children:'hi'}
- 读取
props(this.props.xxx)
class B extends React.Component {
constructor(props) {
super(props);
}
render(){
return
<div onClick={this.props.onClick}>
{this.props.name}
<div>
{this.props.children}
</div>
</div>
}
}
// `B`组件通过`this.props.xxx`读取`props`
注意:子组件禁止修改写入props
理由:
- 修改
props
的值,即修改外部数据的地址。例如this.props = {/*另外一个对象*/}
。既然是外部数据,就应该由外部更新 - 修改
props
的属性,例如this.props.xxx ='hi
。既然是外部数据,就不应该从内部组件来修改值 - 外部数据就由外部数据的主人,即父组件对其进行修改
- 相关钩子
componentWillReceiveProps
- 当组件接受新的
props
时,会触发特殊的函数,即钩子hooks
- 现在更名为
UNSAFE_componentWillReceiveProps
,但是已经被弃用。不推荐使用该钩子
三、State&setState-内部数据
state
是组件对内的数据接口
- 初始化
state
class B extends React.Component {
constructor(props) {
super(props);
this.state = {
user: {name:'frank', age:18}
}
}
render() { /* ... */ }
}
- 读取
state
this.state.user
- 修改/写
state
:this.setState(newState, fn)
this.setState({x: this.state.x+1}) // 或者
this.setState((state)=>({x:state.x+1})) // 推荐这种方法
// setState是异步操作,不会立刻改变`this.state`,会等同步任务执行完,再去更新this.state,从而触发UI更新
// show merge 会将新的state和旧的state进行合并
this.setState((state,props)=> newState,fn)
// 也推荐使用这种方式,更好理解,回调函数fn会在写入成功后执行
注意:React 只会检查新 state 和旧 state 第一层的区别,并把新 state 缺少的数据从旧 state 里拷贝过来
四、生命周期
1. constructor
用途:
- 初始化
props
- 初始化
state
,但此时不能调用setState - 用来写
bind this
constructor(){
/*其他代码略*/
this.onClick = this.onClick.bind(this)
}
//可以用新语法代替
onClick = ()=> {}
constructor(){ /* */ }
- 如果只需要初始化props,可以不写
constructor
React组件在创建的时候,会调用这个函数
2. shouldComponentUpdate
用途:
- 返回false,表示阻止UI更新
- 返回true,表示不阻止UI更新
示例
import React from 'react';
class App extends React.Component {
constructor(props) {
super(props)
this.state = {
n: 1
};
}
onClick = () => {
this.setState(state => ({
n: state.n + 1
}));
this.setState(state => ({
n: state.n - 1
}));
}
render() {
console.log('render了一次')
return (
<div>
App
<div>
{this.state.n}
<button onClick={this.onClick}>+1</button>
</div>
</div>
)
}
}
export default App;
假设有这样一种情况:点击按钮后,对n做了一系列操作,最后发现n的值没变,还是1。(用先+1,再-1模拟)
点击按钮,发现页面上的n没变,还是1.这是没问题的。因为n的值没变。
但是,log打印了两次。为什么n的值没变,还是调用了render呢?
虽然n的值前后都是1,但是 {n:1}
和 {n:1}
是两个不同的对象,地址不同。所以React认为数据变了,就去调用了 render
函数。然后生成了新的虚拟DOM。对比前后两个虚拟DOM,发现n的值都是1,没有不同的地方,就没有更新UI。但是 render
确是被调用了
所以其实从调用 render
往后,生成新DOM,对比,发现没有不同,停止更新,这几步都是多余的。是数据的地址变了,让React误以为数据变了,但是最后又发现没变,所以中间的那些步骤就多余了。
能不能在n的值没变的情况下,就不让 render
被调用了呢?shouldComponentUpdate
钩子可以做到
import React from 'react';
class App extends React.Component {
... ...
}
shouldComponentUpdate(newProps, newState) {
if (newState.n === this.state.n) {
return false
} else {
return true
}
}
render() {
console.log('render了一次')
... ...
}
export default App;
在这个函数里判断:如果新的n和当前的n一样,就返回false,不用更新了。(手动告诉React,数据没变,不要去render了)这时再点击按钮,就没有log了,说明render没有被调用了。
面试常问:shouldComponentUpdate有什么用? 答:它允许我们手动判断是否要进行组件更新。我们可以根据应用场景灵活地设置返回值,以避免不必要的更新。
React.PureComponent
代替 React.Component
上边的 shouldComponentUpdate
钩子还要自己手动判断,有点麻烦,于是React内置了这个功能 ,叫做 React.PureComponent
。在 class extends
后边继承它,就相当于 should
的功能 。
PureComponent
会在 render
之前对比新 state 和旧 state 的每一个 key,以及新 props 和旧 props 的每一个 key。
如果所有 key 的值全都一样,就不会 render;如果有任何一个 key 的值不同,就会 render。(只对比最外面一层)
如果所有 key 的值全都一样,就不会 render
;如果有任何一个 key 的值不同,就会 render
。
class App extends React.PureComponent {}
3. render
用途:
- 创建虚拟DOM,展示视图
return (<div>...</div>)
- return 的元素,只能有一个根元素。
- 如果有两个根元素,要么用一个
<div>
包起来,要么用<React.Fragment></React.Fragment>
包起来, 可以缩写为<></>
- 他们的区别是:
<div>
会被渲染到页面中,而<React.Fragment></React.Fragment>
不会被渲染。
render里可以写任何JS代码,
if...else...
class App extends React.PureComponent {
constructor(props){
super(props)
this.state={
n:1,
array:[1,2,3]
}
}
render() {
if (this.state.n % 2 === 0) {
return <div>偶数</div>
} else {
return <span>奇数</span>
}
}
? :
class App extends React.PureComponent {
... ...
}
render() {
return (
<>
{this.state.n % 2 === 0 ?
<div>偶数</div>:
<span>奇数</span>}
<button onClick={ this.onClick}>+1</button>
</>
)
}
&&
class App extends React.PureComponent {
... ...
}
render() {
return (
<>
{this.state.n % 2 === 0 && <div>偶数</div>}
<button onClick={ this.onClick}>+1</button>
</>
)
}
-
但是render里不能直接写
for循环
,因为render是需要return
的,如果在for循环
里写return
,就只能循环第一次,就直接退出循环了。可以借助数组,循环的时候,把结果push进数组,循环结束后,return 数组。
class App extends React.PureComponent {
... ...
}
render() {
let result=[]
for (let i = 0; i < this.state.array.length; i++) {
result.push(this.state.array[i])
}
return result
}
- 或者使用
array.map()
实现循环
class App extends React.PureComponent {
... ...
}
render(){
return (
this.state.array.map(item=><div key={item}>{item}</div>)
);
}
}
注意:所有循环都需要一个不重复的key
4. componentDidMount
用途:
- 在元素插入页面后执行代码,这些代码依赖DOM
- 此处可以发起加载数据的AJAX请求(官方推荐)
- 首次渲染会执行此钩子
具体解释:
componentDidMount()
会在组件挂载后(插入 DOM 树中,出现在页面)立即调用。依赖于 DOM 节点的初始化应该放在这里。如需通过网络请求获取数据,此处是实例化请求的好地方(发起加载数据的AJAX请求)。当然,首次渲染会执行这个钩子
应用场景:在页面渲染一个div,同时展示这个div的宽度。
解析:要展示一个div的宽度,前提是它已经出现在页面里了,才能去获取。如果在 constructor
里获取,肯定是拿不到的,因为 constructor
是在元素出现在内存后被调用的,此时还没被挂载到页面里。所以需要使用该钩子,在组件挂载后,去获取div的宽度
class App extends React.PureComponent {
constructor(props){
super(props)
this.state={
n:1,
width:undefined
}
}
componentDidMount(){
const div=document.querySelector('#xxx')
const width=div.getBoundingClientRect().width
//析构:const {width}=div.getBoundingClientRect()
this.setState({width:width})
//析构:this.setState({width})
}
render(){
return (
<div id="xxx">
hello world {this.state.width}px
</div>
);
}
}
访问页面里的DOM节点的第二种方式-Refs
上边我们通过id访问DOM节点。React提供了一种访问页面里的DOM节点的方式-Refs
class App extends React.PureComponent {
divRef=undefined// 可以先声明一下,表示我之后要动态创建Ref
constructor(props){
super(props)
this.state={
width:undefined
}
this.divRef=React.createRef() // 1. 创建Refs
}
componentDidMount(){
const div=this.divRef.current // 3. current属性访问节点
const width=div.getBoundingClientRect().width
this.setState({width:width})
}
render(){
return (
<div ref={this.divRef}> // 2. 通过ref属性,关联到元素上
hello world {this.state.width}px
</div>
);
}
}
好处是不会出现id冲突的问题。因为创建的时候,divRef是绑定到this上,也就是实例上的,而不是state上。
5.componentDidUpdate(prevProps, prevState, snapshot)
用途:
- 在视图更新后执行代码
- 首次渲染不会执行此钩子(因为没更新)
- 在此处
setState
可能会引起无限循环,除非放在if里 - 若
shouldComponentUpdate
返回false,则不触发此钩子 - 如果要发起AJAX请求,除了可以在
componentDidAmount
里请求加载数据,还可以在这个钩子里也可以发起AJAX请求,不过是用于更新数据- 官方文档: 如果你对更新前后的 props 进行了比较,也可以选择在此处进行网络请求。(例如,userid改变,需要获取新的用户信息)。
componentDidUpdate(prevProps) {
// 典型用法(不要忘记比较 props):
if (this.props.userID !== prevProps.userID) {
this.fetchData(this.props.userID);
}
}
就是说,假如用户的id变了,那就需要发起一个网络请求获取新的用户信息,这时网络请求就要在componentDidUpdate里做,因为视图更新了才知道用户信息变了。
6. componentWillUnmount()
用途:
- 组件将要被移出页面(取消挂载)然后销毁(从内存消失)时执行
- unmount过的组件不会再次mount
例如:
- 在
componentDidMount
里面监听了window scroll- 那么就要在
componentWillUnmount()
取消监听
- 那么就要在
- 在
componentDidMount
里面创建了Timer- 那么就要在
componentWillUnmount()
取消Timer
- 那么就要在
- 在
componentDidMount
里面创建了AJAX请求- 那么就要在
componentWillUnmount()
取消请求
- 那么就要在
- 原则:谁污染谁治理
总结
分阶段看一下这些钩子会在什么时候执行,以及他们的执行顺序:
constructor()
初始化state和props,创建元素shouldComponentUpdate()
return false阻止更新,不要忘了return true,否则会变成undefinedrender()
渲染、创建虚拟DOMcomponentDidMount()
组件已出现在页面componentDidUpdate()
组件已更新componentWillUnmount()
组件将死