跳到主要内容

81 篇博文 含有标签「React」

查看所有标签

在前端面试中,只要涉及到React框架,diff算法便是我们绕不开的话题,这次让我们来系统性的梳理diff算法,不再被这个知识点所困扰。

什么是diff算法?

在了解diff算法之前,我们要直到diff算法并非React独家首创,但是React针对diff算法做了自己的优化,使得diff算法可以帮助我们计算出Virtual Dom中真正变化的部分,并只针对该部分进行实际的DOM操作,而非渲染整个页面,从而保证了每次操作后页面的高效渲染。

传统diff算法

要想了解React的diff算法,我们首先要知道传统的diff算法是如何设计并实现的。

传统diff算法的时间复杂度

传统diff算法的时间复杂度是O(N^3),其中N是树中节点的总数,这样的时间复杂度意味着如果要展示1000个节点,就要执行多达十亿次的比较,这种指数型的性能消耗对于前段渲染场景来说代价太高了。

React只有将diff算法进行改进,才有可能满足前端渲染所要求的的性能。

之所以传统diff算法的时间复杂度是O(N^3)是因为两个二叉树的每一个节点进行两两对比的时间复杂度是O(N^2),此时如果继续进行树的编辑操作(修改、删除)等还需要O(N)的时间复杂度,所以总的时间复杂度是O(N^3)。

React优化后的diff算法

React通过自己的优化,将O(N^3)的时间复杂度降到了O(N)。

React diff的三个前提策略

  1. Web UI中DOM节点跨层级的移动操作特别少,可以忽略不计。

  2. 拥有相同类的两个组件将会生成相似的树形结构,拥有不同类的两个组件将会生成不同的树形结构。

  3. 对于同一层级的一组子节点,它们可以通过唯一id进行区分。

React进行tree diff、component diff和element diff进行算法优化是基于上面三个前提策略。事实证明上面的三个前提策略是非常有效的。

核心策略1:tree diff(树策略)

对树进行分层比较,两棵树只会对同一层次的节点进行比较。

核心策略2:component diff(组件策略)

React是基于组件构建应用的,对于组件间的比较采用下面的方式:

  • 如果是同一类型的组件,按照原策略继续比较虚拟DOM树,对于同一类型的组件,有可能其虚拟DOM树并没有任何变化,如果能够在比较之前准确的知道这一点,可以节省大量的运算时间,所以React向用户提供了shouldComponentUpdate()来判断该组件是否需要进行diff。

  • 如果不是同一类型的组件,则将该组件判断为dirty component,从而替换掉整个组件下面的所有子节点。

核心策略3:element diff(元素策略)

对于同一层级的一组子节点,通过唯一id进行区分。


JustinReact阅读需 3 分钟

1. 安装echarts-for-react插件(两个都要装)

npm install echarts-for-react
npm install echarts

2. 导入ReactEcharts库

import ReactECharts from 'echarts-for-react';

3. 渲染ReactEcharts组件,并通过option导入数据

<ReactECharts option={this.getOption(sales,stores)} />

4. 设置数据源option

getOption = (sales,stores) => {
return {
title: {
text: 'ECharts 入门示例'
},
tooltip: {},
legend: {
data: ['销量', '库存']
},
xAxis: {
data: ["衬衫", "羊毛衫", "雪纺衫", "裤子", "高跟鞋", "袜子"]
},
yAxis: {},
series: [{
name: '销量',
type: 'bar',
data: [1,2,3,4],
}, {
name: '库存',
type: 'bar',
data: [2,5,4,6]
}]

}
}

如何将柱状图改为折线图

只需将series的对象中的type更改为line即可。

getOption = (sales,stores) => {
return {
title: {
text: 'ECharts 入门示例'
},
tooltip: {},
legend: {
data: ['销量', '库存']
},
xAxis: {
data: ["衬衫", "羊毛衫", "雪纺衫", "裤子", "高跟鞋", "袜子"]
},
yAxis: {},
series: [{
name: '销量',
type: 'line',
data: sales,
}, {
name: '库存',
type: 'line',
data: stores
}]
}

}

>柱状图和折线图的实现效果,如下图所示

![柱状图](https://img-blog.csdnimg.cn/img_convert/4dba1135680613dbcdef254fcbf1f3f8.png)
![折线图](https://img-blog.csdnimg.cn/img_convert/61840f54f0ab7c31d84abd1b3386c816.png)

## 如何修改柱状图的颜色
>通过在option中设置color属性,既可以进行全局的样式柱状图颜色修改,也可以局部的修改某一个柱状图的颜色。更多的属性设置需要去查官方文档或者根据官方的实例进行修改。

**示例代码**
```js
option = {
// 全局调色盘。
color: ['#c23531','#2f4554', '#61a0a8', '#d48265', '#91c7ae','#749f83', '#ca8622', '#bda29a','#6e7074', '#546570', '#c4ccd3'],

series: [{
type: 'bar',
// 此系列自己的调色盘。
color: ['#dd6b66','#759aa0','#e69d87','#8dc1a9','#ea7e53','#eedd78','#73a373','#73b9bc','#7289ab', '#91ca8c','#f49f42'],
...
}, {
type: 'pie',
// 此系列自己的调色盘。
color: ['#37A2DA', '#32C5E9', '#67E0E3', '#9FE6B8', '#FFDB5C','#ff9f7f', '#fb7293', '#E062AE', '#E690D1', '#e7bcf3', '#9d96f5', '#8378EA', '#96BFFF'],
...
}]
}

Echarts的按需引入

import React from 'react';
import {Card} from 'antd';
import echartTheme from './../themeLight'
//不是按需加载的话文件太大
//import echarts from 'echarts'
//下面是按需加载
import echarts from 'echarts/lib/echarts'
//导入折线图
import 'echarts/lib/chart/line'; //折线图是line,饼图改为pie,柱形图改为bar
import 'echarts/lib/component/tooltip';
import 'echarts/lib/component/title';
import 'echarts/lib/component/legend';
import 'echarts/lib/component/markPoint';
import ReactEcharts from 'echarts-for-react';
export default class Line extends React.Component{
componentWillMount(){
//主题的设置要在willmounted中设置
echarts.registerTheme('Imooc',echartTheme);
}
getOption =()=> {
let option = {
title:{
text:'用户骑行订单',
x:'center'
},
tooltip:{
trigger:'axis',
},
xAxis:{
data:['周一','周二','周三','周四','周五','周六','周日']
},
yAxis:{
type:'value'
},
series:[
{
name:'OFO订单量',
type:'line', //这块要定义type类型,柱形图是bar,饼图是pie
data:[1000,2000,1500,3000,2000,1200,800]
}
]
}
return option
}

render(){
return(
<div>
<Card title="折线图表之一">
<ReactEcharts option={this.getOption()} theme="Imooc" style={{height:'400px'}}/>
</Card>

</div>
)
}
}

参考资料


JustinReact阅读需 3 分钟

comineReducers旨在解决什么问题?

这个函数是redux库中的函数,旨在解决多个reducer暴露的问题,因为一个组件往往用到的不止一个reducer。

结合后的reducer函数

import {INCREMENT,DECREMENT} from './action-types'
import {combineReducers} from 'redux'
// 管理count状态的reducer

function count(state=1,action) {
console.log('count',state,action);
switch (action.type) {
case INCREMENT:
return state + action.data
case DECREMENT:
return state - action.data
default:
return state;
}
}
// 管理user状态的reducer
const initUser = {};

function user(state = initUser,action) {
switch (action.type) {
default:
return state;
}
}

export default combineReducers({
count,
user
})

思维导图


JustinReact阅读需 1 分钟

使用redux-thunk实现异步redux

Redux存在一个问题,就是无法实现异步的action,这也就是为什么我们要引入redux-thunk的原因。

在哪里引入redux-thunk?

在redux的核心组件store中引入。我们引入的这个thunk,相当于一个中间件。所以我们同时需要从redux中引入applyMiddleware,放入createStore的第二个参数中。

import {createStore,applyMiddleware} from 'redux';
import reducer from './reducer'
import thunk from 'redux-thunk'

export default createStore(reducer,applyMiddleware(thunk))

异步action和普通的action有什么不同?

普通action返回的是对象,但是异步action返回的是一个函数。

异步action和同步action的区别

// 同步action
export const decrement = (number) => ({type: DECREMENT,data: number});
// 异步增加的action
export const incrementAsync = (number) => {
return dispatch => {
setTimeout(() => {
dispatch({type: INCREMENT,data: number})
},1000)
}
}

最后别忘了,组件中已经没有定时器了,定时器在异步action中。

incrementIfAsync = () => {
const number = this.numberRef.current.value * 1;
this.props.incrementAsync(number);
}

codeSandBox在线演示(使用redux-thunk实现异步action操作状态)


JustinReact阅读需 1 分钟

已经有了redux为什么还要设计react-redux?

因为redux和组件的耦合度太高,为了解耦,所以设计了redux。一旦我们引入了react-redux,我们便不再需要使用store的subscribe自己去订阅状态了。UI组件就像普通组件一样内部没有redux的身影。可读性更高。

UI组件和容器组件

react-redux将组件分为UI组件和容器组件,UI组件只负责UI的呈现,不带有任何业务逻辑,通过props接收数据,不使用Rdux的API,一般保存在components文件夹下,容器组件,只负责管理数据和业务逻辑,不负责UI的呈现,使用redux的API,一般保存在containers文件夹下。

react-redux的核心API

1. Provider:该组件包含的组件能够获取到状态state.

Provider存在的意义相当于可以替换掉redux中的subscribe。

ReactDOM.render((
<Provider store={store}>
<App />
</Provider>
),document.querySelector('#root'))

2. connect:连接UI组件和容器组件以及Redux

react-redux的三个主要作用

作用1

将组件分为了容器组件和UI组件,UI组件通过props来获取状态和操作状态的方法。

作用2

通过Provider组件来取代redux中的store.subscribe来监听组件的状态变化,用于渲染组件。

作用3

在容器组件中通过核心API connect来连接UI组件和redux,connect是一个高阶函数,第一个参数接收的是两个回调函数,回调函数1:将接收一个state,然后返回一个对象对象中包含了UI组件想要的状态。回调函数2:接收一个dispatch,返回一个对象,对象中包含了UI组件想要操作状态的方法。同时还有一个简写方法,就是第二个参数直接传入一个对象,该对象包含操作状态的方法。(核心:就是将state和dispatch映射到UI组件的props中)

核心代码

export default connect(
state => ({count: state}),
dispatch => {
return {
increment: number => dispatch(increment(number)),
decrement: number => dispatch(decrement(number)),
}
}
)(Counter)

下面是简写形式

export default connect(
state => ({count: state}),
{increment,decrement}
)(Counter)

注意事项

  • 渲染的是容器组件,而不是UI组件。(Provider包裹的)

JustinReact阅读需 2 分钟

本文采用总分总的结构,首先给出React生命周期流程图,让大家知道我们的研究目标是什么,第二部分则分别对React生命周期中的重点难点的生命钩子函数进行介绍。第三部分给出React生命周期的总结。

React生命周期流程图

image.png

1. getDerivedStateFromProps(props, state)

官方解释:调用这个钩子函数,会使得state在任何时候的状态值都取决于props.

这个函数是静态的,所以前面要加static.

返回的是什么?

返回的应该是状态对象(或者null),总之返回的应该是一个对象,如果你什么都不返回,会出现警告。这个返回的对象就是render要渲染的state

接收的是什么?

接收两个参数,一个是最新的props,一个是最新的state.

codeSandBox在线演示


2. shouldComponentUpdate(nextProps, nextState)

接收的是什么?

接收两个参数,一个是最新的但是还未render的props,另一个则是最新的但是还未render的state.

返回的是什么?

返回的是布尔值,返回true则让当前组件进行更新,返回false则让当前组件不更新。

codeSandBox在线演示


3. componentDidMount

接受的是什么?

这个生命周期钩子函数是在挂载的最后阶段调用,并未接收参数。

可以在这个钩子函数中处理组件挂载后的一些操作。

4. getSnapshotBeforeUpdate(preProps,preState)

接收的是什么?

接收两个参数,一个是之前的props,一个是之前的state.

返回的是什么?

在这个生命周期钩子函数中,记录了更新DOM之前的一些HTML属性,返回的值,会被componentDidUpdate的第三个参数接收。

codeSandBox在线演示(新闻滚动条案例)

5. componentDidUpdate(prevProps, prevState, snapshot)

接收的是什么?

接收的是之前的props和之前的state,这个state是滞后与DOM的,同时第三个参数是接收的getSnapshotBeforeUpdate传来的参数。

返回的是什么?

并不会返回什么,但是可以在此处进行更新后的对比,并对DOM进行操作,或者发起网络请求。

6. componentWillUnmount()

该生命周期函数会在组件卸载之前调用,在这个方法中可以进行清除定时器等操作。在这个生命周期钩子函数中不应调用setState,因为如果这样组件将永远不会重新渲染。

7. forceUpdate(callback)

该生命周期函数不用更改state或props也能对组件进行更新,调用render,且不用通过shouldComponentUpdate这个钩子。

codeSandBox在线演示

总结生命周期

React生命周期最关键的是要记住每一个生命周期钩子函数接收的是什么?返回的是什么?在什么阶段调用,这是核心也是关键,最后一定要熟记流程图!


JustinReact阅读需 3 分钟

问题引入

React中的setState是用来更新状态的重要工具,但是setState是同步的还是异步的,需要我们进行一定的探讨,接下来让我们好好研究研究。

使用setState的两种形式

  1. 函数形式的setState
test1 = () => {
// 函数形式的setState,函数形式的setState能够接收到两个参数,一个是state,另一个是props
this.setState(state => ({count: state.count + 1}))
}
  1. 对象形式的setState
test2 = () => {
// 对象形式的setState
const count = this.state.count + 1;
this.setState({count})
}

使用过setState之后能否立即获取到状态更新后的值

答案是不能。

test1 = () => {
// 函数形式的setState,函数形式的setState能够接收到两个参数,一个是state,另一个是props
this.setState(state => ({count: state.count + 1}))
console.log('函数形式的setState更新后:',this.state.count);
}

image.png

如何立即获取到状态更新后的值

使用setState的第二个参数,这个参数接收的是一个回调函数,这个回调函数会在界面渲染之后调用。

test3 = () => {
this.setState(state => ({count: state.count + 1}),() => {
console.log('函数形式的setState更新后:',this.state.count);
});
}

image.png

setState()更新状态是同步还是异步的?

回到我们要探讨的正题,setState()更新状态时同步的还是异步的?

判断setState()更新状态时异步还是同步的,主要是看执行setState的位置

  1. 在React控制的回调函数中(生命周期钩子,react事件监听回调)这种情况是异步的。
  2. 在非react控制的异步回调函数中(定时器回调/原生事件监听回调/promise回调)这种情况是同步的。

异步举例

  • 在React事件回调函数中使用setState(异步的)
// React事件回调函数中
update1 = () => {
console.log('React事件回调函数更新之前:',this.state.count);
this.setState(state => ({count: state.count + 1}))
console.log('React事件回调函数更新之后:',this.state.count);
}

image.png

  • 在生命周期钩子函数中使用setState(异步的)
// 在生命周期钩子函数中
componentDidMount() {
console.log('生命周期钩子函数更新之前:',this.state.count);
this.setState(state => ({count: state.count + 1}))
console.log('生命周期钩子函数更新之后:',this.state.count);
}

image.png

同步举例

  • setTimeout
// 定时器回调
update2 = () => {
setTimeout(() => {
console.log('setTimeout 更新之前:', this.state.count);
this.setState(state => ({ count: state.count + 1 }))
console.log('setTimeout 更新之后:', this.state.count);
})
}

image.png

  • 原生onclick
update3 = () => {
const h1 = this.refs.count;
h1.onclick = () => {
console.log('onclick 更新之前:', this.state.count);
this.setState(state => ({ count: state.count + 1 }))
console.log('onclick 更新之后:', this.state.count);
}
}

image.png

  • Promise
update4 = () => {
Promise.resolve().then(value => {
console.log('Promise 更新之前:', this.state.count);
this.setState(state => ({ count: state.count + 1 }))
console.log('Promise 更新之后:', this.state.count);
})
}

image.png

setState多次调用的问题

下面要讨论的多次调用的问题是基于异步的前提下来讨论的。

情况1:两个函数式setState的情况(不会合并)

// 测试函数式 setState合并 与更新的问题
update5 = () => {
console.log('测试通过函数式更新setState的合并问题 更新之前:', this.state.count);
this.setState(state => ({ count: state.count + 1 }))
console.log('测试通过函数式更新setState的合并问题 更新之后:', this.state.count);
console.log('测试通过函数式更新setState的合并问题 更新之前:', this.state.count);
this.setState(state => ({ count: state.count + 1 }))
console.log('测试通过函数式更新setState的合并问题 更新之后:', this.state.count);
}

image.png

情况2:两个对象式setState的情况(会合并)

// 测试对象式 setState合并 与更新的问题
update6 = () => {
console.log('测试通过对象式更新setState的合并问题 更新之前:', this.state.count);
this.setState({count: this.state.count + 1})
console.log('测试通过对象式更新setState的合并问题 更新之后:', this.state.count);
console.log('测试通过对象式更新setState的合并问题 更新之前:', this.state.count);
this.setState({count: this.state.count + 1})
console.log('测试通过对象式更新setState的合并问题 更新之后:', this.state.count);
}

情况3:先函数式后对象式(会合并)

update7 = () => {
console.log('测试通过函数式更新setState的合并问题 更新之前:', this.state.count);
this.setState(state => ({ count: state.count + 1 }))
console.log('测试通过函数式更新setState的合并问题 更新之后:', this.state.count);
console.log('测试通过对象式更新setState的合并问题 更新之前:', this.state.count);
this.setState({count: this.state.count + 1})
console.log('测试通过对象式更新setState的合并问题 更新之后:', this.state.count);
}

image.png

情况4:先对象式后函数式

update7 = () => {
console.log('测试通过对象式更新setState的合并问题 更新之前:', this.state.count);
this.setState({count: this.state.count + 1})
console.log('测试通过对象式更新setState的合并问题 更新之后:', this.state.count);
console.log('测试通过函数式更新setState的合并问题 更新之前:', this.state.count);
this.setState(state => ({ count: state.count + 1 }))
console.log('测试通过函数式更新setState的合并问题 更新之后:', this.state.count);
}

image.png

核心技巧:函数式传入的state总是能够获取到最新的state,但是对象式则不能,但是最后render只会更新一次。

一道经典的setState的面试题(看懂这个,你可能就懂了!)

请问下面的APP组件打印的是什么?

class App extends Component {
state = {
count: 0
}
// 在生命周期钩子函数中
componentDidMount() {
this.setState({ count: this.state.count + 1 })
this.setState({ count: this.state.count + 1 })
console.log(this.state.count);

this.setState(state => ({ count: state.count + 1 }))
this.setState(state => ({ count: state.count + 1 }))
console.log(this.state.count);

setTimeout(() => {
this.setState(state => ({ count: state.count + 1 }))
console.log('setTimeout:', this.state.count);

this.setState(state => ({ count: state.count + 1 }))
console.log('setTimeout:', this.state.count);
}, 0)
Promise.resolve().then(value => {
this.setState(state => ({ count: state.count + 1 }))
console.log('Promise',this.state.count);
this.setState(state => ({ count: state.count + 1 }))
console.log('Promise:',this.state.count);
})
}

render() {
const { count } = this.state;
console.log('render: ', count);
return (
<div>
<h1>当前求和为{count}</h1>
</div>
)
}
}

答案

image.png

答案解析(按输出顺序进行解析)

  1. 第一行: react首先会渲染下组件,此时获取到的count值是state中存的初始值,所以是0.
  2. 第2、3行:执行完render之后,会进入componentDidMount钩子函数,遇到两个对象式的setState会进行合并,但由于此时在钩子函数中,获取state是异步的,所以打印的都是0,但是当遇到函数式的setState,则不会合并,此时count的值已经变为了3.
  3. 第四行:此时componentDidMount中出了Promise和setTimeout外都执行了,上面的代码对JS来说都属于同步代码,此时可以进行更新render了,所以打印了render 3.
  4. 第五行:setTimeout和Promise中,由于Promise是微任务,所以优先执行,在执行的时候,这里的setState是同步更新state的,所以调用一次setState就要调用一次render,所以第五行打印的是render: 4.
  5. 第六行:执行log操作,打印的是Promise: 4。。。

剩下的内容均属于JS事件循环的知识了,如果你有不懂的地方可以参考我的专栏中的事件循环机制的基本认知这篇博文。

codeSandBox


JustinReact阅读需 6 分钟

PureComponent有什么用?

一般组件的shouldComponentUpdate默认返回的是true,但是一旦父组件及时状态或props没有变化,也会造成子组件的render调用,这是很不合理的,我们可以让子组件继承自PureComponent来解决这个问题。

PureComponent的基本原理

  1. 重写了shouldComponentUpdate方法。
  2. 对组件的新/旧 state和props中的数据进行浅比较,如果没有变化则返回false,反之返回true.

PureComponent用法实例

import React, { Component,PureComponent } from 'react'
import ReactDOM from 'react-dom'


class App extends Component {
state = {
m1: 1
}

test1 = () => {
this.setState(state => ({
m1: state.m1 + 1
}))
// this.setState({})
}
render() {
console.log('调用了A render: ');
return (
<div>
<h1>A组件:m1={this.state.m1}</h1>
<button onClick={this.test1}>A 测试1</button>
<B m1={this.state.m1}/>
</div>
)
}
}

class B extends PureComponent {
state = {
m2: 1
}

test2 = () => {
this.setState({})
}
render() {
console.log('调用了 B render: ');
return (
<div>
<h1>B组件:m2={this.state.m2}, m1={this.props.m1}</h1>
<button onClick={this.test2}>B 测试2</button>
</div>
)
}
}

ReactDOM.render(<App />, document.querySelector('#root'));

codeSandBox在线演示


JustinReact阅读需 1 分钟

为什么要使用shouldComponentUpdate?

只要是组件继承自React.Component就会存在当父组件重新render的时候,子组件也会重新执行render,即使这个子组件没有任何变化。子组件只要调用setState就会重新执行render,及时setState的参数是一个空对象。

shouldComponentUpdate的用法

在子组件中:

shouldComponentUpdate(nextProps,nextState) {
if (nextProps.m1 === this.props.m1 && nextState.m2 === this.state.m2) {
return false;
} else {
return true;
}
}

根据下面的React生命周期流程图可知,当shouldComponentUpdate返回为true的时候,当前组件进行render,如果返回的是false则不进行render. image.png

codeSandBox在线演示


JustinReact阅读需 1 分钟

问题描述

  • 给表单中的每一个表单项传入一个参数的时候,参数已经传进去了,但是initialValue并没有发生变化。

原因

这是因为调用resetFileds的时机不对,也就是生命周期的问题。

解决办法

  • 在生命周期函数componentDidUpdate中添加下面的代码即可。
componentDidUpdate() {
if (this.formRef.current !== null) {
this.formRef.current.resetFields();
}
}

全部代码

export default class UpdateForm extends Component {
formRef = React.createRef();
static propTypes = {
categoryName: PropTypes.string.isRequired
}
componentDidUpdate() {
if (this.formRef.current !== null) {
this.formRef.current.resetFields();
}
}
render() {
const { categoryName } = this.props;

console.log(categoryName);

return (
<Form ref={this.formRef}>

<Item initialValue={categoryName} name="categoryName" >
<Input placeholder="请输入分类名称" />
</Item>

</Form>
)
}
}

React阅读需 1 分钟