STAY HUNGRY , STAY FOOLISH.

求知若饥,虚心若愚。

       浏览:

React常见问题及解答(中)

上一章,主要介绍Antd Pro的背景和部分代码流程,这章将会更细致的介绍react其他知识点。

  • 1.纯函数组件、PureComponent、Component之间有什么区别呢?
  • 2.那什么是浅比较?何时使用Component、何时使用PureComponent、何时使用纯函数呢?
  • 3.变量存放的位置有props、state、this,如何确定哪些变量放哪个位置呢?
  • 4.state 和 props 又有什么区别呢?
  • 5.在Antd Pro中,声明变量时,哪些变量放哪个位置?
  • 6.如何修改state呢,神奇的setState?
  • 7.什么是immutable不可变对象?与state直接的关系?
  • 8.如何在JSX语法下map循环嵌套子组件,且在map下做if判断?
  • 9.在react中如何实现vue中的v-if和v-show?
  • 10.什么是CSS Modules模块化方案呢?
  • 11.CSS Modules的基本原理是什么?
  • 12.如何在Antd Pro中定义全局样式?
  • 13.常用的数组Array和对象Object API有哪些呢?

1.纯函数组件、PureComponent、Component之间有什么区别呢?

纯函数组件:

1
2
3
4
5
6
const Comp = (props) => (
<div>
<p>{props.title}</p>
<button onClick={props.handleClick}>点击</button>
</div>
)

PureComponent组件:

1
2
3
4
5
6
7
8
9
10
export default class Comp extends PureComponent {
render() {
return (
<div>
<p>{this.props.title}</p>
<button onClick={this.props.handleClick}>点击</button>
</div>
)
}
}

Component组件:

1
2
3
4
5
6
7
8
9
10
export default class Comp extends Component {
render() {
return (
<div>
<p>{this.props.title}</p>
<button onClick={this.props.handleClick}>点击</button>
</div>
)
}
}

这三种组件在react项目中经常用到,下面来说这三者的区别。
纯函数组件:与另外两种组件比,无组件生命周期无 state无 this,只能通过props的形式去传参,参数可以是变量,也可以是方法。
但是,自React 16.8起,无state这个特点,可以变成有state,通过react hooks提供的API,useState轻松实现。

Component组件:react官方提供的常规组件有组件生命周期有 state有 this可自定义shouldComponentUpdate()

PureComponent组件:react官方提供的Component组件的进化版,唯一的区别就是PureComponent组件默认实现shouldComponentUpdate()的功能。

在生命周期shouldComponentUpdate中,PureComponent进行了浅比较,而Component没有。进行浅比较的好处是可以减少render调用次数来减少性能损耗。当组件更新时,如果组件的props和state都没发生改变,render方法就不会触发。


2.那什么是浅比较?何时使用Component、何时使用PureComponent、何时使用纯函数呢?

浅比较,顾名思义就是当组件props或state发生变化时,会对比组件之前的props或state。

1
2
3
4
if (this._compositeType === CompositeTypes.PureClass) {
shouldUpdate = !shallowEqual(prevProps, nextProps)
|| !shallowEqual(inst.state, nextState);
}

浅比较它只会比较基本数据类型的值是否相等,引用数据类型(如对象或数组)只比较props和state的内存地址,如果内存地址相同,则shouldComponentUpdate生命周期就返回false,返回false时不会重写render。

PureComponent中如果有数据操作最好配合一个第三方组件——Immutable一起使用,因为Immutable可以保证数据的不变性

在Dva的官方文档中,也明确提到了不可变数据(immutable data):

dva_immut


dva_immut


既然知道了浅比较,那么我们何时使用Component、何时使用PureComponent、何时使用纯函数呢?
纯展示,那么选纯函数组件,尤其是需要map遍历的组件,比如列表中的每一行;
简单的state prop变化,那么选PureComponent,比如页面的局部模块,小组件等;
复杂的state prop变化,那么选Component,比如页面的整体布局,动态菜单等。

最后总结一下:
1.从性能对比,纯函数 > PureComponent > Component
性能越高,用户体验就会越好,因此能多用纯函数组件去实现就多用,尤其是无需使用生命周期函数的时候,纯函数组件完全可以胜任。
2.大部分的时候都可以使用PureComponent组件来替换Component组件,所以建议直接使用PureComponent,而不是Component。


3.变量存放的位置有props、state、this,如何确定哪些变量放哪个位置呢?

在开发react的时候,一个组件内存放变量的地方其实还挺多的,变量在哪个位置声明其实需要一个整体的规范。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Photo extends PureComponent {
static defaultProps = {...};
constructor(props) {
super(props);
this.state = { ... }
this.xxx = {...}
}

handleClick = () => {
this.setState({...}, () => { ... })
}

render() {
const { ... } = this
const { ... } = this.porps
const { ... } = this.state
return (
<div>
<p>{...}</p>
<button onClick={this.props.handleClick}>点击</button>
</div>
)
}

}

首先我们要明确在state、props、this下声明变量的意义。
在此之前,分析一下代码。一个组件都是使用 ES6 的class定义的,所以组件的属性其实也就是class的属性。

在 ES6 中,可以使用this.{属性名}定义一个class的属性,也可以说属性是直接挂载到this下的变量。因此,state、props实际上也是组件的属性,只不过它们是React为我们在Component class中预定义好的属性。除了state、props以外的其他组件属性称为组件的普通属性


4.state 和 props 又有什么区别呢?

state 和 props 都直接和组件的UI渲染有关,它们的变化都会触发组件重新渲染,但 props 对于使用它的组件来说是只读的,是通过父组件传递过来的,要想修改 props,只能在父组件中修改;而 state 是组件内部自己维护的状态,是可变的
其实区分 state 和 props 的关键就是,控制权是在组件自身,还是由其父组件来控制的


回过来,回答第3个的问题,如何确定哪些变量放哪个位置?
简而言之,不需要更新视图的数据,不应该放在 state 或 props 里,而是直接挂载到普通属性this里。

举个实际栗子:
state:放请求后的数据data、组件的显隐、样式变化
props:放父组件传来的数据data或method、dva 通过装饰器向组件的props属性中注入的dispatch方法和model中的state
this:放antd中table组件columns配置项、方法重载时加的类型区分、初始化数据等

最后总结一下:
state属性:存放引起更新视图的数据
props属性:存放父组件或dva传来的数据或方法
this普通属性:存放不引起更新视图的数据


5.在Antd Pro中,声明变量时,哪些变量放哪个位置?

上面我们明白了state属性、props属性、this普通属性的区别后,在Antd Pro中,声明变量在哪些位置,就会清晰很多。
下面是整个项目的目录结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
├── ...
├── src
│ ├── models # 全局dva model
│ ├── pages # 业务页面入口和常用模板
│ ├── student # 学生管理
│ ├── studentList # 学生列表模块
│ ├── studentDetail # 学生详情模块
│ └── models # 局部model只限学生管理内使用
│ ├── teacher # 老师管理
│ ├── teacherList # 老师列表模块
│ └── teacherDetail # 老师详情模块
│ └── models # 局部model只限老师管理内使用
│ └─ parent # 家长管理
│ └── global.ts # 全局 JS
├── ...
└── package.json

src目录下的models,在所有pages页面内都可以使用。
student目录下的models,只能在studentListstudentDetail页面内使用,teacherListteacherDetail页面无法使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
@connect(({ user, loading }) => ({
user,
loading: loading.effects['user/getUserInfo'],
}))
class Photo extends React.PureComponent {
static defaultProps = {...} // 声明当前组件默认props属性
constructor(props) {
super(props);
this.state = { ... } // 声明当前组件,引起更新视图的数据
this.xxx = {...} // 声明当前组件,不引起更新视图的数据
}
componentDidMount() {
this.getUserInfo()
}
getUserInfo = () => {
const { userId, dispatch } = this.props
// dispatch 是 dva 注入在props属性内的方法
dispatch({
type: 'user/getUserInfo',
paylod: { userId },
})
}
avatarError = () => {
const { ... } = this // 从当前组件的this获取的变量
const { ... } = this.state // 从当前组件的state获取的变量
this.setState({...}) // 更新当前组件的state值
}
...
render() {
const { userInfo: { name, url }, loading } = this.props
// userInfo 是 dva 注入在props属性内的变量
// loading 是 dva-loading 注入在props属性内的变量
return (
<Fragment>
{ loading && <Avatar src={url} onError={this.avatarError}/> }
{ loading && <p style={styles.name}>{name}</p> }
</Fragment>
);
}
}
export default Photo;

声明变量的时候,遵循以下规则,存放即可:

  • 变量是否引起页面渲染?
  • 不渲染,放defaultPropsthis.xxx中;
    • 渲染,是否请求?是否跨组件?
      • 是,放model的state中;
      • 否,放当前组件的this.state中。

6.如何修改state呢,神奇的setState?

明确以下几点内容:
1.不是所有的变量和数据都应该在state中维护,上面也说过了
在react中想触发视图更新,唯一的方式就是改变state,即使用setState方法。因为 props 是只读的,只是通过父组件的state值传过来,导致该组件渲染的。

1
2
3
4
5
6
7
// 错误
this.state.title = 'React';

// 正确
this.setState({
title: 'React'
})

2.state的更新是异步的
调用setState时,组件的state不会立即改变,setState只是把要修改的状态放入一个队列中,React会优化真正的执行时机,并且出于性能原因,可能会将多次setState的状态修改合并成一次状态修改

例如,如果 Parent 和 Child 在同一个 click 事件中都调用了 setState ,这样就可以确保 Child 不会被重新渲染两次。取而代之的是,React 会将该 state “冲洗” 到浏览器事件结束的时候,再统一地进行更新。这种机制可以在大型应用中得到很好的性能提升。

1
2
3
4
this.setState({...}, () => {
// state更新完毕的回调
....
})

7.什么是immutable不可变对象?与state直接的关系?

什么是不可变对象?
不可变对象:在对象保持不变的前提下,数据不能改变
对象不变,可以理解成内存地址不变,不会产生新的对象。

在JS中哪些类型是不可变对象呢?
JS基本类型属于不可变对象,对象类型不属于不可变对象。
在JS中,基本类型有boolean、number、string、undefined、null,对象有array、object。

1
2
3
4
5
6
7
8
9
10
11
12
13
var a = false / 1/ '1' / undefined / null
var b = false / 1/ '1' / undefined / null
a === b // true

// 数组
var a = []
var b = []
a === b // false

// 对象
var a = {}
var b = {}
a === b // flase

为什么基本数据类型都是不可变对象呢?
因为基本数据类型存储的是,对象类型存储的是内存地址


state与不可变对象的关系:
React官方建议把state当作不可变对象,state中包含的所有变量也都应该是不可变对象。当state中的某个变量发生变化时,应该重新创建这个变量对象,而不是直接修改原来的变量

可以分为下面三种情况:
1.变量的类型是基本类型:

1
2
3
4
5
this.setState({
count: 1, // 数字类型
title: 'React', // 字符串类型
success: true // 布尔类型
})

2.变量的类型是数组:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 数组类型
// 方法一:通过 concat 创建新数组
this.setState(state => ({
words: state.words.concat(['marklar']),
words: state.words.slice(1, 3),
words: state.words.filter(item => { return item !=== 'marklar' }),
}));

// 方法二:通过 ES6 的扩展运算符
this.setState(state => ({
words: [...state.words, 'marklar'],
}));

// 当需要对数组有其他操作时
// 通过slice截取数组、filter过滤数组、map获取数组某一项
this.setState(state => ({
words: state.words.slice(1, 3),
words: state.words.filter(e => { return e !=== 'marklar' }),
words: state.words.map(e => e.id),
}));

注意,不要使用push、pop、shift、unshift、splice等方法修改数组变量,因为这些方法都是在原数组的基础上修改的,而concat、slice、filter、map会返回一个新的数组
3.变量的类型是对象:

1
2
3
4
5
6
7
8
9
10
// 对象类型
// 方法一:通过 ES6 的Object.assgin方法
this.setState(state => ({
owner: Object.assign({}, state.owner, {name: 'Tony'});
}))

// 方法二:通过 ES6 的扩展运算符
this.setState(state => ({
owner: {...state.owner, name: 'Tony'};
}))

总结一下,将state当作不可变对象的关键是,避免使用会直接修改原对象的方法,而是使用可以返回一个新对象的方法。当然,也可以使用一些Immutable的JS库(如Immutable.jsSeamless-ImmutableImmer)实现类似的效果。

再回过头来写Antd Pro中 model 中的 reducers 时,能形成好的编写习惯:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
reducers: {
show(state, { payload }) {
return {
...state,
...payload,
}
},
saveLessons(state, { payload }) {
return {
...state,
lessons: payload.items,
lessonIds: payload.items.map(e => e.id),
}
},
saveStudentList(state, { payload }) {
return {
...state,
studentList: payload.items.filter(e => e.role === '学生'),
}
},
}

8.如何在JSX语法下map循环嵌套子组件,且在map下做if判断?

写法一:使用()

1
2
3
{lessonList.map((item) => (
<Lesson key={item.id}/>
))}

写法二:使用{}

1
2
3
4
5
{lessonList.map((item) => {
return (
<Lesson key={item.id}/>
)
})}

虽然写法二也没啥大问题,但是推荐写法一。


如果需要两层map的话,该怎么实现呢?
答案是拆成两个组件,因为JSX语法不支持两个map的嵌套:

1
2
3
4
5
6
7
8
9
{lessonList.map((item) => {
return (
<Lesson key={item.id} sections={item.sections}/>
)
})}

{props.sections.map((item, index) => (
<Section key={index} item={item}/>
))}

如何在map中写if判断呢?
JSX语法在map中不能使用if判断语句,但是可以用表达式,因此答案是三目运算符

1
2
3
{lessonList.map((item) => (
item.checked ? <Lesson key={item.id} /> : <Section key={item.id} />
))}

9.在react中如何实现vue中的v-if和v-show?

在vue中,v-if指令相当于创建和销毁DOM,而v-show指令相当于display:none,DOM始终存在,只是简单地基于 CSS 进行切换。
在react中,实现v-if使用&&三元运算符即可:

1
2
3
4
{checked && <Lesson />}
...
{checked ? <Lesson /> : <Section />}
...

在react,实现v-show:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// jsx中的style
style={props.show ? 'display:block' : 'display:none'}

// jsx中的className
className={['divWrapper', props.show ?
'show' : 'hide'].join(' ')}

// css中
.show {
display: block;
}
.hide {
dispaly: none;
}

在Andt Pro项目中,实现v-show:

1
2
className={[`${styles.pageWrapper}`, props.show 
? `${styles.show}` : ''].join(' ')}

为什么这样写?
因为Ant Design Pro 默认使用 less 作为样式语言,且使用了CSS Modules模块化方案。


10.什么是CSS Modules模块化方案呢?

cssmodules

在样式开发过程中,有两个问题比较突出:

  • 全局污染 —— CSS 文件中的选择器是全局生效的,不同文件中的同名选择器,根据 build 后生成文件中的先后顺序,后面的样式会将前面的覆盖;
  • 选择器复杂 —— 为了避免上面的问题,我们在编写样式的时候不得不小心翼翼,类名里会带上限制范围的标识,变得越来越长,多人开发时还很容易导致命名风格混乱,一个元素上使用的选择器个数也可能越来越多。

为了解决上述问题,Antd Pro脚手架默认使用 CSS Modules 模块化方案


CSS 模块化的解决方案有很多,但主要有两类。
一类是彻底抛弃 CSS,使用 JS 或 JSON 来写样式。Radium,jsxstyle,react-style 属于这一类。优点是能给 CSS 提供 JS 同样强大的模块化能力;缺点是不能利用成熟的 CSS 预处理器(或后处理器) Sass/Less/PostCSS,:hover 和 :active 伪类处理起来复杂。
另一类是依旧使用 CSS,但使用 JS 来管理样式依赖,代表是 CSS Modules。
CSS Modules 能最大化地结合现有 CSS 生态和 JS 模块化能力,API 简洁到几乎零学习成本。它并不依赖于 React,只要你使用 Webpack,可以在 Vue/Angular/jQuery 中使用。
CSS Modules 是我认为目前最好的 CSS 模块化解决方案。


11.CSS Modules的基本原理是什么?

CSS Modules 的基本原理很简单,就是对每个类名按照一定规则进行转换,保证它的唯一性
来看下在CSS Modules这种模式下怎么写样式:

1
2
3
4
5
6
7
8
9
import styles from './example.less';
...
<div className={styles.title}>{props.title}</div>
...
.title {
color: #FFF;
font-weight: 600;
margin-bottom: 16px;
}

如果在浏览器里查看这个示例的 dom结构,你会发现实际渲染出来是这样的:

1
<div class="title___3TqAx">title</div>

类名被自动添加了一个hash值,这保证了它的唯一性。
CSS Modules 只会对 className 以及 id 进行转换,其他的比如属性选择器,标签选择器都不进行处理,推荐尽量使用 className
由于不用担心类名重复,你的 className 可以在基本语意化的前提下尽量简单一点儿


12.如何在Antd Pro中定义全局样式?

1
2
3
4
5
6
7
8
9
10
11
/* 定义全局样式 */
:global(.text) {
font-size: 16px;
}

/* 覆盖antd button默认样式 */
.example {
:global(.ant-btn-primary) {
padding: 0 53px!important;
}
}

定义全局样式或覆盖antd组件默认样式,必须放到:global中
想了解更多 CSS Modules 的知识点,可参考:
1.github/css-modules
2.CSS Modules 用法教程
3.CSS Modules 详解及 React 中实践


13.常用的数组Array和对象Object API有哪些呢?

Array:
1.转换一个像数组的对象到数组
Array.from

1
2
3
4
5
6
7
/**
* 如:NodeList转Array
*/
const divs = document.querySelectorAll('div');
Array.isArray(divs); // false
const node = Array.from(divs);
Array.isArray(node); // true

2.获取数组键或值
Object.keysObject.values

1
2
3
const arr = [1, 2, 3];
Object.keys(arr); // ["0", "1", "2"]
Object.values(arr); // [1, 2, 3]

3.数组转key、value二维数组

1
2
const arr = [1, 2, 3];
Object.entries(arr); // [["0",1], ["1",2], ["2",3]]

4.增删单个选项
push、popunshift、shift

1
2
3
4
5
6
7
8
9
10
/**
* push、pop 从数组最后一个开始添加、删除
* unshift、shift 从数组开头一个开始添加、删除
*/
const arr = [1, 2, 3];
arr.push(4);// [1, 2, 3, 4]
arr.pop();// [1, 2, 3]

arr.unshift(4); // [4, 1, 2, 3]
arr.shift(); // [1, 2, 3]

5.合并插入截取
concat、joinsplice、slice

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* concat 合并两个或多个数组
* join 数组链接成字符串
* splice(startIndex, 0添加 非0删除个数, 添加的值)
* 删除现有元素的内容,或插入现有元素的内容
* slice(startIndex, endIndex)
* 截取数组选定的元素
*/
const a1 = [1, 2];
const a2 = [3, 4, 5];
const a3 = a1.concat(b2); // [1, 2, 3, 4, 5]

const str = a3.join(); // '1,2,3,4,5'
const str2 = a3.join(''); // '12345'

const arr = [1, 2, 3];
arr.splice(1, 0,4); // [1, 4, 2, 3]
arr.splice(0, 1); // [4, 2, 3]

const arr = [1, 2, 3, 4];
arr.slice(1, 3); // [2, 3]

6.排序倒序
sort、reverse

1
2
3
4
5
6
7
8
/**
* sort 排序
* reverse 倒序
*/
const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11];
arr.sort(); // 错误 [1, 10, 11, 2, 3, 4, 5, 6, 7, 8, 9]
arr.sort((a, b) => { return a - b });// 正确 [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
arr.reverse(); // [11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1]

下面来讲解Array的高阶函数map、filter、find、findIndex、every、some、reduce

7.遍历数组
尽管有for、for of、for in、forEach,但推荐map的简洁

1
2
const items = [1, 2, 3, 4];
items.map(e => e * 2); // [2, 4, 6, 8]

8.过滤筛选
filter

1
2
const items = [1, 2, 3, 4];
items.filter(e => e > 2); // [3, 4]

9.查找某个选项的值或索引
find、findIndex

1
2
3
const items = [1, 2, 3, 4];
items.find(e => e === 2) // 值2
items.findIndex(e => e === 2) // 索引1

10.检测所有元素或部分元素
some、every

1
2
3
const items = [1, 2, 3, 4];
items.some(e => e > 2); // true 是否有大于2
items.every(e => e > 2); // false 是否都大于2

11.复制数组
...扩展符

1
2
const items = [1, 2, 3, 4];
const items_copy = [...items]; // [1, 2, 3, 4]

12.聚合
reduce

1
2
3
4
5
6
7
[{x:1},{y:2},{z:3}].reduce((prev, next) => {
return Object.assign(prev, next); // 不推荐
}) // {x:1, y:2, z:3}

[{x:1},{y:2},{z:3}].reduce((prev, next) => {
return {...prev, ...next}; // 推荐
}) // {x:1, y:2, z:3}

Object:
1.复制对象
Object.assign...扩展符

1
2
3
const obj = {name:"ww", age:26, gender:"mail"};
const obj_like = Object.assign(obj, { like:"coding" }); // 不推荐
const obj_like = {...obj, like:"coding"}; // 推荐

2.获取对象键或值
Object.keysObject.values

1
2
3
const obj = {name:"ww", age:26, gender:"mail"};
Object.keys(obj); // ["name", "age", "gender"]
Object.values(obj); // ["ww", 26, "mail"]

3.对象转key、value二维数组
Object.entries

1
2
const obj = {name:"ww", age:26, gender:"mail"};
Object.entries(obj); // [["name","ww"], ["age",26], ["gender","mail"]]

值得注意的是Array的reduce

1
2
3
[{x:1},{y:2},{z:3}].reduce((prev, next) => {
return {...prev, ...next};
}) // {x:1, y:2, z:3}

是不是和redux或dva的Reducer的写法一样?
没错,Reducer 的概念来自于函数式编程,很多语言中都有 reduce API。


14.什么是Reducer?Dva中的Reducer?

Reducer(也称为 reducing function)函数接受两个参数:之前已经累积运算的结果和当前要被累积的值,返回的是一个新的累积结果。该函数把一个集合归并成一个单值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
reducers: {
saveLessonIds(state, { payload }) {
return {
...state,
lessonIds: payload.items,
}
},
saveLessons(state, { payload }) {
return {
...state,
lessons: payload.items,
lessonIds: payload.items.map(e => e.id),
}
},
saveLessonList(state, { payload }) {
const { items } = payload
return {
...state,
lessonList: items,
}
},
}

在 dva 中,reducers 聚合积累的结果是当前 model 的 state 对象。通过 actions 中传入的值,与当前 reducers 中的值进行运算获得新的值(也就是新的 state)。需要注意的是 Reducer 必须是纯函数,所以同样的输入必然得到同样的输出,它们不应该产生任何副作用。并且,每一次的计算都应该使用immutable data,这种特性简单理解就是每次操作都是返回一个全新的数据(独立,纯净),所以热重载和时间旅行这些功能才能够使用。