# 高级指引
# 1.代码分割
对应用进行代码分割能够帮助你“懒加载”当前用户所需要的内容,能够显著地提高你的应用性能。在你的应用中引入代码分割的最佳方式是通过动态 import()
语法。
# React.lazy
React.lazy
函数能让你像渲染常规组件一样处理动态引入(的组件)。
React.lazy
接受一个函数,这个函数需要动态调用 import()
。它必须返回一个 Promise
,该 Promise 需要 resolve 一个 defalut
export 的 React 组件。
然后应在 Suspense
组件中渲染 lazy 组件,如此使得我们可以使用在等待加载 lazy 组件时做优雅降级(如 loading 指示器等)。
import React, { Suspense } from 'react';
const OtherComponent = React.lazy(() => import('./OtherComponent'));
function MyComponent() {
return (
<div>
<Suspense fallback={<div>Loading...</div>}>
<OtherComponent />
</Suspense>
</div>
);
}
2
3
4
5
6
7
8
9
10
11
12
13
# 基于路由的代码分割
在你的应用中使用 React.lazy
和 React Router (opens new window) 这类的第三方库,来配置基于路由的代码分割。
import React, { Suspense, lazy } from 'react';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
const Home = lazy(() => import('./routes/Home'));
const About = lazy(() => import('./routes/About'));
const App = () => (
<Router>
<Suspense fallback={<div>Loading...</div>}>
<Switch>
<Route exact path="/" component={Home}/>
<Route path="/about" component={About}/>
</Switch>
</Suspense>
</Router>
);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 2.全局数据 Context
组件间的数据是从上而下传递的,对于那些需要共享的数据如地区偏好等属性,会比较繁琐。
Context 就是为了共享那些对于一个组件树而言是“全局”的数据。
class App extends React.Component {
render() {
return <Toolbar theme="dark" />;
}
}
function Toolbar(props) {
// Toolbar 组件接受一个额外的“theme”属性,然后传递给 ThemedButton 组件。
// 如果应用中每一个单独的按钮都需要知道 theme 的值,这会是件很麻烦的事,
// 因为必须将这个值层层传递所有组件。
return (
<div>
<ThemedButton theme={props.theme} />
</div>
);
}
class ThemedButton extends React.Component {
render() {
return <Button theme={this.props.theme} />;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
使用 context, 我们可以避免通过中间元素传递 props:
// Context 可以让我们无须明确地传遍每一个组件,就能将值深入传递进组件树。
// 为当前的 theme 创建一个 context(“light”为默认值)。
const ThemeContext = React.createContext('light');
class App extends React.Component {
render() {
// 使用一个 Provider 来将当前的 theme 传递给以下的组件树。
// 无论多深,任何组件都能读取这个值。
// 在这个例子中,我们将 “dark” 作为当前的值传递下去。
return (
<ThemeContext.Provider value="dark">
<Toolbar />
</ThemeContext.Provider>
);
}
}
// 中间的组件再也不必指明往下传递 theme 了。
function Toolbar() {
return (
<div>
<ThemedButton />
</div>
);
}
class ThemedButton extends React.Component {
// 指定 contextType 读取当前的 theme context。
// React 会往上找到最近的 theme Provider,然后使用它的值。
// 在这个例子中,当前的 theme 值为 “dark”。
static contextType = ThemeContext;
render() {
return <Button theme={this.context} />;
}
}
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
类似于 vue 中的 provide
# React.createContext
const MyContext = React.createContext(defaultValue);
创建一个 Context 对象。只有当组件所处的树中没有匹配到 Provider 时,其 defaultValue
参数才会生效。
// import { GamesItem } from '@/types/page/games'
// import { ResEventsList } from '@/types/request'
import { createContext, useContext, useState } from 'react'
// eslint-disable-next-line @typescript-eslint/no-empty-interface
interface DiscoveryProviderContext {
// BannerList: GamesItem[]
// OngoingEvents:ResEventsList
}
export const DiscoveryProviderContext =
createContext<DiscoveryProviderContext | null>(null)
export const useDiscoveryProviderContext = () => {
const context = useContext(DiscoveryProviderContext)
if (!context)
throw new Error(
'useDiscoveryProviderContextt must be use inside DiscoveryProviderContext'
)
return context
}
type Props = { children: React.ReactNode }
const DiscoveryProvider = ({ children }: Props) => {
return (
<DiscoveryProviderContext.Provider value={{}}>
{children}
</DiscoveryProviderContext.Provider>
)
}
export default DiscoveryProvider
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
# Context.Provider
<MyContext.Provider value={/* 某个值 */}>
每个 Context 对象都会返回一个 Provider React 组件,它允许消费组件订阅 context 的变化。
Provider 接收一个 value
属性,传递给消费组件。
当 Provider 的 value
值发生变化时,它内部的所有消费组件都会重新渲染。
通过新旧值检测来确定变化,使用了与 Object.is
(opens new window) 相同的算法。
当传递对象给
value
时,检测变化的方式会导致一些问题:父组件重新渲染时,Provider 重新渲染,value
属性总是被赋值为新的对象,导致下面的所有消费组件全部重新渲染,因此可以将value
状态提升到父节点的state
里,将state
进行传递。
# Class.contextType
class MyClass extends React.Component {
componentDidMount() {
let value = this.context;
/* 在组件挂载完成后,使用 MyContext 组件的值来执行一些有副作用的操作 */
}
componentDidUpdate() {
let value = this.context;
/* ... */
}
componentWillUnmount() {
let value = this.context;
/* ... */
}
render() {
let value = this.context;
/* 基于 MyContext 组件的值进行渲染 */
}
}
MyClass.contextType = MyContext;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
挂载在 class 上的 contextType
属性可以赋值为由 React.createContext()
(opens new window) 创建的 Context 对象。
此属性可以让你使用 this.context
来获取最近 Context 上的值。你可以在任何生命周期中访问到它,包括 render 函数中。
# Context.Consumer
<MyContext.Consumer>
{value => /* 基于 context 值进行渲染*/}
</MyContext.Consumer>
2
3
一个 React 组件可以订阅 context 的变更,此组件可以让你在函数式组件 (opens new window)中可以订阅 context。
这需要函数作为子元素(function as a child) (opens new window)这种做法。这个函数接收当前的 context 值,返回一个 React 节点。传递给函数的 value
值等同于往上组件树离这个 context 最近的 Provider 提供的 value
值。如果没有对应的 Provider,value
参数等同于传递给 createContext()
的 defaultValue
。
# Context.displayName
const MyContext = React.createContext(/* some value */);
MyContext.displayName = 'MyDisplayName';
<MyContext.Provider> // "MyDisplayName.Provider" 在 DevTools 中
<MyContext.Consumer> // "MyDisplayName.Consumer" 在 DevTools 中
2
3
4
context 对象接受一个名为 displayName
的 property,类型为字符串。React DevTools 使用该字符串来确定 context 要显示的内容。
# 嵌套组件中更新 Context
从一个在组件树中嵌套很深的组件中更新 context 是很有必要的。在这种场景下,可以通过 context 传递一个函数,使得 consumers 组件更新 context。
theme-context.js
export const themes = {
light: {
foreground: '#000000',
background: '#eeeeee',
},
dark: {
foreground: '#ffffff',
background: '#222222',
},
};
// 确保传递给 createContext 的默认值数据结构是调用的组件(consumers)所能匹配的!
export const ThemeContext = React.createContext({
theme: themes.dark,
toggleTheme: () => {},
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
theme-toggler-button.js
import {ThemeContext} from './theme-context';
function ThemeTogglerButton() {
// Theme Toggler 按钮不仅仅只获取 theme 值,它也从 context 中获取到一个 toggleTheme 函数
return (
<ThemeContext.Consumer>
{({theme, toggleTheme}) => (
<button onClick={toggleTheme}
style={{backgroundColor: theme.background}}>
Toggle Theme
</button>
)}
</ThemeContext.Consumer>
);
}
export default ThemeTogglerButton;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
app.js
import {ThemeContext, themes} from './theme-context';
import ThemeTogglerButton from './theme-toggler-button';
class App extends React.Component {
constructor(props) {
super(props);
this.toggleTheme = () => {
this.setState(state => ({
theme:
state.theme === themes.dark
? themes.light
: themes.dark,
}));
};
// State 也包含了更新函数,因此它会被传递进 context provider。
this.state = {
theme: themes.light,
toggleTheme: this.toggleTheme,
};
}
render() {
// 整个 state 都被传递进 provider
return (
<ThemeContext.Provider value={this.state}>
<Content />
</ThemeContext.Provider>
);
}
}
function Content() {
return (
<div>
<ThemeTogglerButton />
</div>
);
}
ReactDOM.render(<App />, document.root);
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
# 消费多个 Context
为了确保 context 快速进行重渲染,React 需要使每一个 consumers 组件的 context 在组件树中成为一个单独的节点。
// Theme context,默认的 theme 是 “light” 值
const ThemeContext = React.createContext('light');
// 用户登录 context
const UserContext = React.createContext({
name: 'Guest',
});
class App extends React.Component {
render() {
const {signedInUser, theme} = this.props;
// 提供初始 context 值的 App 组件
return (
<ThemeContext.Provider value={theme}>
<UserContext.Provider value={signedInUser}>
<Layout />
</UserContext.Provider>
</ThemeContext.Provider>
);
}
}
function Layout() {
return (
<div>
<Sidebar />
<Content />
</div>
);
}
// 一个组件可能会消费多个 context
function Content() {
return (
<ThemeContext.Consumer>
{theme => (
<UserContext.Consumer>
{user => (
<ProfilePage user={user} theme={theme} />
)}
</UserContext.Consumer>
)}
</ThemeContext.Consumer>
);
}
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
如果两个或者更多的 context 值经常被一起使用,那你可能要考虑一下另外创建你自己的渲染组件,以提供这些值。
# 3.错误边界
部分 UI 的 JavaScript 错误不应该导致整个应用崩溃,为了解决这个问题,React 16 引入了一个新的概念 —— 错误边界。
错误边界是一种 React 组件,这种组件可以捕获并打印发生在其子组件树任何位置的 JavaScript 错误,并且,它会渲染出备用 UI,而不是渲染那些崩溃了的子组件树。错误边界在渲染期间、生命周期方法和整个组件树的构造函数中捕获错误。
错误边界无法捕获以下场景中产生的错误:
- 事件处理(了解更多 (opens new window))
- 异步代码(例如
setTimeout
或requestAnimationFrame
回调函数)- 服务端渲染
- 它自身抛出来的错误(并非它的子组件)
如果一个 class 组件中定义了 static getDerivedStateFromError()
(opens new window) 或 componentDidCatch()
(opens new window) 这两个生命周期方法中的任意一个(或两个)时,那么它就变成一个错误边界。
当抛出错误后,请使用 static getDerivedStateFromError()
渲染备用 UI ,使用 componentDidCatch()
打印错误信息。
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;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
然后你可以将它作为一个常规组件去使用:
<ErrorBoundary>
<MyWidget />
</ErrorBoundary>
2
3
# 4.Refs 转发
Ref 转发(forwardRef)使得某些组件可以接收 ref
,并将其向下传递(换句话说,“转发”它)给子组件。
笔记
类似于 vue 中的 ref 。
即在父组件中引用子节点的 DOM 节点。
通常不建议这样做,因为它会打破组件的封装,但它偶尔可用于触发焦点或测量子 DOM 节点。
在下面的示例中,FancyButton
使用 React.forwardRef
来获取传递给它的 ref
,然后转发到它渲染的 DOM button
:
const FancyButton = React.forwardRef((props, ref) => (
<button ref={ref} className="FancyButton">
{props.children}
</button>
));
// 你可以直接获取 DOM button 的 ref:
const ref = React.createRef();
<FancyButton ref={ref}>Click me!</FancyButton>;
2
3
4
5
6
7
8
9
这样,使用 FancyButton
的组件可以获取底层 DOM 节点 button
的 ref ,并在必要时访问,就像其直接使用 DOM button
一样。
以下是对上述示例发生情况的逐步解释:
- 我们通过调用
React.createRef
创建了一个 React ref (opens new window) 并将其赋值给ref
变量。 - 我们通过指定
ref
为 JSX 属性,将其向下传递给<FancyButton ref={ref}>
。 - React 传递
ref
给forwardRef
内函数(props, ref) => ...
,作为其第二个参数。 - 我们向下转发该
ref
参数到<button ref={ref}>
,将其指定为 JSX 属性。 - 当 ref 挂载完成,
ref.current
将指向<button>
DOM 节点。
注意
第二个参数
ref
只在使用React.forwardRef
定义组件时存在。常规函数和 class 组件不接收ref
参数,且 props 中也不存在ref
。Ref 转发不仅限于 DOM 组件,你也可以转发 refs 到 class 组件实例中。
# 5.Fragments
Fragments 允许你将子列表分组,而无需向 DOM 添加额外节点。
render() {
return (
<React.Fragment>
<ChildA />
<ChildB />
<ChildC />
</React.Fragment>
);
}
2
3
4
5
6
7
8
9
# 用法
父组件
class Table extends React.Component {
render() {
return (
<table>
<tr>
<Columns />
</tr>
</table>
);
}
}
2
3
4
5
6
7
8
9
10
11
子组件
<Columns />
需要返回多个 <td>
元素以使渲染的 HTML 有效。如果在 <Columns />
的 render()
中使用了父 div,则生成的 HTML 将无效。
class Columns extends React.Component {
render() {
return (
<React.Fragment>
<td>Hello</td>
<td>World</td>
</React.Fragment>
);
}
}
2
3
4
5
6
7
8
9
10
输出
<table>
<tr>
<td>Hello</td>
<td>World</td>
</tr>
</table>
2
3
4
5
6
# 带 key 的 Fragments
使用显式 <React.Fragment>
语法声明的片段可能具有 key。一个使用场景是将一个集合映射到一个 Fragments 数组 - 举个例子,创建一个描述列表:
function Glossary(props) {
return (
<dl>
{props.items.map(item => (
// 没有`key`,React 会发出一个关键警告
<React.Fragment key={item.id}>
<dt>{item.term}</dt>
<dd>{item.description}</dd>
</React.Fragment>
))}
</dl>
);
}
2
3
4
5
6
7
8
9
10
11
12
13
# 新语法
使用 <> </>
,除了它不支持 key 或属性。
class Columns extends React.Component {
render() {
return (
<>
<td>Hello</td>
<td>World</td>
</>
);
}
}
2
3
4
5
6
7
8
9
10
# 6.高阶组件(HOC)
**高阶组件是参数为组件,返回值为新组件的函数。**是一种基于 react 特性形成的一种设计模式(react 中实现装饰器模式)。常用于第三方库中,进行共享逻辑。
笔记
类似于 mixins 方案,但是 mixins 会产生更多麻烦。
组件是 React 中代码复用的基本单元。但你会发现某些模式并不适合传统组件。
例如,假设有一个 CommentList
组件,它订阅外部数据源,用以渲染评论列表:
class CommentList extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.state = {
// 假设 "DataSource" 是个全局范围内的数据源变量
comments: DataSource.getComments()
};
}
componentDidMount() {
// 订阅更改
DataSource.addChangeListener(this.handleChange);
}
componentWillUnmount() {
// 清除订阅
DataSource.removeChangeListener(this.handleChange);
}
handleChange() {
// 当数据源更新时,更新组件状态
this.setState({
comments: DataSource.getComments()
});
}
render() {
return (
<div>
{this.state.comments.map((comment) => (
<Comment comment={comment} key={comment.id} />
))}
</div>
);
}
}
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
稍后,编写了一个用于订阅单个博客帖子的组件,该帖子遵循类似的模式:
class BlogPost extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.state = {
blogPost: DataSource.getBlogPost(props.id)
};
}
componentDidMount() {
DataSource.addChangeListener(this.handleChange);
}
componentWillUnmount() {
DataSource.removeChangeListener(this.handleChange);
}
handleChange() {
this.setState({
blogPost: DataSource.getBlogPost(this.props.id)
});
}
render() {
return <TextBlock text={this.state.blogPost} />;
}
}
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
它们实现了类似的功能:
- 在挂载时,向
DataSource
添加一个更改侦听器。 - 在侦听器内部,当数据源发生变化时,调用
setState
。 - 在卸载时,删除侦听器。
# 抽象出共享逻辑
const CommentListWithSubscription = withSubscription(
CommentList,
(DataSource) => DataSource.getComments()
);
const BlogPostWithSubscription = withSubscription(
BlogPost,
(DataSource, props) => DataSource.getBlogPost(props.id)
);
2
3
4
5
6
7
8
9
第一个参数是被包装组件。第二个参数通过 DataSource
和当前的 props 返回我们需要的数据。
当渲染 CommentListWithSubscription
和 BlogPostWithSubscription
时, CommentList
和 BlogPost
将传递一个 data
prop,其中包含从 DataSource
检索到的最新数据:
// 此函数接收一个组件...
function withSubscription(WrappedComponent, selectData) {
// ...并返回另一个组件...
return class extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.state = {
data: selectData(DataSource, props)
};
}
componentDidMount() {
// ...负责订阅相关的操作...
DataSource.addChangeListener(this.handleChange);
}
componentWillUnmount() {
DataSource.removeChangeListener(this.handleChange);
}
handleChange() {
this.setState({
data: selectData(DataSource, this.props)
});
}
render() {
// ... 并使用新数据渲染被包装的组件!
// 请注意,我们可能还会传递其他属性
return <WrappedComponent data={this.state.data} {...this.props} />;
}
};
}
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
再看一下另一个案例
# 实例:页面复用
假设我们有两个页面 pageA
和 pageB
分别渲染两个分类的电影列表,普通写法可能是这样:
// pages/page-a.js
class PageA extends React.Component {
state = {
movies: [],
}
// ...
async componentWillMount() {
const movies = await fetchMoviesByType('science-fiction');
this.setState({
movies,
});
}
render() {
return <MovieList movies={this.state.movies} />
}
}
export default PageA;
// pages/page-b.js
class PageB extends React.Component {
state = {
movies: [],
}
// ...
async componentWillMount() {
const movies = await fetchMoviesByType('action');
this.setState({
movies,
});
}
render() {
return <MovieList movies={this.state.movies} />
}
}
export default PageB;
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
页面少的时候可能没什么问题,但是假如随着业务的进展,需要上线的越来越多类型的电影,就会写很多的重复代码,所以我们需要重构一下:
// 这里使用了柯里化的ES6写法,本质上和上面一个案例一致
const withFetching = fetching => WrappedComponent => {
return class extends React.Component {
state = {
data: [],
}
async componentWillMount() {
const data = await fetching();
this.setState({
data,
});
}
render() {
return <WrappedComponent data={this.state.data} {...this.props} />;
}
}
}
// pages/page-a.js
export default withFetching(fetching('science-fiction'))(MovieList);
// pages/page-b.js
export default withFetching(fetching('action'))(MovieList);
// pages/page-other.js
export default withFetching(fetching('some-other-type'))(MovieList);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 约定
# 1.将不相关的 props 传递给被包裹的组件
HOC 应该透传与自身无关的 props。大多数 HOC 都应该包含一个类似于下面的 render 方法:
render() {
// 过滤掉非此 HOC 额外的 props,且不要进行透传
const { extraProp, ...passThroughProps } = this.props;
// 将 props 注入到被包装的组件中。
// 通常为 state 的值或者实例方法。
const injectedProp = someStateOrInstanceMethod;
// 将 props 传递给被包装组件
return (
<WrappedComponent
injectedProp={injectedProp}
{...passThroughProps}
/>
);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
也就是说,我们我们在使用这个被高阶组件包裹过的组件后,可能会收到一些无关的属性,我们不应该将这些无关的属性传给被包裹组件,而应该透传,这样可以保证HOC的复用性和灵活性,否则,一些无关的属性可能影响他包裹的组件。
什么是实传和透传?
实传就是你在组件内部进行了注册,可以在这个组件内部使用,同时你可以将嵌套 (opens new window)组件已经注册的属性传递下去。
透传是父组件传递一些在子组件没有注册的属性和没有使用的方法,实际上这些message是用于给子组件的嵌套组件使用的。
# 2.最大化可组合性
HOC 通常可以接收多个参数。如上面提到的页面复用案例。
export default withFetching(fetching('science-fiction'))(MovieList); // 不推荐这种写法!!!
建议编写组合式工具函数
// compose(f, g, h) 等同于 (...args) => f(g(h(...args)))
const enhance = compose(
// 这些都是单参数的 HOC
withFetching,
fetching('science-fiction')
)
const EnhancedComponent = enhance(MovieList)
2
3
4
5
6
7
许多第三方库都提供了 compose
工具函数,包括 lodash (比如 lodash.flowRight
(opens new window)), Redux (opens new window) 和 Ramda (opens new window)。
# 3.包装显示名称以便调试
HOC 创建的容器组件会与任何其他组件一样,会显示在 React Developer Tools (opens new window) 中。为了方便调试,请选择一个显示名称,以表明它是 HOC 的产物。
最常见的方式是用 HOC 包住被包装组件的显示名称。比如高阶组件名为 withSubscription
,并且被包装组件的显示名称为 CommentList
,显示名称应该为 WithSubscription(CommentList)
:
function withSubscription(WrappedComponent) {
class WithSubscription extends React.Component {/* ... */}
WithSubscription.displayName = `WithSubscription(${getDisplayName(WrappedComponent)})`;
return WithSubscription;
}
function getDisplayName(WrappedComponent) {
return WrappedComponent.displayName || WrappedComponent.name || 'Component';
}
2
3
4
5
6
7
8
9
# 7.Portals
可以将子节点渲染到存在于父组件以外的 DOM 节点。常用于能够在视觉上“跳出”其容器。例如,对话框、悬浮卡以及提示框。
React Portal之所以叫Portal,因为做的就是和“传送门”一样的事情:render到一个组件里面去,实际改变的是网页上另一处的DOM结构。
ReactDOM.createPortal(child, container)
第一个参数(child
)是任何可渲染的 React 子元素 (opens new window),例如一个元素,字符串或 fragment。第二个参数(container
)是一个 DOM 元素。
# 用法
通常来讲,当你从组件的 render 方法返回一个元素时,该元素将被挂载到 DOM 节点中离其最近的父节点:
render() {
// React 挂载了一个新的 div,并且把子元素渲染其中
return (
<div> {this.props.children}
</div> );
}
2
3
4
5
6
然而,有时候将子元素插入到 DOM 节点中的不同位置也是有好处的:
render() {
// React 并*没有*创建一个新的 div。它只是把子元素渲染到 `domNode` 中。
// `domNode` 是一个可以在任何位置的有效 DOM 节点。
return ReactDOM.createPortal(
this.props.children,
domNode );
}
2
3
4
5
6
7
如实现一个对话框(Dialog),最直观的做法,就是直接在 JSX 中把 Dialog 画出来,像下面代码的样子。
<div class="foo">
<div> ... </div>
{ needDialog ? <Dialog /> : null }
</div>
2
3
4
但上面代码存在一个局限性,那就是 Dialog 最终只能渲染在上面的 HTML 中。而当 Dialog 被包在其他组件中,要用 CSS 的 position 属性控制 Dialog位置,就要求从 Dialog 往上一直到 body 没有其他 postion 是 relative 的元素干扰,这……有点难为作为通用组件的 Dialog,毕竟,谁管得住所有组件不用 position 呢。此外,Dialog 的样式,因为包在其他元素中,各种样式纠缠,CSS 样式太容易搞成一坨浆糊了。
使用 Portal 就可以让Dialog这样的组件在表示层和其他组件没有任何差异,但是渲染的东西却像经过传送门一样出现在另一个地方。
import React from 'react';
import {createPortal} from 'react-dom';
class Dialog extends React.Component {
constructor() {
super(...arguments);
const doc = window.document;
this.node = doc.createElement('div');
doc.body.appendChild(this.node);
}
render() {
return createPortal(
<div class="dialog">
{this.props.children}
</div>, //塞进传送门的JSX
this.node //传送门的另一端DOM node
);
}
componentWillUnmount() {
window.document.body.removeChild(this.node);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 通过 Portal 进行事件冒泡
一个从 portal 内部触发的事件会一直冒泡至包含 React 树的祖先,即便这些元素并不是 DOM 树 中的祖先。
在 React v16之前的 React Portal 实现方法,有一个小小的缺陷,就是Portal是单向的,内容通过Portal传到另一个出口,在那个出口DOM上发生的事件是不会冒泡传送回进入那一端的。
也就是说,这样的代码。
<div onClick={onDialogClick}>
<Dialog>
What ever shit
</Dialog>
</div>
2
3
4
5
在Dialog画出的内容上点击,onDialogClick是不会被触发的。
当然,这只是一个小小的缺陷,大部分场景下事件不传过来也没什么大问题。
在React v16中,通过 Portal 渲染出去的 DOM,事件是会冒泡从传送门的入口端冒出来的,上面的 onDialogClick 也就会被调用到了。
# 8.Diff算法
# 状态变更
当 state 或 props 更新时,react 会产生一个新的 DOM 树,并基于这两棵树的差别来控制渲染。
# 元素类型比对
# 1.元素类型改变
如当<a>
变成 <img>
,React 会直接销毁原有的树,其对应的 DOM 节点也都全部销毁,并将新树的 DOM 插入到对应的 DOM 节点中。所有跟原有的树所关联的 state 也会被销毁。触发了一个完整的重建流程,所有的生命周期也都完整的执行了一遍。
# 2.元素类型未改变
# 属性变更
当元素类型未改变时,React 会保留 DOM 节点,仅对比有改变的属性。
如 style
变更时:
<div style={{color: 'red', fontWeight: 'bold'}} />
<div style={{color: 'green', fontWeight: 'bold'}} />
2
3
通过比对这两个元素,React 知道只需要修改 DOM 元素上的 color
样式,无需修改 fontWeight
。
在处理完当前节点之后,React 继续对子节点进行递归。
# 组件更新
组件更新时,组件实例保持不变,这样 state 在跨越不同的渲染时保持一致。React 将更新该组件实例的 props 以跟最新的元素保持一致,并且调用该实例的 componentWillReceiveProps()
和 componentWillUpdate()
方法。
# 子节点进行递归
React 支持 key
属性。当子元素拥有 key 时,React 使用 key 来匹配原有树上的子元素以及最新树上的子元素。以下例子在新增 key
之后使得之前的低效转换变得高效:
<ul>
<li key="2015">Duke</li>
<li key="2016">Villanova</li>
</ul>
<ul>
<li key="2014">Connecticut</li>
<li key="2015">Duke</li>
<li key="2016">Villanova</li>
</ul>
2
3
4
5
6
7
8
9
10
如果没有 key, React 会销毁所有元素并重新创建。
现在 React 知道只有带着 '2014'
key 的元素是新元素,带着 '2015'
以及 '2016'
key 的元素仅仅移动了。
# 9.Refs & DOM
refs 提供了一种 props 以外的方式修改子组件。
# 何时使用 Refs
下面是几个适合使用 refs 的情况:
- 管理焦点,文本选择或媒体播放。
- 触发强制动画。
- 集成第三方 DOM 库。
避免使用 refs 来做任何可以通过声明式实现来完成的事情。
举个例子,避免在 Dialog
组件里暴露 open()
和 close()
方法,最好传递 isOpen
属性。
# 创建Refs
Refs 是使用 React.createRef()
创建的,并通过 ref
属性附加到 React 元素。在构造组件时,通常将 Refs 分配给实例属性,以便可以在整个组件中引用它们。
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.myRef = React.createRef(); }
render() {
return <div ref={this.myRef} />; }
}
2
3
4
5
6
7
# 访问 Refs
当 ref 被传递给 render
中的元素时,对该节点的引用可以在 ref 的 current
属性中被访问。
const node = this.myRef.current;
ref 的值根据节点的类型而有所不同:
- 当
ref
属性用于 HTML 元素时,构造函数中使用React.createRef()
创建的ref
接收底层 DOM 元素作为其current
属性。 - 当
ref
属性用于自定义 class 组件时,ref
对象接收组件的挂载实例作为其current
属性。 - 你不能在函数组件上使用
ref
属性,因为他们没有实例,但你可以在其内部使用,只要其指向的是一个 DOM 元素或 Class 组件。
function CustomTextInput(props) {
// 这里必须声明 textInput,这样 ref 才可以引用它
const textInput = useRef(null);
function handleClick() {
textInput.current.focus();
}
return (
<div>
<input
type="text"
ref={textInput} />
<input
type="button"
value="Focus the text input"
onClick={handleClick}
/>
</div>
);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 回调Refs
另一种设置 refs 的方式,称为“回调 refs”。它能更精细地控制何时 refs 被设置和解除。
不同于传递 createRef()
创建的 ref
属性,通过传递一个函数,这个函数中接受 React 组件实例或 HTML DOM 元素作为参数,以使它们能在其他地方被存储和访问。
class CustomTextInput extends React.Component {
constructor(props) {
super(props);
this.textInput = null;
this.setTextInputRef = element => {
this.textInput = element;
};
this.focusTextInput = () => {
// 使用原生 DOM API 使 text 输入框获得焦点
if (this.textInput) this.textInput.focus();
};
}
componentDidMount() {
// 组件挂载后,让文本框自动获得焦点
this.focusTextInput(); }
render() {
// 使用 `ref` 的回调函数将 text 输入框 DOM 节点的引用存储到 React
// 实例上(比如 this.textInput)
return (
<div>
<input
type="text"
ref={this.setTextInputRef}/>
<input
type="button"
value="Focus the text input"
onClick={this.focusTextInput}/>
</div>
);
}
}
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
组件间传递回调形式的refs
function CustomTextInput(props) {
return (
<div>
<input ref={props.inputRef} />
</div>
);
}
class Parent extends React.Component {
render() {
return (
<CustomTextInput
inputRef={el => this.inputElement = el}
/>
);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
在上面的例子中,Parent
把它的 refs 回调函数当作 inputRef
props 传递给了 CustomTextInput
,而且 CustomTextInput
把相同的函数作为特殊的 ref
属性传递给了 <input>
。结果是,在 Parent
中的 this.inputElement
会被设置为与 CustomTextInput
中的 input
元素相对应的 DOM 节点。
# 10.Render Props
使用一个函数式的 prop 来共享代码,即组件间共享状态。
具有 render prop 的组件接受一个函数,该函数返回一个 React 元素并调用它而不是实现自己的渲染逻辑,更具体地说,render prop 是一个用于告知组件需要渲染什么内容的函数 prop。
<DataProvider render={data => (
<h1>Hello {data.target}</h1>
)}/>
2
3
现在有一个鼠标移动组件 Mouse ,和一个跟随鼠标移动的组件 Cat,使用 Render Props 就能让 Cat 组件轻松的在 Mouse 组件外获取鼠标状态。
class Cat extends React.Component {
render() {
const mouse = this.props.mouse;
return (
<img src="/cat.jpg" style={{ position: 'absolute', left: mouse.x, top: mouse.y }} />
);
}
}
class Mouse extends React.Component {
constructor(props) {
super(props);
this.handleMouseMove = this.handleMouseMove.bind(this);
this.state = { x: 0, y: 0 };
}
handleMouseMove(event) {
this.setState({
x: event.clientX,
y: event.clientY
});
}
render() {
return (
<div style={{ height: '100vh' }} onMouseMove={this.handleMouseMove}>
{/*
使用 `render`prop 动态决定要渲染的内容,
而不是给出一个 <Mouse> 渲染结果的静态表示
有点像插槽(并为插槽注入属性)~
*/}
{this.props.render(this.state)}
</div>
);
}
}
class MouseTracker extends React.Component {
render() {
return (
<div>
<h1>移动鼠标!</h1>
{/* 为 render 注入属性,和插槽差不多 */}
<Mouse render={mouse => (
<Cat mouse={mouse} />
)}/>
</div>
);
}
}
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
记住,children
prop 并不真正需要添加到 JSX 元素的 “attributes” 列表中。相反,你可以直接放置到元素的内部!
<Mouse>
{mouse => (
<p>鼠标的位置是 {mouse.x},{mouse.y}</p>
)}
</Mouse>
2
3
4
5