本文来自#React系列教程:https://mp.weixin.qq.com/mp/appmsgalbum?__biz=Mzg5MDAzNzkwNA==&action=getalbum&album_id=1566025152667107329)
很多的事物都有从创建到销毁的整个过程,这个过程称之为是生命周期;
React组件也有自己的生命周期,了解组件的生命周期可以让我们在最合适的地方完成自己想要的功能;
生命周期和生命周期函数的关系:
componentDidMount
函数:组件已经挂载到DOM上时,就会回调;componentDidUpdate
函数:组件已经发生了更新时,就会回调;componentWillUnmount
函数:组件即将被移除时,就会回调;我们可以在这些回调函数中编写自己的逻辑代码,来完成自己的需求功能;
我们谈React生命周期时,主要谈的类的生命周期,因为函数式组件是没有生命周期函数的;(后面我们可以通过hooks来模拟一些生命周期的回调)
我们先来学习一下最基础、最常用的生命周期函数:
上图第一个区域解析:
constructor
构造方法来创建组件;render
函数,获取要渲染的DOM结构(jsx),并且开始渲染DOM;componentDidMount
生命周期函数;上图第二个区域解析:
props
,或者调用setState
修改内部状态,或者直接调用forceUpdate
时会重新调用render
函数,进行更新操作;componentDidUpdate
生命周期函数;上图第三个区域解析:
componentWillUnmount
生命周期函数;constructor
constructor(props)
如果不初始化 state
或不进行方法绑定,则不需要为 React 组件实现构造函数。
constructor
中通常只做两件事情:
this.state
赋值对象来初始化内部的state
;this
);componentDidMount
componentDidMount()
componentDidMount()
会在组件挂载后(插入 DOM 树中)立即调用。
componentDidMount
中通常进行哪里操作呢?
componentWillUnmount
取消订阅);componentDidUpdate
componentDidUpdate(prevProps, prevState, snapshot)
componentDidUpdate()
会在更新后会被立即调用,首次渲染不会执行此方法。
props
进行了比较,也可以选择在此处进行网络请求;(例如,当 props
未发生变化时,则不会执行网络请求)。componentDidUpdate(prevProps) {
// 典型用法(不要忘记比较 props):
if (this.props.userID !== prevProps.userID) {
this.fetchData(this.props.userID);
}
}
componentWillUnmount
componentWillUnmount()
componentWillUnmount()
会在组件卸载及销毁之前直接调用。
componentDidMount()
中创建的订阅等;代码验证所有的生命周期函数:
import React, { Component } from 'react';
class HYTestCpn extends Component {
render() {
return <h2>HYTestCpn</h2>
}
componentWillUnmount() {
console.log("HYTestCpn componentWillUnmount");
}
}
export default class App extends Component {
constructor(props) {
super(props);
this.state = {
counter: 0
}
console.log("调用constructor方法");
}
render() {
console.log("调用render方法")
return (
<div>
<h2>当前计数: {this.state.counter}</h2>
{this.state.counter <= 5 && <HYTestCpn/>}
<button onClick={e => this.increment()}>+1</button>
</div>
)
}
increment() {
this.setState({
counter: this.state.counter + 1
})
}
componentDidMount() {
console.log("调用componentDidMount方法");
}
componentDidUpdate() {
console.log("调用componentDidUpdate方法");
}
componentWillUnmount() {
console.log("调用componentWillUnmount方法");
}
}
除了上面介绍的生命周期函数之外,还有一些不常用的生命周期函数:
getDerivedStateFromProps
:state
的值在任何时候都依赖于 props
时使用;该方法返回一个对象来更新state
;getSnapshotBeforeUpdate
:在React更新DOM之前回调的一个函数,可以获取DOM更新前的一些信息(比如说滚动位置);shouldComponentUpdate
:该生命周期函数很常用,但是我们等待讲性能优化时再来详细讲解;另外,React中还提供了一些过期的生命周期函数,这些函数已经不推荐使用。
更详细的生命周期相关的内容,可以参考官网:https://zh-hans.reactjs.org/docs/react-component.html
在 constructor()
函数中不要调用 setState()
方法。如果你的组件需要使用内部state
,请直接在构造函数中为this.state
赋值初始 state
:
constructor(props) {
super(props);
// 不要在这里调用 this.setState()
this.state = { counter: 0};
this.handleClick = this.handleClick.bind(this);
}
只能在构造函数中直接为 this.state
赋值。如需在其他方法中赋值,你应使用 this.setState()
替代。
你可以用类似的方式改写代码来避免可变对象的产生。例如,我们有一个叫做 colormap
的对象。我们希望写一个方法来将 colormap.right
设置为 'blue
'。我们可以这么写:
function updateColorMap(colormap) {
colormap.right = 'blue';
}
为了不改变原本的对象,我们可以使用 Object.assign
方法:
function updateColorMap(colormap) {
return Object.assign({}, colormap, {right: 'blue'});
}
现在 updateColorMap
返回了一个新的对象,而不是修改老对象。Object.assign
是ES6的方法,需要polyfill。
避免该问题最简单的方式是避免更改你正用于props
或 state
的值。例如,上面handleClick
方法可以用 concat
重写:
handleClick() {
this.setState(state => ({
words: state.words.concat(['marklar'])
}));
}
ES6数组支持扩展运算符,这让代码写起来更方便了。如果你在使用Create React App
,该语法已经默认支持了。
handleClick() {
this.setState(state => ({
words: [...state.words, 'marklar'],
}));
};
参数一为带有形式参数的 updater
函数:(state, props) => stateChange
state
是对应用变化时组件状态的引用。当然,它不应直接被修改。state
和 props
构建的新对象来表示变化。例如,假设我们想根据 props.step
来增加 state
:
this.setState((state, props) => {
return {counter: state.counter + props.step};
});
updater
函数中接收的state
和props
都保证为最新。updater
的返回值会与state
进行浅合并。
setState的更新是异步的?
import React, { Component } from 'react'
export default class App extends Component {
constructor(props) {
super(props);
this.state = {
message: "Hello World"
}
}
render() {
return (
<div>
<h2>{this.state.message}</h2>
<button onClick={e => this.changeText()}>改变文本</button>
</div>
)
}
changeText() {
this.setState({
message: "你好啊,李银河"
})
console.log(this.state.message); // Hello World
}
}
最终打印结果是Hello World
;可见setState
是异步的操作,我们并不能在执行完setState
之后立马拿到最新的state
的结果。
为什么setState
设计为异步呢?
setState
设计为异步其实之前在GitHub上也有很多的讨论;我对其回答做一个简单的总结:
setState
设计为异步,可以显著的提升性能;
setState
都进行一次更新,那么意味着render
函数会被频繁调用,界面重新渲染,这样效率是很低的;state
,但是还没有执行render
函数,那么state
和props
不能保持同步;
state
和props
不能保持一致性,会在开发中产生很多的问题;那么如何可以获取到更新后的值呢?
setState
接受两个参数:第二个参数是一个回调函数,这个回调函数会在更新后会执行;setState(partialState, callback)
changeText() {
this.setState({
message: "你好啊,李银河"
}, () => {
console.log(this.state.message); // 你好啊,李银河
});
}
当然,我们也可以在生命周期函数:
componentDidUpdate(prevProps, provState, snapshot) {
console.log(this.state.message);
}
疑惑:setState
一定是异步更新的吗?
验证一:在setTimeout
中的更新:
changeText() {
setTimeout(() => {
this.setState({
message: "你好啊,李银河"
});
console.log(this.state.message); // 你好啊,李银河
}, 0);
}
验证二:原生DOM事件:
componentDidMount() {
const btnEl = document.getElementById("btn");
btnEl.addEventListener('click', () => {
this.setState({
message: "你好啊,李银河"
});
console.log(this.state.message); // 你好啊,李银河
})
}
其实分成两种情况:
setState
是异步;setTimeout
或者原生 DOM 事件中,setState
是同步;React中其实是通过一个函数来确定的:enqueueSetState
部分实现(react-reconciler/ReactFiberClassComponent.js)
enqueueSetState(inst, payload, callback) {
const fiber = getInstance(inst);
// 会根据React上下文计算一个当前时间
const currentTime = requestCurrentTimeForUpdate();
const suspenseConfig = requestCurrentSuspenseConfig();
// 这个函数会返回当前是同步还是异步更新(准确的说是优先级)
const expirationTime = computeExpirationForFiber(
currentTime,
fiber,
suspenseConfig,
);
const update = createUpdate(expirationTime, suspenseConfig);
...
}
computeExpirationForFiber
函数的部分实现:
Sync
是优先级最高的,即创建就更新;export function computeExpirationForFiber(
currentTime: ExpirationTime,
fiber: Fiber,
suspenseConfig: null | SuspenseConfig,
): ExpirationTime {
const mode = fiber.mode;
if ((mode & BlockingMode) === NoMode) {
return Sync;
}
const priorityLevel = getCurrentPriorityLevel();
if ((mode & ConcurrentMode) === NoMode) {
return priorityLevel === ImmediatePriority ? Sync : Batched;
}
假如我们有这样的数据:
this.state = {
name: "coderwhy",
message: "Hello World"
}
我们需要更新message
:
changeText() {
this.setState({
message: "你好啊,李银河"
});
}
setState
去修改message
,是不会对name
产生影响的;为什么不会产生影响呢?源码中其实是有对 原对象 和 新对象 进行合并的:
Object.assign(target, ...sources)
来完成的;比如我们还是有一个counter
属性,记录当前的数字:
counter
会变成几呢?答案是1
;state
进行合并; increment() {
this.setState({
counter: this.state.counter + 1
});
this.setState({
counter: this.state.counter + 1
});
this.setState({
counter: this.state.counter + 1
});
}
其实在源码的processUpdateQueue
中有一个do...while
循环,就是从队列中取出多个state
进行合并的;
如何可以做到,让counter
最终变成3
呢?
increment() {
this.setState((state, props) => {
return {
counter: state.counter + 1
}
})
this.setState((state, props) => {
return {
counter: state.counter + 1
}
})
this.setState((state, props) => {
return {
counter: state.counter + 1
}
})
}
为什么传入一个函数就可以变出3呢?
那么React的更新流程呢?
React在props
或state
发生改变时,会调用React的render
方法,会创建一颗不同的树。
React需要基于这两颗不同的树之间的差别来判断如何有效的更新UI:
于是,React对这个算法进行了优化,将其优化成了O(n),如何优化的呢?
key
来指定哪些节点在不同的渲染下保持稳定;当节点为不同的元素,React会拆卸原有的树,并且建立起新的树:
变成
,从
变成
,或从
变成 都会触发一个完整的重建流程;
- 当卸载一棵树时,对应的DOM节点也会被销毁,组件实例将执行
componentWillUnmount()
方法;
- 当建立一棵新的树时,对应的 DOM 节点会被创建以及插入到 DOM 中,组件实例将执行
componentWillMount()
方法,紧接着 componentDidMount()
方法;
比如下面的代码更改:
- React 会销毁
Counter
组件并且重新装载一个新的组件,而不会对Counter
进行复用;
<div>
<Counter />
div>
<span>
<Counter />
span>
2. 对比同一类型的元素
当比对两个相同类型的 React 元素时,React 会保留 DOM 节点,仅比对及更新有改变的属性。
比如下面的代码更改:
- 通过比对这两个元素,React 知道只需要修改 DOM 元素上的
className
属性;
<div className="before" title="stuff" />
<div className="after" title="stuff" />
比如下面的代码更改:
- 当更新
style
属性时,React 仅更新有所更变的属性。
- 通过比对这两个元素,React 知道只需要修改 DOM 元素上的
color
样式,无需修改 fontWeight
。
<div style={{color: 'red', fontWeight: 'bold'}} />
<div style={{color: 'green', fontWeight: 'bold'}} />
如果是同类型的组件元素:
- 组件会保持不变,React会更新该组件的
props
,并且调用componentWillReceiveProps()
和 componentWillUpdate()
方法;
- 下一步,调用
render()
方法,diff 算法将在之前的结果以及新的结果中进行递归;
3. 对子节点进行递归
在默认条件下,当递归 DOM 节点的子元素时,React 会同时遍历两个子元素的列表;当产生差异时,生成一个 mutation
。
我们来看一下在最后插入一条数据的情况:
<ul>
<li>firstli>
<li>secondli>
ul>
<ul>
<li>firstli>
<li>secondli>
<li>thirdli>
ul>
- 前面两个比较是完全相同的,所以不会产生
mutation
;
- 最后一个比较,产生一个
mutation
,将其插入到新的DOM树中即可;
但是如果我们是在中间插入一条数据:
<ul>
<li>星际穿越li>
<li>盗梦空间li>
ul>
<ul>
<li>大话西游li>
<li>星际穿越li>
<li>盗梦空间li>
ul>
- React会对每一个子元素产生一个
mutation
,而不是保持 - 星际穿越
和- 盗梦空间
的不变;
- 这种低效的比较方式会带来一定的性能问题;
keys的优化
我们在前面遍历列表时,总是会提示一个警告,让我们加入一个key
属性:
我们来看一个案例:
import React, { Component } from 'react'
export default class App extends Component {
constructor(props) {
super(props);
this.state = {
movies: ["星际穿越", "盗梦空间"]
}
}
render() {
return (
<div>
<h2>电影列表</h2>
<ul>
{
this.state.movies.map((item, index) => {
return <li>{item}</li>
})
}
</ul>
<button onClick={e => this.insertMovie()}>插入数据</button>
</div>
)
}
insertMovie() {
}
}
方式一:在最后位置插入数据
- 这种情况,有无
key
意义并不大
insertMovie() {
const newMovies = [...this.state.movies, "大话西游"];
this.setState({
movies: newMovies
})
}
方式二:在前面插入数据
- 这种做法,在没有
key
的情况下,所有的li
都需要进行修改;
insertMovie() {
const newMovies = ["大话西游", ...this.state.movies];
this.setState({
movies: newMovies
})
}
当子元素(这里的li
)拥有 key
时,React 使用 key
来匹配原有树上的子元素以及最新树上的子元素:
- 在下面这种场景下,
key
为111
和222
的元素仅仅进行位移,不需要进行任何的修改;
- 将
key
为333
的元素插入到最前面的位置即可;
<ul>
<li key="111">星际穿越li>
<li key="222">盗梦空间li>
ul>
<ul>
<li key="333">Connecticutli>
<li key="111">星际穿越li>
<li key="222">盗梦空间li>
ul>
key
的注意事项:
key
应该是唯一的;
key
不要使用随机数(随机数在下一次render
时,会重新生成一个数字);
- 使用
index
作为key
,对性能是没有优化的;
SCU的优化(shouldComponentUpdate)
render函数被调用
我们使用之前的一个嵌套案例:
import React, { Component } from 'react';
function Header() {
console.log("Header Render 被调用");
return <h2>Header</h2>
}
class Main extends Component {
render() {
console.log("Main Render 被调用");
return (
<div>
<Banner/>
<ProductList/>
</div>
)
}
}
function Banner() {
console.log("Banner Render 被调用");
return <div>Banner</div>
}
function ProductList() {
console.log("ProductList Render 被调用");
return (
<ul>
<li>商品1</li>
<li>商品2</li>
<li>商品3</li>
<li>商品4</li>
<li>商品5</li>
</ul>
)
}
function Footer() {
console.log("Footer Render 被调用");
return <h2>Footer</h2>
}
export default class App extends Component {
constructor(props) {
super(props);
this.state = {
counter: 0
}
}
render() {
console.log("App Render 被调用");
return (
<div>
<h2>当前计数: {this.state.counter}</h2>
<button onClick={e => this.increment()}>+1</button>
<Header/>
<Main/>
<Footer/>
</div>
)
}
increment() {
this.setState({
counter: this.state.counter + 1
})
}
}
那么,我们可以思考一下,在以后的开发中,我们只要是修改了App
中的数据,所有的组件都需要重新render
,进行diff
算法,性能必然是很低的:
- 事实上,很多的组件没有必须要重新
render
;
- 它们调用
render
应该有一个前提,就是依赖的数据(state、props
)发生改变时,再调用自己的render
方法;
如何来控制render
方法是否被调用呢?
- 通过
shouldComponentUpdate
方法即可;
shouldComponentUpdate
React给我们提供了一个生命周期方法 shouldComponentUpdate
(很多时候,我们简称为SCU),这个方法接受参数,并且需要有返回值:
该方法有两个参数:
- 参数一:
nextProps
修改之后,最新的props
属性
- 参数二:
nextState
修改之后,最新的state
属性
该方法返回值是一个boolean
类型
- 返回值为
true
,那么就需要调用render
方法;
- 返回值为
false
,那么就不需要调用render
方法;
- 默认返回的是
true
,也就是只要state
发生改变,就会调用render
方法;
shouldComponentUpdate(nextProps, nextState) {
return true;
}
我们可以控制它返回的内容,来决定是否需要重新渲染。
比如我们在App
中增加一个message
属性:
export default class App extends Component {
constructor(props) {
super(props);
this.state = {
counter: 0,
message: "Hello World"
}
}
render() {
console.log("App Render 被调用");
return (
<div>
<h2>当前计数: {this.state.counter}</h2>
<button onClick={e => this.increment()}>+1</button>
<button onClick={e => this.changeText()}>改变文本</button>
<Header/>
<Main/>
<Footer/>
</div>
)
}
increment() {
this.setState({
counter: this.state.counter + 1
})
}
changeText() {
this.setState({
message: "你好啊,李银河"
})
}
}
jsx
中并没有依赖这个message
,那么它的改变不应该引起重新渲染;
- 但是因为
render
监听到state
的改变,就会重新render
,所以最后render
方法还是被重新调用了;
这个时候,我们可以通过实现shouldComponentUpdate
来决定要不要重新调用render
方法:
shouldComponentUpdate(nextProps, nextState) {
if (nextState.counter !== this.state.counter) {
return true;
}
return false;
}
- 这个时候,我们改变
counter
时,会重新渲染;
- 如果,我们改变的是
message
,那么默认返回的是false
,那么就不会重新渲染;
但是我们的代码依然没有优化到最好,因为当counter
改变时,所有的子组件依然重新渲染了:
- 所以,事实上,我们应该实现所有的子组件的
shouldComponentUpdate
;
比如Main
组件,可以进行如下实现:
class Main extends Component {
shouldComponentUpdate(nextProps, nextState) {
return false;
}
render() {
console.log("Main Render 被调用");
return (
<div>
<Banner/>
<ProductList/>
</div>
)
}
}
shouldComponentUpdate
默认返回一个false
;
- 在特定情况下,需要更新时,我们在上面添加对应的条件即可;
PureComponent 和 memo
如果所有的类,我们都需要手动来实现 shouldComponentUpdate
,那么会给我们开发者增加非常多的工作量。
我们来设想一下shouldComponentUpdate
中的各种判断的目的是什么?
props
或者state
中的数据是否发生了改变,来决定shouldComponentUpdate
返回true
或者false
;
事实上React已经考虑到了这一点,所以React已经默认帮我们实现好了,如何实现呢?
- 将
class
继承自PureComponent
。
比如我们修改Main
组件的代码:
class Main extends PureComponent {
render() {
console.log("Main Render 被调用");
return (
<div>
<Banner/>
<ProductList/>
</div>
)
}
}
PureComponent的原理是什么呢?
- 对
props
和state
进行浅层比较;
查看PureComponent
相关的源码:
react/ReactBaseClasses.js
中:
- 在
PureComponent
的原型上增加一个isPureReactComponent
为true
的属性
React-reconcilier/ReactFiberClassComponent.js
:
这个方法中,调用 !shallowEqual(oldProps, newProps) || !shallowEqual(oldState, newState)
,这个shallowEqual
就是进行浅层比较:
那么,如果是一个函数式组件呢?
我们需要使用一个高阶组件memo
:
- 我们将之前的
Header、Banner、ProductList
都通过memo
函数进行一层包裹;
Footer
没有使用memo
函数进行包裹;
- 最终的效果是,当
counter
发生改变时,Header、Banner、ProductList
的函数不会重新执行,而Footer
的函数会被重新执行;
import React, { Component, PureComponent, memo } from 'react';
const MemoHeader = memo(function() {
console.log("Header Render 被调用");
return <h2>Header</h2>
})
class Main extends PureComponent {
render() {
console.log("Main Render 被调用");
return (
<div>
<MemoBanner/>
<MemoProductList/>
</div>
)
}
}
const MemoBanner = memo(function() {
console.log("Banner Render 被调用");
return <div>Banner</div>
})
const MemoProductList = memo(function() {
console.log("ProductList Render 被调用");
return (
<ul>
<li>商品1</li>
<li>商品2</li>
<li>商品3</li>
<li>商品4</li>
<li>商品5</li>
</ul>
)
})
function Footer() {
console.log("Footer Render 被调用");
return <h2>Footer</h2>
}
export default class App extends Component {
constructor(props) {
super(props);
this.state = {
counter: 0,
message: "Hello World"
}
}
render() {
console.log("App Render 被调用");
return (
<div>
<h2>当前计数: {this.state.counter}</h2>
<button onClick={e => this.increment()}>+1</button>
<button onClick={e => this.changeText()}>改变文本</button>
<MemoHeader/>
<Main/>
<Footer/>
</div>
)
}
increment() {
this.setState({
counter: this.state.counter + 1
})
}
shouldComponentUpdate(nextProps, nextState) {
if (nextState.counter !== this.state.counter) {
return true;
}
return false;
}
changeText() {
this.setState({
message: "你好啊,李银河"
})
}
}
memo
的原理是什么呢?
react/memo.js
:
- 最终返回一个对象,这个对象中有一个
compare
函数
- 默认是进行了浅比较
不可变数据的力量
我们通过一个案例来演练我们之前说的不可变数据的重要性:
import React, { PureComponent } from 'react'
export default class App extends PureComponent {
constructor(props) {
super(props);
this.state = {
friends: [
{ name: "lilei", age: 20, height: 1.76 },
{ name: "lucy", age: 18, height: 1.65 },
{ name: "tom", age: 30, height: 1.78 }
]
}
}
render() {
return (
<div>
<h2>朋友列表</h2>
<ul>
{
this.state.friends.map((item, index) => {
return (
<li key={item.name}>
<span>{`姓名:${item.name} 年龄: ${item.age}`}</span>
<button onClick={e => this.incrementAge(index)}>年龄+1</button>
</li>
)
})
}
</ul>
<button onClick={e => this.insertFriend()}>添加新数据</button>
</div>
)
}
insertFriend() {
}
incrementAge(index) {
}
}
我们来思考一下inertFriend
应该如何实现?
实现方式一:
insertFriend() {
this.state.friends.push({name: "why", age: 18, height: 1.88});
this.setState({
friends: this.state.friends
})
}
- 这种方式会造成界面不会发生刷新,添加新的数据;
- 原因是继承自
PureComponent
,会进行浅层比较,浅层比较过程中两个friends
是相同的对象;
实现方式二:
insertFriend() {
this.setState({
friends: [...this.state.friends, {name: "why", age: 18, height: 1.88}]
})
}
[...this.state.friends, {name: "why", age: 18, height: 1.88}]
会生成一个新的数组引用;
- 在进行浅层比较时,两个引用的是不同的数组,所以它们是不相同的;
我们再来思考一下incrementAge
应该如何实现?
实现方式一:
incrementAge(index) {
this.state.friends[index].age += 1;
this.setState({
friends: this.state.friends
})
}
- 和上面方式一类似
实现方式二:
incrementAge(index) {
const newFriends = [...this.state.friends];
newFriends[index].age += 1;
this.setState({
friends: newFriends
})
}
- 和上面方式二类似
所以,在真实开发中,我们要尽量保证state、props
中的数据不可变性,这样我们才能合理和安全的使用PureComponent
和memo
。
当然,后面项目中我会结合immutable.js
来保证数据的不可变性。
总之,更新state的原则是,不要直接修改state中的原始对象或数组,而是要通过新创建一个对象或数组的拷贝,在拷贝的对象上进行修改之后,再通过setState
设置更新给state中的原始对象。
官网对 shouldComponentUpdate 的描述
四. React的脚手架:create-react-app
create-react-app 创建项目以及 npm、yarn 包管理工具的使用 参考这里
快捷创建react应用的脚手架命令:
启动React项目
编译React项目
五. React开发依赖
认识 Babel
引入 React