Tiny'Wo | 小窝

网络中的一小块自留地

面试官:说说对Fiber架构的理解?解决了什么问题?

一、问�?

JavaScript 引擎和页面渲染引擎两个线程是互斥的,当其中一个线程执行时,另一个线程只能挂起等�?
如果 JavaScript 线程长时间地占用了主线程,那么渲染层面的更新就不得不长时间地等待,界面长时间不更新,会导致页面响应度变差,用户可能会感觉到卡�?
而这也正�?React 15 �?Stack Reconciler 所面临的问题,�?React 在渲染组件时,从开始到渲染完成整个过程是一气呵成的,无法中�?
如果组件较大,那么js线程会一直执行,然后等到整棵VDOM树计算完成后,才会交给渲染的线程

这就会导致一些用户交互、动画等任务无法立即得到处理,导致卡顿的情况

二、是什�?

React Fiber �?Facebook 花费两年余时间对 React 做出的一个重大改变与优化,是�?React 核心算法的一次重新实现。从Facebook�?React Conf 2017 会议上确认,React Fiber 在React 16 版本发布

react中,主要做了以下的操作:

  • 为每个增加了优先级,优先级高的任务可以中断低优先级的任务。然后再重新,注意是重新执行优先级低的任�?- 增加了异步任务,调用requestIdleCallback api,浏览器空闲的时候执�?- dom diff树变成了链表,一个dom对应两个fiber(一个链表),对应两个队列,这都是为找到被中断的任务,重新执�?
    从架构角度来看,Fiber 是对 React 核心算法(即调和过程)的重写

从编码角度来看,Fiber �?React 内部所定义的一种数据结构,它是 Fiber 树结构的节点单位,也就是 React 16 新架构下的虚拟DOM

一�?fiber 就是一�?JavaScript 对象,包含了元素的信息、该元素的更新操作队列、类型,其数据结构如下:

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
44
45
46
47
48
49
50
51
52
53
54
55
type Fiber = {
// 用于标记fiber的WorkTag类型,主要表示当前fiber代表的组件类型如FunctionComponent、ClassComponent�? tag: WorkTag,
// ReactElement里面的key
key: null | string,
// ReactElement.type,调用`createElement`的第一个参�? elementType: any,
// The resolved function/class/ associated with this fiber.
// 表示当前代表的节点类�? type: any,
// 表示当前FiberNode对应的element组件实例
stateNode: any,

// 指向他在Fiber节点树中的`parent`,用来在处理完这个节点之后向上返�? return: Fiber | null,
// 指向自己的第一个子节点
child: Fiber | null,
// 指向自己的兄弟结构,兄弟节点的return指向同一个父节点
sibling: Fiber | null,
index: number,

ref: null | (((handle: mixed) => void) & { _stringRef: ?string }) | RefObject,

// 当前处理过程中的组件props对象
pendingProps: any,
// 上一次渲染完成之后的props
memoizedProps: any,

// 该Fiber对应的组件产生的Update会存放在这个队列里面
updateQueue: UpdateQueue<any> | null,

// 上一次渲染的时候的state
memoizedState: any,

// 一个列表,存放这个Fiber依赖的context
firstContextDependency: ContextDependency<mixed> | null,

mode: TypeOfMode,

// Effect
// 用来记录Side Effect
effectTag: SideEffectTag,

// 单链表用来快速查找下一个side effect
nextEffect: Fiber | null,

// 子树中第一个side effect
firstEffect: Fiber | null,
// 子树中最后一个side effect
lastEffect: Fiber | null,

// 代表任务在未来的哪个时间点应该被完成,之后版本改名为 lanes
expirationTime: ExpirationTime,

// 快速确定子树中是否有不在等待的变化
childExpirationTime: ExpirationTime,

// fiber的版本池,即记录fiber更新过程,便于恢�? alternate: Fiber | null,
}

三、如何解�?

Fiber把渲染更新过程拆分成多个子任务,每次只做一小部分,做完看是否还有剩余时间,如果有继续下一个任务;如果没有,挂起当前任务,将时间控制权交给主线程,等主线程不忙的时候在继续执行

即可以中断与恢复,恢复后也可以复用之前的中间状态,并给不同的任务赋予不同的优先级,其中每个任务更新单元�?React Element 对应�?Fiber 节点

实现的上述方式的是requestIdleCallback方法

window.requestIdleCallback()方法将在浏览器的空闲时段内调用的函数排队。这使开发者能够在主事件循环上执行后台和低优先级工作,而不会影响延迟关键事件,如动画和输入响应

首先 React 中任务切割为多个步骤,分批完成。在完成一部分任务之后,将控制权交回给浏览器,让浏览器有时间再进行页面的渲染。等浏览器忙完之后有剩余时间,再继续之前 React 未完成的任务,是一种合作式调度�?
该实现过程是基于 Fiber 节点实现,作为静态的数据结构来说,每�?Fiber 节点对应一�?React element,保存了该组件的类型(函数组�?类组�?原生组件等等)、对应的 DOM 节点等信息�?
作为动态的工作单元来说,每�?Fiber 节点保存了本次更新中该组件改变的状态、要执行的工作�?
每个 Fiber 节点有个对应�?React element,多�?Fiber 节点根据如下三个属性构建一颗树�?

1
2
3
4
5
6
// 指向父级Fiber节点
this.return = null
// 指向子Fiber节点
this.child = null
// 指向右边第一个兄弟Fiber节点
this.sibling = null

通过这些属性就能找到下一个执行目�?

参考文�?

面试官:说说对高阶组件的理解?应用场�?

一、是什�?

高阶函数(Higher-order function),至少满足下列一个条件的函数

  • 接受一个或多个函数作为输入
  • 输出一个函�?
    React中,高阶组件即接受一个或多个组件作为参数并且返回一个组件,本质也就是一个函数,并不是一个组�?
    1
    const EnhancedComponent = highOrderComponent(WrappedComponent);

上述代码中,该函数接受一个组件WrappedComponent作为参数,返回加工过的新组件EnhancedComponent

高阶组件的这种实现方式,本质上是一个装饰者设计模�?

二、如何编�?

最基本的高阶组件的编写模板如下�?

1
2
3
4
5
6
7
8
9
10
import React, { Component } from 'react';

export default (WrappedComponent) => {
return class EnhancedComponent extends Component {
// do something
render() {
return <WrappedComponent />;
}
}
}

通过对传入的原始组件 WrappedComponent 做一些你想要的操作(比如操作 props,提�?state,给原始组件包裹其他元素等),从而加工出想要的组�?EnhancedComponent

把通用的逻辑放在高阶组件中,对组件实现一致的处理,从而实现代码的复用

所以,高阶组件的主要功能是封装并分离组件的通用逻辑,让通用逻辑在组件间更好地被复用

但在使用高阶组件的同时,一般遵循一些约定,如下�?

  • props 保持一�?- 你不能在函数式(无状态)组件上使�?ref 属性,因为它没有实�?- 不要以任何方式改变原始组�?WrappedComponent
  • 透传不相�?props 属性给被包裹的组件 WrappedComponent
  • 不要�?render() 方法中使用高阶组�?- 使用 compose 组合高阶组件
  • 包装显示名字以便于调�?
    这里需要注意的是,高阶组件可以传递所有的props,但是不能传递ref

如果向一个高阶组件添加refe引用,那么ref 指向的是最外层容器组件实例的,而不是被包裹的组件,如果需要传递refs的话,则使用React.forwardRef,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function withLogging(WrappedComponent) {
class Enhance extends WrappedComponent {
componentWillReceiveProps() {
console.log('Current props', this.props);
console.log('Next props', nextProps);
}
render() {
const {forwardedRef, ...rest} = this.props;
// �?forwardedRef 赋值给 ref
return <WrappedComponent {...rest} ref={forwardedRef} />;
}
};

// React.forwardRef 方法会传�?props �?ref 两个参数给其回调函数
// 所以这边的 ref 是由 React.forwardRef 提供�? function forwardRef(props, ref) {
return <Enhance {...props} forwardRef={ref} />
}

return React.forwardRef(forwardRef);
}
const EnhancedComponent = withLogging(SomeComponent);

三、应用场�?

通过上面的了解,高阶组件能够提高代码的复用性和灵活性,在实际应用中,常常用于与核心业务无关但又在多个模块使用的功能,如权限控制、日志记录、数据校验、异常处理、统计上报等

举个例子,存在一个组件,需要从缓存中获取数据,然后渲染。一般情况,我们会如下编写:

1
2
3
4
5
6
7
8
9
10
11
12
13
import React, { Component } from 'react'

class MyComponent extends Component {

componentWillMount() {
let data = localStorage.getItem('data');
this.setState({data});
}

render() {
return <div>{this.state.data}</div>
}
}

上述代码当然可以实现该功能,但是如果还有其他组件也有类似功能的时候,每个组件都需要重复写componentWillMount中的代码,这明显是冗杂的

下面就可以通过高价组件来进行改写,如下�?

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

function withPersistentData(WrappedComponent) {
return class extends Component {
componentWillMount() {
let data = localStorage.getItem('data');
this.setState({data});
}

render() {
// 通过{...this.props} 把传递给当前组件的属性继续传递给被包装的组件WrappedComponent
return <WrappedComponent data={this.state.data} {...this.props} />
}
}
}

class MyComponent2 extends Component {
render() {
return <div>{this.props.data}</div>
}
}

const MyComponentWithPersistentData = withPersistentData(MyComponent2)

再比如组件渲染性能监控,如下:

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
class Home extends React.Component {
render() {
return (<h1>Hello World.</h1>);
}
}
function withTiming(WrappedComponent) {
return class extends WrappedComponent {
constructor(props) {
super(props);
this.start = 0;
this.end = 0;
}
componentWillMount() {
super.componentWillMount && super.componentWillMount();
this.start = Date.now();
}
componentDidMount() {
super.componentDidMount && super.componentDidMount();
this.end = Date.now();
console.log(`${WrappedComponent.name} 组件渲染时间�?${this.end - this.start} ms`);
}
render() {
return super.render();
}
};
}

export default withTiming(Home);

参考文�?

面试官:说说 React 性能优化的手段有哪些�?

一、是什�?

React凭借virtual DOMdiff算法拥有高效的性能,但是某些情况下,性能明显可以进一步提�?
在前面文章中,我们了解到类组件通过调用setState方法�?就会导致render,父组件一旦发生render渲染,子组件一定也会执行render渲染

当我们想要更新一个子组件的时候,如下图绿色部分:

理想状态只调用该路径下的组件render�?

但是react的默认做法是调用所有组件的render,再对生成的虚拟DOM进行对比(黄色部分),如不变则不进行更新

从上图可见,黄色部分diff算法对比是明显的性能浪费的情�?

二、如何做

React中如何避免不必要的render中,我们了解到如何避免不必要的render来应付上面的问题,主要手段是通过shouldComponentUpdatePureComponentReact.memo,这三种形式这里就不再复�?
除此之外�?常见性能优化常见的手段有如下�?

  • 避免使用内联函数

  • 使用 React Fragments 避免额外标记

  • 使用 Immutable

  • 懒加载组�?

  • 事件绑定方式

  • 服务端渲�?

避免使用内联函数

如果我们使用内联函数,则每次调用render函数时都会创建一个新的函数实例,如下�?

1
2
3
4
5
6
7
8
9
10
11
12
import React from "react";

export default class InlineFunctionComponent extends React.Component {
render() {
return (
<div>
<h1>Welcome Guest</h1>
<input type="button" onClick={(e) => { this.setState({inputValue: e.target.value}) }} value="Click For Inline Function" />
</div>
)
}
}

我们应该在组件内部创建一个函数,并将事件绑定到该函数本身。这样每次调�?render 时就不会创建单独的函数实例,如下�?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import React from "react";

export default class InlineFunctionComponent extends React.Component {

setNewStateData = (event) => {
this.setState({
inputValue: e.target.value
})
}

render() {
return (
<div>
<h1>Welcome Guest</h1>
<input type="button" onClick={this.setNewStateData} value="Click For Inline Function" />
</div>
)
}
}

使用 React Fragments 避免额外标记

用户创建新组件时,每个组件应具有单个父标签。父级不能有两个标签,所以顶部要有一个公共标签,所以我们经常在组件顶部添加额外标签div

这个额外标签除了充当父标签之外,并没有其他作用,这时候则可以使用fragement

其不会向组件引入任何额外标记,但它可以作为父级标签的作用,如下所示:

1
2
3
4
5
6
7
8
9
10
export default class NestedRoutingComponent extends React.Component {
render() {
return (
<>
<h1>This is the Header Component</h1>
<h2>Welcome To Demo Page</h2>
</>
)
}
}

事件绑定方式

事件绑定方式中,我们了解到四种事假绑定的方式

从性能方面考虑,在render方法中使用bindrender方法中使用箭头函数这两种形式在每次组件render的时候都会生成新的方法实例,性能欠缺

constructorbind事件与定义阶段使用箭头函数绑定这两种形式只会生成一个方法实例,性能方面会有所改善

使用 Immutable

理解Immutable中,我们了解到使用 Immutable可以�?React 应用带来性能的优化,主要体现在减少渲染的次数

在做react性能优化的时候,为了避免重复渲染,我们会在shouldComponentUpdate()中做对比,当返回true执行render方法

Immutable通过is方法则可以完成对比,而无需像一样通过深度比较的方式比�?

懒加载组�?

从工程方面考虑,webpack存在代码拆分能力,可以为应用创建多个包,并在运行时动态加载,减少初始包的大小

而在react中使用到了Suspense �?lazy组件实现代码拆分功能,基本使用如下:

1
2
3
4
5
6
7
const johanComponent = React.lazy(() => import(/* webpackChunkName: "johanComponent" */ './myAwesome.component'));

export const johanAsyncComponent = props => (
<React.Suspense fallback={<Spinner />}>
<johanComponent {...props} />
</React.Suspense>
);

服务端渲�?

采用服务端渲染端方式,可以使用户更快的看到渲染完成的页面

服务端渲染,需要起一个node服务,可以使用expresskoa等,调用reactrenderToString方法,将根组件渲染成字符串,再输出到响应�?
例如�?

1
2
3
4
5
6
7
8
9
import { renderToString } from "react-dom/server";
import MyPage from "./MyPage";
app.get("/", (req, res) => {
res.write("<!DOCTYPE html><html><head><title>My Page</title></head><body>");
res.write("<div id='content'>");
res.write(renderToString(<MyPage/>));
res.write("</div></body></html>");
res.end();
});

客户端使用render方法来生成HTML

1
2
3
import ReactDOM from 'react-dom';
import MyPage from "./MyPage";
ReactDOM.render(<MyPage />, document.getElementById('app'));

其他

除此之外,还存在的优化手段有组件拆分、合理使用hooks等性能优化手段…

三、总结

通过上面初步学习,我们了解到react常见的性能优化可以分成三个层面�?

  • 代码层面
  • 工程层面
  • 框架机制层面

通过这三个层面的优化结合,能够使基于react项目的性能更上一层楼

参考文�?

面试官:说说React Jsx转换成真实DOM过程�?

一、是什�?

react通过将组件编写的JSX映射到屏幕,以及组件中的状态发生了变化之后 React会将这些「变化」更新到屏幕�?
在前面文章了解中,JSX通过babel最终转化成React.createElement这种形式,例如:

1
2
3
4
<div>
< img src="avatar.png" className="profile" />
<Hello />
</div>

会被bebel转化成如下:

1
2
3
4
5
6
7
8
9
React.createElement(
"div",
null,
React.createElement("img", {
src: "avatar.png",
className: "profile"
}),
React.createElement(Hello, null)
);

在转化过程中,babel在编译时会判�?JSX 中组件的首字母:

  • 当首字母为小写时,其被认定为原生 DOM 标签,createElement 的第一个变量被编译为字符串

  • 当首字母为大写时,其被认定为自定义组件,createElement 的第一个变量被编译为对�?
    最终都会通过RenderDOM.render(...)方法进行挂载,如下:

1
ReactDOM.render(<App />,  document.getElementById("root"));

二、过�?

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
26
27
28
29
30
31
class ClassComponent extends Component {
static defaultProps = {
color: "pink"
};
render() {
return (
<div className="border">
<h3>ClassComponent</h3>
<p className={this.props.color}>{this.props.name}</p >
</div>
);
}
}

function FunctionComponent(props) {
return (
<div className="border">
FunctionComponent
<p>{props.name}</p >
</div>
);
}

const jsx = (
<div className="border">
<p>xx</p >
< a href=" ">xxx</ a>
<FunctionComponent name="函数组件" />
<ClassComponent name="类组�? color="red" />
</div>
);

这些类别最终都会被转化成React.createElement这种形式

React.createElement其被调用时会传⼊标签类型type,标签属性props及若干子元素children,作用是生成一个虚拟Dom对象,如下所示:

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
function createElement(type, config, ...children) {
if (config) {
delete config.__self;
delete config.__source;
}
// ! 源码中做了详细处理,⽐如过滤掉key、ref�? const props = {
...config,
children: children.map(child =>
typeof child === "object" ? child : createTextNode(child)
)
};
return {
type,
props
};
}
function createTextNode(text) {
return {
type: TEXT,
props: {
children: [],
nodeValue: text
}
};
}
export default {
createElement
};

createElement会根据传入的节点信息进行一个判断:

  • 如果是原生标签节点, type 是字符串,如div、span
  • 如果是文本节点, type就没有,这里�?TEXT
  • 如果是函数组件,type 是函数名
  • 如果是类组件,type 是类�?
    虚拟DOM会通过ReactDOM.render进行渲染成真实DOM,使用方法如下:
1
ReactDOM.render(element, container[, callback])

当首次调用时,容器节点里的所�?DOM 元素都会被替换,后续的调用则会使�?React �?diff算法进行高效的更�?
如果提供了可选的回调函数callback,该回调将在组件被渲染或更新之后被执�?
render大致实现方法如下�?

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
function render(vnode, container) {
console.log("vnode", vnode); // 虚拟DOM对象
// vnode _> node
const node = createNode(vnode, container);
container.appendChild(node);
}

// 创建真实DOM节点
function createNode(vnode, parentNode) {
let node = null;
const {type, props} = vnode;
if (type === TEXT) {
node = document.createTextNode("");
} else if (typeof type === "string") {
node = document.createElement(type);
} else if (typeof type === "function") {
node = type.isReactComponent
? updateClassComponent(vnode, parentNode)
: updateFunctionComponent(vnode, parentNode);
} else {
node = document.createDocumentFragment();
}
reconcileChildren(props.children, node);
updateNode(node, props);
return node;
}

// 遍历下子vnode,然后把子vnode->真实DOM节点,再插入父node�?function reconcileChildren(children, node) {
for (let i = 0; i < children.length; i++) {
let child = children[i];
if (Array.isArray(child)) {
for (let j = 0; j < child.length; j++) {
render(child[j], node);
}
} else {
render(child, node);
}
}
}
function updateNode(node, nextVal) {
Object.keys(nextVal)
.filter(k => k !== "children")
.forEach(k => {
if (k.slice(0, 2) === "on") {
let eventName = k.slice(2).toLocaleLowerCase();
node.addEventListener(eventName, nextVal[k]);
} else {
node[k] = nextVal[k];
}
});
}

// 返回真实dom节点
// 执行函数
function updateFunctionComponent(vnode, parentNode) {
const {type, props} = vnode;
let vvnode = type(props);
const node = createNode(vvnode, parentNode);
return node;
}

// 返回真实dom节点
// 先实例化,再执行render函数
function updateClassComponent(vnode, parentNode) {
const {type, props} = vnode;
let cmp = new type(props);
const vvnode = cmp.render();
const node = createNode(vvnode, parentNode);
return node;
}
export default {
render
};

三、总结

react源码中,虚拟Dom转化成真实Dom整体流程如下图所示:

其渲染流程如下所示:

  • 使用React.createElement或JSX编写React组件,实际上所有的 JSX 代码最后都会转换成React.createElement(…) ,Babel帮助我们完成了这个转换的过程�?- createElement函数对key和ref等特殊的props进行处理,并获取defaultProps对默认props进行赋值,并且对传入的孩子节点进行处理,最终构造成一个虚拟DOM对象
  • ReactDOM.render将生成好的虚拟DOM渲染到指定容器上,其中采用了批处理、事务等机制并且对特定浏览器进行了性能优化,最终转换为真实DOM

参考文�?

面试官:说说对React Hooks的理解?解决了什么问题?

一、是什�?

Hook �?React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他�?React 特�?
至于为什么引入hook,官方给出的动机是解决长时间使用和维护react过程中常遇到的问题,例如�?

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

二、有哪些

上面讲到,Hooks让我们的函数组件拥有了类组件的特性,例如组件内的状态、生命周�?
最常见的hooks有如下:

  • useState
  • useEffect
  • 其他

useState

首先给出一个例子,如下�?

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

function Example() {
// 声明一个叫 "count" �?state 变量
const [count, setCount] = useState(0);

return (
<div>
<p>You clicked {count} times</p >
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}

在函数组件中通过useState实现函数内部维护state,参数为state默认的值,返回值是一个数组,第一个值为当前的state,第二个值为更新state的函�?
该函数组件等价于的类组件如下�?

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

render() {
return (
<div>
<p>You clicked {this.state.count} times</p >
<button onClick={() => this.setState({ count: this.state.count + 1 })}>
Click me
</button>
</div>
);
}
}

从上述两种代码分析,可以看出两者区别:

  • state声明方式:在函数组件中通过 useState 直接获取,类组件通过constructor 构造函数中设置
  • state读取方式:在函数组件中直接使用变量,类组件通过this.state.count的方式获�?
  • state更新方式:在函数组件中通过 setCount 更新,类组件通过this.setState()

总的来讲,useState 使用起来更为简洁,减少了this指向不明确的情况

useEffect

useEffect可以让我们在函数组件中进行一些带有副作用的操�?
同样给出一个计时器示例�?

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
class Example extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
}

componentDidMount() {
document.title = `You clicked ${this.state.count} times`;
}
componentDidUpdate() {
document.title = `You clicked ${this.state.count} times`;
}

render() {
return (
<div>
<p>You clicked {this.state.count} times</p >
<button onClick={() => this.setState({ count: this.state.count + 1 })}>
Click me
</button>
</div>
);
}
}

从上面可以看见,组件在加载和更新阶段都执行同样操�?
而如果使用useEffect后,则能够将相同的逻辑抽离出来,这是类组件不具备的方法

对应的useEffect示例如下�?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import React, { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);

useEffect(() => { document.title = `You clicked ${count} times`; });
return (
<div>
<p>You clicked {count} times</p >
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}

useEffect第一个参数接受一个回调函数,默认情况下,useEffect会在第一次渲染和更新之后都会执行,相当于在componentDidMountcomponentDidUpdate两个生命周期函数中执行回�?
如果某些特定值在两次重渲染之间没有发生变化,你可以跳过对 effect 的调用,这时候只需要传入第二个参数,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]); // 仅在 count 更改时更�?```

上述传入第二个参数后,如�?`count` 的值是 `5`,而且我们的组件重渲染的时�?`count` 还是等于 `5`React 将对前一次渲染的 `[5]` 和后一次渲染的 `[5]` 进行比较,如果是相等则跳过`effects`执行

回调函数中可以返回一个清除函数,这是`effect`可选的清除机制,相当于类组件中`componentwillUnmount`生命周期函数,可做一些清除副作用的操作,如下�?
```jsx
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}

ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});

所以, useEffect相当于componentDidMountcomponentDidUpdate �?componentWillUnmount 这三个生命周期函数的组合

其它 hooks

在组件通信过程中可以使用useContextrefs学习中我们也用到了useRef获取DOM结构……

还有很多额外的hooks,如�?

  • useReducer
  • useCallback
  • useMemo
  • useRef

三、解决什�?

通过对上面的初步认识,可以看到hooks能够更容易解决状态相关的重用的问题:

  • 每调用useHook一次都会生成一份独立的状�?
  • 通过自定义hook能够更好的封装我们的功能

编写hooks为函数式编程,每个功能都包裹在函数中,整体风格更清爽,更优雅

hooks的出现,使函数组件的功能得到了扩充,拥有了类组件相似的功能,在我们日常使用中,使用hooks能够解决大多数问题,并且还拥有代码复用机制,因此优先考虑hooks

参考文�?

面试官:说说React Router有几种模式?实现原理�?

一、是什�?

在单页应用中,一个web项目只有一个html页面,一旦页面加载完成之后,就不用因为用户的操作而进行页面的重新加载或者跳转,其特性如下:

  • 改变 url 且不让浏览器像服务器发送请�?
  • 在不刷新页面的前提下动态改变浏览器地址栏中的URL地址

其中主要分成了两种模式:

  • hash 模式:在url后面加上#,如http://127.0.0.1:5500/home/#/page1
  • history 模式:允许操作浏览器的曾经在标签页或者框架里访问的会话历史记�?

二、使�?

React Router对应的hash模式和history模式对应的组件为�?

  • HashRouter
  • BrowserRouter

这两个组件的使用都十分的简单,作为最顶层组件包裹其他组件,如下所�?

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
// 1.import { BrowserRouter as Router } from "react-router-dom";
// 2.import { HashRouter as Router } from "react-router-dom";

import React from 'react';
import {
BrowserRouter as Router,
// HashRouter as Router
Switch,
Route,
} from "react-router-dom";
import Home from './pages/Home';
import Login from './pages/Login';
import Backend from './pages/Backend';
import Admin from './pages/Admin';


function App() {
return (
<Router>
<Route path="/login" component={Login}/>
<Route path="/backend" component={Backend}/>
<Route path="/admin" component={Admin}/>
<Route path="/" component={Home}/>
</Router>
);
}

export default App;

三、实现原�?

路由描述�?URL �?UI 之间的映射关系,这种映射是单向的,即 URL 变化引起 UI 更新(无需刷新页面�?
下面以hash模式为例子,改变hash值并不会导致浏览器向服务器发送请求,浏览器不发出请求,也就不会刷新页�?
hash 值改变,触发全局 window 对象上的 hashchange 事件。所�?hash 模式路由就是利用 hashchange 事件监听 URL 的变化,从而进�?DOM 操作来模拟页面跳�?
react-router也是基于这个特性实现路由的跳转

下面以HashRouter组件分析进行展开�?

HashRouter

HashRouter包裹了整应用�?
通过window.addEventListener('hashChange',callback)监听hash值的变化,并传递给其嵌套的组件

然后通过contextlocation数据往后代组件传递,如下�?

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
import React, { Component } from 'react';
import { Provider } from './context'
// 该组件下Api提供给子组件使用
class HashRouter extends Component {
constructor() {
super()
this.state = {
location: {
pathname: window.location.hash.slice(1) || '/'
}
}
}
// url路径变化 改变location
componentDidMount() {
window.location.hash = window.location.hash || '/'
window.addEventListener('hashchange', () => {
this.setState({
location: {
...this.state.location,
pathname: window.location.hash.slice(1) || '/'
}
}, () => console.log(this.state.location))
})
}
render() {
let value = {
location: this.state.location
}
return (
<Provider value={value}>
{
this.props.children
}
</Provider>
);
}
}

export default HashRouter;

Router

Router组件主要做的是通过BrowserRouter传过来的当前值,通过props传进来的pathcontext传进来的pathname进行匹配,然后决定是否执行渲染组�?

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';
import { Consumer } from './context'
const { pathToRegexp } = require("path-to-regexp");
class Route extends Component {
render() {
return (
<Consumer>
{
state => {
console.log(state)
let {path, component: Component} = this.props
let pathname = state.location.pathname
let reg = pathToRegexp(path, [], {end: false})
// 判断当前path是否包含pathname
if(pathname.match(reg)) {
return <Component></Component>
}
return null
}
}
</Consumer>
);
}
}
export default Route;

参考文�?

面试官:说说你对React Router的理解?常用的Router组件有哪些?

一、是什�?

react-router等前端路由的原理大致相同,可以实现无刷新的条件下切换显示不同的页�?
路由的本质就是页面的URL发生改变时,页面的显示结果可以根据URL的变化而变化,但是页面不会刷新

因此,可以通过前端路由可以实现单页(SPA)应用

react-router主要分成了几个不同的包:

  • react-router: 实现了路由的核心功能
  • react-router-dom�?基于 react-router,加入了在浏览器运行环境下的一些功�?- react-router-native:基�?react-router,加入了 react-native 运行环境下的一些功�?
  • react-router-config: 用于配置静态路由的工具�?

二、有哪些

这里主要讲述的是react-router-dom的常用API,主要是提供了一些组件:

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

BrowserRouter、HashRouter

Router中包含了对路径改变的监听,并且会将相应的路径传递给子组�?
BrowserRouterhistory模式,HashRouter模式

使用两者作为最顶层组件包裹其他组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import { BrowserRouter as Router } from "react-router-dom";

export default function App() {
return (
<Router>
<main>
<nav>
<ul>
<li>
< a href=" ">Home</ a>
</li>
<li>
< a href="/about">About</ a>
</li>
<li>
< a href="/contact">Contact</ a>
</li>
</ul>
</nav>
</main>
</Router>
);
}

Route

Route用于路径的匹配,然后进行组件的渲染,对应的属性如下:

  • path 属性:用于设置匹配到的路径
  • component 属性:设置匹配到路径后,渲染的组件
  • render 属性:设置匹配到路径后,渲染的内容
  • exact 属性:开启精准匹配,只有精准匹配到完全一致的路径,才会渲染对应的组件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import { BrowserRouter as Router, Route } from "react-router-dom";

export default function App() {
return (
<Router>
<main>
<nav>
<ul>
<li>
< a href="/">Home</ a>
</li>
<li>
< a href="/about">About</ a>
</li>
<li>
< a href="/contact">Contact</ a>
</li>
</ul>
</nav>
<Route path="/" render={() => <h1>Welcome!</h1>} />
</main>
</Router>
);
}

通常路径的跳转是使用Link组件,最终会被渲染成a元素,其中属性to代替a标题的href属�?
NavLink是在Link基础之上增加了一些样式属性,例如组件被选中时,发生样式变化,则可以设置NavLink的一下属性:

  • activeStyle:活跃时(匹配时)的样式
  • activeClassName:活跃时添加的class

如下�?

1
2
3
<NavLink to="/" exact activeStyle={{color: "red"}}>首页</NavLink>
<NavLink to="/about" activeStyle={{color: "red"}}>关于</NavLink>
<NavLink to="/profile" activeStyle={{color: "red"}}>我的</NavLink>

如果需要实现js实现页面的跳转,那么可以通过下面的形式:

通过Route作为顶层组件包裹其他组件�?页面组件就可以接收到一些路由相关的东西,比如props.history

1
2
3
4
5
6
7
const Contact = ({ history }) => (
<Fragment>
<h1>Contact</h1>
<button onClick={() => history.push("/")}>Go to home</button>
<FakeText />
</Fragment>
);

props 中接收到的history对象具有一些方便的方法,如goBackgoForward,push

redirect

用于路由的重定向,当这个组件出现时,就会执行跳转到对应的to路径中,如下例子�?

1
2
3
4
5
6
7
8
9
10
11
12
const About = ({
match: {
params: { name },
},
}) => (
// props.match.params.name
<Fragment>
{name !== "tom" ? <Redirect to="/" /> : null}
<h1>About {name}</h1>
<FakeText />
</Fragment>
)

上述组件当接收到的路由参数name 不等�?tom 的时候,将会自动重定向到首页

switch

swich组件的作用适用于当匹配到第一个组件的时候,后面的组件就不应该继续匹�?
如下例子�?

1
2
3
4
5
6
7
<Switch>
<Route exact path="/" component={Home} />
<Route path="/about" component={About} />
<Route path="/profile" component={Profile} />
<Route path="/:userid" component={User} />
<Route component={NoMatch} />
</Switch>

如果不使用switch组件进行包裹

除了一些路由相关的组件之外,react-router还提供一些hooks,如下:

  • useHistory
  • useParams
  • useLocation

useHistory

useHistory可以让组件内部直接访问history,无须通过props获取

1
2
3
4
5
6
7
8
9
10
11
import { useHistory } from "react-router-dom";

const Contact = () => {
const history = useHistory();
return (
<Fragment>
<h1>Contact</h1>
<button onClick={() => history.push("/")}>Go to home</button>
</Fragment>
);
};

useParams

1
2
3
4
5
6
7
8
9
10
11
const About = () => {
const { name } = useParams();
return (
// props.match.params.name
<Fragment>
{name !== "John Doe" ? <Redirect to="/" /> : null}
<h1>About {name}</h1>
<Route component={Contact} />
</Fragment>
);
};

useLocation

useLocation 会返回当�?URL �?location 对象

1
2
3
4
5
6
7
8
9
10
11
12
import { useLocation } from "react-router-dom";

const Contact = () => {
const { pathname } = useLocation();

return (
<Fragment>
<h1>Contact</h1>
<p>Current URL: {pathname}</p >
</Fragment>
);
};

三、参数传�?

这些路由传递参数主要分成了三种形式�?

  • 动态路由的方式
  • search传递参�?- to传入对象

动态路�?

动态路由的概念指的是路由中的路径并不会固定

例如将pathRoute匹配时写成/detail/:id,那�?/detail/abc/detail/123都可以匹配到该Route

1
2
3
4
5
6
7
<NavLink to="/detail/abc123">详情</NavLink>

<Switch>
... 其他Route
<Route path="/detail/:id" component={Detail}/>
<Route component={NoMatch} />
</Switch>

获取参数方式如下�?

1
console.log(props.match.params.xxx)

search传递参�?

在跳转的路径中添加了一些query参数�?

1
2
3
4
5
<NavLink to="/detail2?name=why&age=18">详情2</NavLink>

<Switch>
<Route path="/detail2" component={Detail2}/>
</Switch>

获取形式如下�?

1
console.log(props.location.search)

to传入对象

传递方式如下:

1
2
3
4
5
6
7
8
<NavLink to={{
pathname: "/detail2",
query: {name: "kobe", age: 30},
state: {height: 1.98, address: "洛杉�?},
search: "?apikey=123"
}}>
详情2
</NavLink>

获取参数的形式如下:

1
console.log(props.location)

参考文�?

面试官:说说对React refs 的理解?应用场景�?

一、是什�?Refs 在计算机中称为弹性文件系统(英语:Resilient File System,简称ReFS�?

React 中的 Refs提供了一种方式,允许我们访问 DOM 节点或在 render 方法中创建的 React 元素

本质为ReactDOM.render()返回的组件实例,如果是渲染组件则返回的是组件实例,如果渲染dom则返回的是具体的dom节点

二、如何使�?

创建ref的形式有三种�?

  • 传入字符串,使用时通过 this.refs.传入的字符串的格式获取对应的元素
  • 传入对象,对象是通过 React.createRef() 方式创建出来,使用时获取到创建的对象中存�?current 属性就是对应的元素
  • 传入函数,该函数会在 DOM 被挂载时进行回调,这个函数会传入一�?元素对象,可以自己保存,使用时,直接拿到之前保存的元素对象即�?- 传入hook,hook是通过 useRef() 方式创建,使用时通过生成hook对象�?current 属性就是对应的元素

传入字符�?

只需要在对应元素或组件中ref属�?

1
2
3
4
5
6
7
8
9
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.myRef = React.createRef();
}
render() {
return <div ref="myref" />;
}
}

访问当前节点的方式如下:

1
this.refs.myref.innerHTML = "hello";

传入对象

refs通过React.createRef()创建,然后将ref属性添加到React元素中,如下�?

1
2
3
4
5
6
7
8
9
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.myRef = React.createRef();
}
render() {
return <div ref={this.myRef} />;
}
}

�?ref 被传递给 render 中的元素时,对该节点的引用可以在 ref �?current 属性中访问

1
const node = this.myRef.current;

传入函数

ref传入为一个函数的时候,在渲染过程中,回调函数参数会传入一个元素对象,然后通过实例将对象进行保�?

1
2
3
4
5
6
7
8
9
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.myRef = React.createRef();
}
render() {
return <div ref={element => this.myref = element} />;
}
}

获取ref对象只需要通过先前存储的对象即�?

1
const node = this.myref 

传入hook

通过useRef创建一个ref,整体使用方式与React.createRef一�?

1
2
3
4
5
6
7
8
function App(props) {
const myref = useRef()
return (
<>
<div ref={myref}></div>
</>
)
}

获取ref属性也是通过hook对象的current属�?

1
const node = myref.current;

上述三种情况都是ref属性用于原生HTML元素上,如果ref设置的组件为一个类组件的时候,ref对象接收到的是组件的挂载实例

注意的是,不能在函数组件上使用ref属性,因为他们并没有实�?

三、应用场�?

在某些情况下,我们会通过使用refs来更新组件,但这种方式并不推荐,更多情况我们是通过propsstate的方式进行去重新渲染子元�?
过多使用refs,会使组件的实例或者是DOM结构暴露,违反组件封装的原则

例如,避免在 Dialog 组件里暴�?open() �?close() 方法,最好传�?isOpen 属�?
但下面的场景使用refs非常有用�?

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

参考文�?

面试官:说说�?React 的理解?有哪些特性?

一、是什�?

React,用于构建用户界面的 JavaScript 库,只提供了 UI 层面的解决方�?
遵循组件设计模式、声明式编程范式和函数式编程概念,以使前端应用程序更高效

使用虚拟 DOM 来有效地操作 DOM,遵循从高阶组件到低阶组件的单向数据�?
帮助我们将界面成了各个独立的小块,每一个块就是组件,这些组件之间可以组合、嵌套,构成整体页面

react 类组件使用一个名�?render() 的方法或者函数组件return,接收输入的数据并返回需要展示的内容

1
2
3
4
5
6
7
8
9
10
class HelloMessage extends React.Component {
render() {
return <div>Hello {this.props.name}</div>;
}
}

ReactDOM.render(
<HelloMessage name="Taylor" />,
document.getElementById("hello-example")
);

上述这种类似 XML 形式就是 JSX,最终会�?babel 编译为合法的 JS 语句调用

被传入的数据可在组件中通过 this.props �?render() 访问

二、特�?

React 特性有很多,如�?

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

着重介绍下声明式编程及 Component

声明式编�?

声明式编程是一种编程范式,它关注的是你要做什么,而不是如何做

它表达逻辑而不显式地定义步骤。这意味着我们需要根据逻辑的计算来声明要显示的组件

如实现一个标记的地图�?
通过命令式创建地图、创建标记、以及在地图上添加的标记的步骤如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 创建地图
const map = new Map.map(document.getElementById("map"), {
zoom: 4,
center: { lat, lng },
});

// 创建标记
const marker = new Map.marker({
position: { lat, lng },
title: "Hello Marker",
});

// 地图上添加标�?marker.setMap(map);

而用 React 实现上述功能则如下:

1
2
3
<Map zoom={4} center={(lat, lng)}>
<Marker position={(lat, lng)} title={"Hello Marker"} />
</Map>

声明式编程方式使�?React 组件很容易使用,最终的代码简单易于维�?

Component

�?React 中,一切皆为组件。通常将应用程序的整个逻辑分解为小的单个部分�?我们将每个单独的部分称为组件

组件可以是一个函数或者是一个类,接受数据输入,处理它并返回�?UI 中呈现的 React 元素

函数式组件如下:

1
2
3
4
5
6
7
const Header = () => {
return (
<Jumbotron style={{ backgroundColor: "orange" }}>
<h1>TODO App</h1>
</Jumbotron>
);
};

类组件(有状态组件)如下�?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Dashboard extends React.Component {
constructor(props) {
super(props);

this.state = {};
}
render() {
return (
<div className="dashboard">
<ToDoForm />
<ToDolist />
</div>
);
}
}

一个组件该有的特点如下�?

  • 可组合:每个组件易于和其它组件一起使用,或者嵌套在另一个组件内�?- 可重用:每个组件都是具有独立功能的,它可以被使用在多�?UI 场景
  • 可维护:每个小的组件仅仅包含自身的逻辑,更容易被理解和维护

三、优�?

通过上面的初步了解,可以感受�?React 存在的优势:

  • 高效灵活
  • 声明式的设计,简单使�?- 组件式开发,提高代码复用�?- 单向响应的数据流会比双向绑定的更安全,速度更快

参考文�?

面试官:说说对Redux中间件的理解?常用的中间件有哪些?实现原理?

一、是什�?

中间件(Middleware)是介于应用系统和系统软件之间的一类软件,它使用系统软件所提供的基础服务(功能),衔接网络上应用系统的各个部分或不同的应用,能够达到资源共享、功能共享的目的

在上篇文章中,了解到了Redux整个工作流程,当action发出之后,reducer立即算出state,整个过程是一个同步的操作

那么如果需要支持异步操作,或者支持错误处理、日志监控,这个过程就可以用上中间件

Redux中,中间件就是放在就是在dispatch过程,在分发action进行拦截处理,如下图�?

其本质上一个函数,对store.dispatch方法进行了改造,在发�?Action 和执�?Reducer 这两步之间,添加了其他功�?

二、常用的中间�?

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

  • redux-thunk:用于异步操�?- redux-logger:用于日志记�?
    上述的中间件都需要通过applyMiddlewares进行注册,作用是将所有的中间件组成一个数组,依次执行

然后作为第二个参数传入到createStore�?

1
2
3
4
const store = createStore(
reducer,
applyMiddleware(thunk, logger)
);

redux-thunk

redux-thunk是官网推荐的异步处理中间�?
默认情况下的dispatch(action)action需要是一个JavaScript的对�?
redux-thunk中间件会判断你当前传进来的数据类型,如果是一个函数,将会给函数传入参数值(dispatch,getState�?

  • dispatch函数用于我们之后再次派发action
  • getState函数考虑到我们之后的一些操作需要依赖原来的状态,用于让我们可以获取之前的一些状�?
    所以dispatch可以写成下述函数的形式:
1
2
3
4
5
6
7
8
9
const getHomeMultidataAction = () => {
return (dispatch) => {
axios.get("http://xxx.xx.xx.xx/test").then(res => {
const data = res.data.data;
dispatch(changeBannersAction(data.banner.list));
dispatch(changeRecommendsAction(data.recommend.list));
})
}
}

redux-logger

如果想要实现一个日志功能,则可以使用现成的redux-logger

1
2
3
4
5
6
7
8
9

import { applyMiddleware, createStore } from 'redux';
import createLogger from 'redux-logger';
const logger = createLogger();

const store = createStore(
reducer,
applyMiddleware(logger)
);

这样我们就能简单通过中间件函数实现日志记录的信息

三、实现原�?

首先看看applyMiddlewares的源�?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
export default function applyMiddleware(...middlewares) {
return (createStore) => (reducer, preloadedState, enhancer) => {
var store = createStore(reducer, preloadedState, enhancer);
var dispatch = store.dispatch;
var chain = [];

var middlewareAPI = {
getState: store.getState,
dispatch: (action) => dispatch(action)
};
chain = middlewares.map(middleware => middleware(middlewareAPI));
dispatch = compose(...chain)(store.dispatch);

return {...store, dispatch}
}
}

所有中间件被放进了一个数组chain,然后嵌套执行,最后执行store.dispatch。可以看到,中间件内部(middlewareAPI)可以拿到getStatedispatch这两个方�?
在上面的学习中,我们了解到了redux-thunk的基本使�?
内部会将dispatch进行一个判断,然后执行对应操作,原理如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
function patchThunk(store) {
let next = store.dispatch;

function dispatchAndThunk(action) {
if (typeof action === "function") {
action(store.dispatch, store.getState);
} else {
next(action);
}
}

store.dispatch = dispatchAndThunk;
}

实现一个日志输出的原理也非常简单,如下�?

1
2
3
4
5
6
7
8
9
let next = store.dispatch;

function dispatchAndLog(action) {
console.log("dispatching:", addAction(10));
next(addAction(5));
console.log("新的state:", store.getState());
}

store.dispatch = dispatchAndLog;

参考文�?

0%