Blog Logo

React常见问题及解答(中)

写于2020-03-21 13:50 阅读耗时26分钟 阅读量


上一章,主要介绍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之间有什么区别呢?

纯函数组件:

const Comp = (props) => (
    <div>
        <p>{props.title}</p>
        <button onClick={props.handleClick}>点击</button>
    </div>
)

PureComponent组件:

export default class Comp extends PureComponent {
    render() {
        return (
        <div>
            <p>{this.props.title}</p>
            <button onClick={this.props.handleClick}>点击</button>
        </div>
        )
    }
}

Component组件:

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。

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的时候,一个组件内存放变量的地方其实还挺多的,变量在哪个位置声明其实需要一个整体的规范。

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中,声明变量在哪些位置,就会清晰很多。 下面是整个项目的目录结构:

├── ...
├── 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页面无法使用。

@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值传过来,导致该组件渲染的。

// 错误
this.state.title = 'React';

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

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

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

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

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

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

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

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.变量的类型是基本类型:

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

2.变量的类型是数组:

// 数组类型
// 方法一:通过 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.变量的类型是对象:

// 对象类型
// 方法一:通过 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 时,能形成好的编写习惯:

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判断?

写法一:使用()

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

写法二:使用{}

{lessonList.map((item) => {
    return (
        <Lesson key={item.id}/>
    )
 })}

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


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

{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判断语句,但是可以用表达式,因此答案是三目运算符

{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使用&&三元运算符即可:

{checked && <Lesson />}
...
{checked ? <Lesson /> : <Section />}
...

在react,实现v-show:

// 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:

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这种模式下怎么写样式:

import styles from './example.less';
...
<div className={styles.title}>{props.title}</div>
...
.title {
  color: #FFF;
  font-weight: 600;
  margin-bottom: 16px;
}

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

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

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


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

/* 定义全局样式 */
: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

/**
 * 如:NodeList转Array
 */
const divs = document.querySelectorAll('div');
Array.isArray(divs); // false
const node = Array.from(divs);
Array.isArray(node); // true

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

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

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

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

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

/**
 * 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

/**
 * 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

/**
 * 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的简洁

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

8.过滤筛选 filter

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

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

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

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

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

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

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

12.聚合 reduce

[{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...扩展符

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

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

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

值得注意的是Array的reduce

[{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)函数接受两个参数:之前已经累积运算的结果和当前要被累积的值,返回的是一个新的累积结果。该函数把一个集合归并成一个单值。

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,这种特性简单理解就是每次操作都是返回一个全新的数据(独立,纯净),所以热重载和时间旅行这些功能才能够使用。

Headshot of Maxi Ferreira

怀着敬畏之心,做好每一件事。