Tiny'Wo | 小窝

网络中的一小块自留地

面试官:说说react中引入css的方式有哪几种?区别�?

一、是什�?

组件式开发选择合适的css解决方案尤为重要

通常会遵循以下规则:

  • 可以编写局部css,不会随意污染其他组件内的原生;
  • 可以编写动态的css,可以获取当前组件的一些状态,根据状态的变化生成不同的css样式�?- 支持所有的css特性:伪类、动画、媒体查询等�?- 编写起来简洁方便、最好符合一贯的css风格特点

在这一方面,vue使用css起来更为简洁:

  • 通过 style 标签编写样式
  • scoped 属性决定编写的样式是否局部有�?- lang 属性设置预处理�?- 内联样式风格的方式来根据最新状态设置和改变css

而在react中,引入CSS就不如Vue方便简洁,其引入css的方式有很多种,各有利弊

二、方�?

常见的CSS引入方式有以下:

  • 在组件内直接使用
  • 组件中引�?.css 文件
  • 组件中引�?.module.css 文件
  • CSS in JS

在组件内直接使用

直接在组件中书写css样式,通过style属性直接引入,如下�?

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
import React, { Component } from "react";

const div1 = {
width: "300px",
margin: "30px auto",
backgroundColor: "#44014C", //驼峰�? minHeight: "200px",
boxSizing: "border-box"
};

class Test extends Component {
constructor(props, context) {
super(props);
}

render() {
return (
<div>
<div style={div1}>123</div>
<div style={{backgroundColor:"red"}}>
</div>
);
}
}

export default Test;

上面可以看到,css属性需要转换成驼峰写法

这种方式优点�?

  • 内联样式, 样式之间不会有冲�?- 可以动态获取当前state中的状�?
    缺点�?

  • 写法上都需要使用驼峰标�?

  • 某些样式没有提示

  • 大量的样�? 代码混乱

  • 某些样式无法编写(比如伪类/伪元�?

组件中引入css文件

css单独写在一个css文件中,然后在组件中直接引入

App.css文件�?

1
2
3
4
5
6
7
8
9
.title {
color: red;
font-size: 20px;
}

.desc {
color: green;
text-decoration: underline;
}

组件中引入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import React, { PureComponent } from 'react';

import Home from './Home';

import './App.css';

export default class App extends PureComponent {
render() {
return (
<div className="app">
<h2 className="title">我是App的标�?/h2>
<p className="desc">我是App中的一段文字描�?/p >
<Home/>
</div>
)
}
}

这种方式存在不好的地方在于样式是全局生效,样式之间会互相影响

组件中引�?.module.css 文件

css文件作为一个模块引入,这个模块中的所有css,只作用于当前组件。不会影响当前组件的后代组件

这种方式是webpack特工的方案,只需要配置webpack配置文件中modules:true即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import React, { PureComponent } from 'react';

import Home from './Home';

import './App.module.css';

export default class App extends PureComponent {
render() {
return (
<div className="app">
<h2 className="title">我是App的标�?/h2>
<p className="desc">我是App中的一段文字描�?/p >
<Home/>
</div>
)
}
}

这种方式能够解决局部作用域问题,但也有一定的缺陷�?

  • 引用的类名,不能使用连接�?.xxx-xx),在 JavaScript 中是不识别的
  • 所有的 className 都必须使�?{style.className} 的形式来编写
  • 不方便动态来修改某些样式,依然需要使用内联样式的方式�?

CSS in JS

CSS-in-JS�?是指一种模式,其中CSS�?JavaScript 生成而不是在外部文件中定�?
此功能并不是 React 的一部分,而是由第三方库提供,例如�?

  • styled-components
  • emotion
  • glamorous

下面主要看看styled-components的基本使�?
本质是通过函数的调用,最终创建出一个组件:

  • 这个组件会被自动添加上一个不重复的class
  • styled-components会给该class添加相关的样�?
    基本使用如下�?
    创建一个style.js文件用于存放样式组件�?
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    export const SelfLink = styled.div`
    height: 50px;
    border: 1px solid red;
    color: yellow;
    `;

    export const SelfButton = styled.div`
    height: 150px;
    width: 150px;
    color: ${props => props.color};
    background-image: url(${props => props.src});
    background-size: 150px 150px;
    `;

引入样式组件也很简单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import React, { Component } from "react";

import { SelfLink, SelfButton } from "./style";

class Test extends Component {
constructor(props, context) {
super(props);
}

render() {
return (
<div>
<SelfLink title="People's Republic of China">app.js</SelfLink>
<SelfButton color="palevioletred" style={{ color: "pink" }} src={fist}>
SelfButton
</SelfButton>
</div>
);
}
}

export default Test;

三、区�?

通过上面四种样式的引入,可以看到�?

  • 在组件内直接使用css该方式编写方便,容易能够根据状态修改样式属性,但是大量的演示编写容易导致代码混�?- 组件中引�?.css 文件符合我们日常的编写习惯,但是作用域是全局的,样式之间会层�?- 引入.module.css 文件能够解决局部作用域问题,但是不方便动态修改样式,需要使用内联的方式进行样式的编�?
  • 通过css in js 这种方法,可以满足大部分场景的应用,可以类似于预处理器一样样式嵌套、定义、修改状态等

至于使用react用哪种方案引入css,并没有一个绝对的答案,可以根据各自情况选择合适的方案

参考文�?

面试官:说说你是如何提高组件的渲染效率的?在React中如何避免不必要的render�?

一、是什�?

react 基于虚拟 DOM 和高�?Diff 算法的完美配合,实现了对 DOM 最小粒度的更新,大多数情况下,React �?DOM 的渲染效率足以我们的业务日常

复杂业务场景下,性能问题依然会困扰我们。此时需要采取一些措施来提升运行性能,避免不必要的渲染则是业务中常见的优化手段之一

二、如何做

在之前文章中,我们了解到render的触发时机,简单来讲就是类组件通过调用setState方法�?就会导致render,父组件一旦发生render渲染,子组件一定也会执行render渲染

从上面可以看到,父组件渲染导致子组件渲染,子组件并没有发生任何改变,这时候就可以从避免无谓的渲染,具体实现的方式有如下:

  • shouldComponentUpdate
  • PureComponent
  • React.memo

shouldComponentUpdate

通过shouldComponentUpdate生命周期函数来比�?state �?props,确定是否要重新渲染

默认情况下返回true表示重新渲染,如果不希望组件重新渲染,返�?false 即可

PureComponent

shouldComponentUpdate 原理基本一致,通过�?props �?state的浅比较结果来实�?shouldComponentUpdate,源码大致如下:

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

shallowEqual对应方法大致如下�?

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
42
const hasOwnProperty = Object.prototype.hasOwnProperty;

/**
* is 方法来判断两个值是否是相等的值,为何这么写可以移�?MDN 的文�? * https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/is
*/
function is(x: mixed, y: mixed): boolean {
if (x === y) {
return x !== 0 || y !== 0 || 1 / x === 1 / y;
} else {
return x !== x && y !== y;
}
}

function shallowEqual(objA: mixed, objB: mixed): boolean {
// 首先对基本类型进行比�? if (is(objA, objB)) {
return true;
}

if (typeof objA !== 'object' || objA === null ||
typeof objB !== 'object' || objB === null) {
return false;
}

const keysA = Object.keys(objA);
const keysB = Object.keys(objB);

// 长度不相等直接返回false
if (keysA.length !== keysB.length) {
return false;
}

// key相等的情况下,再去循环比�? for (let i = 0; i < keysA.length; i++) {
if (
!hasOwnProperty.call(objB, keysA[i]) ||
!is(objA[keysA[i]], objB[keysA[i]])
) {
return false;
}
}

return true;
}

当对象包含复杂的数据结构时,对象深层的数据已改变却没有触�?render

注意:在react中,是不建议使用深层次结构的数据

React.memo

React.memo用来缓存组件的渲染,避免不必要的更新,其实也是一个高阶组件,�?PureComponent 十分类似。但不同的是�?React.memo 只能用于函数组件

1
2
3
4
5
6
7
import { memo } from 'react';

function Button(props) {
// Component code
}

export default memo(Button);

如果需要深层次比较,这时候可以给memo第二个参数传递比较函�?

1
2
3
4
5
6
function arePropsEqual(prevProps, nextProps) {
// your code
return prevProps === nextProps;
}

export default memo(Button, arePropsEqual);

三、总结

在实际开发过程中,前端性能问题是一个必须考虑的问题,随着业务的复杂,遇到性能问题的概率也在增�?
除此之外,建议将页面进行更小的颗粒化,如果一个过大,当状态发生修改的时候,就会导致整个大组件的渲染,而对组件进行拆分后,粒度变小了,也能够减少子组件不必要的渲染

参考文�?

面试官:React中的key有什么作用?

一、是什�?

首先,先给出react组件中进行列表渲染的一个示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const data = [
{ id: 0, name: 'abc' },
{ id: 1, name: 'def' },
{ id: 2, name: 'ghi' },
{ id: 3, name: 'jkl' }
];

const ListItem = (props) => {
return <li>{props.name}</li>;
};

const List = () => {
return (
<ul>
{data.map((item) => (
<ListItem name={item.name}></ListItem>
))}
</ul>
);
};

然后在输出就可以看到react所提示的警告信息:

1
Each child in a list should have a unique "key" prop.

根据意思就可以得到渲染列表的每一个子元素都应该需要一个唯一的key�?
在这里可以使用列表的id属性作为key值以解决上面这个警告

1
2
3
4
5
6
7
8
9
const List = () => {
return (
<ul>
{data.map((item) => (
<ListItem name={item.name} key={item.id}></ListItem>
))}
</ul>
);
};

二、作�?

Vue一样,React 也存�?Diff算法,而元素key属性的作用是用于判断元素是新创建的还是被移动的元素,从而减少不必要的元素渲�?
因此key的值需要为每一个元素赋予一个确定的标识

如果列表数据渲染中,在数据后面插入一条数据,key作用并不大,如下�?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
this.state = {
numbers:[111,222,333]
}

insertMovie() {
const newMovies = [...this.state.numbers, 444];
this.setState({
movies: newMovies
})
}

<ul>
{
this.state.movies.map((item, index) => {
return <li>{item}</li>
})
}
</ul>

前面的元素在diff算法中,前面的元素由于是完全相同的,并不会产生删除创建操作,在最后一个比较的时候,则需要插入到新的DOM树中

因此,在这种情况下,元素有无key属性意义并不大

下面再来看看在前面插入数据时,使用key与不使用key的区别:

1
2
3
4
5
6
insertMovie() {
const newMovies = [000 ,...this.state.numbers];
this.setState({
movies: newMovies
})
}

当拥有key的时候,react根据key属性匹配原有树上的子元素以及最新树上的子元素,像上述情况只需要将000元素插入到最前面位置

当没有key的时候,所有的li标签都需要进行修�?
同样,并不是拥有key值代表性能越高,如果说只是文本内容改变了,不写key反而性能和效率更�?
主要是因为不写key是将所有的文本内容替换一下,节点不会发生变化

而写key则涉及到了节点的增和删,发现旧key不存在了,则将其删除,新key在之前没有,则插入,这就增加性能的开销

三、总结

良好使用key属性是性能优化的非常关键的一步,注意事项为:

  • key 应该是唯一�?- key不要使用随机值(随机数在下一�?render 时,会重新生成一个数字)

  • 使用 index 作为 key值,对性能没有优化

react判断key的流程具体如下图�?

参考文�?

面试官:说说 React 生命周期有哪些不同阶段?每个阶段对应的方法是�?

一、是什�?

以前文章中,我们了解到生命周期定�?
生命周期(Life Cycle)的概念应用很广泛,特别是在经济、环境、技术、社会等诸多领域经常出现,其基本涵义可以通俗地理解为“从摇篮到坟墓”(Cradle-to-Grave)的整个过�?
Vue一样,React整个组件生命周期包括从创建、初始化数据、编译模板、挂载Dom→渲染、更新→渲染、卸载等一系列过程

二、流�?

这里主要讲述react16.4之后的生命周期,可以分成三个阶段�?

  • 创建阶段
  • 更新阶段
  • 卸载阶段

创建阶段

创建阶段主要分成了以下几个生命周期方法:

  • constructor
  • getDerivedStateFromProps
  • render
  • componentDidMount

constructor

实例过程中自动调用的方法,在方法内部通过super关键字获取来自父组件的props

在该方法中,通常的操作为初始化state状态或者在this上挂载方�?

getDerivedStateFromProps

该方法是新增的生命周期方法,是一个静态的方法,因此不能访问到组件的实�?
执行时机:组件创建和更新阶段,不论是props变化还是state变化,也会调�?
在每次render方法前调用,第一个参数为即将更新的props,第二个参数为上一个状态的state,可以比较props �?state来加一些限制条件,防止无用的state更新

该方法需要返回一个新的对象作为新的state或者返回null表示state状态不需要更�?

render

类组件必须实现的方法,用于渲染DOM结构,可以访问组件stateprop属�?
注意�?不要�?render 里面 setState, 否则会触发死循环导致内存崩溃

componentDidMount

组件挂载到真实DOM节点后执行,其在render方法之后执行

此方法多用于执行一些数据获取,事件监听等操�?

更新阶段

该阶段的函数主要为如下方法:

  • getDerivedStateFromProps
  • shouldComponentUpdate
  • render
  • getSnapshotBeforeUpdate
  • componentDidUpdate

getDerivedStateFromProps

该方法介绍同�?

shouldComponentUpdate

用于告知组件本身基于当前的propsstate是否需要重新渲染组件,默认情况返回true

执行时机:到新的props或者state时都会调用,通过返回true或者false告知组件更新与否

一般情况,不建议在该周期方法中进行深层比较,会影响效率

同时也不能调用setState,否则会导致无限循环调用更新

render

介绍如上

getSnapshotBeforeUpdate

该周期函数在render后执行,执行之时DOM元素还没有被更新

该方法返回的一个Snapshot值,作为componentDidUpdate第三个参数传�?

1
2
3
4
5
6
7
8
getSnapshotBeforeUpdate(prevProps, prevState) {
console.log('#enter getSnapshotBeforeUpdate');
return 'foo';
}

componentDidUpdate(prevProps, prevState, snapshot) {
console.log('#enter componentDidUpdate snapshot = ', snapshot);
}

此方法的目的在于获取组件更新前的一些信息,比如组件的滚动位置之类的,在组件更新后可以根据这些信息恢复一些UI视觉上的状�?

componentDidUpdate

执行时机:组件更新结束后触发

在该方法中,可以根据前后的propsstate的变化做相应的操作,如获取数据,修改DOM样式�?

卸载阶段

componentWillUnmount

此方法用于组件卸载前,清理一些注册是监听事件,或者取消订阅的网络请求�?
一旦一个组件实例被卸载,其不会被再次挂载,而只可能是被重新创建

三、总结

新版生命周期整体流程如下图所示:

旧的生命周期流程图如下:

通过两个图的对比,可以发现新版的生命周期减少了以下三种方法:

  • componentWillMount
  • componentWillReceiveProps
  • componentWillUpdate

其实这三个方法仍然存在,只是在前者加上了UNSAFE_前缀,如UNSAFE_componentWillMount,并不像字面意思那样表示不安全,而是表示这些生命周期的代码可能在未来�?react 版本可能废除

同时也新增了两个生命周期函数�?

  • getDerivedStateFromProps
  • getSnapshotBeforeUpdate

参考文�?

说说你对Redux的理解?其工作原理?

一、是什�?

React是用于构建用户界面的,帮助我们解决渲染DOM的过�?
而在整个应用中会存在很多个组件,每个组件的state是由自身进行管理,包括组件定义自身的state、组件之间的通信通过props传递、使用Context实现数据共享

如果让每个组件都存储自身相关的状态,理论上来讲不会影响应用的运行,但在开发及后续维护阶段,我们将花费大量精力去查询状态的变化过程

这种情况下,如果将所有的状态进行集中管理,当需要更新状态的时候,仅需要对这个管理集中处理,而不用去关心状态是如何分发到每一个组件内部的

redux就是一个实现上述集中管理的容器,遵循三大基本原则:

  • 单一数据�?- state 是只读的
  • 使用纯函数来执行修改

注意的是,redux并不是只应用在react中,还与其他界面库一起使用,如Vue

二、工作原�?

redux 要求我们把数据都放在 store 公共存储空间

一个组件改变了 store 里的数据内容,其他组件就能感知到 store 的变化,再来取数据,从而间接的实现了这些数据传递的功能

工作流程图如下所示:

根据流程图,可以想象,React Components 是借书的用户, Action Creactor 是借书时说的话(借什么书)�?Store 是图书馆管理员,Reducer 是记录本(借什么书,还什么书,在哪儿,需要查一�?�?state 是书籍信�?
整个流程就是借书的用户需要先存在,然后需要借书,需要一句话来描述借什么书,图书馆管理员听到后需要查一下记录本,了解图书的位置,最后图书馆管理员会把这本书给到这个借书�?
转换为代码是,React Components 需要获取一些数�? 然后它就告知 Store 需要获取数据,这就是就�?Action Creactor , Store 接收到之后去 Reducer 查一下, Reducer 会告�?Store 应该给这个组件什么数�?

三、如何使�?

创建一个store的公共数据区�?

1
import { createStore } from 'redux' // 引入一个第三方的方�?const store = createStore() // 创建数据的公共存储区域(管理员)

还需要创建一个记录本去辅助管理数据,也就是reduecer,本质就是一个函数,接收两个参数stateaction,返回state

1
2
3
4
5
6
// 设置默认�?const initialState = {
counter: 0
}

const reducer = (state = initialState, action) => {
}

然后就可以将记录本传递给store,两者建立连接。如下:

1
const store = createStore(reducer)

如果想要获取store里面的数据,则通过store.getState()来获取当前state

1
console.log(store.getState());

下面再看看如何更改store里面数据,是通过dispatch来派发action,通常action中都会有type属性,也可以携带其他的数据

1
2
3
4
5
6
7
8
9
10
11
12
store.dispatch({
type: "INCREMENT"
})

store.dispath({
type: "DECREMENT"
})

store.dispatch({
type: "ADD_NUMBER",
number: 5
})

下面再来看看修改reducer中的处理逻辑�?

1
2
3
4
5
6
7
8
9
10
11
12
const reducer = (state = initialState, action) => {
switch (action.type) {
case "INCREMENT":
return {...state, counter: state.counter + 1};
case "DECREMENT":
return {...state, counter: state.counter - 1};
case "ADD_NUMBER":
return {...state, counter: state.counter + action.number}
default:
return state;
}
}

注意,reducer是一个纯函数,不需要直接修改state

这样派发action之后,既可以通过store.subscribe监听store的变化,如下�?

1
2
3
store.subscribe(() => {
console.log(store.getState());
})

React项目中,会搭配react-redux进行使用

完整代码如下�?

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
42
43
const redux = require('redux');

const initialState = {
counter: 0
}

// 创建reducer
const reducer = (state = initialState, action) => {
switch (action.type) {
case "INCREMENT":
return {...state, counter: state.counter + 1};
case "DECREMENT":
return {...state, counter: state.counter - 1};
case "ADD_NUMBER":
return {...state, counter: state.counter + action.number}
default:
return state;
}
}

// 根据reducer创建store
const store = redux.createStore(reducer);

store.subscribe(() => {
console.log(store.getState());
})

// 修改store中的state
store.dispatch({
type: "INCREMENT"
})
// console.log(store.getState());

store.dispatch({
type: "DECREMENT"
})
// console.log(store.getState());

store.dispatch({
type: "ADD_NUMBER",
number: 5
})
// console.log(store.getState());

小结

  • createStore可以帮助创建 store
  • store.dispatch 帮助派发 action , action 会传递给 store
  • store.getState 这个方法可以帮助获取 store 里边所有的数据内容
  • store.subscrible 方法订阅 store 的改变,只要 store 发生改变�?store.subscrible 这个函数接收的这个回调函数就会被执行

参考文�?

面试官:说说React render方法的原理?在什么时候会被触发?

一、原�?

首先,render函数在react中有两种形式�?
在类组件中,指的是render方法�?

1
2
3
4
5
class Foo extends React.Component {
render() {
return <h1> Foo </h1>;
}
}

在函数组件中,指的是函数组件本身�?

1
2
3
function Foo() {
return <h1> Foo </h1>;
}

render中,我们会编写jsxjsx通过babel编译后就会转化成我们熟悉的js格式,如下:

1
2
3
4
5
6
7
return (
<div className='cn'>
<Header> hello </Header>
<div> start </div>
Right Reserve
</div>
)

babel编译后:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
return (
React.createElement(
'div',
{
className : 'cn'
},
React.createElement(
Header,
null,
'hello'
),
React.createElement(
'div',
null,
'start'
),
'Right Reserve'
)
)

从名字上来看,createElement方法用来元素�?
react中,这个元素就是虚拟DOM树的节点,接收三个参数:

  • type:标�?- attributes:标签属性,若无则为null

  • children:标签的子节�?
    这些虚拟DOM树最终会渲染成真实DOM

render过程中,React 将新调用�?render 函数返回的树与旧版本的树进行比较,这一步是决定如何更新 DOM 的必要步骤,然后进行 diff 比较,更�?DOM �?

二、触发时�?

render的执行时机主要分成了两部分:

  • 类组件调�?setState 修改状�?
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    class Foo extends React.Component {
    state = { count: 0 };

    increment = () => {
    const { count } = this.state;

    const newCount = count < 10 ? count + 1 : count;

    this.setState({ count: newCount });
    };

    render() {
    const { count } = this.state;
    console.log("Foo render");

    return (
    <div>
    <h1> {count} </h1>
    <button onClick={this.increment}>Increment</button>
    </div>
    );
    }
    }

点击按钮,则调用setState方法,无论count发生变化辩护,控制台都会输出Foo render,证明render执行�?

  • 函数组件通过useState hook修改状�?
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    function Foo() {
    const [count, setCount] = useState(0);

    function increment() {
    const newCount = count < 10 ? count + 1 : count;
    setCount(newCount);
    }

    console.log("Foo render");

    return (
    <div>
    <h1> {count} </h1>
    <button onClick={increment}>Increment</button>
    </div>
    );
    }

函数组件通过useState这种形式更新数据,当数组的值不发生改变了,就不会触发render

  • 类组件重新渲�?
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    class App extends React.Component {
    state = { name: "App" };
    render() {
    return (
    <div className="App">
    <Foo />
    <button onClick={() => this.setState({ name: "App" })}>
    Change name
    </button>
    </div>
    );
    }
    }

    function Foo() {
    console.log("Foo render");

    return (
    <div>
    <h1> Foo </h1>
    </div>
    );
    }

只要点击�?App 组件内的 Change name 按钮,不�?Foo 具体实现是什么,都会被重新render渲染

  • 函数组件重新渲染
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function App(){
const [name,setName] = useState('App')

return (
<div className="App">
<Foo />
<button onClick={() => setName("aaa")}>
{ name }
</button>
</div>
)
}

function Foo() {
console.log("Foo render");

return (
<div>
<h1> Foo </h1>
</div>
);
}

可以发现,使用useState来更新状态的时候,只有首次会触发Foo render,后面并不会导致Foo render

三、总结

render函数里面可以编写JSX,转化成createElement这种形式,用于生成虚拟DOM,最终转化成真实DOM

React 中,类组件只要执行了 setState 方法,就一定会触发 render 函数执行,函数组件使用useState更改状态不一定导致重新render

组件的 props 改变了,不一定触�?render 函数的执行,但是如果 props 的值来自于父组件或者祖先组件的 state

在这种情况下,父组件或者祖先组件的 state 发生了改变,就会导致子组件的重新渲染

所以,一旦执行了setState就会执行render方法,useState 会判断当前值有无发生改变确定是否执行render方法,一旦父组件发生渲染,子组件也会渲染

参考文�?

面试官:说说React服务端渲染怎么做?原理是什么?

一、是什�?

SSR中,我们了解到Server-Side Rendering ,简称SSR,意为服务端渲染

指由服务侧完成页面的 HTML 结构拼接的页面处理技术,发送到浏览器,然后为其绑定状态与事件,成为完全可交互页面的过�?

其解决的问题主要有两个:

  • SEO,由于搜索引擎爬虫抓取工具可以直接查看完全渲染的页面
  • 加速首屏加载,解决首屏白屏问题

二、如何做

react中,实现SSR主要有两种形式:

  • 手动搭建一�?SSR 框架
  • 使用成熟的SSR 框架,如 Next.JS

这里主要以手动搭建一个SSR框架进行实现

首先通过express启动一个app.js文件,用于监�?000端口的请求,当请求根目录时,返回HTML,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const express = require('express')
const app = express()
app.get('/', (req,res) => res.send(`
<html>
<head>
<title>ssr demo</title>
</head>
<body>
Hello world
</body>
</html>
`))

app.listen(3000, () => console.log('Exampleapp listening on port 3000!'))

然后再服务器中编写react代码,在app.js中进行应引用

1
2
3
4
5
6
7
8
9
import React from 'react'

const Home = () =>{

return <div>home</div>

}

export default Home

为了让服务器能够识别JSX,这里需要使用webpakc对项目进行打包转换,创建一个配置文件webpack.server.js并进行相关配置,如下�?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const path = require('path')    //node的path模块
const nodeExternals = require('webpack-node-externals')

module.exports = {
target:'node',
mode:'development', //开发模�? entry:'./app.js', //入口
output: { //打包出口
filename:'bundle.js', //打包后的文件�? path:path.resolve(__dirname,'build') //存放到根目录的build文件�? },
externals: [nodeExternals()], //保持node中require的引用方�? module: {
rules: [{ //打包规则
test: /\.js?$/, //对所有js文件进行打包
loader:'babel-loader', //使用babel-loader进行打包
exclude: /node_modules/,//不打包node_modules中的js文件
options: {
presets: ['react','stage-0',['env', {
//loader时额外的打包规则,对react,JSX,ES6进行转换
targets: {
browsers: ['last 2versions'] //对主流浏览器最近两个版本进行兼�? }
}]]
}
}]
}
}

接着借助react-dom提供了服务端渲染�?renderToString方法,负责把React组件解析成html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import express from 'express'
import React from 'react'//引入React以支持JSX的语�?import { renderToString } from 'react-dom/server'//引入renderToString方法
import Home from'./src/containers/Home'

const app= express()
const content = renderToString(<Home/>)
app.get('/',(req,res) => res.send(`
<html>
<head>
<title>ssr demo</title>
</head>
<body>
${content}
</body>
</html>
`))

app.listen(3001, () => console.log('Exampleapp listening on port 3001!'))

上面的过程中,已经能够成功将组件渲染到了页面�?
但是像一些事件处理的方法,是无法在服务端完成,因此需要将组件代码在浏览器中再执行一遍,这种服务器端和客户端共用一套代码的方式就称之为同构

重构通俗讲就是一套React代码在服务器上运行一遍,到达浏览器又运行一遍:

  • 服务端渲染完成页面结�?- 浏览器端渲染完成事件绑定

浏览器实现事件绑定的方式为让浏览器去拉取JS文件执行,让JS代码来控制,因此需要引入script标签

通过script标签为页面引入客户端执行的react代码,并通过expressstatic中间件为js文件配置路由,修改如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import express from 'express'
import React from 'react'//引入React以支持JSX的语�?import { renderToString } from'react-dom/server'//引入renderToString方法
import Home from './src/containers/Home'

const app = express()
app.use(express.static('public'));
//使用express提供的static中间�?中间件会将所有静态文件的路由指向public文件�? const content = renderToString(<Home/>)

app.get('/',(req,res)=>res.send(`
<html>
<head>
<title>ssr demo</title>
</head>
<body>
${content}
<script src="/index.js"></script>
</body>
</html>
`))

app.listen(3001, () =>console.log('Example app listening on port 3001!'))

然后再客户端执行以下react代码,新建webpack.client.js作为客户端React代码的webpack配置文件如下�?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const path = require('path')                    //node的path模块

module.exports = {
mode:'development', //开发模�? entry:'./src/client/index.js', //入口
output: { //打包出口
filename:'index.js', //打包后的文件�? path:path.resolve(__dirname,'public') //存放到根目录的build文件�? },
module: {
rules: [{ //打包规则
test: /\.js?$/, //对所有js文件进行打包
loader:'babel-loader', //使用babel-loader进行打包
exclude: /node_modules/, //不打包node_modules中的js文件
options: {
presets: ['react','stage-0',['env', {
//loader时额外的打包规则,这里对react,JSX进行转换
targets: {
browsers: ['last 2versions'] //对主流浏览器最近两个版本进行兼�? }
}]]
}
}]
}
}

这种方法就能够简单实现首页的react服务端渲染,过程对应如下图:

在做完初始渲染的时候,一个应用会存在路由的情况,配置信息如下�?

1
2
3
4
5
6
7
8
9
import React from 'react'                   //引入React以支持JSX
import { Route } from 'react-router-dom' //引入路由
import Home from './containers/Home' //引入Home组件

export default (
<div>
<Route path="/" exact component={Home}></Route>
</div>
)

然后可以通过index.js引用路由信息,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import React from 'react'
import ReactDom from 'react-dom'
import { BrowserRouter } from'react-router-dom'
import Router from'../Routers'

const App= () => {
return (
<BrowserRouter>
{Router}
</BrowserRouter>
)
}

ReactDom.hydrate(<App/>, document.getElementById('root'))

这时候控制台会存在报错信息,原因在于每个Route组件外面包裹着一层div,但服务端返回的代码中并没有这个div

解决方法只需要将路由信息在服务端执行一遍,使用使用StaticRouter来替代BrowserRouter,通过context进行参数传�?

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
import express from 'express'
import React from 'react'//引入React以支持JSX的语�?import { renderToString } from 'react-dom/server'//引入renderToString方法
import { StaticRouter } from 'react-router-dom'
import Router from '../Routers'

const app = express()
app.use(express.static('public'));
//使用express提供的static中间�?中间件会将所有静态文件的路由指向public文件�?
app.get('/',(req,res)=>{
const content = renderToString((
//传入当前path
//context为必填参�?用于服务端渲染参数传�? <StaticRouter location={req.path} context={{}}>
{Router}
</StaticRouter>
))
res.send(`
<html>
<head>
<title>ssr demo</title>
</head>
<body>
<div id="root">${content}</div>
<script src="/index.js"></script>
</body>
</html>
`)
})


app.listen(3001, () => console.log('Exampleapp listening on port 3001!'))

这样也就完成了路由的服务端渲�?

三、原�?

整体react服务端渲染原理并不复杂,具体如下�?
node server 接收客户端请求,得到当前的请求url 路径,然后在已有的路由表内查找到对应的组件,拿到需要请求的数据,将数据作为 propscontext或者store 形式传入组件

然后基于 react 内置的服务端渲染方法 renderToString()把组件渲染为 html字符串在把最终的 html 进行输出前需要将数据注入到浏览器�?
浏览器开始进行渲染和节点对比,然后执行完成组件内事件绑定和一些交互,浏览器重用了服务端输出的 html 节点,整个流程结�?

参考文�?

面试官:说说 React中的setState执行机制

一、是什�?

一个组件的显示形态可以由数据状态和外部参数所决定,而数据状态就是state

当需要修改里面的值的状态需要通过调用setState来改变,从而达到更新组件内部数据的作用

如下例子�?

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
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: "JS每日一�?
})
}
}

通过点击按钮触发onclick事件,执行this.setState方法更新state状态,然后重新执行render函数,从而导致页面的视图更新

如果直接修改state的状态,如下�?

1
2
3
changeText() {
this.state.message = "你好�?李银�?;
}

我们会发现页面并不会有任何反应,但是state的状态是已经发生了改�?
这是因为React并不像vue2中调用Object.defineProperty数据响应式或者Vue3调用Proxy监听数据的变�?
必须通过setState方法来告知react组件state已经发生了改�?
关于state方法的定义是从React.Component中继承,定义的源码如下:

1
2
3
4
5
6
7
8
9
10
Component.prototype.setState = function(partialState, callback) {
invariant(
typeof partialState === 'object' ||
typeof partialState === 'function' ||
partialState == null,
'setState(...): takes an object of state variables to update or a ' +
'function which returns an object of state variables.',
);
this.updater.enqueueSetState(this, partialState, callback, 'setState');
};

从上面可以看到setState第一个参数可以是一个对象,或者是一个函数,而第二个参数是一个回调函数,用于可以实时的获取到更新之后的数�?

二、更新类�?

在使用setState更新数据的时候,setState的更新类型分成:

  • 异步更新
  • 同步更新

异步更新

先举出一个例子:

1
2
3
4
5
6
changeText() {
this.setState({
message: "你好�?
})
console.log(this.state.message); // Hello World
}

从上面可以看到,最终打印结果为Hello world,并不能在执行完setState之后立马拿到最新的state的结�?
如果想要立刻获取更新后的值,在第二个参数的回调中更新后会执行

1
2
3
4
5
6
changeText() {
this.setState({
message: "你好�?
}, () => {
console.log(this.state.message); // 你好�? });
}

同步更新

同样先给出一个在setTimeout中更新的例子�?

1
2
3
4
5
6
changeText() {
setTimeout(() => {
this.setState({
message: "你好�? });
console.log(this.state.message); // 你好�? }, 0);
}

上面的例子中,可以看到更新是同步

再来举一个原生DOM事件的例子:

1
2
3
4
5
6
7
8
componentDidMount() {
const btnEl = document.getElementById("btn");
btnEl.addEventListener('click', () => {
this.setState({
message: "你好�?李银�?
});
console.log(this.state.message); // 你好�?李银�? })
}

小结

  • 在组件生命周期或React合成事件中,setState是异�?- 在setTimeout或者原生dom事件中,setState是同�?

三、批量更�?

同样先给出一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
handleClick = () => {
this.setState({
count: this.state.count + 1,
})
console.log(this.state.count) // 1

this.setState({
count: this.state.count + 1,
})
console.log(this.state.count) // 1

this.setState({
count: this.state.count + 1,
})
console.log(this.state.count) // 1
}

点击按钮触发事件,打印的都是 1,页面显�?count 的值为 2

对同一个值进行多�?setState �?setState 的批量更新策略会对其进行覆盖,取最后一次的执行结果

上述的例子,实际等价于如下:

1
2
3
4
5
6
Object.assign(
previousState,
{index: state.count+ 1},
{index: state.count+ 1},
...
)

由于后面的数据会覆盖前面的更改,所以最终只加了一�?
如果是下一个state依赖前一个state的话,推荐给setState一个参数传入一个function,如下:

1
2
3
4
5
6
7
8
onClick = () => {
this.setState((prevState, props) => {
return {count: prevState.count + 1};
});
this.setState((prevState, props) => {
return {count: prevState.count + 1};
});
}

而在setTimeout或者原生dom事件中,由于是同步的操作,所以并不会进行覆盖现象

参考文�?

面试官:state �?props 有什么区别?

一、state

一个组件的显示形态可以由数据状态和外部参数所决定,而数据状态就�?state,一般在 constructor 中初始化

当需要修改里面的值的状态需要通过调用 setState 来改变,从而达到更新组件内部数据的作用,并且重新调用组�?render 方法,如下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Button extends React.Component {
constructor() {
super();
this.state = {
count: 0,
};
}

updateCount() {
this.setState((prevState, props) => {
return { count: prevState.count + 1 };
});
}

render() {
return (
<button onClick={() => this.updateCount()}>
Clicked {this.state.count} times
</button>
);
}
}

setState 还可以接受第二个参数,它是一个函数,会在 setState 调用完成并且组件开始重新渲染时被调用,可以用来监听渲染是否完成

1
2
3
4
5
6
this.setState(
{
name: "JS每日一�?,
},
() => console.log("setState finished")
);

二、props

React 的核心思想就是组件化思想,页面会被切分成一些独立的、可复用的组�?
组件从概念上看就是一个函数,可以接受一个参数作为输入值,这个参数就是 props,所以可以把 props 理解为从外部传入组件内部的数�?
react 具有单向数据流的特性,所以他的主要作用是从父组件向子组件中传递数�?
props 除了可以传字符串,数字,还可以传递对象,数组甚至是回调函数,如下�?

1
2
3
4
5
6
7
class Welcome extends React.Component {
render() {
return <h1>Hello {this.props.name}</h1>;
}
}

const element = <Welcome name="Sara" onNameChanged={this.handleName} />;

上述 name 属性与 onNameChanged 方法都能在子组件�?props 变量中访�?
在子组件中,props 在内部不可变的,如果想要改变它看,只能通过外部组件传入新的 props 来重新渲染子组件,否则子组件�?props 和展示形式不会改�?

三、区�?

相同点:

  • 两者都�?JavaScript 对象
  • 两者都是用于保存信�?- props �?state 都能触发渲染更新

区别�?

  • props 是外部传递给组件的,�?state 是在组件内被组件自己管理的,一般在 constructor 中初始化
  • props 在组件内部是不可修改的,�?state 在组件内部可以进行修�?- state 是多变的、可以修�?

参考文�?

面试官:说说你在使用React 过程中遇到的常见问题?如何解�?

一、前言

在使用react开发项目过程中,每个人或多或少都会遇到一�?奇�?的问题,本质上都是我们对其理解的不够透彻

react 系列�?3个工作日�?3次凌晨还在亮起的台灯,到今天就圆满画上句号了,比�?
在系列中我们列出了很多比较经典的考题,工作中遇到的问题也往往就藏中其中,只是以不同的表现形式存在罢了

今天的题解不算题解,准确来说是对整个系列的一次贯穿,总结

目录:

  • react 有什么特�?- 生命周期有哪些不同阶段?每个阶段对应的方法是�?- state �?props有什么区别?
  • super()和super(props)有什么区别?
  • setState执行机制�?- React的事件机制?
  • 事件绑定的方式有哪些�?- 构建组件的方式有哪些?区别?
  • 组件之间如何通信�?- key有什么作用?
  • refs 的理解?应用场景�?- Hooks的理解?解决了什么问题?
  • 如何引入css�?- redux工作原理�?- redux中间件有哪些�?- react-router组件有哪些?
  • render触发时机�?- 如何减少render�?- JSX转化DOM过程�?- 性能优化手段有哪�?- 如何做服务端渲染�?

react 有什么特�?

主要的特性分为:

  • JSX语法
  • 单向数据绑定
  • 虚拟DOM
  • 声明式编�?- Component

借助这些特性,react整体使用起来更加简单高效,组件式开发提高了代码的复用率

生命周期有哪些不同阶段?每个阶段对应的方法是�?

主要分成了新的生命周期和旧的生命周期�?

  • 新版生命周期整体流程如下图所示:

    旧的生命周期流程图如下:

state �?props有什么区别?

两者相同点�?

  • 两者都�?JavaScript 对象
  • 两者都是用于保存信�?- props �?state 都能触发渲染更新

区别�?

  • props 是外部传递给组件的,�?state 是在组件内被组件自己管理的,一般在 constructor 中初始化
  • props 在组件内部是不可修改的,�?state 在组件内部可以进行修�?- state 是多变的、可以修�?

super()和super(props)有什么区别?

React中,类组件基于ES6,所以在constructor中必须使用super

在调用super过程,无论是否传入propsReact内部都会将porps赋值给组件实例porps属性中

如果只调用了super(),那么this.propssuper()和构造函数结束之间仍是undefined

setState执行机制�?

react类组件的状态需要通过setState进行更改,在不同场景下对应不同的执行顺序�?

  • 在组件生命周期或React合成事件中,setState是异�?- 在setTimeout或者原生dom事件中,setState是同�?
    当我们批量更改state的值的时候,react内部会将其进行覆盖,只取最后一次的执行结果

当需要下一个state依赖当前state的时候,则可以在setState中传递一个回调函数进行下次更�?

React的事件机制?

React基于浏览器的事件机制自身实现了一套事件机制,包括事件注册、事件的合成、事件冒泡、事件派发等

组件注册的事件最终会绑定在document这个 DOM 上,而不�?React 组件对应�?DOM,从而节省内存开销

自身实现了一套事件冒泡机制,阻止不同时间段的冒泡行为,需要对应使用不同的方法

事件绑定的方式有哪些�?

react常见的绑定方式有如下�?

  • render方法中使用bind
  • render方法中使用箭头函�?- constructor中bind
  • 定义阶段使用箭头函数绑定

前两种方式在每次组件render的时候都会生成新的方法实例,性能问题欠缺

构建组件的方式有哪些?区别?

组件的创建主要分成了三种方式�?

  • 函数式创�?- 继承 React.Component 创建
  • 通过 React.createClass 方法创建

如今一般都是前两种方式,对于一些无状态的组件创建,建议使用函数式创建的方式,再比如hooks的机制下,函数式组件能做类组件对应的事情,所以建议都使用函数式的方式来创建组�?

组件之间如何通信�?

组件间通信可以通过props、传递回调函数、contextredux等形式进行组件之间通讯

key有什么作用?

使用keyreact性能优化的手段,在一系列数据最前面插入元素,如果没有key的值,则所有的元素都需要进行更换,而有key的情况只需要将最新元素插入到前面,不涉及删除操作

在使用key的时候应保证�?

  • key 应该是唯一�?- key不要使用随机值(随机数在下一�?render 时,会重新生成一个数字)
  • 避免使用 index 作为 key

refs 的理解?应用场景�?

Refs允许我们访问 DOM 节点或在 render 方法中创建的 React 元素

下面的场景使用refs非常有用�?

  • 对Dom元素的焦点控制、内容选择、控�?- 对Dom元素的内容设置及媒体播放
  • 对Dom元素的操作和对组件实例的操作
  • 集成第三�?DOM �?

Hooks的理解?解决了什么问题?

Hook �?React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他�?React 特�?
解决问题如下�?

  • 难以重用和共享组件中的与状态相关的逻辑
  • 逻辑复杂的组件难以开发与维护,当我们的组件需要处理多个互不相关的 local state 时,每个生命周期函数中可能会包含着各种互不相关的逻辑在里�?- 类组件中的this增加学习成本,类组件在基于现有工具的优化上存在些许问�?- 由于业务变动,函数组件不得不改为类组件等�?

如何引入css�?

常见的CSS引入方式有以下:

  • 在组件内直接使用
  • 组件中引�?.css 文件
  • 组件中引�?.module.css 文件
  • CSS in JS

组件内直接使用css会导致大量的代码,而文件中直接引入css文件是全局作用域,发生层叠

引入.module.css 文件能够解决局部作用域问题,但是不方便动态修改样式,需要使用内联的方式进行样式的编�?
css in js 这种方法,可以满足大部分场景的应用,可以类似于预处理器一样样式嵌套、定义、修改状态等

redux工作原理�?

redux 要求我们把数据都放在 store 公共存储空间

一个组件改变了 store 里的数据内容,其他组件就能感知到 store 的变化,再来取数据,从而间接的实现了这些数据传递的功能

工作流程图如下所示:

redux中间件有哪些�?

市面上有很多优秀的redux中间件,如:

  • redux-thunk:用于异步操�?- redux-logger:用于日志记�?

react-router组件有哪些?

常见的组件有�?

  • BrowserRouter、HashRouter
  • Route
  • Link、NavLink
  • switch
  • redirect

render触发时机�?

React 中,类组件只要执行了 setState 方法,就一定会触发 render 函数执行

函数组件useState 会判断当前值有无发生改变确定是否执行render方法,一旦父组件发生渲染,子组件也会渲染

如何减少render�?

父组件渲染导致子组件渲染,子组件并没有发生任何改变,这时候就可以从避免无谓的渲染,具体实现的方式有如下:

  • shouldComponentUpdate
  • PureComponent
  • React.memo

JSX转化DOM过程�?

jsx首先会转化成React.createElement这种形式,React.createElement作用是生成一个虚拟Dom对象,然后会通过ReactDOM.render进行渲染成真实DOM

性能优化手段有哪�?

除了减少render的渲染之外,还可以通过以下手段进行优化�?
除此之外�?常见性能优化常见的手段有如下�?

  • 避免使用内联函数
  • 使用 React Fragments 避免额外标记
  • 使用 Immutable
  • 懒加载组�?- 事件绑定方式
  • 服务端渲�?

如何做服务端渲染�?

node server 接收客户端请求,得到当前的请求url 路径,然后在已有的路由表内查找到对应的组件,拿到需要请求的数据,将数据作为 propscontext或者store 形式传入组件

然后基于 react 内置的服务端渲染方法 renderToString()把组件渲染为 html字符串在把最终的 html 进行输出前需要将数据注入到浏览器�?
浏览器开始进行渲染和节点对比,然后执行完成组件内事件绑定和一些交互,浏览器重用了服务端输出的 html 节点,整个流程结�?

0%