React最佳实践

事件机制,合成事件

是什么

React 基于浏览器的事件机制⾃身实现了⼀套事件机制,包括事件注册、事件的合成、事件冒泡、事件派发等。
在 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
38
39
40
41
42
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 上,⽽不是 React 组件对应的 DOM(减少内存开销就是因为所有的事件都绑定在 document 上,其他节点没有绑定事件)
  • React 通过队列的形式,从触发的组件向⽗组件回溯,然后调⽤他们 JSX 中定义的 callback
  • React 有⼀套⾃⼰的合成事件 SyntheticEvent

React中引入CSS

在组件内直接使⽤

内联style引入

组件中引⼊ .css ⽂件

全局生效,样式之间会互相影响

组件中引⼊ .module.css ⽂件

将 css ⽂件作为⼀个模块引⼊,这个模块中的所有 css ,只作⽤于当前组件。不会影响当前组件的后代组件
这种⽅式是 webpack 特⼯的⽅案,只需要配置 webpack 配置⽂件中 modules:true 即可

CSS in JS

CSS-in-JS, 是指⼀种模式,其中 CSS 由 JavaScript ⽣成⽽不是在外部⽂件中定义
此功能并不是 React 的⼀部分,⽽是由第三⽅库提供,例如:

  • styled-components
  • emotion
  • glamorous

本质是通过函数的调⽤,最终创建出⼀个组件:

  • 这个组件会被⾃动添加上⼀个不重复的class
  • styled-components会给该class添加相关的样式

组件通信

爷孙组件通信

爷孙组件通信主要有 3 种方式:

  1. 将孙子组件的 props 封装在一个固定字段中
  2. 通过 children 透传
  3. 通过 context 传递

假设有个三层组件,爷爷分别给儿子和孙子发红包

先看青铜解决方案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function Grandpa() {
const [someMoneyForMe] = useState(100);
const [someMoneyForDaddy] = useState(101);
return <Daddy money={someMoneyForDaddy} moneyForSon={someMoneyForMe} />;
}
function Daddy(props: { money: number; moneyForSon: number }) {
const { money, moneyForSon } = props;
return (
<div className="daddy">
<h2>This is Daddy, received ${money}</h2>
<Me money={moneyForSon} />
</div>
);
}
function Me(props: { money: number }) {
const { money } = props;
return (
<div className="son">
<h3>This is Me, received ${money}</h3>
</div>
);
}

Daddy 组件会透传爷爷给孙子的组件给 Me。这种方案的缺点很明显,以后爷爷要给 Daddy 和 Me 发糖果的时候,Daddy 还得加字段。

将孙子组件的 props 封装在一个固定字段中

按照 1 的方案,我们可以固定给 Daddy 添加一个 sonProps 的字段,然后将 Grandpa 需要传给孙子的状态全部通过 sonProps 传递

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function Grandpa() {
const [someMoneyForMe] = useState(100);
const [someMoneyForDaddy] = useState(101);
return <Daddy money={someMoneyForDaddy} sonProps={{money: someMoneyForMe}} />;
}
function Daddy(props: { money: number; sonProps: Parameters<typeof Me>[0]; }) {
const { money, sonProps } = props;
return (
<div className="daddy">
<h2>This is Daddy, received ${money}</h2>
<Me {...sonProps}/>
</div>
);
}
function Me(props: { money: number }) {
const { money } = props;
return (
<div className="son">
<h3>This is Me, received ${money}</h3>
</div>
);
}

这样以后要给 Me 加字段,就不用改 Daddy 了。但要测试 Daddy 时还得 mock Me 组件的数据,Daddy 和 Son 耦合。

通过 children 透传

children 类似于 vue 中的 slot,可以完成一些嵌套组件通信的功能

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
function Grandpa() {
const [someMoneyForMe] = useState(100);
const [someMoneyForDaddy] = useState(101);
return (
<Daddy money={someMoneyForDaddy}>
<Me money={someMoneyForMe} />
</Daddy>
);
}
function Daddy(props: { money: number; children?: React.ChildNode }) {
const { money, children } = props;
return (
<div className="daddy">
<h2>This is Daddy, received ${money}</h2>
{children}
</div>
);
}
function Me(props: { money: number }) {
const { money } = props;
return (
<div className="son">
<h3>This is Me, received ${money}</h3>
</div>
);
}

将 Daddy 的嵌套部分用 children 替代后,解耦了子组件和孙子组件的依赖关系,Daddy 组件更加独立。

作为替代,也可以传递一个组件实例:

三种方案的决策

  1. 第一种方案一般用于固定结构和跨组件有互相依赖的场景,多见于 UI 框架中的复合组件与原子组件的设计中
  2. 第二种常用在嵌套层级不深的业务代码中,比如表单场景。优点是顶层 Grandpa 的业务收敛度很高,一眼能看清 UI 结构及状态绑定关系,相当于拍平了 React 组件树
  3. 第三种比较通用,适合复杂嵌套透传场景。缺点是范式代码较多,且会造成 react dev tools 层级过多;Context 无法在父组件看出依赖关系,必须到子组件文件中才能知道数据来源

高阶组件

本质上是一个函数,接收一个或多个组件作为参数,并返回一个组件

错误边界

错误边界是一种 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
25
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;
}
}

下⾯这些情况⽆法捕获到异常:

  • 事件处理
  • 异步代码(例如 setTimeout 或 requestAnimationFrame 回调函数)
  • 服务端渲染
  • 它自身抛出来的错误(仅能捕获子组件抛出的错误)

setState更新,批量更新

render原理(jsx经过babel编译)、render触发时机

Diff算法

React Portal

Portal 提供了一种将子节点渲染到存在于父组件以外的 DOM 节点的优秀的方案。
ReactDOM.createPortal(child, container)
第一个参数(child)是任何可渲染的 React 子元素,例如一个元素,字符串或 fragment。第二个参数(container)是一个 DOM 元素。

React router

是什么

react-router 等前端路由的原理⼤致相同,可以实现⽆刷新的条件下切换显示不同的⻚⾯
路由的本质就是⻚⾯的 URL 发⽣改变时,⻚⾯的显示结果可以根据 URL 的变化⽽变化,但是⻚⾯不会刷新因此,可以通过前端路由可以实现单⻚(SPA)应⽤

模式
HashRouter(Hash模式)
  • 采用监听 window 上的 hashchange 事件实现;
  • path的表现形式是 localhost:3000/#/demo/test
  • 服务器无须做额外配置(因为对于服务器而言,http://example.com/#/homehttp://example.com这两个请求在服务器看来是完全一样的,因为服务器只处理http://example.com这个部分,而#/home这个哈希部分的变化不会触发新的服务器请求)
BrowserRouter(History模式)
  • BrowserRouter使用的是H5的history API来实现;
  • path的表现形式是 localhost:3000/demo/test
  • 服务器需要进行额外的配置(当path变化时,浏览器会向服务器发送请求。服务器需要能够识别这些请求并返回正确的页面或者资源。例如,在服务器重定向配置中,需要设置一个通配符路由(*)来处理所有可能的路由请求,将请求重定向到应用的入口文件(通常是index.html),这样客户端的 JavaScript 代码才能根据 URL 中的路径来正确地渲染页面)

lazyload

render props、children

Hooks相关

React fiber

React性能优化

React状态管理

Redux

react如何实现ssr