Tiny'Wo | 小窝

网络中的一小块自留地

面试官:说说 Real DOM �?Virtual DOM 的区别?优缺点?

一、是什�?

Real DOM,真�?DOM,意思为文档对象模型,是一个结构化文本的抽象,在页面渲染出的每一个结点都是一个真�?DOM 结构,如下:

Virtual Dom,本质上是以 JavaScript 对象形式存在的对 DOM 的描�?
创建虚拟 DOM 目的就是为了更好将虚拟的节点渲染到页面视图中,虚�?DOM 对象的节点与真实 DOM 的属性一一照应

�?React 中,JSX 是其一大特性,可以让你�?JS 中通过使用 XML 的方式去直接声明界面�?DOM 结构

1
2
3
4
5
// 创建 h1 标签,右边千万不能加引号
const vDom = <h1>Hello World</h1>;
// 找到 <div id="root"></div> 节点
const root = document.getElementById("root");
// 把创建的 h1 标签渲染�?root 节点�?ReactDOM.render(vDom, root);

上述中,ReactDOM.render() 用于将你创建好的虚拟 DOM 节点插入到某个真实节点上,并渲染到页面上

JSX 实际是一种语法糖,在使用过程中会�?babel 进行编译转化�?JS 代码,上�?VDOM 转化为如下:

1
2
3
4
const vDom = React.createElement(
'h1'�? { className: 'hClass', id: 'hId' },
'hello world'
)

可以看到,JSX 就是为了简化直接调�?React.createElement() 方法�?

  • 第一个参数是标签名,例如 h1、span、table…

  • 第二个参数是个对象,里面存着标签的一些属性,例如 id、class �?

  • 第三个参数是节点中的文本

通过 console.log(VDOM),则能够得到虚拟 VDOM 消息

所以可以得到,JSX 通过 babel 的方式转化成 React.createElement 执行,返回值是一个对象,也就是虚�?DOM

二、区�?

两者的区别如下�?

  • 虚拟 DOM 不会进行排版与重绘操作,而真�?DOM 会频繁重排与重绘
  • 虚拟 DOM 的总损耗是“虚�?DOM 增删�?真实 DOM 差异增删�?排版与重绘”,真实 DOM 的总损耗是“真�?DOM 完全增删�?排版与重绘�?
    以前文章举过的例子:

传统的原�?api �?jQuery 去操�?DOM 时,浏览器会从构�?DOM 树开始从头到尾执行一遍流�?
当你在一次操作时,需要更�?10 �?DOM 节点,浏览器没这么智能,收到第一个更�?DOM 请求后,并不知道后续还有 9 次更新操作,因此会马上执行流程,最终执�?10 次流�?
而通过 VNode,同样更�?10 �?DOM 节点,虚�?DOM 不会立即操作 DOM,而是将这 10 次更新的 diff 内容保存到本地的一�?js 对象中,最终将这个 js 对象一次�?attach �?DOM 树上,避免大量的无谓计算

三、优缺点

真实 DOM 的优势:

  • 易用

缺点�?

  • 效率低,解析速度慢,内存占用量过�?- 性能差:频繁操作真实 DOM,易于导致重绘与回流

使用虚拟 DOM 的优势如下:

  • 简单方便:如果使用手动操作真实 DOM 来完成页面,繁琐又容易出错,在大规模应用下维护起来也很困�?
  • 性能方面:使�?Virtual DOM,能够有效避免真�?DOM 数频繁更新,减少多次引起重绘与回流,提高性能
  • 跨平台:React 借助虚拟 DOM,带来了跨平台的能力,一套代码多端运�?
    缺点�?
  • 在一些性能要求极高的应用中虚拟 DOM 无法进行针对性的极致优化
  • 首次渲染大量 DOM 时,由于多了一层虚�?DOM 的计算,速度比正常稍�?

参考文�?

面试官:说说React的事件机制?

一、是什�?

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

React中这套事件机制被称之为合成事�?

合成事件(SyntheticEvent�?

合成事件�?React 模拟原生 DOM 事件所有能力的一个事件对象,即浏览器原生事件的跨浏览器包装器

根据 W3C 规范来定义合成事件,兼容所有浏览器,拥有与浏览器原生事件相同的接口,例如:

1
const button = <button onClick={handleClick}>按钮</button>

如果想要获得原生DOM事件,可以通过e.nativeEvent属性获�?

1
2
const handleClick = (e) => console.log(e.nativeEvent);;
const button = <button onClick={handleClick}>按钮</button>

从上面可以看到React事件和原生事件也非常的相似,但也有一定的区别�?

  • 事件名称命名方式不同
1
2
3
4
5
// 原生事件绑定方式
<button onclick="handleClick()">按钮命名</button>

// React 合成事件绑定方式
const button = <button onClick={handleClick}>按钮命名</button>
  • 事件处理函数书写不同
1
2
3
4
5
// 原生事件 事件处理函数写法
<button onclick="handleClick()">按钮命名</button>

// React 合成事件 事件处理函数写法
const button = <button onClick={handleClick}>按钮命名</button>

虽然onclick看似绑定到DOM元素上,但实际并不会把事件代理函数直接绑定到真实的节点上,而是把所有的事件绑定到结构的最外层,使用一个统一的事件去监听

这个事件监听器上维持了一个映射来保存所有组件内部的事件监听和处理函数。当组件挂载或卸载时,只是在这个统一的事件监听器上插入或删除一些对�?
当事件发生时,首先被这个统一的事件监听器处理,然后在映射里找到真正的事件处理函数并调用。这样做简化了事件处理和回收机制,效率也有很大提升

二、执行顺�?

关于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
32
33
34
35
36
37
import  React  from 'react';
class App extends React.Component{

constructor(props) {
super(props);
this.parentRef = React.createRef();
this.childRef = React.createRef();
}
componentDidMount() {
console.log("React componentDidMount�?);
this.parentRef.current?.addEventListener("click", () => {
console.log("原生事件:父元素 DOM 事件监听�?);
});
this.childRef.current?.addEventListener("click", () => {
console.log("原生事件:子元素 DOM 事件监听�?);
});
document.addEventListener("click", (e) => {
console.log("原生事件:document DOM 事件监听�?);
});
}
parentClickFun = () => {
console.log("React 事件:父元素事件监听�?);
};
childClickFun = () => {
console.log("React 事件:子元素事件监听�?);
};
render() {
return (
<div ref={this.parentRef} onClick={this.parentClickFun}>
<div ref={this.childRef} onClick={this.childClickFun}>
分析事件执行顺序
</div>
</div>
);
}
}
export default App;

输出顺序为:

1
2
3
4
5
原生事件:子元素 DOM 事件监听�?
原生事件:父元素 DOM 事件监听�?
React 事件:子元素事件监听�?
React 事件:父元素事件监听�?
原生事件:document DOM 事件监听�?

可以得出以下结论�?

  • React 所有事件都挂载�?document 对象�?- 当真�?DOM 元素触发事件,会冒泡�?document 对象后,再处�?React 事件
  • 所以会先执行原生事件,然后处理 React 事件
  • 最后真正执�?document 上挂载的事件

对应过程如图所示:

所以想要阻止不同时间段的冒泡行为,对应使用不同的方法,对应如下�?

  • 阻止合成事件间的冒泡,用e.stopPropagation()

  • 阻止合成事件与最外层 document 上的事件间的冒泡,用e.nativeEvent.stopImmediatePropagation()

  • 阻止合成事件与除最外层document上的原生事件上的冒泡,通过判断e.target来避�?

    1
    2
    3
    4
    5
    6
    document.body.addEventListener('click', e => {   
    if (e.target && e.target.matches('div.code')) {
    return;
    }
    this.setState({ active: false, }); });
    }

三、总结

React事件机制总结如下�?

  • React 上注册的事件最终会绑定在document这个 DOM 上,而不�?React 组件对应�?DOM(减少内存开销就是因为所有的事件都绑定在 document 上,其他节点没有绑定事件)
  • React 自身实现了一套事件冒泡机制,所以这也就是为什么我�?event.stopPropagation()无效的原因�?- React 通过队列的形式,从触发的组件向父组件回溯,然后调用他�?JSX 中定义的 callback
  • React 有一套自己的合成事件 SyntheticEvent

参考文�?- https://zh-hans.reactjs.org/docs/events.html

面试官:在react中组件间过渡动画如何实现�?

一、是什�?

在日常开发中,页面切换时的转场动画是比较基础的一个场�?
当一个组件在显示与消失过程中存在过渡动画,可以很好的增加用户的体�?
react中实现过渡动画效果会有很多种选择,如react-transition-groupreact-motionAnimated,以及原生的CSS都能完成切换动画

二、如何实�?

react中,react-transition-group是一种很好的解决方案,其为元素添加enterenter-activeexitexit-active这一系列勾子

可以帮助我们方便的实现组件的入场和离场动�?
其主要提供了三个主要的组件:

  • CSSTransition:在前端开发中,结�?CSS 来完成过渡动画效�?- SwitchTransition:两个组件显示和隐藏切换时,使用该组�?- TransitionGroup:将多个动画组件包裹在其中,一般用于列表中元素的动�?

CSSTransition

其实现动画的原理在于,当CSSTransitionin属性置为true时,CSSTransition首先会给其子组件加上xxx-enterxxx-enter-activeclass执行动画

当动画执行结束后,会移除两个class,并且添加-enter-doneclass

所以可以利用这一点,通过csstransition属性,让元素在两个状态之间平滑过渡,从而得到相应的动画效果

in属性置为false时,CSSTransition会给子组件加上xxx-exitxxx-exit-activeclass,然后开始执行动画,当动画结束后,移除两个class,然后添加-enter-doneclass

如下例子�?

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
export default class App2 extends React.PureComponent {

state = {show: true};

onToggle = () => this.setState({show: !this.state.show});

render() {
const {show} = this.state;
return (
<div className={'container'}>
<div className={'square-wrapper'}>
<CSSTransition
in={show}
timeout={500}
classNames={'fade'}
unmountOnExit={true}
>
<div className={'square'} />
</CSSTransition>
</div>
<Button onClick={this.onToggle}>toggle</Button>
</div>
);
}
}

对应css样式如下�?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
.fade-enter {
opacity: 0;
transform: translateX(100%);
}

.fade-enter-active {
opacity: 1;
transform: translateX(0);
transition: all 500ms;
}

.fade-exit {
opacity: 1;
transform: translateX(0);
}

.fade-exit-active {
opacity: 0;
transform: translateX(-100%);
transition: all 500ms;
}

SwitchTransition

SwitchTransition可以完成两个组件之间切换的炫酷动�?
比如有一个按钮需要在onoff之间切换,我们希望看到on先从左侧退出,off再从右侧进入

SwitchTransition中主要有一个属性mode,对应两个值:

  • in-out:表示新组件先进入,旧组件再移除�?- out-in:表示就组件先移除,新组建再进入

SwitchTransition组件里面要有CSSTransition,不能直接包裹你想要切换的组�?
里面的CSSTransition组件不再像以前那样接受in属性来判断元素是何种状态,取而代之的是key属�?
下面给出一个按钮入场和出场的示例,如下�?

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
import { SwitchTransition, CSSTransition } from "react-transition-group";

export default class SwitchAnimation extends PureComponent {
constructor(props) {
super(props);

this.state = {
isOn: true
}
}

render() {
const {isOn} = this.state;

return (
<SwitchTransition mode="out-in">
<CSSTransition classNames="btn"
timeout={500}
key={isOn ? "on" : "off"}>
{
<button onClick={this.btnClick.bind(this)}>
{isOn ? "on": "off"}
</button>
}
</CSSTransition>
</SwitchTransition>
)
}

btnClick() {
this.setState({isOn: !this.state.isOn})
}
}

css文件对应如下�?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
.btn-enter {
transform: translate(100%, 0);
opacity: 0;
}

.btn-enter-active {
transform: translate(0, 0);
opacity: 1;
transition: all 500ms;
}

.btn-exit {
transform: translate(0, 0);
opacity: 1;
}

.btn-exit-active {
transform: translate(-100%, 0);
opacity: 0;
transition: all 500ms;
}

TransitionGroup

当有一组动画的时候,就可将这些CSSTransition放入到一个TransitionGroup中来完成动画

同样CSSTransition里面没有in属性,用到了key属�?
TransitionGroup在感知children发生变化的时候,先保存移除的节点,当动画结束后才真正移除

其处理方式如下:

  • 插入的节点,先渲染dom,然后再做动�?
  • 删除的节点,先做动画,然后再删除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
29
30
31
32
33
34
35
36
37
import React, { PureComponent } from 'react'
import { CSSTransition, TransitionGroup } from 'react-transition-group';

export default class GroupAnimation extends PureComponent {
constructor(props) {
super(props);

this.state = {
friends: []
}
}

render() {
return (
<div>
<TransitionGroup>
{
this.state.friends.map((item, index) => {
return (
<CSSTransition classNames="friend" timeout={300} key={index}>
<div>{item}</div>
</CSSTransition>
)
})
}
</TransitionGroup>
<button onClick={e => this.addFriend()}>+friend</button>
</div>
)
}

addFriend() {
this.setState({
friends: [...this.state.friends, "coderwhy"]
})
}
}

对应css如下�?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
.friend-enter {
transform: translate(100%, 0);
opacity: 0;
}

.friend-enter-active {
transform: translate(0, 0);
opacity: 1;
transition: all 500ms;
}

.friend-exit {
transform: translate(0, 0);
opacity: 1;
}

.friend-exit-active {
transform: translate(-100%, 0);
opacity: 0;
transition: all 500ms;
}

参考文�?

面试官:说说你在React项目是如何捕获错误的�?

一、是什�?

错误在我们日常编写代码是非常常见�?
举个例子,在react项目中去编写组件内JavaScript代码错误会导�?React 的内部状态被破坏,导致整个应用崩溃,这是不应该出现的现象

作为一个框架,react也有自身对于错误的处理的解决方案

二、如何做

为了解决出现的错误导致整个应用崩溃的问题,react16引用�?*错误边界**新的概念

错误边界是一�?React 组件,这种组件可以捕获发生在其子组件树任何位置的 JavaScript 错误,并打印这些错误,同时展示降�?UI,而并不会渲染那些发生崩溃的子组件�?
错误边界在渲染期间、生命周期方法和整个组件树的构造函数中捕获错误

形成错误边界组件的两个条件:

  • 使用�? static getDerivedStateFromError()
  • 使用�?componentDidCatch()

抛出错误后,请使�?static getDerivedStateFromError() 渲染备用 UI ,使�?componentDidCatch() 打印错误信息,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}

static getDerivedStateFromError(error) {
// 更新 state 使下一次渲染能够显示降级后�?UI
return { hasError: true };
}

componentDidCatch(error, errorInfo) {
// 你同样可以将错误日志上报给服务器
logErrorToMyService(error, errorInfo);
}

render() {
if (this.state.hasError) {
// 你可以自定义降级后的 UI 并渲�? return <h1>Something went wrong.</h1>;
}

return this.props.children;
}
}

然后就可以把自身组件的作为错误边界的子组件,如下�?

1
2
3
<ErrorBoundary>
<MyWidget />
</ErrorBoundary>

下面这些情况无法捕获到异常:

  • 事件处理
  • 异步代码
  • 服务端渲�?- 自身抛出来的错误

react 16版本之后,会把渲染期间发生的所有错误打印到控制�?
除了错误信息�?JavaScript 栈外,React 16 还提供了组件栈追踪。现在你可以准确地查看发生在组件树内的错误信息:

可以看到在错误信息下方文字中存在一个组件栈,便于我们追踪错�?
对于错误边界无法捕获的异常,如事件处理过程中发生问题并不会捕获到,是因为其不会在渲染期间触发,并不会导致渲染时候问�?
这种情况可以使用jstry...catch...语法,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.state = { error: null };
this.handleClick = this.handleClick.bind(this);
}

handleClick() {
try {
// 执行操作,如有错误则会抛�? } catch (error) {
this.setState({ error });
}
}

render() {
if (this.state.error) {
return <h1>Caught an error.</h1>
}
return <button onClick={this.handleClick}>Click Me</button>
}
}

除此之外还可以通过监听onerror事件

1
window.addEventListener('error', function(event) { ... })

参考文�?

面试官:说说对React中类组件和函数组件的理解?有什么区别?

一、类组件

类组件,顾名思义,也就是通过使用ES6类的编写形式去编写组件,该类必须继承React.Component

如果想要访问父组件传递过来的参数,可通过this.props的方式去访问

在组件中必须实现render方法,在return中返回React对象,如下:

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

二、函数组�?

函数组件,顾名思义,就是通过函数编写的形式去实现一个React组件,是React中定义组件最简单的方式

1
2
3
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}

函数第一个参数为props用于接收父组件传递过来的参数

三、区�?

针对两种React组件,其区别主要分成以下几大方向�?

  • 编写形式

  • 状态管�?- 生命周期

  • 调用方式

  • 获取渲染的�?

编写形式

两者最明显的区别在于编写形式的不同,同一种功能的实现可以分别对应类组件和函数组件的编写形�?
函数组件�?

1
2
3
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}

类组件:

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

状态管�?

hooks出来之前,函数组件就是无状态组件,不能保管组件的状态,不像类组件中调用setState

如果想要管理state状态,可以使用useState,如下:

1
2
3
4
5
6
7
8
9
10
11
const FunctionalComponent = () => {
const [count, setCount] = React.useState(0);

return (
<div>
<p>count: {count}</p >
<button onClick={() => setCount(count + 1)}>Click</button>
</div>
);
};

在使用hooks情况下,一般如果函数组件调用state,则需要创建一个类组件或者state提升到你的父组件中,然后通过props对象传递到子组�?

生命周期

在函数组件中,并不存在生命周期,这是因为这些生命周期钩子都来自于继承的React.Component

所以,如果用到生命周期,就只能使用类组�?
但是函数组件使用useEffect也能够完成替代生命周期的作用,这里给出一个简单的例子�?

1
2
3
4
5
6
const FunctionalComponent = () => {
useEffect(() => {
console.log("Hello");
}, []);
return <h1>Hello, World</h1>;
};

上述简单的例子对应类组件中的componentDidMount生命周期

如果在useEffect回调函数中return 一个函数,则return函数会在组件卸载的时候执行,正如componentWillUnmount

1
2
3
4
5
6
7
8
9
const FunctionalComponent = () => {
React.useEffect(() => {
return () => {
console.log("Bye");
};
}, []);
return <h1>Bye, World</h1>;
};

调用方式

如果是一个函数组件,调用则是执行函数即可�?

1
2
3
4
5
6
// 你的代码 
function SayHi() {
return <p>Hello, React</p >
}
// React内部
const result = SayHi(props) // » <p>Hello, React</p >

如果是一个类组件,则需要将组件进行实例化,然后调用实例对象的render方法�?

1
2
3
4
5
6
7
8
9
// 你的代码 
class SayHi extends React.Component {
render() {
return <p>Hello, React</p >
}
}
// React内部
const instance = new SayHi(props) // » SayHi {}
const result = instance.render() // » <p>Hello, React</p >

获取渲染的�?

首先给出一个示�?
函数组件对应如下�?

1
2
3
4
5
6
7
8
9
10
11
12
13
function ProfilePage(props) {
const showMessage = () => {
alert('Followed ' + props.user);
}

const handleClick = () => {
setTimeout(showMessage, 3000);
}

return (
<button onClick={handleClick}>Follow</button>
)
}

类组件对应如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
class ProfilePage extends React.Component {
showMessage() {
alert('Followed ' + this.props.user);
}

handleClick() {
setTimeout(this.showMessage.bind(this), 3000);
}

render() {
return <button onClick={this.handleClick.bind(this)}>Follow</button>
}
}

两者看起来实现功能是一致的,但是在类组件中,输出this.props.userProps �?React 中是不可变的所以它永远不会改变,但�?this 总是可变的,以便您可以在 render 和生命周期函数中读取新版�?
因此,如果我们的组件在请求运行时更新。this.props 将会改变。showMessage 方法从“最新”的 props 中读�?user

而函数组件,本身就不存在thisprops并不发生改变,因此同样是点击,alert的内容仍旧是之前的内�?

小结

两种组件都有各自的优缺点

函数组件语法更短、更简单,这使得它更容易开发、理解和测试

而类组件也会因大量使�?this 而让人感到困�?

参考文�?

面试官:React中组件之间如何通信�?

一、是什�?

我们将组件间通信可以拆分为两个词�?

  • 组件
  • 通信

回顾Vue系列的文章,组件是vue中最强大的功能之一,同样组件化是React的核心思想

相比vueReact的组件更加灵活和多样,按照不同的方式可以分成很多类型的组�?
而通信指的是发送者通过某种媒体以某种格式来传递信息到收信者以达到某个目的,广义上,任何信息的交通都是通信

组件间通信即指组件通过某种方式来传递信息以达到某个目的

二、如何通信

组件传递的方式有很多种,根据传送者和接收者可以分为如下:

  • 父组件向子组件传�?- 子组件向父组件传�?- 兄弟组件之间的通信
  • 父组件向后代组件传�?- 非关系组件传�?

父组件向子组件传�?

由于React的数据流动为单向的,父组件向子组件传递是最常见的方�?
父组件在调用子组件的时候,只需要在子组件标签内传递参数,子组件通过props属性就能接收父组件传递过来的参数

1
2
3
4
5
6
7
8
9
function EmailInput(props) {
return (
<label>
Email: <input value={props.email} />
</label>
);
}

const element = <EmailInput email="[email protected]" />;

子组件向父组件传�?

子组件向父组件通信的基本思路是,父组件向子组件传一个函数,然后通过这个函数的回调,拿到子组件传过来的�?
父组件对应代码如下:

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

getItemPrice(e) {
this.setState({
price: e
});
}

render() {
return (
<div>
<div>price: {this.state.price}</div>
{/* 向子组件中传入一个函�? */}
<Child getPrice={this.getItemPrice.bind(this)} />
</div>
);
}
}

子组件对应代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Child extends Component {
clickGoods(e) {
// 在此函数中传入�? this.props.getPrice(e);
}

render() {
return (
<div>
<button onClick={this.clickGoods.bind(this, 100)}>goods1</button>
<button onClick={this.clickGoods.bind(this, 1000)}>goods2</button>
</div>
);
}
}

兄弟组件之间的通信

如果是兄弟组件之间的传递,则父组件作为中间层来实现数据的互通,通过使用父组件传�?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Parent extends React.Component {
constructor(props) {
super(props)
this.state = {count: 0}
}
setCount = () => {
this.setState({count: this.state.count + 1})
}
render() {
return (
<div>
<SiblingA
count={this.state.count}
/>
<SiblingB
onClick={this.setCount}
/>
</div>
);
}
}

父组件向后代组件传�?

父组件向后代组件传递数据是一件最普通的事情,就像全局数据一�?
使用context提供了组件之间通讯的一种方式,可以共享数据,其他数据都能读取对应的数据

通过使用React.createContext创建一个context

1
const PriceContext = React.createContext('price')

context创建成功后,其下存在Provider组件用于创建数据源,Consumer组件用于接收数据,使用实例如下:

Provider组件通过value属性用于给后代组件传递数据:

1
2
<PriceContext.Provider value={100}>
</PriceContext.Provider>

如果想要获取Provider传递的数据,可以通过Consumer组件或者或者使用contextType属性接收,对应分别如下�?

1
2
3
4
5
6
7
class MyClass extends React.Component {
static contextType = PriceContext;
render() {
let price = this.context;
/* 基于这个值进行渲染工�?*/
}
}

Consumer组件�?

1
2
3
4
5
6
<PriceContext.Consumer>
{ /*这里是一个函�?/ }
{
price => <div>price:{price}</div>
}
</PriceContext.Consumer>

非关系组件传�?

如果组件之间关系类型比较复杂的情况,建议将数据进行一个全局资源管理,从而实现通信,例如redux。关于redux的使用后续再详细介绍

三、总结

由于React是单向数据流,主要思想是组件不会改变接收的数据,只会监听数据的变化,当数据发生变化时它们会使用接收到的新值,而不是去修改已有的�?
因此,可以看到通信过程中,数据的存储位置都是存放在上级位置�?

参考文�?

面试官:说说对受控组件和非受控组件的理解?应用场景?

一、受控组�?

受控组件,简单来讲,就是受我们控制的组件,组件的状态全程响应外部数�?
举个简单的例子�?

1
2
3
4
5
6
7
8
9
class TestComponent extends React.Component {
constructor (props) {
super(props);
this.state = { username: 'lindaidai' };
}
render () {
return <input name="username" value={this.state.username} />
}
}

这时候当我们在输入框输入内容的时候,会发现输入的内容并无法显示出来,也就是input标签是一个可读的状�?
这是因为valuethis.state.username所控制住。当用户输入新的内容时,this.state.username并不会自动更新,这样的话input内的内容也就不会变了

如果想要解除被控制,可以为input标签设置onChange事件,输入的时候触发事件函数,在函数内部实现state的更新,从而导致input框的内容页发现改�?
因此,受控组件我们一般需要初始状态和一个状态更新事件函�?

二、非受控组件

非受控组件,简单来讲,就是不受我们控制的组�?
一般情况是在初始化的时候接受外部数据,然后自己在内部存储其自身状�?
当需要时,可以使用ref 查询 DOM 并查找其当前值,如下�?

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

export class UnControll extends Component {
constructor (props) {
super(props);
this.inputRef = React.createRef();
}
handleSubmit = (e) => {
console.log('我们可以获得input内的值为', this.inputRef.current.value);
e.preventDefault();
}
render () {
return (
<form onSubmit={e => this.handleSubmit(e)}>
<input defaultValue="lindaidai" ref={this.inputRef} />
<input type="submit" value="提交" />
</form>
)
}
}

关于refs的详情使用可以参考之前文章

三、应用场�?

大部分时候推荐使用受控组件来实现表单,因为在受控组件中,表单数据由React组件负责处理

如果选择非受控组件的话,控制能力较弱,表单数据就由DOM本身处理,但更加方便快捷,代码量�?
针对两者的区别,其应用场景如下图所示:

参考文�?

面试官:说说React diff的原理是什么?

一、是什�?

Vue一致,React通过引入Virtual DOM的概念,极大地避免无效的Dom操作,使我们的页面的构建效率提到了极大的提升

diff算法就是更高效地通过对比新旧Virtual DOM来找出真正的Dom变化之处

传统diff算法通过循环递归对节点进行依次对比,效率低下,算法复杂度达到 O(n^3),react将算法进行一个优化,复杂度姜维O(n),两者效率差距如下图�?

二、原�?

reactdiff算法主要遵循三个层级的策略:

  • tree层级

  • conponent 层级

  • element 层级

tree层级

DOM节点跨层级的操作不做优化,只会对相同层级的节点进行比�?

只有删除、创建操作,没有移动操作,如下图�?

react发现新树中,R节点下没有了A,那么直接删除A,在D节点下创建A以及下属节点

上述操作中,只有删除和创建操�?

conponent层级

如果是同一个类的组件,则会继续往下diff运算,如果不是一个类的组件,那么直接删除这个组件下的所有子节点,创建新�?

component D 换成了component G 后,即使两者的结构非常类似,也会将D删除再重新创建G

element层级

对于比较同一层级的节点们,每个节点在对应的层级用唯一的key作为标识

提供�?3 种节点操作,分别�?INSERT_MARKUP (插入)、MOVE_EXISTING (移动)�?REMOVE_NODE (删除)

如下场景�?

通过key可以准确地发现新旧集合中的节点都是相同的节点,因此无需进行节点删除和创建,只需要将旧集合中节点的位置进行移动,更新为新集合中节点的位置

流程如下表:

  • index�?新集合的遍历下标�?- oldIndex:当前节点在老集合中的下�?- maxIndex:在新集合访问过的节点中,其在老集合的最大下�?
    如果当前节点在新集合中的位置比老集合中的位置靠前的话,是不会影响后续节点操作的,这里这时候被动字节不用动

操作过程中只比较oldIndex和maxIndex,规则如下:

  • 当oldIndex>maxIndex时,将oldIndex的值赋值给maxIndex
  • 当oldIndex=maxIndex时,不操�?- 当oldIndex<maxIndex时,将当前节点移动到index的位�?
    diff过程如下�?
  • 节点B:此�?maxIndex=0,oldIndex=1;满�?maxIndex< oldIndex,因此B节点不动,此时maxIndex= Math.max(oldIndex, maxIndex),就�?
  • 节点A:此时maxIndex=1,oldIndex=0;不满足maxIndex< oldIndex,因此A节点进行移动操作,此时maxIndex= Math.max(oldIndex, maxIndex),还�?
  • 节点D:此时maxIndex=1, oldIndex=3;满足maxIndex< oldIndex,因此D节点不动,此时maxIndex= Math.max(oldIndex, maxIndex),就�?
  • 节点C:此时maxIndex=3,oldIndex=2;不满足maxIndex< oldIndex,因此C节点进行移动操作,当前已经比较完�?
    当ABCD节点比较完成后,diff过程还没完,还会整体遍历老集合中节点,看有没有没用到的节点,有的话,就删�?

三、注意事�?

对于简单列表渲染而言,不使用key比使用key的性能,例如:

将一个[1,2,3,4,5],渲染成如下的样子:

1
2
3
4
5
<div>1</div>
<div>2</div>
<div>3</div>
<div>4</div>
<div>5</div>

后续更改成[1,3,2,5,4],使用key与不使用key作用如下�?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
1.加key
<div key='1'>1</div> <div key='1'>1</div>
<div key='2'>2</div> <div key='3'>3</div>
<div key='3'>3</div> ========> <div key='2'>2</div>
<div key='4'>4</div> <div key='5'>5</div>
<div key='5'>5</div> <div key='4'>4</div>
操作:节�?移动至下标为2的位置,节点4移动至下标为4的位置�?
2.不加key
<div>1</div> <div>1</div>
<div>2</div> <div>3</div>
<div>3</div> ========> <div>2</div>
<div>4</div> <div>5</div>
<div>5</div> <div>4</div>
操作:修改第1个到�?个节点的innerText

如果我们对这个集合进行增删的操作改成[1,3,2,5,6]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
1.加key
<div key='1'>1</div> <div key='1'>1</div>
<div key='2'>2</div> <div key='3'>3</div>
<div key='3'>3</div> ========> <div key='2'>2</div>
<div key='4'>4</div> <div key='5'>5</div>
<div key='5'>5</div> <div key='6'>6</div>
操作:节�?移动至下标为2的位置,新增节点6至下标为4的位置,删除节点4�?
2.不加key
<div>1</div> <div>1</div>
<div>2</div> <div>3</div>
<div>3</div> ========> <div>2</div>
<div>4</div> <div>5</div>
<div>5</div> <div>6</div>
操作:修改第1个到�?个节点的innerText

由于dom节点的移动操作开销是比较昂贵的,没有key的情况下要比有key的性能更好

参考文�?

面试官:你在React项目中是如何使用Redux�? 项目结构是如何划分的�?

一、背�?

在前面文章了解中,我们了解到redux是用于数据状态管理,而react是一个视图层面的�?
如果将两者连接在一起,可以使用官方推荐react-redux库,其具有高效且灵活的特�?
react-redux将组件分成:

  • 容器组件:存在逻辑处理
  • UI 组件:只负责现显示和交互,内部不处理逻辑,状态由外部控制

通过redux将整个应用状态存储到store中,组件可以派发dispatch行为actionstore

其他组件通过订阅store中的状态state来更新自身的视图

二、如何做

使用react-redux分成了两大核心:

  • Provider
  • connection

Provider

redux中存在一个store用于存储state,如果将这个store存放在顶层元素中,其他组件都被包裹在顶层元素之上

那么所有的组件都能够受到redux的控制,都能够获取到redux中的数据

使用方式如下�?

1
2
3
<Provider store = {store}>
<App />
<Provider>

connection

connect方法将store上的getState �?dispatch 包装成组件的props

导入conect如下�?

1
import { connect } from "react-redux";

用法如下�?

1
connect(mapStateToProps, mapDispatchToProps)(MyComponent)

可以传递两个参数:

  • mapStateToProps

  • mapDispatchToProps

mapStateToProps

redux中的数据映射到react中的props中去

如下�?

1
2
3
4
5
const mapStateToProps = (state) => {
return {
// prop : state.xxx | 意思是将state中的某个数据映射到props�? foo: state.bar
}
}

组件内部就能够通过props获取到store中的数据

1
2
3
4
5
6
7
8
9
10
11
12
13
class Foo extends Component {
constructor(props){
super(props);
}
render(){
return(
// 这样子渲染的其实就是state.bar的数据了
<div>this.props.foo</div>
)
}
}
Foo = connect()(Foo)
export default Foo

mapDispatchToProps

redux中的dispatch映射到组件内部的props�?

1
2
3
4
5
6
7
8
9
10
const mapDispatchToProps = (dispatch) => { // 默认传递参数就是dispatch
return {
onClick: () => {
dispatch({
type: 'increatment'
});
}
};
}

1
2
3
4
5
6
7
8
9
10
11
12
13
class Foo extends Component {
constructor(props){
super(props);
}
render(){
return(

<button onClick = {this.props.onClick}>点击increase</button>
)
}
}
Foo = connect()(Foo);
export default Foo;

小结

整体流程图大致如下所示:

三、项目结�?

可以根据项目具体情况进行选择,以下列出两种常见的组织结构

按角色组织(MVC�?

角色如下�?

  • reducers
  • actions
  • components
  • containers

参考如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
reducers/
todoReducer.js
filterReducer.js
actions/
todoAction.js
filterActions.js
components/
todoList.js
todoItem.js
filter.js
containers/
todoListContainer.js
todoItemContainer.js
filterContainer.js

按功能组�?

使用redux使用功能组织项目,也就是把完成同一应用功能的代码放在一个目录下,一个应用功能包含多个角色的代码

Redux中,不同的角色就是reduceractions和视图,而应用功能对应的就是用户界面的交互模�?
参考如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
todoList/
actions.js
actionTypes.js
index.js
reducer.js
views/
components.js
containers.js
filter/
actions.js
actionTypes.js
index.js
reducer.js
views/
components.js
container.js

每个功能模块对应一个目录,每个目录下包含同样的角色文件�?

  • actionTypes.js 定义action类型
  • actions.js 定义action构造函�?- reducer.js 定义这个功能模块如果响应actions.js定义的动�?- views 包含功能模块中所有的React组件,包括展示组件和容器组件
  • index.js 把所有的角色导入,统一导出

其中index模块用于导出对外的接�?

1
2
3
4
5
import * as actions from './actions.js';
import reducer from './reducer.js';
import view from './views/container.js';

export { actions, reducer, view };

导入方法如下�?

1
import { actions, reducer, view as TodoList } from './xxxx'

参考文�?

面试官:说说你对immutable的理解?如何应用在react项目中?

一、是什�?

Immutable,不可改变的,在计算机中,即指一旦创建,就不能再被更改的数据

�?Immutable 对象的任何修改或添加删除操作都会返回一个新�?Immutable 对象

Immutable 实现的原理是 Persistent Data Structure(持久化数据结构�?

  • 用一种数据结构来保存数据
  • 当数据被修改时,会返回一个对象,但是新的对象会尽可能的利用之前的数据结构而不会对内存造成浪费

也就是使用旧数据创建新数据时,要保证旧数据同时可用且不变,同时为了避�?deepCopy 把所有节点都复制一遍带来的性能损耗,Immutable 使用�?Structural Sharing(结构共享)

如果对象树中一个节点发生变化,只修改这个节点和受它影响的父节点,其它节点则进行共享

如下图所示:

二、如何使�?

使用Immutable对象最主要的库是immutable.js

immutable.js 是一个完全独立的库,无论基于什么框架都可以用它

其出现场景在于弥�?Javascript 没有不可变数据结构的问题,通过 structural sharing来解决的性能问题

内部提供了一套完整的 Persistent Data Structure,还有很多易用的数据类型,如CollectionListMapSetRecordSeq,其中:

  • List: 有序索引集,类似 JavaScript 中的 Array

  • Map: 无序索引集,类似 JavaScript 中的 Object

  • Set: 没有重复值的集合

主要的方法如下:

  • fromJS():将一个js数据转换为Immutable类型的数�?

    1
    const obj = Immutable.fromJS({a:'123',b:'234'})
  • toJS():将一个Immutable数据转换为JS类型的数�?- is():对两个对象进行比较

1
2
3
4
5
6
import { Map, is } from 'immutable'
const map1 = Map({ a: 1, b: 1, c: 1 })
const map2 = Map({ a: 1, b: 1, c: 1 })
map1 === map2 //false
Object.is(map1, map2) // false
is(map1, map2) // true
  • get(key):对数据或对象取�?
  • getIn([]) :对嵌套对象或数组取值,传参为数组,表示位置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let abs = Immutable.fromJS({a: {b:2}});
abs.getIn(['a', 'b']) // 2
abs.getIn(['a', 'c']) // 子级没有�?
let arr = Immutable.fromJS([1 ,2, 3, {a: 5}]);
arr.getIn([3, 'a']); // 5
arr.getIn([3, 'c']); // 子级没有�?```

-

如下例子:使用方法如下:

```js
import Immutable from 'immutable';
foo = Immutable.fromJS({a: {b: 1}});
bar = foo.setIn(['a', 'b'], 2); // 使用 setIn 赋�?console.log(foo.getIn(['a', 'b'])); // 使用 getIn 取值,打印 1
console.log(foo === bar); // 打印 false

如果换到原生的js,则对应如下�?

1
2
3
4
5
let foo = {a: {b: 1}};
let bar = foo;
bar.a.b = 2;
console.log(foo.a.b); // 打印 2
console.log(foo === bar); // 打印 true

三、在React中应�?

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

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

Immutable通过is方法则可以完成对比,而无需像一样通过深度比较的方式比�?
在使用redux过程中也可以结合Immutable,不使用Immutable前修改一个数据需要做一个深拷贝

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import '_' from 'lodash';

const Component = React.createClass({
getInitialState() {
return {
data: { times: 0 }
}
},
handleAdd() {
let data = _.cloneDeep(this.state.data);
data.times = data.times + 1;
this.setState({ data: data });
}
}

使用 Immutable 后:

1
2
3
4
5
6
7
8
9
getInitialState() {
return {
data: Map({ times: 0 })
}
},
handleAdd() {
this.setState({ data: this.state.data.update('times', v => v + 1) });
// 这时�?times 并不会改�? console.log(this.state.data.get('times'));
}

同理,在redux中也可以将数据进行fromJS处理

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
import * as constants from './constants'
import {fromJS} from 'immutable'
const defaultState = fromJS({ //将数据转化成immutable数据
home:true,
focused:false,
mouseIn:false,
list:[],
page:1,
totalPage:1
})
export default(state=defaultState,action)=>{
switch(action.type){
case constants.SEARCH_FOCUS:
return state.set('focused',true) //更改immutable数据
case constants.CHANGE_HOME_ACTIVE:
return state.set('home',action.value)
case constants.SEARCH_BLUR:
return state.set('focused',false)
case constants.CHANGE_LIST:
// return state.set('list',action.data).set('totalPage',action.totalPage)
//merge效率更高,执行一次改变多个数�? return state.merge({
list:action.data,
totalPage:action.totalPage
})
case constants.MOUSE_ENTER:
return state.set('mouseIn',true)
case constants.MOUSE_LEAVE:
return state.set('mouseIn',false)
case constants.CHANGE_PAGE:
return state.set('page',action.page)
default:
return state
}
}

参考文�?

0%