夜猫子的知识栈 夜猫子的知识栈
首页
  • 前端文章

    • JavaScript
  • 学习笔记

    • 《JavaScript教程》
    • 《Web Api》
    • 《ES6教程》
    • 《Vue》
    • 《React》
    • 《TypeScript》
    • 《Git》
    • 《Uniapp》
    • 小程序笔记
    • 《Electron》
    • JS设计模式总结
  • 《前端架构》

    • 《微前端》
    • 《权限控制》
    • monorepo
  • 全栈项目

    • 任务管理日历
    • 无代码平台
    • 图书管理系统
  • HTML
  • CSS
  • Nodejs
  • Midway
  • Nest
  • MySql
  • 其他
  • 技术文档
  • GitHub技巧
  • 博客搭建
  • Ajax
  • Vite
  • Vitest
  • Nuxt
  • UI库文章
  • Docker
  • 学习
  • 面试
  • 心情杂货
  • 实用技巧
  • 友情链接
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

夜猫子

前端练习生
首页
  • 前端文章

    • JavaScript
  • 学习笔记

    • 《JavaScript教程》
    • 《Web Api》
    • 《ES6教程》
    • 《Vue》
    • 《React》
    • 《TypeScript》
    • 《Git》
    • 《Uniapp》
    • 小程序笔记
    • 《Electron》
    • JS设计模式总结
  • 《前端架构》

    • 《微前端》
    • 《权限控制》
    • monorepo
  • 全栈项目

    • 任务管理日历
    • 无代码平台
    • 图书管理系统
  • HTML
  • CSS
  • Nodejs
  • Midway
  • Nest
  • MySql
  • 其他
  • 技术文档
  • GitHub技巧
  • 博客搭建
  • Ajax
  • Vite
  • Vitest
  • Nuxt
  • UI库文章
  • Docker
  • 学习
  • 面试
  • 心情杂货
  • 实用技巧
  • 友情链接
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • 核心概念

  • 高级指引

    • 高级指引
      • 1.代码分割
        • React.lazy
        • 基于路由的代码分割
      • 2.全局数据 Context
        • React.createContext
        • Context.Provider
        • Class.contextType
        • Context.Consumer
        • Context.displayName
        • 嵌套组件中更新 Context
        • 消费多个 Context
      • 3.错误边界
      • 4.Refs 转发
      • 5.Fragments
        • 用法
        • 带 key 的 Fragments
        • 新语法
      • 6.高阶组件(HOC)
        • 抽象出共享逻辑
        • 实例:页面复用
        • 约定
        • 1.将不相关的 props 传递给被包裹的组件
        • 2.最大化可组合性
        • 3.包装显示名称以便调试
      • 7.Portals
        • 用法
        • 通过 Portal 进行事件冒泡
      • 8.Diff算法
        • 状态变更
        • 元素类型比对
        • 1.元素类型改变
        • 2.元素类型未改变
      • 9.Refs & DOM
        • 何时使用 Refs
        • 创建Refs
        • 访问 Refs
        • 回调Refs
      • 10.Render Props
  • Hook

  • 案例演示

  • Router

  • Redux

  • Jotai

  • Mobx

  • 其他

  • 《React》笔记
  • 高级指引
夜猫子
2021-03-25
目录

高级指引

# 高级指引

# 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>
  );
}
1
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>
);
1
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} />;
  }
}
1
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} />;  
  }
}
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

类似于 vue 中的 provide

# React.createContext

const MyContext = React.createContext(defaultValue);
1

创建一个 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
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

# Context.Provider

<MyContext.Provider value={/* 某个值 */}>
1

每个 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;
1
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>
1
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 中
1
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: () => {},
});
1
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;
1
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);
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

# 消费多个 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>
  );
}
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

如果两个或者更多的 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; 
  }
}
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

然后你可以将它作为一个常规组件去使用:

<ErrorBoundary>
  <MyWidget />
</ErrorBoundary>
1
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>;
1
2
3
4
5
6
7
8
9

这样,使用 FancyButton 的组件可以获取底层 DOM 节点 button 的 ref ,并在必要时访问,就像其直接使用 DOM button 一样。

以下是对上述示例发生情况的逐步解释:

  1. 我们通过调用 React.createRef 创建了一个 React ref (opens new window) 并将其赋值给 ref 变量。
  2. 我们通过指定 ref 为 JSX 属性,将其向下传递给 <FancyButton ref={ref}>。
  3. React 传递 ref 给 forwardRef 内函数 (props, ref) => ...,作为其第二个参数。
  4. 我们向下转发该 ref 参数到 <button ref={ref}>,将其指定为 JSX 属性。
  5. 当 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>
  );
}
1
2
3
4
5
6
7
8
9

# 用法

父组件

class Table extends React.Component {
  render() {
    return (
      <table>
        <tr>
          <Columns />
        </tr>
      </table>
    );
  }
}
1
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>
    );
  }
}
1
2
3
4
5
6
7
8
9
10

输出

<table>
  <tr>
    <td>Hello</td>
    <td>World</td>
  </tr>
</table>
1
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>
  );
}
1
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>
      </>
    );
  }
}
1
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>
    );
  }
}
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

稍后,编写了一个用于订阅单个博客帖子的组件,该帖子遵循类似的模式:

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} />;
  }
}
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

它们实现了类似的功能:

  • 在挂载时,向 DataSource 添加一个更改侦听器。
  • 在侦听器内部,当数据源发生变化时,调用 setState。
  • 在卸载时,删除侦听器。

# 抽象出共享逻辑

const CommentListWithSubscription = withSubscription(
  CommentList,
  (DataSource) => DataSource.getComments()
);

const BlogPostWithSubscription = withSubscription(
  BlogPost,
  (DataSource, props) => DataSource.getBlogPost(props.id)
);
1
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} />;
    }
  };
}
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

再看一下另一个案例

# 实例:页面复用

假设我们有两个页面 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;
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

页面少的时候可能没什么问题,但是假如随着业务的进展,需要上线的越来越多类型的电影,就会写很多的重复代码,所以我们需要重构一下:

 // 这里使用了柯里化的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);
1
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}
    />
  );
}
1
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); // 不推荐这种写法!!!
1

建议编写组合式工具函数

// compose(f, g, h) 等同于 (...args) => f(g(h(...args)))
const enhance = compose(
  // 这些都是单参数的 HOC
  withFetching,
  fetching('science-fiction')
)
const EnhancedComponent = enhance(MovieList)
1
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';
}
1
2
3
4
5
6
7
8
9

# 7.Portals

可以将子节点渲染到存在于父组件以外的 DOM 节点。常用于能够在视觉上“跳出”其容器。例如,对话框、悬浮卡以及提示框。

React Portal之所以叫Portal,因为做的就是和“传送门”一样的事情:render到一个组件里面去,实际改变的是网页上另一处的DOM结构。

ReactDOM.createPortal(child, container)
1

第一个参数(child)是任何可渲染的 React 子元素 (opens new window),例如一个元素,字符串或 fragment。第二个参数(container)是一个 DOM 元素。

# 用法

通常来讲,当你从组件的 render 方法返回一个元素时,该元素将被挂载到 DOM 节点中离其最近的父节点:

render() {
  // React 挂载了一个新的 div,并且把子元素渲染其中
  return (
    <div>      {this.props.children}
    </div>  );
}
1
2
3
4
5
6

然而,有时候将子元素插入到 DOM 节点中的不同位置也是有好处的:

render() {
  // React 并*没有*创建一个新的 div。它只是把子元素渲染到 `domNode` 中。
  // `domNode` 是一个可以在任何位置的有效 DOM 节点。
  return ReactDOM.createPortal(
    this.props.children,
    domNode  );
}
1
2
3
4
5
6
7

如实现一个对话框(Dialog),最直观的做法,就是直接在 JSX 中把 Dialog 画出来,像下面代码的样子。

<div class="foo">
   <div> ... </div>
   { needDialog ? <Dialog /> : null }
</div>
1
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);
  }
}
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

# 通过 Portal 进行事件冒泡

一个从 portal 内部触发的事件会一直冒泡至包含 React 树的祖先,即便这些元素并不是 DOM 树 中的祖先。

在 React v16之前的 React Portal 实现方法,有一个小小的缺陷,就是Portal是单向的,内容通过Portal传到另一个出口,在那个出口DOM上发生的事件是不会冒泡传送回进入那一端的。

也就是说,这样的代码。

<div onClick={onDialogClick}>   
   <Dialog>
     What ever shit
   </Dialog>
</div>
1
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'}} />
1
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>
1
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} />;  }
}
1
2
3
4
5
6
7

# 访问 Refs

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

const node = this.myRef.current;
1

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>
  );
}
1
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>
    );
  }
}
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

组件间传递回调形式的refs

function CustomTextInput(props) {
  return (
    <div>
      <input ref={props.inputRef} />    
    </div>
  );
}

class Parent extends React.Component {
  render() {
    return (
      <CustomTextInput
        inputRef={el => this.inputElement = el}      
      />
    );
  }
}
1
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>
)}/>
1
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>
    );
  }
}
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

记住,children prop 并不真正需要添加到 JSX 元素的 “attributes” 列表中。相反,你可以直接放置到元素的内部!

<Mouse>
  {mouse => (
    <p>鼠标的位置是 {mouse.x},{mouse.y}</p>
  )}
</Mouse>
1
2
3
4
5
编辑 (opens new window)
#React
上次更新: 2024/12/19 17:25:54
React哲学
Hook概述

← React哲学 Hook概述→

最近更新
01
IoC 解决了什么痛点问题?
03-10
02
如何调试 Nest 项目
03-10
03
Provider注入对象
03-10
更多文章>
Copyright © 2019-2025 Study | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式