求知若饥,虚心若愚。
上一章,主要介绍Antd Pro的背景和部分代码流程,这章将会更细致的介绍react其他知识点。
纯函数组件:
1 | const Comp = (props) => ( |
PureComponent组件:
1 | export default class Comp extends PureComponent { |
Component组件:
1 | export default class Comp extends Component { |
这三种组件在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方法就不会触发。
浅比较,顾名思义就是当组件props或state发生变化时,会对比组件之前的props或state。
1 | if (this._compositeType === CompositeTypes.PureClass) { |
浅比较它只会比较基本数据类型的值是否相等
,引用数据类型(如对象或数组)只比较props和state的内存地址
,如果内存地址相同,则shouldComponentUpdate生命周期就返回false,返回false时不会重写render。
PureComponent中如果有数据操作最好配合一个第三方组件——Immutable
一起使用,因为Immutable可以保证数据的不变性
。
在Dva的官方文档中,也明确提到了不可变数据(immutable data):
既然知道了浅比较,那么我们何时使用Component、何时使用PureComponent、何时使用纯函数呢?纯展示
,那么选纯函数组件
,尤其是需要map遍历的组件
,比如列表中的每一行;简单的state prop变化
,那么选PureComponent
,比如页面的局部模块,小组件等;复杂的state prop变化
,那么选Component
,比如页面的整体布局,动态菜单等。
最后总结一下:
1.从性能对比,纯函数 > PureComponent > Component
性能越高,用户体验就会越好,因此能多用纯函数组件去实现就多用,尤其是无需使用生命周期函数的时候,纯函数组件完全可以胜任。
2.大部分的时候都可以使用PureComponent
组件来替换Component
组件,所以建议直接使用PureComponent,而不是Component。
在开发react的时候,一个组件内存放变量的地方其实还挺多的,变量在哪个位置声明其实需要一个整体的规范。
1 | class Photo extends PureComponent { |
首先我们要明确在state、props、this下声明变量的意义。
在此之前,分析一下代码。一个组件都是使用 ES6 的class
定义的,所以组件的属性其实也就是class的属性。
在 ES6 中,可以使用this.{属性名}定义一个class的属性
,也可以说属性是直接挂载到this下的变量
。因此,state、props实际上也是组件的属性
,只不过它们是React为我们在Component class中预定义好的属性。除了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普通属性:存放不引起更新视图的数据
上面我们明白了state属性、props属性、this普通属性的区别后,在Antd Pro中,声明变量在哪些位置,就会清晰很多。
下面是整个项目的目录结构:
1 | ├── ... |
src
目录下的models,在所有pages
页面内都可以使用。student
目录下的models,只能在studentList
和studentDetail
页面内使用,teacherList
和teacherDetail
页面无法使用。
1 | ({ user, loading }) => ({ ( |
声明变量的时候,遵循以下规则,存放即可:
defaultProps
或this.xxx
中;model的state
中;当前组件的this.state
中。明确以下几点内容:1.不是所有的变量和数据都应该在state中维护,上面也说过了
。
在react中想触发视图更新,唯一的方式就是改变state
,即使用setState
方法。因为 props 是只读的,只是通过父组件的state
值传过来,导致该组件渲染的。
1 | // 错误 |
2.state的更新是异步的
调用setState
时,组件的state不会立即改变
,setState只是把要修改的状态放入一个队列
中,React会优化真正的执行时机,并且出于性能原因,可能会将多次setState的状态修改合并成一次状态修改
。
例如,如果 Parent 和 Child 在同一个 click 事件中都调用了 setState ,这样就可以确保 Child 不会被重新渲染两次。取而代之的是,React 会将该 state “冲洗” 到浏览器事件结束的时候,再统一地进行更新。这种机制可以在大型应用中得到很好的性能提升。
1 | this.setState( , () => { |
什么是不可变对象?
不可变对象:在对象保持不变的前提下,数据不能改变
。
对象不变,可以理解成内存地址不变
,不会产生新的对象。
在JS中哪些类型是不可变对象呢?
JS基本类型
属于不可变对象,对象类型
不属于不可变对象。
在JS中,基本类型有boolean、number、string、undefined、null,对象有array、object。
1 | var a = false / 1/ '1' / undefined / null |
为什么基本数据类型都是不可变对象呢?
因为基本数据类型存储的是值
,对象类型存储的是内存地址
。
state与不可变对象的关系:
React官方建议把state当作不可变对象
,state中包含的所有变量也都应该是不可变对象。当state中的某个变量发生变化时,应该重新创建
这个变量对象,而不是直接修改原来的变量
。
可以分为下面三种情况:
1.变量的类型是基本类型:
1 | this.setState({ |
2.变量的类型是数组:
1 | // 数组类型 |
注意,不要使用push、pop、shift、unshift、splice等方法修改数组变量
,因为这些方法都是在原数组的基础上修改的,而concat、slice、filter、map会返回一个新的数组
。
3.变量的类型是对象:
1 | // 对象类型 |
总结一下,将state当作不可变对象
的关键是,避免使用会直接修改原对象的方法,而是使用可以返回一个新对象的方法
。当然,也可以使用一些Immutable的JS库(如Immutable.js
、Seamless-Immutable
、Immer
)实现类似的效果。
再回过头来写Antd Pro中 model 中的 reducers 时,能形成好的编写习惯:
1 | reducers: { |
写法一:使用()
1 | {lessonList.map((item) => ( |
写法二:使用{}
1 | {lessonList.map((item) => { |
虽然写法二也没啥大问题,但是推荐写法一。
如果需要两层map的话,该怎么实现呢?
答案是拆成两个组件
,因为JSX语法不支持两个map的嵌套:
1 | {lessonList.map((item) => { |
如何在map中写if判断呢?
JSX语法在map中不能使用if判断语句,但是可以用表达式,因此答案是三目运算符
:
1 | {lessonList.map((item) => ( |
在vue中,v-if指令相当于创建和销毁DOM
,而v-show指令相当于display:none
,DOM始终存在,只是简单地基于 CSS 进行切换。
在react中,实现v-if使用&&
或三元运算符
即可:
1 | {checked && <Lesson />} |
在react,实现v-show:
1 | // jsx中的style |
在Andt Pro项目中,实现v-show:
1 | className={[`${styles.pageWrapper}`, props.show |
为什么这样写?
因为Ant Design Pro 默认使用 less
作为样式语言,且使用了CSS Modules
模块化方案。
在样式开发过程中,有两个问题比较突出:
为了解决上述问题,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 模块化解决方案。
CSS Modules 的基本原理很简单,就是对每个类名按照一定规则进行转换,保证它的唯一性
。
来看下在CSS Modules这种模式下怎么写样式:
1 | import styles from './example.less'; |
如果在浏览器里查看这个示例的 dom结构,你会发现实际渲染出来是这样的:
1 | <div class="title___3TqAx">title</div> |
类名被自动添加了一个hash值
,这保证了它的唯一性。
CSS Modules 只会对 className
以及 id
进行转换,其他的比如属性选择器,标签选择器都不进行处理,推荐尽量使用 className
。
由于不用担心类名重复,你的 className
可以在基本语意化的前提下尽量简单一点儿
。
1 | /* 定义全局样式 */ |
定义全局样式或覆盖antd组件默认样式,必须放到:global中
想了解更多 CSS Modules 的知识点,可参考:
1.github/css-modules
2.CSS Modules 用法教程
3.CSS Modules 详解及 React 中实践
Array:
1.转换一个像数组的对象到数组Array.from
1 | /** |
2.获取数组键或值Object.keys
和Object.values
1 | const arr = [1, 2, 3]; |
3.数组转key、value二维数组
1 | const arr = [1, 2, 3]; |
4.增删单个选项push、pop
和unshift、shift
1 | /** |
5.合并插入截取concat、join
和splice、slice
1 | /** |
6.排序倒序sort、reverse
1 | /** |
下面来讲解Array的高阶函数
:map、filter、find、findIndex、every、some、reduce
。
7.遍历数组
尽管有for、for of、for in、forEach,但推荐map
的简洁
1 | const items = [1, 2, 3, 4]; |
8.过滤筛选filter
1 | const items = [1, 2, 3, 4]; |
9.查找某个选项的值或索引find、findIndex
1 | const items = [1, 2, 3, 4]; |
10.检测所有元素或部分元素some、every
1 | const items = [1, 2, 3, 4]; |
11.复制数组...扩展符
1 | const items = [1, 2, 3, 4]; |
12.聚合reduce
1 | [{x:1},{y:2},{z:3}].reduce((prev, next) => { |
Object:
1.复制对象Object.assign
和...扩展符
1 | const obj = {name:"ww", age:26, gender:"mail"}; |
2.获取对象键或值Object.keys
和Object.values
1 | const obj = {name:"ww", age:26, gender:"mail"}; |
3.对象转key、value二维数组Object.entries
1 | const obj = {name:"ww", age:26, gender:"mail"}; |
值得注意的是Array的reduce
:
1 | [{x:1},{y:2},{z:3}].reduce((prev, next) => { |
是不是和redux或dva的Reducer
的写法一样?
没错,Reducer 的概念来自于函数式编程,很多语言中都有 reduce API。
Reducer(也称为 reducing function)函数接受两个参数:之前已经累积运算的结果和当前要被累积的值,返回的是一个新的累积结果。该函数把一个集合归并成一个单值。
1 | reducers: { |
在 dva 中,reducers 聚合积累的结果是当前 model 的 state 对象。通过 actions 中传入的值,与当前 reducers 中的值进行运算获得新的值(也就是新的 state)。需要注意的是 Reducer 必须是纯函数,所以同样的输入必然得到同样的输出,它们不应该产生任何副作用。并且,每一次的计算都应该使用immutable data,这种特性简单理解就是每次操作都是返回一个全新的数据(独立,纯净),所以热重载和时间旅行这些功能才能够使用。